diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 19560850..864bdc41 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -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', diff --git a/fusion_plating/fusion_plating_jobs/models/__init__.py b/fusion_plating/fusion_plating_jobs/models/__init__.py index 37a8fa38..5d87aad9 100644 --- a/fusion_plating/fusion_plating_jobs/models/__init__.py +++ b/fusion_plating/fusion_plating_jobs/models/__init__.py @@ -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 diff --git a/fusion_plating/fusion_plating_jobs/models/fp_batch.py b/fusion_plating/fusion_plating_jobs/models/fp_batch.py new file mode 100644 index 00000000..40928cae --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_batch.py @@ -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', + ) diff --git a/fusion_plating/fusion_plating_jobs/models/fp_certificate.py b/fusion_plating/fusion_plating_jobs/models/fp_certificate.py new file mode 100644 index 00000000..2b15b08d --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_certificate.py @@ -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.", + ) diff --git a/fusion_plating/fusion_plating_jobs/models/fp_delivery.py b/fusion_plating/fusion_plating_jobs/models/fp_delivery.py new file mode 100644 index 00000000..f89f5086 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_delivery.py @@ -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.', + ) diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index 290fc655..457001f0 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -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() diff --git a/fusion_plating/fusion_plating_jobs/models/fp_quality_hold.py b/fusion_plating/fusion_plating_jobs/models/fp_quality_hold.py new file mode 100644 index 00000000..971596e3 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_quality_hold.py @@ -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, + ) diff --git a/fusion_plating/fusion_plating_jobs/models/fp_racking_inspection.py b/fusion_plating/fusion_plating_jobs/models/fp_racking_inspection.py new file mode 100644 index 00000000..1abe31bc --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_racking_inspection.py @@ -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.', + ) diff --git a/fusion_plating/fusion_plating_jobs/models/fp_thickness_reading.py b/fusion_plating/fusion_plating_jobs/models/fp_thickness_reading.py new file mode 100644 index 00000000..87e194a1 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_thickness_reading.py @@ -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, + ) diff --git a/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py b/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py index 1250b468..e5e8af1a 100644 --- a/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py +++ b/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py @@ -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)