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:
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user