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',
|
'name': 'Fusion Plating — MRP Bridge',
|
||||||
'version': '19.0.1.0.0',
|
'version': '19.0.2.1.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
|
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MrpProduction(models.Model):
|
class MrpProduction(models.Model):
|
||||||
"""Extend manufacturing order with Fusion Plating references and
|
"""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):
|
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()
|
res = super().action_confirm()
|
||||||
PortalJob = self.env['fusion.plating.portal.job']
|
PortalJob = self.env['fusion.plating.portal.job']
|
||||||
for mo in self:
|
for mo in self:
|
||||||
@@ -102,6 +212,10 @@ class MrpProduction(models.Model):
|
|||||||
'company_id': mo.company_id.id,
|
'company_id': mo.company_id.id,
|
||||||
})
|
})
|
||||||
mo.x_fc_portal_job_id = job
|
mo.x_fc_portal_job_id = job
|
||||||
|
|
||||||
|
# Generate work orders from recipe (after portal job creation)
|
||||||
|
self._generate_workorders_from_recipe()
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user