fusion_plating_bridge_mrp: per-job recipe overrides with config wizard (v19.0.2.0.0)

Links recipes to manufacturing orders via x_fc_recipe_id on mrp.production.
New model fusion.plating.job.node.override stores per-job opt-in/out
decisions for optional recipe steps.

Config wizard (fp.recipe.config.wizard) shows all optional nodes as a
checklist — opt-in steps default unchecked, opt-out steps default checked.
Planner toggles and confirms. "Overrides" stat button on MO form opens wizard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 15:36:03 -04:00
parent cb57585b5a
commit 4185b149bd
10 changed files with 319 additions and 1 deletions

View File

@@ -4,3 +4,4 @@
# Part of the Fusion Plating product family.
from . import models
from . import wizard

View File

@@ -47,6 +47,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
],
'data': [
'security/ir.model.access.csv',
'wizard/fp_recipe_config_wizard_views.xml',
'views/mrp_workcenter_views.xml',
'views/mrp_workorder_views.xml',
'views/mrp_production_views.xml',

View File

@@ -11,4 +11,5 @@ from . import fp_portal_job
from . import fp_quality_hold
from . import fp_delivery
from . import fp_batch
from . import fp_job_node_override
from . import account_move

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class FpJobNodeOverride(models.Model):
"""Per-job override for optional recipe steps.
When a recipe is assigned to a manufacturing order, nodes with
opt_in_out != 'disabled' can be toggled on or off for that specific
job. Opt-in nodes default to excluded; opt-out nodes default to
included. The planner changes these via the configuration wizard.
"""
_name = 'fusion.plating.job.node.override'
_description = 'Fusion Plating — Job Node Override'
_order = 'node_sequence, id'
production_id = fields.Many2one(
'mrp.production',
string='Manufacturing Order',
required=True,
ondelete='cascade',
index=True,
)
node_id = fields.Many2one(
'fusion.plating.process.node',
string='Recipe Step',
required=True,
ondelete='cascade',
)
node_name = fields.Char(
related='node_id.name',
string='Step Name',
readonly=True,
)
node_type = fields.Selection(
related='node_id.node_type',
string='Type',
readonly=True,
)
node_sequence = fields.Integer(
related='node_id.sequence',
string='Sequence',
readonly=True,
store=True,
)
opt_in_out = fields.Selection(
related='node_id.opt_in_out',
string='Default',
readonly=True,
)
included = fields.Boolean(
string='Included',
default=True,
help='Whether this optional step is active for this job.',
)
_sql_constraints = [
('unique_production_node',
'unique(production_id, node_id)',
'Each recipe step can only have one override per job.'),
]

View File

@@ -3,7 +3,8 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class MrpProduction(models.Model):
@@ -27,6 +28,44 @@ class MrpProduction(models.Model):
string='Portal Job',
help='The portal job linked to this manufacturing order.',
)
x_fc_recipe_id = fields.Many2one(
'fusion.plating.process.node',
string='Recipe',
domain=[('node_type', '=', 'recipe')],
help='Process recipe template for this manufacturing order.',
tracking=True,
)
x_fc_override_ids = fields.One2many(
'fusion.plating.job.node.override',
'production_id',
string='Recipe Overrides',
)
x_fc_override_count = fields.Integer(
string='Overrides',
compute='_compute_override_count',
)
@api.depends('x_fc_override_ids')
def _compute_override_count(self):
for rec in self:
rec.x_fc_override_count = len(rec.x_fc_override_ids)
def action_configure_recipe_steps(self):
"""Open the wizard to configure opt-in/out steps for this job."""
self.ensure_one()
if not self.x_fc_recipe_id:
raise UserError(_('Please select a recipe first.'))
return {
'type': 'ir.actions.act_window',
'name': f'Configure Steps — {self.x_fc_recipe_id.name}',
'res_model': 'fp.recipe.config.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_production_id': self.id,
'default_recipe_id': self.x_fc_recipe_id.id,
},
}
# ------------------------------------------------------------------
# GAP 2: SO confirm → MO confirm → auto-create Portal Job

View File

