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:
@@ -4,3 +4,4 @@
|
|||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
'wizard/fp_recipe_config_wizard_views.xml',
|
||||||
'views/mrp_workcenter_views.xml',
|
'views/mrp_workcenter_views.xml',
|
||||||
'views/mrp_workorder_views.xml',
|
'views/mrp_workorder_views.xml',
|
||||||
'views/mrp_production_views.xml',
|
'views/mrp_production_views.xml',
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ from . import fp_portal_job
|
|||||||
from . import fp_quality_hold
|
from . import fp_quality_hold
|
||||||
from . import fp_delivery
|
from . import fp_delivery
|
||||||
from . import fp_batch
|
from . import fp_batch
|
||||||
|
from . import fp_job_node_override
|
||||||
from . import account_move
|
from . import account_move
|
||||||
|
|||||||
@@ -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.'),
|
||||||
|
]
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# 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):
|
class MrpProduction(models.Model):
|
||||||
@@ -27,6 +28,44 @@ class MrpProduction(models.Model):
|
|||||||
string='Portal Job',
|
string='Portal Job',
|
||||||
help='The portal job linked to this manufacturing order.',
|
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
|
# GAP 2: SO confirm → MO confirm → auto-create Portal Job
|
||||||
|
|||||||
@@ -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_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_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_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
|
||||||
|
|||||||
|
@@ -21,10 +21,20 @@
|
|||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="x_fc_portal_job_id"/>
|
<field name="x_fc_portal_job_id"/>
|
||||||
|
<field name="x_fc_recipe_id"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</xpath>
|
</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>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user