From 47a54eac8fef45a53c3c8073b532723cd221f0d2 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 25 Apr 2026 04:05:02 -0400 Subject: [PATCH] feat(jobs): cutover - bridge_mrp gate, menu nesting, migration robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../fusion_plating/views/fp_jobs_menu.xml | 29 +++++++---- .../models/sale_order.py | 7 +++ .../scripts/migrate_to_fp_jobs.py | 51 ++++++++++++------- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/fusion_plating/fusion_plating/views/fp_jobs_menu.xml b/fusion_plating/fusion_plating/views/fp_jobs_menu.xml index 163cad60..6b23dcae 100644 --- a/fusion_plating/fusion_plating/views/fp_jobs_menu.xml +++ b/fusion_plating/fusion_plating/views/fp_jobs_menu.xml @@ -1,28 +1,35 @@ - - + + + sequence="55" + groups="fusion_plating.group_fusion_plating_manager"/> diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py b/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py index 878d1ba2..169a0a41 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/sale_order.py @@ -81,6 +81,13 @@ class SaleOrder(models.Model): # ------------------------------------------------------------------ def action_confirm(self): 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: try: so._fp_auto_create_mo() diff --git a/fusion_plating/fusion_plating_jobs/scripts/migrate_to_fp_jobs.py b/fusion_plating/fusion_plating_jobs/scripts/migrate_to_fp_jobs.py index dc50b716..9968dc7a 100644 --- a/fusion_plating/fusion_plating_jobs/scripts/migrate_to_fp_jobs.py +++ b/fusion_plating/fusion_plating_jobs/scripts/migrate_to_fp_jobs.py @@ -78,7 +78,8 @@ def _resolve_partner(env, mo): Order of preference: 1. mo.x_fc_customer_id (some custom modules add this) 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: 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) if so: 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): @@ -402,23 +411,29 @@ def run(env): print('Migrating %d MOs and their WOs...' % len(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: - job = migrate_mo(env, mo, audit) - for wo in mo.workorder_ids: - try: - migrate_wo(env, wo, job, audit) - except Exception as e: - audit['errors'].append({ - 'wo': wo.id, - 'wo_name': wo.name, - 'mo': mo.id, - 'error': str(e), - }) - _logger.error( - 'Migration failed for WO %s (MO %s): %s', - wo.name, mo.name, e, - ) - rebind_dependents(env, mo, job, audit) + with env.cr.savepoint(): + job = migrate_mo(env, mo, audit) + for wo in mo.workorder_ids: + try: + with env.cr.savepoint(): + migrate_wo(env, wo, job, audit) + except Exception as e: + audit['errors'].append({ + 'wo': wo.id, + 'wo_name': wo.name, + 'mo': mo.id, + 'error': str(e), + }) + _logger.error( + 'Migration failed for WO %s (MO %s): %s', + wo.name, mo.name, e, + ) + rebind_dependents(env, mo, job, audit) except Exception as e: audit['errors'].append({ 'mo': mo.id,