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

@@ -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>