From 838b41cb89ecf563bf9e1de06c0eeb83b2be003e Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 17 Apr 2026 02:18:08 -0400 Subject: [PATCH] fix(bridge_mrp): WO recipe generator + demo work-order backfill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bridge_mrp._generate_workorders_from_recipe was writing 'description' on mrp.workorder, which doesn't exist in Odoo 19 — instead the step instructions now post to each WO's chatter after bulk create, which is where the operator sees them anyway. Demo seeder now creates the full WO chain: - 9 MRP work centres paired with 9 FP work centres (FP-QUEUE, -RACK, -MASK, -EN, -BAKE, -INSP, -DERACK, -DEMASK, -POSTBAKE) with costs_hour set so actuals-vs-quoted margin can compute. - Wires the existing ENP-ALUM-BASIC recipe's 9 operation nodes to those FP work centres by matching names. - Links every coating config to the recipe so the auto-assign hook (mrp.production.action_confirm → _auto_assign_recipe_from_so) has something to pull. - Backfills work orders on all existing demo MOs: calls the generator once recipe is set. For historical (done) MOs, marks all their WOs done with backdated durations (25-90 min). For the Cyclone active MO, sets a realistic progression: first WO done, second in progress (priority: Hot), rest in 'ready'. Verified: 90 WOs live, 10 per work centre. One MO shows the full progression state mix. WO Traveller PDF renders (132KB) — both portrait + landscape variants still work. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/mrp_production.py | 18 ++- fusion_plating/scripts/fp_demo_seed.py | 135 ++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) 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 7c169dd7..81c5e32f 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py @@ -288,6 +288,7 @@ class MrpProduction(models.Model): # Walk tree and collect operation WO values wo_vals_list = [] + wo_steps = {} # {sequence: instruction text} — posted to WO chatter after create seq_counter = [10] # mutable for closure, increments by 10 def _is_node_included(node): @@ -341,14 +342,19 @@ class MrpProduction(models.Model): steps.append(line) step_num += 1 + # Odoo 19 mrp.workorder has no 'description' field; + # store step instructions on the operation via the + # existing `operation_id.note` path, or just log them + # to the WO chatter. 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 '', }) + if steps: + wo_steps[seq_counter[0]] = '\n'.join(steps) seq_counter[0] += 10 elif node.node_type in ('recipe', 'sub_process'): @@ -362,7 +368,15 @@ class MrpProduction(models.Model): # Bulk create work orders if wo_vals_list: - WorkOrder.create(wo_vals_list) + created_wos = WorkOrder.create(wo_vals_list) + # Post step instructions to each WO's chatter where present + for wo in created_wos: + steps_txt = wo_steps.get(wo.sequence) + if steps_txt: + wo.message_post( + body=_('Recipe steps:
%s
') % steps_txt, + subtype_xmlid='mail.mt_note', + ) production.message_post( body=_('%d work orders generated from recipe "%s".') % ( len(wo_vals_list), production.x_fc_recipe_id.name), diff --git a/fusion_plating/scripts/fp_demo_seed.py b/fusion_plating/scripts/fp_demo_seed.py index 6c06d7f8..934ee330 100644 --- a/fusion_plating/scripts/fp_demo_seed.py +++ b/fusion_plating/scripts/fp_demo_seed.py @@ -1080,6 +1080,141 @@ except Exception: LOG(f" Certifications seeded for {employee.name}") +# ============================================================ +# Phase 14.5: Work centres + recipe wiring + WO backfill +# ============================================================ +LOG("Phase 14.5: Work centres + recipe wiring") + +# Create MRP work centres matching the ENP-ALUM-BASIC recipe operations +# (names must match the node names for the auto-mapping to find them) +WC_DEFS = [ + ('Ready for processing', 'FP-QUEUE', 25.0), + ('Racking', 'FP-RACK', 35.0), + ('Masking', 'FP-MASK', 35.0), + ('E-Nickel Plating', 'FP-EN', 55.0), + ('Oven baking', 'FP-BAKE', 30.0), + ('Post-plate Inspection', 'FP-INSP', 45.0), + ('De-racking', 'FP-DERACK', 30.0), + ('De-Masking', 'FP-DEMASK', 30.0), + ('Oven bake (Post de-rack)', 'FP-POSTBAKE', 30.0), +] + +# Ensure one FP facility for mapping +if not facility: + facility = env['fusion.plating.facility'].search([], limit=1) + +# Create MRP work centres first +mrp_wc_by_code = {} +for name, code, rate in WC_DEFS: + mrp_wc = env['mrp.workcenter'].search([('code', '=', code)], limit=1) + if not mrp_wc: + mrp_wc = env['mrp.workcenter'].create({ + 'name': name, 'code': code, + 'costs_hour': rate, 'time_efficiency': 100.0, + }) + mrp_wc_by_code[code] = mrp_wc + +# Create matching FP work centres, linked to their MRP counterparts +fp_wc_by_name = {} +for name, code, _rate in WC_DEFS: + fp_wc = env['fusion.plating.work.center'].search([('code', '=', code)], limit=1) + if not fp_wc: + fp_wc = env['fusion.plating.work.center'].create({ + 'name': name, 'code': code, + 'facility_id': facility.id, + 'x_fc_mrp_workcenter_id': mrp_wc_by_code[code].id, + }) + else: + fp_wc.x_fc_mrp_workcenter_id = mrp_wc_by_code[code].id + fp_wc_by_name[name] = fp_wc + +LOG(f" {len(WC_DEFS)} MRP + FP work centres (paired)") + +# Wire recipe operation nodes to FP work centres by matching names +recipe_main = env['fusion.plating.process.node'].search( + [('node_type', '=', 'recipe')], limit=1, +) +wired = 0 +for op in env['fusion.plating.process.node'].search([('node_type', '=', 'operation')]): + if op.work_center_id: + continue + wc = fp_wc_by_name.get(op.name) + if wc: + op.work_center_id = wc.id + wired += 1 +LOG(f" Wired {wired} recipe operations to work centres (recipe: {recipe_main.name if recipe_main else 'none'})") + +# Link every coating config to the main recipe +if recipe_main: + for cfg in env['fp.coating.config'].search([('recipe_id', '=', False)]): + cfg.recipe_id = recipe_main.id + LOG(f" Linked coating configs → recipe") + + +# ============================================================ +# Phase 14.6: Backfill work orders on existing demo MOs +# ============================================================ +LOG("Phase 14.6: Backfill work orders on existing MOs") + +def _populate_workorders(mo): + """Assign recipe from SO coating config + generate WOs.""" + if not mo.x_fc_recipe_id and mo.origin: + so = env['sale.order'].search([('name', '=', mo.origin)], limit=1) + if so and so.x_fc_coating_config_id and so.x_fc_coating_config_id.recipe_id: + mo.x_fc_recipe_id = so.x_fc_coating_config_id.recipe_id.id + if mo.x_fc_recipe_id and not mo.workorder_ids: + mo._generate_workorders_from_recipe() + +wo_created = 0 +for mo in env['mrp.production'].search([('state', 'in', ['confirmed', 'progress', 'done'])]): + before = len(mo.workorder_ids) + _populate_workorders(mo) + wo_created += len(mo.workorder_ids) - before + +LOG(f" Generated {wo_created} work orders across existing MOs") + +# Drive work orders into a mix of states for the demo: +# - For done MOs: mark all WOs done with backdated durations +# - For one active MO (Cyclone): first WO done, second in progress, rest ready +# - Others: leave in pending +import random as _r +for mo in env['mrp.production'].search([('state', '=', 'done')]): + for wo in mo.workorder_ids: + if wo.state != 'done': + wo.write({ + 'state': 'done', + 'duration': _r.uniform(25, 90), + 'duration_expected': 45.0, + }) + +cyc_mo = env['mrp.production'].search( + [('origin', 'in', env['sale.order'].search( + [('x_fc_po_number', '=', 'FPDEMO-ACTIVE-CYCLONE')] + ).mapped('name'))], limit=1, +) +if cyc_mo and cyc_mo.workorder_ids: + wos = cyc_mo.workorder_ids.sorted('sequence') + for i, wo in enumerate(wos): + if i == 0: + wo.write({'state': 'done', 'duration': 42.0}) + elif i == 1: + wo.write({'state': 'progress', 'duration': 18.0, + 'x_fc_priority': '1'}) + elif i == 2: + wo.write({'state': 'ready'}) + else: + wo.write({'state': 'ready'}) + LOG(f" Cyclone MO {cyc_mo.name}: WO progression set (done / in progress / ready / pending)") + +# Give all work orders reasonable expected durations if blank +env.cr.execute( + "UPDATE mrp_workorder SET duration_expected = 45 WHERE duration_expected IS NULL OR duration_expected = 0" +) + +final_wo_count = env['mrp.workorder'].search_count([]) +LOG(f" TOTAL work orders now in DB: {final_wo_count}") + + # ============================================================ # Phase 15: Final commit # ============================================================