From 1630a2025f1f4d4cd170ed038aebb30ee40c2dfa Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 4 Jun 2026 21:28:34 -0400 Subject: [PATCH] fix(fusion_clock): planning port defers when planning ORM not loaded during -u fusion_clock doesn't depend on planning, so planning's models load AFTER it during -u and the port saw no data. Now detect planning tables via SQL, defer (no marker) when the ORM isn't loaded, and finish the port from the deploy odoo-shell step (full registry). Marker now owned by the method. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migrations/19.0.5.0.0/post-migrate.py | 21 ++++++----- fusion_clock/models/clock_schedule.py | 35 +++++++++++++++---- fusion_clock/tests/test_planning_migration.py | 4 +++ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/fusion_clock/migrations/19.0.5.0.0/post-migrate.py b/fusion_clock/migrations/19.0.5.0.0/post-migrate.py index 9a9c9e2e..3d6094cb 100644 --- a/fusion_clock/migrations/19.0.5.0.0/post-migrate.py +++ b/fusion_clock/migrations/19.0.5.0.0/post-migrate.py @@ -16,13 +16,13 @@ from odoo import api, SUPERUSER_ID _logger = logging.getLogger(__name__) -_MARKER = 'fusion_clock.planning_migrated' - def migrate(cr, version): - """Port Odoo Planning data into the native models, once. The heavy lifting - lives in fusion.clock.schedule._fclk_port_planning_data so it can be unit - tested on an Enterprise clone where planning is installed.""" + """Drop the legacy one-shift-per-day constraint and attempt the planning -> + native port. The port (fusion.clock.schedule._fclk_port_planning_data) is + marker-guarded and self-defers: because fusion_clock doesn't depend on + planning, planning's ORM may not be loaded here, in which case the deploy + shell step finishes the port. Lives in the model so it's unit-testable.""" env = api.Environment(cr, SUPERUSER_ID, {}) # Phase B drops the hard one-shift-per-day uniqueness so split/open shifts @@ -32,10 +32,9 @@ def migrate(cr, version): "ALTER TABLE fusion_clock_schedule " "DROP CONSTRAINT IF EXISTS fusion_clock_schedule_employee_date_unique") - ICP = env['ir.config_parameter'].sudo() - if ICP.get_param(_MARKER): - _logger.info("Fusion Clock: planning data already migrated; skipping.") - return counts = env['fusion.clock.schedule'].sudo()._fclk_port_planning_data() - ICP.set_param(_MARKER, '1') - _logger.info("Fusion Clock: planning -> native migration done: %s", counts) + if counts.get('deferred'): + _logger.info("Fusion Clock: planning models not loaded during migration; " + "data will be ported by the deploy shell step.") + else: + _logger.info("Fusion Clock: planning -> native migration: %s", counts) diff --git a/fusion_clock/models/clock_schedule.py b/fusion_clock/models/clock_schedule.py index 1460e7ac..6860bebb 100644 --- a/fusion_clock/models/clock_schedule.py +++ b/fusion_clock/models/clock_schedule.py @@ -620,17 +620,38 @@ class FusionClockSchedule(models.Model): @api.model def _fclk_port_planning_data(self): """Port Odoo Planning data (roles, employee roles, slots) into the - native models. Safe no-op when planning is not installed. Returns a - dict of counts. Called by the 19.0.5.0.0 migration and by tests.""" + native models. Idempotent (marker-guarded). Returns a dict of counts. + + Because fusion_clock does NOT depend on planning, during a `-u` planning + may load AFTER us, so its ORM models aren't available in the migration's + registry. When that happens we set ``deferred`` and do nothing; the + deploy then runs this again from `odoo shell`, where the whole registry + (planning included) is loaded. Called by the 19.0.5.0.0 migration, the + deploy shell step, and tests.""" import pytz - counts = {'roles': 0, 'employees': 0, 'slots': 0, 'skipped': 0} + counts = {'roles': 0, 'employees': 0, 'slots': 0, 'skipped': 0, 'deferred': False} env = self.env - has_roles = 'planning.role' in env - has_slots = 'planning.slot' in env - if not has_roles and not has_slots: + ICP = env['ir.config_parameter'].sudo() + if ICP.get_param('fusion_clock.planning_migrated'): return counts + # Do the planning tables exist at all? (raw SQL — independent of whether + # planning's ORM models are loaded in this registry.) + env.cr.execute( + "SELECT to_regclass('public.planning_role'), to_regclass('public.planning_slot')") + role_tbl, slot_tbl = env.cr.fetchone() + if not role_tbl and not slot_tbl: + ICP.set_param('fusion_clock.planning_migrated', '1') # Community / fresh + return counts + + # Tables exist but the ORM models may not be loaded yet -> defer. + if 'planning.slot' not in env or 'planning.role' not in env: + counts['deferred'] = True + return counts + + has_roles = bool(role_tbl) + has_slots = bool(slot_tbl) Role = env['fusion.clock.role'].sudo() role_map = {} if has_roles: @@ -692,6 +713,8 @@ class FusionClockSchedule(models.Model): except Exception as exc: counts['skipped'] += 1 _logger.warning("Fusion Clock: skip planning.slot %s (%s).", slot.id, exc) + + ICP.set_param('fusion_clock.planning_migrated', '1') return counts diff --git a/fusion_clock/tests/test_planning_migration.py b/fusion_clock/tests/test_planning_migration.py index e8fba4b4..32bd2c29 100644 --- a/fusion_clock/tests/test_planning_migration.py +++ b/fusion_clock/tests/test_planning_migration.py @@ -18,6 +18,10 @@ class TestPlanningMigration(TransactionCase): if 'planning.slot' not in self.env: self.skipTest('planning not installed (Community / local dev)') + # Ensure the port actually runs (it is marker-guarded for production). + self.env['ir.config_parameter'].sudo().search( + [('key', '=', 'fusion_clock.planning_migrated')]).unlink() + prole = self.env['planning.role'].create({'name': 'PortLead', 'color': 5}) emp = self.env['hr.employee'].create({'name': 'Porty McPort'}) if 'default_planning_role_id' in emp._fields: