feat(jobs): cutover - bridge_mrp gate, menu nesting, migration robustness

Three changes to support live cutover on entech (2026-04-25):

1. Bridge_mrp gate: sale.order.action_confirm in
   fusion_plating_bridge_mrp now skips _fp_auto_create_mo when the
   x_fc_use_native_jobs config flag is True. Without this, every SO
   confirm would create both an mrp.production AND an fp.job
   (duplicate work). The gate is the only modification to bridge_mrp
   during the migration — the rest stays untouched.

2. Menu nesting: Plating Jobs (Native) now lives INSIDE the existing
   Plating app (parent=menu_fp_root) instead of as a separate
   top-level app. Two parallel 'Plating' apps was confusing UX. Work
   Centres (Native) goes under the existing Configuration sub-menu.
   '(Native)' suffix is temporary — drops at full legacy removal.

3. Migration script robustness: per-MO savepoints (so one bad MO
   doesn't abort the whole transaction with cascading 'transaction
   aborted' errors) + extended partner resolver fallback chain
   (warehouse partner → company partner) for orphan demo MOs without
   SO link or x_fc_customer_id. Verified: 43 MOs + 297 WOs migrated
   on entech with 0 errors after these fixes.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-25 04:05:02 -04:00
parent 5c009d3dcf
commit 47a54eac8f
3 changed files with 58 additions and 29 deletions

View File

@@ -1,28 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<odoo> <odoo>
<!-- Top-level "Plating Jobs (new)" menu, manager-only during Phase 1. <!-- Native job model menus, nested INSIDE the existing Plating app
The fully-fledged operator-facing menu structure lands in (menu_fp_root). Manager-only during the migration period so
Phase 6 when shopfloor is rewritten. --> operators don't see two parallel job lists.
<menuitem id="menu_fp_jobs_root"
name="Plating Jobs (new)" Naming uses "(Native)" suffix to distinguish from the legacy
sequence="47" MO/WO menus that bridge_mrp ships under Operations. After
cutover and legacy uninstall, the suffix can be dropped. -->
<menuitem id="menu_fp_jobs_native_root"
name="Plating Jobs (Native)"
parent="menu_fp_root"
sequence="4"
groups="fusion_plating.group_fusion_plating_manager"/> groups="fusion_plating.group_fusion_plating_manager"/>
<menuitem id="menu_fp_jobs_jobs" <menuitem id="menu_fp_jobs_jobs"
name="Jobs" name="Jobs"
parent="menu_fp_jobs_root" parent="menu_fp_jobs_native_root"
action="action_fp_job" action="action_fp_job"
sequence="10"/> sequence="10"/>
<menuitem id="menu_fp_jobs_steps" <menuitem id="menu_fp_jobs_steps"
name="Steps (Admin)" name="Steps (Admin)"
parent="menu_fp_jobs_root" parent="menu_fp_jobs_native_root"
action="action_fp_job_step" action="action_fp_job_step"
sequence="20"/> sequence="20"/>
<menuitem id="menu_fp_jobs_work_centres" <menuitem id="menu_fp_jobs_work_centres"
name="Work Centres" name="Work Centres (Native)"
parent="menu_fp_jobs_root" parent="menu_fp_config"
action="action_fp_work_centre" action="action_fp_work_centre"
sequence="30"/> sequence="55"
groups="fusion_plating.group_fusion_plating_manager"/>
</odoo> </odoo>

View File

@@ -81,6 +81,13 @@ class SaleOrder(models.Model):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def action_confirm(self): def action_confirm(self):
res = super().action_confirm() res = super().action_confirm()
# Cutover gate (2026-04-25): when the native job model is the
# primary, skip MO creation here — fusion_plating_jobs handles
# SO → fp.job. Both modules' SO-confirm hooks would otherwise
# run on every confirm and create duplicate work.
ICP = self.env['ir.config_parameter'].sudo()
if ICP.get_param('fusion_plating_jobs.use_native_jobs') == 'True':
return res
for so in self: for so in self:
try: try:
so._fp_auto_create_mo() so._fp_auto_create_mo()

View File

@@ -78,7 +78,8 @@ def _resolve_partner(env, mo):
Order of preference: Order of preference:
1. mo.x_fc_customer_id (some custom modules add this) 1. mo.x_fc_customer_id (some custom modules add this)
2. partner from sale.order matching mo.origin 2. partner from sale.order matching mo.origin
3. False (caller decides whether to error) 3. mo.picking_type_id.warehouse_id.partner_id (warehouse address)
4. The company partner (last-resort placeholder for orphan MOs)
""" """
if 'x_fc_customer_id' in mo._fields and mo.x_fc_customer_id: if 'x_fc_customer_id' in mo._fields and mo.x_fc_customer_id:
return mo.x_fc_customer_id.id return mo.x_fc_customer_id.id
@@ -86,7 +87,15 @@ def _resolve_partner(env, mo):
so = env['sale.order'].search([('name', '=', mo.origin)], limit=1) so = env['sale.order'].search([('name', '=', mo.origin)], limit=1)
if so: if so:
return so.partner_id.id return so.partner_id.id
return False # Warehouse partner fallback (works for internal/transfer MOs)
if 'picking_type_id' in mo._fields and mo.picking_type_id:
wh = mo.picking_type_id.warehouse_id
if wh and wh.partner_id:
return wh.partner_id.id
# Last resort: company partner. This is a placeholder for orphan
# demo/legacy MOs that have no SO link and no warehouse partner.
# Audit log will flag these so they can be reassigned manually.
return mo.company_id.partner_id.id if mo.company_id else env.company.partner_id.id
def migrate_mo(env, mo, audit): def migrate_mo(env, mo, audit):
@@ -402,23 +411,29 @@ def run(env):
print('Migrating %d MOs and their WOs...' % len(all_mos)) print('Migrating %d MOs and their WOs...' % len(all_mos))
for mo in all_mos: for mo in all_mos:
# Wrap each MO migration in a savepoint so a failure on one
# MO doesn't abort the whole transaction (which would cascade
# "current transaction is aborted" errors on every subsequent
# MO and prevent any successful migration from committing).
try: try:
job = migrate_mo(env, mo, audit) with env.cr.savepoint():
for wo in mo.workorder_ids: job = migrate_mo(env, mo, audit)
try: for wo in mo.workorder_ids:
migrate_wo(env, wo, job, audit) try:
except Exception as e: with env.cr.savepoint():
audit['errors'].append({ migrate_wo(env, wo, job, audit)
'wo': wo.id, except Exception as e:
'wo_name': wo.name, audit['errors'].append({
'mo': mo.id, 'wo': wo.id,
'error': str(e), 'wo_name': wo.name,
}) 'mo': mo.id,
_logger.error( 'error': str(e),
'Migration failed for WO %s (MO %s): %s', })
wo.name, mo.name, e, _logger.error(
) 'Migration failed for WO %s (MO %s): %s',
rebind_dependents(env, mo, job, audit) wo.name, mo.name, e,
)
rebind_dependents(env, mo, job, audit)
except Exception as e: except Exception as e:
audit['errors'].append({ audit['errors'].append({
'mo': mo.id, 'mo': mo.id,