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