diff --git a/fusion-plating/fusion_plating_bridge_mrp/__init__.py b/fusion-plating/fusion_plating_bridge_mrp/__init__.py index 3c90fa80..6179cf28 100644 --- a/fusion-plating/fusion_plating_bridge_mrp/__init__.py +++ b/fusion-plating/fusion_plating_bridge_mrp/__init__.py @@ -4,3 +4,4 @@ # Part of the Fusion Plating product family. from . import models +from . import wizard diff --git a/fusion-plating/fusion_plating_bridge_mrp/__manifest__.py b/fusion-plating/fusion_plating_bridge_mrp/__manifest__.py index 64f6c32a..b2ba84b4 100644 --- a/fusion-plating/fusion_plating_bridge_mrp/__manifest__.py +++ b/fusion-plating/fusion_plating_bridge_mrp/__manifest__.py @@ -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', diff --git a/fusion-plating/fusion_plating_bridge_mrp/models/__init__.py b/fusion-plating/fusion_plating_bridge_mrp/models/__init__.py index 9e2bcff9..d2cb12b1 100644 --- a/fusion-plating/fusion_plating_bridge_mrp/models/__init__.py +++ b/fusion-plating/fusion_plating_bridge_mrp/models/__init__.py @@ -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 diff --git a/fusion-plating/fusion_plating_bridge_mrp/models/fp_job_node_override.py b/fusion-plating/fusion_plating_bridge_mrp/models/fp_job_node_override.py new file mode 100644 index 00000000..855831f3 --- /dev/null +++ b/fusion-plating/fusion_plating_bridge_mrp/models/fp_job_node_override.py @@ -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.'), + ] diff --git a/fusion-plating/fusion_plating_bridge_mrp/models/mrp_production.py b/fusion-plating/fusion_plating_bridge_mrp/models/mrp_production.py index 7adc1b2c..3ee328af 100644 --- a/fusion-plating/fusion_plating_bridge_mrp/models/mrp_production.py +++ b/fusion-plating/fusion_plating_bridge_mrp/models/mrp_production.py @@ -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 diff --git a/fusion-plating/fusion_plating_bridge_mrp/security/ir.model.access.csv b/fusion-plating/fusion_plating_bridge_mrp/security/ir.model.access.csv index 2911921f..b23822a3 100644 --- a/fusion-plating/fusion_plating_bridge_mrp/security/ir.model.access.csv +++ b/fusion-plating/fusion_plating_bridge_mrp/security/ir.model.access.csv @@ -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 diff --git a/fusion-plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml b/fusion-plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml index f2966614..ea0baf71 100644 --- a/fusion-plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml +++ b/fusion-plating/fusion_plating_bridge_mrp/views/mrp_production_views.xml @@ -21,10 +21,20 @@ + + + + + + + diff --git a/fusion-plating/fusion_plating_bridge_mrp/wizard/__init__.py b/fusion-plating/fusion_plating_bridge_mrp/wizard/__init__.py new file mode 100644 index 00000000..588768fa --- /dev/null +++ b/fusion-plating/fusion_plating_bridge_mrp/wizard/__init__.py @@ -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 diff --git a/fusion-plating/fusion_plating_bridge_mrp/wizard/fp_recipe_config_wizard.py b/fusion-plating/fusion_plating_bridge_mrp/wizard/fp_recipe_config_wizard.py new file mode 100644 index 00000000..0c6e455e --- /dev/null +++ b/fusion-plating/fusion_plating_bridge_mrp/wizard/fp_recipe_config_wizard.py @@ -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, + ) diff --git a/fusion-plating/fusion_plating_bridge_mrp/wizard/fp_recipe_config_wizard_views.xml b/fusion-plating/fusion_plating_bridge_mrp/wizard/fp_recipe_config_wizard_views.xml new file mode 100644 index 00000000..3c53c1f0 --- /dev/null +++ b/fusion-plating/fusion_plating_bridge_mrp/wizard/fp_recipe_config_wizard_views.xml @@ -0,0 +1,47 @@ + + + + + + fp.recipe.config.wizard.form + fp.recipe.config.wizard + + + + + + + + + Toggle which optional steps are included for this job. + Opt-In steps are skipped by default — check to include. + Opt-Out steps are included by default — uncheck to skip. + + + + + + + + + + + + + + + + +
+ Toggle which optional steps are included for this job. + Opt-In steps are skipped by default — check to include. + Opt-Out steps are included by default — uncheck to skip. +