@@ -5,3 +5,10 @@ access_fp_bridge_mrp_workorder_manager,fp.bridge.mrp.workorder.manager,mrp_worko
access_fp_bridge_mrp_workorder_supervisor,fp.bridge.mrp.workorder.supervisor,mrp_workorder.model_mrp_workorder,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
access_fp_bridge_mrp_production_manager,fp.bridge.mrp.production.manager,mrp.model_mrp_production,fusion_plating.group_fusion_plating_manager,1,1,1,0
access_fp_bridge_mrp_production_supervisor,fp.bridge.mrp.production.supervisor,mrp.model_mrp_production,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
access_fp_job_node_override_operator,fp.job.node.override.operator,model_fusion_plating_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_fusion_plating_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_fusion_plating_job_node_override,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_recipe_config_wizard_supervisor,fp.recipe.config.wizard.supervisor,model_fp_recipe_config_wizard,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_recipe_config_wizard_manager,fp.recipe.config.wizard.manager,model_fp_recipe_config_wizard,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_recipe_config_wizard_line_supervisor,fp.recipe.config.wizard.line.supervisor,model_fp_recipe_config_wizard_line,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_recipe_config_wizard_line_manager,fp.recipe.config.wizard.line.manager,model_fp_recipe_config_wizard_line,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 access_fp_bridge_mrp_workorder_supervisor fp.bridge.mrp.workorder.supervisor mrp_workorder.model_mrp_workorder fusion_plating.group_fusion_plating_supervisor 1 0 0 0
6 access_fp_bridge_mrp_production_manager fp.bridge.mrp.production.manager mrp.model_mrp_production fusion_plating.group_fusion_plating_manager 1 1 1 0
7 access_fp_bridge_mrp_production_supervisor fp.bridge.mrp.production.supervisor mrp.model_mrp_production fusion_plating.group_fusion_plating_supervisor 1 0 0 0
8 access_fp_job_node_override_operator fp.job.node.override.operator model_fusion_plating_job_node_override fusion_plating.group_fusion_plating_operator 1 0 0 0
9 access_fp_job_node_override_supervisor fp.job.node.override.supervisor model_fusion_plating_job_node_override fusion_plating.group_fusion_plating_supervisor 1 1 1 0
10 access_fp_job_node_override_manager fp.job.node.override.manager model_fusion_plating_job_node_override fusion_plating.group_fusion_plating_manager 1 1 1 1
11 access_fp_recipe_config_wizard_supervisor fp.recipe.config.wizard.supervisor model_fp_recipe_config_wizard fusion_plating.group_fusion_plating_supervisor 1 1 1 0
12 access_fp_recipe_config_wizard_manager fp.recipe.config.wizard.manager model_fp_recipe_config_wizard fusion_plating.group_fusion_plating_manager 1 1 1 1
13 access_fp_recipe_config_wizard_line_supervisor fp.recipe.config.wizard.line.supervisor model_fp_recipe_config_wizard_line fusion_plating.group_fusion_plating_supervisor 1 1 1 0
14 access_fp_recipe_config_wizard_line_manager fp.recipe.config.wizard.line.manager model_fp_recipe_config_wizard_line fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -21,10 +21,20 @@
</group>
<group>
<field name="x_fc_portal_job_id"/>
<field name="x_fc_recipe_id"/>
</group>
</group>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_configure_recipe_steps" type="object"
class="oe_stat_button" icon="fa-sliders"
invisible="not x_fc_recipe_id">
<field name="x_fc_override_count" widget="statinfo"
string="Overrides"/>
</button>
</xpath>
</field>
</record>

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import fp_recipe_config_wizard

View File

