diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 9720dded..1e8ea381 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.1.1', + 'version': '19.0.1.2.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'description': """ diff --git a/fusion_plating/fusion_plating_jobs/models/__init__.py b/fusion_plating/fusion_plating_jobs/models/__init__.py index a81477b7..789b1929 100644 --- a/fusion_plating/fusion_plating_jobs/models/__init__.py +++ b/fusion_plating/fusion_plating_jobs/models/__init__.py @@ -6,3 +6,4 @@ # task-by-task in Tasks 2.2 onwards. from . import fp_job +from . import fp_job_node_override diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index 77b74d07..dde70dab 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -36,3 +36,8 @@ class FpJob(models.Model): 'fusion.plating.delivery', string='Delivery', ) + override_ids = fields.One2many( + 'fp.job.node.override', + 'job_id', + string='Recipe Overrides', + ) diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job_node_override.py b/fusion_plating/fusion_plating_jobs/models/fp_job_node_override.py new file mode 100644 index 00000000..20f3b9d0 --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_job_node_override.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# fp.job.node.override — per-job opt-in/out decisions for opt_in/opt_out +# recipe nodes. Mirrors fusion.plating.job.node.override from bridge_mrp, +# but bound to fp.job instead of mrp.production. +# +# bridge_mrp keeps its version alive so legacy MO-flow keeps working. +# Both coexist during the migration period. + +from odoo import fields, models + + +class FpJobNodeOverride(models.Model): + _name = 'fp.job.node.override' + _description = 'Plating Job Recipe Node Override' + _order = 'job_id, node_id' + + job_id = fields.Many2one( + 'fp.job', + required=True, + ondelete='cascade', + index=True, + ) + node_id = fields.Many2one( + 'fusion.plating.process.node', + string='Recipe Node', + required=True, + domain="[('opt_in_out', 'in', ('opt_in', 'opt_out'))]", + ) + included = fields.Boolean( + string='Included', + default=True, + help='When True, this opt-in/out node is included in step generation.', + ) + + _unique_job_node = models.Constraint( + 'unique(job_id, node_id)', + 'A job can only have one override per recipe node.', + ) diff --git a/fusion_plating/fusion_plating_jobs/security/ir.model.access.csv b/fusion_plating/fusion_plating_jobs/security/ir.model.access.csv index 97dd8b91..24535aee 100644 --- a/fusion_plating/fusion_plating_jobs/security/ir.model.access.csv +++ b/fusion_plating/fusion_plating_jobs/security/ir.model.access.csv @@ -1 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fp_job_node_override_operator,fp.job.node.override.operator,model_fp_job_node_override,fusion_plating.group_fusion_plating_operator,1,0,0,0 +access_fp_job_node_override_supervisor,fp.job.node.override.supervisor,model_fp_job_node_override,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 +access_fp_job_node_override_manager,fp.job.node.override.manager,model_fp_job_node_override,fusion_plating.group_fusion_plating_manager,1,1,1,1 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 bc0ebbdc..ef3ad7bc 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 @@ -98,3 +98,59 @@ class TestFpJobExtensions(TransactionCase): self.assertEqual(job.customer_spec_id, spec) self.assertEqual(job.portal_job_id, portal) self.assertEqual(job.delivery_id, delivery) + + +class TestFpJobNodeOverride(TransactionCase): + def setUp(self): + super().setUp() + self.partner = self.env['res.partner'].create({'name': 'C'}) + self.product = self.env['product.product'].create({'name': 'W'}) + self.job = self.env['fp.job'].create({ + 'partner_id': self.partner.id, + 'product_id': self.product.id, + 'qty': 1.0, + }) + # Create a recipe + opt-in node + self.recipe = self.env['fusion.plating.process.node'].create({ + 'name': 'TestRecipe', + 'node_type': 'recipe', + }) + self.opt_in_node = self.env['fusion.plating.process.node'].create({ + 'name': 'OptInOp', + 'node_type': 'operation', + 'parent_id': self.recipe.id, + 'opt_in_out': 'opt_in', + }) + + def test_create_override(self): + ovr = self.env['fp.job.node.override'].create({ + 'job_id': self.job.id, + 'node_id': self.opt_in_node.id, + 'included': True, + }) + self.assertEqual(ovr.job_id, self.job) + self.assertTrue(ovr.included) + + def test_unique_constraint(self): + from psycopg2 import IntegrityError + from odoo.tools import mute_logger + self.env['fp.job.node.override'].create({ + 'job_id': self.job.id, + 'node_id': self.opt_in_node.id, + 'included': True, + }) + with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): + with self.env.cr.savepoint(): + self.env['fp.job.node.override'].create({ + 'job_id': self.job.id, + 'node_id': self.opt_in_node.id, + 'included': False, + }) + + def test_override_ids_one2many(self): + ovr = self.env['fp.job.node.override'].create({ + 'job_id': self.job.id, + 'node_id': self.opt_in_node.id, + }) + self.job.invalidate_recordset(['override_ids']) + self.assertIn(ovr, self.job.override_ids)