feat(bridge_mrp): recipe-to-workorder generation on MO confirm (v19.0.2.1.0)

Add _generate_workorders_from_recipe() which walks the recipe tree,
creates one mrp.workorder per operation node, and formats child step
nodes as plain-text WO instructions. Respects opt-in/out overrides
from the per-job configuration wizard. Called automatically at the end
of action_confirm() after portal job creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 20:01:31 -04:00
parent 24656cc02b
commit ab99aaa5da
2 changed files with 117 additions and 3 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — MRP Bridge',
'version': '19.0.1.0.0',
'version': '19.0.2.1.0',
'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """

View File

@@ -3,9 +3,13 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import api, fields, models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class MrpProduction(models.Model):
"""Extend manufacturing order with Fusion Plating references and
@@ -68,10 +72,116 @@ class MrpProduction(models.Model):
}
# ------------------------------------------------------------------
# GAP 2: SO confirm → MO confirm → auto-create Portal Job
# Recipe → Work Order generation
# ------------------------------------------------------------------
def _generate_workorders_from_recipe(self):
"""Generate mrp.workorder records from the assigned recipe.
Walks the recipe tree, creates one WO per 'operation' node,
and formats child 'step' nodes as WO instructions.
Respects opt-in/out overrides from x_fc_override_ids.
"""
WorkOrder = self.env['mrp.workorder']
for production in self:
if not production.x_fc_recipe_id:
continue # No recipe assigned
if production.workorder_ids:
continue # WOs already exist — don't duplicate
# Build lookup of overrides keyed by node ID
override_map = {} # {node_id: included_bool}
for override in production.x_fc_override_ids:
override_map[override.node_id.id] = override.included
# Walk tree and collect operation WO values
wo_vals_list = []
seq_counter = [10] # mutable for closure, increments by 10
def _is_node_included(node):
"""Determine if a node should be included based on opt-in/out
logic and per-job overrides.
- disabled: always included (not configurable)
- opt_in: excluded by default, included only with override
- opt_out: included by default, excluded only with override
"""
nid = node.id
opt = node.opt_in_out or 'disabled'
if opt == 'disabled':
return True
if nid in override_map:
return override_map[nid]
# No override → use default
if opt == 'opt_in':
return False # Default excluded
# opt_out → default included
return True
def walk_node(node):
if not _is_node_included(node):
return
if node.node_type == 'operation':
# Map FP work centre → MRP work centre
mrp_wc = False
if node.work_center_id and node.work_center_id.x_fc_mrp_workcenter_id:
mrp_wc = node.work_center_id.x_fc_mrp_workcenter_id.id
if not mrp_wc:
_logger.warning(
'MO %s: operation "%s" has no mapped MRP work centre — '
'skipping WO creation.',
production.name, node.name,
)
# Still recurse into children for nested sub-operations
for child in node.child_ids.sorted('sequence'):
walk_node(child)
return
# Collect step instructions from child 'step' nodes
steps = []
step_num = 1
for child in node.child_ids.sorted('sequence'):
if child.node_type == 'step' and _is_node_included(child):
line = '%d. %s' % (step_num, child.name)
if child.estimated_duration:
line += ' (%.0f min)' % child.estimated_duration
steps.append(line)
step_num += 1
wo_vals_list.append({
'production_id': production.id,
'name': node.name,
'workcenter_id': mrp_wc,
'duration_expected': node.estimated_duration or 0,
'sequence': seq_counter[0],
'description': '\n'.join(steps) if steps else '',
})
seq_counter[0] += 10
elif node.node_type in ('recipe', 'sub_process'):
# Container nodes — recurse into children
for child in node.child_ids.sorted('sequence'):
walk_node(child)
# 'step' nodes at top level are handled by their parent operation
# Start walking from recipe root
walk_node(production.x_fc_recipe_id)
# Bulk create work orders
if wo_vals_list:
WorkOrder.create(wo_vals_list)
production.message_post(
body=_('%d work orders generated from recipe "%s".') % (
len(wo_vals_list), production.x_fc_recipe_id.name),
)
# ------------------------------------------------------------------
# GAP 2: SO confirm → MO confirm → auto-create Portal Job + WOs
# ------------------------------------------------------------------
def action_confirm(self):
"""Override to auto-create a portal job when the MO is confirmed."""
"""Override to auto-create a portal job and generate work orders
from the assigned recipe when the MO is confirmed.
"""
res = super().action_confirm()
PortalJob = self.env['fusion.plating.portal.job']
for mo in self:
@@ -102,6 +212,10 @@ class MrpProduction(models.Model):
'company_id': mo.company_id.id,
})
mo.x_fc_portal_job_id = job
# Generate work orders from recipe (after portal job creation)
self._generate_workorders_from_recipe()
return res
# ------------------------------------------------------------------