@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpRecipeConfigWizard(models.TransientModel):
"""Wizard to configure which optional recipe steps are included
for a specific manufacturing order.
Shows all nodes where opt_in_out != 'disabled' as a checklist.
Opt-in nodes default unchecked (skipped), opt-out nodes default
checked (included). On confirm, creates or updates override records.
"""
_name = 'fp.recipe.config.wizard'
_description = 'Configure Recipe Steps'
production_id = fields.Many2one(
'mrp.production',
string='Manufacturing Order',
required=True,
)
recipe_id = fields.Many2one(
'fusion.plating.process.node',
string='Recipe',
required=True,
)
line_ids = fields.One2many(
'fp.recipe.config.wizard.line',
'wizard_id',
string='Optional Steps',
)
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
production_id = res.get('production_id') or self.env.context.get('default_production_id')
recipe_id = res.get('recipe_id') or self.env.context.get('default_recipe_id')
if not production_id or not recipe_id:
return res
production = self.env['mrp.production'].browse(production_id)
recipe = self.env['fusion.plating.process.node'].browse(recipe_id)
# Collect all optional nodes (recursive)
optional_nodes = self._get_optional_nodes(recipe)
if not optional_nodes:
return res
# Check for existing overrides
existing = {
ov.node_id.id: ov.included
for ov in production.x_fc_override_ids
}
lines = []
for node in optional_nodes:
if node.id in existing:
included = existing[node.id]
else:
# Default: opt-in → False (skipped), opt-out → True (included)
included = node.opt_in_out == 'opt_out'
lines.append((0, 0, {
'node_id': node.id,
'included': included,
}))
res['line_ids'] = lines
return res
def _get_optional_nodes(self, node):
"""Recursively collect all nodes with opt_in_out != 'disabled'."""
result = []
if node.opt_in_out and node.opt_in_out != 'disabled':
result.append(node)
for child in node.child_ids.sorted('sequence'):
result.extend(self._get_optional_nodes(child))
return result
def action_confirm(self):
"""Save overrides and close wizard."""
self.ensure_one()
Override = self.env['fusion.plating.job.node.override']
production = self.production_id
# Delete existing overrides for this MO and recreate
production.x_fc_override_ids.unlink()
for line in self.line_ids:
Override.create({
'production_id': production.id,
'node_id': line.node_id.id,
'included': line.included,
})
return {'type': 'ir.actions.act_window_close'}
class FpRecipeConfigWizardLine(models.TransientModel):
"""One line in the recipe config wizard — an optional step."""
_name = 'fp.recipe.config.wizard.line'
_description = 'Recipe Config Wizard Line'
_order = 'node_sequence, id'
wizard_id = fields.Many2one(
'fp.recipe.config.wizard',
string='Wizard',
required=True,
ondelete='cascade',
)
node_id = fields.Many2one(
'fusion.plating.process.node',
string='Step',
required=True,
)
node_name = fields.Char(
related='node_id.name',
string='Step Name',
readonly=True,
)
node_type = fields.Selection(
related='node_id.node_type',
string='Type',
readonly=True,
)
node_sequence = fields.Integer(
related='node_id.sequence',
string='Seq',
readonly=True,
store=True,
)
opt_in_out = fields.Selection(
related='node_id.opt_in_out',
string='Default',
readonly=True,
)
included = fields.Boolean(
string='Include in Job',
default=True,
)

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
-->
<odoo>
<record id="view_fp_recipe_config_wizard_form" model="ir.ui.view">
<field name="name">fp.recipe.config.wizard.form</field>
<field name="model">fp.recipe.config.wizard</field>
<field name="arch" type="xml">
<form string="Configure Recipe Steps">
<group>
<field name="production_id" readonly="True"/>
<field name="recipe_id" readonly="True"/>
</group>
<separator string="Optional Steps"/>
<p class="text-muted">
Toggle which optional steps are included for this job.
<strong>Opt-In</strong> steps are skipped by default — check to include.
<strong>Opt-Out</strong> steps are included by default — uncheck to skip.
</p>
<field name="line_ids">
<list editable="bottom" no_open="True">
<field name="node_name" string="Step"/>
<field name="node_type" widget="badge"
decoration-success="node_type == 'operation'"
decoration-warning="node_type == 'sub_process'"
decoration-muted="node_type == 'step'"/>
<field name="opt_in_out" widget="badge"
decoration-info="opt_in_out == 'opt_in'"
decoration-warning="opt_in_out == 'opt_out'"/>
<field name="included" widget="boolean_toggle"/>
<field name="node_id" column_invisible="True"/>
<field name="node_sequence" column_invisible="True"/>
</list>
</field>
<footer>
<button name="action_confirm" type="object"
string="Confirm" class="btn-primary"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>