feat(jobs): Phase 3 light refactors — parallel job/step links on dependent models
Adds x_fc_job_id / x_fc_step_id Many2ones via _inherit on: - fusion.plating.batch (workorder_id stays for legacy MRP-bound batches) - fusion.plating.quality.hold - fp.certificate - fp.thickness.reading - fusion.plating.delivery (parallel to existing job_ref Char) - fp.racking.inspection (parallel to existing production_id) fp.job.action_confirm now also calls a best-effort racking-inspection auto-create. The current fp.racking.inspection still has a required production_id, so the helper skips cleanly when this job has no MO link (pure-native mode). Phase 9 cutover flips the required FK to fp.job. Strategy: parallel coexistence — bridge_mrp's existing fields stay populated; this adds NEW fields populated by the native flow. Phase 9 cutover stops populating the old fields. Adds fusion_plating_batch + fusion_plating_receiving to jobs module depends. Note: spec referenced fp.batch / fp.quality.hold; the actual models in this codebase are fusion.plating.batch / fusion.plating.quality.hold — used the real model names. Manifest 19.0.1.5.0 → 19.0.1.6.0. 29 jobs tests pass. 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:
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.1.5.0',
|
||||
'version': '19.0.1.6.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'description': """
|
||||
@@ -24,11 +24,13 @@ full design rationale and §6.2 of the implementation plan for task list.
|
||||
""",
|
||||
'depends': [
|
||||
'fusion_plating', # fp.job, fp.job.step, fp.work.centre
|
||||
'fusion_plating_batch', # fusion.plating.batch (Phase 3)
|
||||
'fusion_plating_certificates', # fp.certificate, fp.thickness.reading
|
||||
'fusion_plating_configurator', # fp.part.catalog, fp.coating.config
|
||||
'fusion_plating_portal', # fusion.plating.portal.job
|
||||
'fusion_plating_logistics', # fusion.plating.delivery
|
||||
'fusion_plating_quality', # fusion.plating.customer.spec, fp.quality.hold
|
||||
'fusion_plating_certificates', # fp.certificate
|
||||
'fusion_plating_portal', # fusion.plating.portal.job
|
||||
'fusion_plating_quality', # fusion.plating.customer.spec, fusion.plating.quality.hold
|
||||
'fusion_plating_receiving', # fp.racking.inspection (Phase 3)
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
@@ -11,3 +11,11 @@ from . import fp_portal_job
|
||||
from . import account_move
|
||||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
|
||||
# Phase 3 — parallel job/step links on dependent modules' models.
|
||||
from . import fp_batch
|
||||
from . import fp_quality_hold
|
||||
from . import fp_certificate
|
||||
from . import fp_thickness_reading
|
||||
from . import fp_delivery
|
||||
from . import fp_racking_inspection
|
||||
|
||||
26
fusion_plating/fusion_plating_jobs/models/fp_batch.py
Normal file
26
fusion_plating/fusion_plating_jobs/models/fp_batch.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Phase 3 — parallel job/step links on fusion.plating.batch.
|
||||
# The legacy workorder_id link to mrp.workorder stays in place.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FusionPlatingBatch(models.Model):
|
||||
_inherit = 'fusion.plating.batch'
|
||||
|
||||
x_fc_step_id = fields.Many2one(
|
||||
'fp.job.step',
|
||||
string='Plating Step',
|
||||
index=True,
|
||||
help='Native fp.job.step link. Coexists with the legacy '
|
||||
'workorder_id link to mrp.workorder.',
|
||||
)
|
||||
x_fc_job_id = fields.Many2one(
|
||||
'fp.job',
|
||||
related='x_fc_step_id.job_id',
|
||||
store=True,
|
||||
string='Plating Job',
|
||||
)
|
||||
19
fusion_plating/fusion_plating_jobs/models/fp_certificate.py
Normal file
19
fusion_plating/fusion_plating_jobs/models/fp_certificate.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Phase 3 — parallel job link on fp.certificate.
|
||||
# Coexists with bridge_mrp's production_id link.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpCertificate(models.Model):
|
||||
_inherit = 'fp.certificate'
|
||||
|
||||
x_fc_job_id = fields.Many2one(
|
||||
'fp.job',
|
||||
string='Plating Job',
|
||||
index=True,
|
||||
help="Native fp.job link. Coexists with bridge_mrp's production_id.",
|
||||
)
|
||||
19
fusion_plating/fusion_plating_jobs/models/fp_delivery.py
Normal file
19
fusion_plating/fusion_plating_jobs/models/fp_delivery.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Phase 3 — parallel job link on fusion.plating.delivery.
|
||||
# Coexists with the legacy job_ref Char.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FusionPlatingDelivery(models.Model):
|
||||
_inherit = 'fusion.plating.delivery'
|
||||
|
||||
x_fc_job_id = fields.Many2one(
|
||||
'fp.job',
|
||||
string='Plating Job',
|
||||
index=True,
|
||||
help='Native fp.job link. Coexists with the legacy job_ref Char.',
|
||||
)
|
||||
@@ -259,8 +259,49 @@ class FpJob(models.Model):
|
||||
for job in self:
|
||||
job._fp_create_portal_job()
|
||||
job._fp_create_qc_check_if_needed()
|
||||
job._fp_create_racking_inspection()
|
||||
return result
|
||||
|
||||
def _fp_create_racking_inspection(self):
|
||||
"""Auto-create a draft racking inspection on job confirm.
|
||||
|
||||
Mirrors bridge_mrp's behaviour for MO confirm. Best-effort: the
|
||||
legacy fp.racking.inspection model still requires a production_id
|
||||
(mrp.production), so we can only create one when this job is
|
||||
bound to an MO via bridge_mrp. Otherwise we skip cleanly — Phase
|
||||
9 will flip the required-FK to fp.job.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if 'fp.racking.inspection' not in self.env:
|
||||
return
|
||||
Inspection = self.env['fp.racking.inspection'].sudo()
|
||||
# The model still requires production_id today. If the job has
|
||||
# no MO link (which it won't in pure-native mode), skip rather
|
||||
# than crash. The link exists when fusion_plating_bridge_mrp is
|
||||
# installed and a production was created in parallel.
|
||||
production = False
|
||||
if 'production_id' in self._fields and self.production_id:
|
||||
production = self.production_id
|
||||
elif 'mrp_production_id' in self._fields and getattr(
|
||||
self, 'mrp_production_id', False):
|
||||
production = self.mrp_production_id
|
||||
if not production:
|
||||
_logger.debug(
|
||||
"Job %s: no MO link — skipping racking-inspection auto-create "
|
||||
"(required production_id not yet on fp.job).", self.name,
|
||||
)
|
||||
return
|
||||
try:
|
||||
vals = {'production_id': production.id}
|
||||
if 'x_fc_job_id' in Inspection._fields:
|
||||
vals['x_fc_job_id'] = self.id
|
||||
Inspection.create(vals)
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
"Job %s: failed to auto-create racking inspection: %s",
|
||||
self.name, e,
|
||||
)
|
||||
|
||||
def _fp_create_portal_job(self):
|
||||
"""Create the fusion.plating.portal.job mirror record."""
|
||||
self.ensure_one()
|
||||
|
||||
25
fusion_plating/fusion_plating_jobs/models/fp_quality_hold.py
Normal file
25
fusion_plating/fusion_plating_jobs/models/fp_quality_hold.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Phase 3 — parallel job/step links on fusion.plating.quality.hold.
|
||||
# Coexists with bridge_mrp's existing production_id link.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FusionPlatingQualityHold(models.Model):
|
||||
_inherit = 'fusion.plating.quality.hold'
|
||||
|
||||
x_fc_job_id = fields.Many2one(
|
||||
'fp.job',
|
||||
string='Plating Job',
|
||||
index=True,
|
||||
help="Native fp.job link. Coexists with bridge_mrp's production_id "
|
||||
"link.",
|
||||
)
|
||||
x_fc_step_id = fields.Many2one(
|
||||
'fp.job.step',
|
||||
string='Plating Step',
|
||||
index=True,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Phase 3 — parallel job link on fp.racking.inspection.
|
||||
# Coexists with the legacy production_id (mrp.production) link.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpRackingInspection(models.Model):
|
||||
_inherit = 'fp.racking.inspection'
|
||||
|
||||
x_fc_job_id = fields.Many2one(
|
||||
'fp.job',
|
||||
string='Plating Job',
|
||||
index=True,
|
||||
help='Native fp.job link. Coexists with the legacy production_id.',
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Phase 3 — parallel job/step links on fp.thickness.reading.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpThicknessReading(models.Model):
|
||||
_inherit = 'fp.thickness.reading'
|
||||
|
||||
x_fc_job_id = fields.Many2one(
|
||||
'fp.job',
|
||||
string='Plating Job',
|
||||
index=True,
|
||||
)
|
||||
x_fc_step_id = fields.Many2one(
|
||||
'fp.job.step',
|
||||
string='Plating Step',
|
||||
index=True,
|
||||
)
|
||||
@@ -382,3 +382,83 @@ class TestJobLifecycleHooks(TransactionCase):
|
||||
job.action_cancel()
|
||||
with self.assertRaises(UserError):
|
||||
job.button_mark_done()
|
||||
|
||||
|
||||
class TestPhase3Refactors(TransactionCase):
|
||||
"""Phase 3 — verify parallel job/step links exist on the dependent
|
||||
modules' models. Field-presence is enough; the migration logic is
|
||||
Phase 9's concern."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({'name': 'C'})
|
||||
self.product = self.env['product.product'].create({'name': 'P'})
|
||||
|
||||
def test_fusion_plating_batch_has_x_fc_step_id(self):
|
||||
self.assertIn('x_fc_step_id', self.env['fusion.plating.batch']._fields)
|
||||
self.assertIn('x_fc_job_id', self.env['fusion.plating.batch']._fields)
|
||||
# Verify comodels
|
||||
self.assertEqual(
|
||||
self.env['fusion.plating.batch']._fields['x_fc_step_id'].comodel_name,
|
||||
'fp.job.step',
|
||||
)
|
||||
self.assertEqual(
|
||||
self.env['fusion.plating.batch']._fields['x_fc_job_id'].comodel_name,
|
||||
'fp.job',
|
||||
)
|
||||
|
||||
def test_fusion_plating_quality_hold_has_x_fc_job_id(self):
|
||||
self.assertIn(
|
||||
'x_fc_job_id',
|
||||
self.env['fusion.plating.quality.hold']._fields,
|
||||
)
|
||||
self.assertIn(
|
||||
'x_fc_step_id',
|
||||
self.env['fusion.plating.quality.hold']._fields,
|
||||
)
|
||||
|
||||
def test_fp_certificate_has_x_fc_job_id(self):
|
||||
self.assertIn('x_fc_job_id', self.env['fp.certificate']._fields)
|
||||
self.assertEqual(
|
||||
self.env['fp.certificate']._fields['x_fc_job_id'].comodel_name,
|
||||
'fp.job',
|
||||
)
|
||||
|
||||
def test_fp_thickness_reading_has_x_fc_job_id(self):
|
||||
self.assertIn('x_fc_job_id', self.env['fp.thickness.reading']._fields)
|
||||
self.assertIn('x_fc_step_id', self.env['fp.thickness.reading']._fields)
|
||||
|
||||
def test_fusion_plating_delivery_has_x_fc_job_id(self):
|
||||
self.assertIn(
|
||||
'x_fc_job_id',
|
||||
self.env['fusion.plating.delivery']._fields,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.env['fusion.plating.delivery']._fields['x_fc_job_id'].comodel_name,
|
||||
'fp.job',
|
||||
)
|
||||
|
||||
def test_fp_racking_inspection_has_x_fc_job_id(self):
|
||||
self.assertIn(
|
||||
'x_fc_job_id',
|
||||
self.env['fp.racking.inspection']._fields,
|
||||
)
|
||||
|
||||
def test_racking_inspection_helper_skips_without_mo(self):
|
||||
"""The auto-create helper should silently skip when the job
|
||||
has no production_id (pure-native mode). Should NOT raise."""
|
||||
job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
})
|
||||
# action_confirm should run cleanly even though we cannot
|
||||
# satisfy the model's required production_id today.
|
||||
job.action_confirm()
|
||||
# No exception is the assertion. No inspection should exist
|
||||
# for this job since the helper skipped.
|
||||
if 'x_fc_job_id' in self.env['fp.racking.inspection']._fields:
|
||||
inspections = self.env['fp.racking.inspection'].search(
|
||||
[('x_fc_job_id', '=', job.id)],
|
||||
)
|
||||
self.assertFalse(inspections)
|
||||
|
||||
Reference in New Issue
Block a user