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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 21:28:34 -04:00
parent 498963e83a
commit 1630a2025f
3 changed files with 43 additions and 17 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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: