feat(jobs): add fp.job.node.override for per-job opt-in/out decisions
Mirror of fusion.plating.job.node.override from bridge_mrp, but
bound to fp.job. bridge_mrp's version stays alive for legacy MO
flow during the migration. Both coexist.
Adds override_ids One2many to fp.job via _inherit, plus
unique(job_id, node_id) constraint.
Note: spec-suggested _sql_constraints syntax is deprecated in
Odoo 19 ("Model attribute '_sql_constraints' is no longer
supported, please define model.Constraint on the model"). Used
the new class-attribute form: _unique_job_node = models.Constraint(...).
Verified the UNIQUE index is created on the table.
3 new tests: create, uniqueness, one2many backref.
Manifest 19.0.1.1.1 -> 19.0.1.2.0.
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.1.1',
|
||||
'version': '19.0.1.2.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'description': """
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
# task-by-task in Tasks 2.2 onwards.
|
||||
|
||||
from . import fp_job
|
||||
from . import fp_job_node_override
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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.',
|
||||
)
|
||||
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user