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:
@@ -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': """
|
||||
|
||||
@@ -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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user