From 1d0d4afdbffbf405a33b9c947c00a3814702422b Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 27 May 2026 00:11:25 -0400 Subject: [PATCH] fix(jobs): override_map always wins in _is_node_included + 2 wiring fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three cascading bugs caused DOD-00153/WO-30061 to confirm with zero steps (and DOD-00150 to keep masking/bake even with overrides): 1. _is_node_included() in fp_job._generate_steps_from_recipe consulted the per-job override_map ONLY when node.opt_in_out was 'opt_in' or 'opt_out'. Default is 'disabled' (mandatory), so overrides on mandatory recipe nodes (Masking, De-Masking, Oven baking) were silently ignored. Fix: consult override_map FIRST — explicit per-job override always wins, regardless of node's opt_in_out value. 2. fp.direct.order.line.recipe_choice_ids didn't include the wizard's material_process recipe (Express Orders order-level recipe), so the line's process_variant_id domain rejected propagation. Added a 4th tier to the compute that pulls the order's header recipe in. 3. sale_order._fp_resolve_recipe_for_line fell back from line picker to part.default_process_id with nothing between. Added Express header recipe (self.x_fc_material_process) as a 2nd-priority fallback — catches cases where G3 propagation failed to reach the line but the SO header has the recipe set. Also fixed an unrelated G4 bug: _FP_PART_SYNC_FIELDS mapped process_variant_id → 'default_process_variant_id' which doesn't exist. Real field is 'default_process_id' (singular). Cleaned up DOD-00153/WO-30061 manually: backfilled line + job.recipe_id, regenerated steps with overrides respected. 8 steps now visible, masking/bake correctly omitted. --- .../wizard/fp_direct_order_line.py | 12 ++++++++++-- .../fusion_plating_jobs/models/fp_job.py | 12 ++++++++++-- .../fusion_plating_jobs/models/sale_order.py | 17 +++++++++++------ 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py index 666e04e0..488c27fe 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py @@ -81,7 +81,8 @@ class FpDirectOrderLine(models.Model): string='Allowed Recipes (computed)', ) - @api.depends('part_catalog_id', 'wizard_id.partner_id') + @api.depends('part_catalog_id', 'wizard_id.partner_id', + 'wizard_id.material_process') def _compute_recipe_choice_ids(self): Node = self.env['fusion.plating.process.node'] SOL = self.env['sale.order.line'] @@ -107,6 +108,11 @@ class FpDirectOrderLine(models.Model): ], order='create_date desc', limit=500 ).mapped('x_fc_process_variant_id') ids.update(used.ids) + # 4) The wizard's order-level Material/Process recipe — must be + # selectable on the line so the G3 propagation can write it + # without the domain rejecting (2026-05-27 fix). + if rec.wizard_id and rec.wizard_id.material_process: + ids.add(rec.wizard_id.material_process.id) rec.recipe_choice_ids = [(6, 0, list(ids))] save_as_default_process = fields.Boolean( string='Set as Part Default', @@ -652,7 +658,9 @@ class FpDirectOrderLine(models.Model): 'bake_instructions': 'default_bake_instructions', 'thickness_range': 'x_fc_default_thickness_range', 'masking_enabled': 'default_masking_enabled', - 'process_variant_id': 'default_process_variant_id', + # Real field is default_process_id on fp.part.catalog (singular, + # not default_process_variant_id which doesn't exist). + 'process_variant_id': 'default_process_id', } def _fp_sync_to_part(self): diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index cb8b638d..b59d365e 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -1383,15 +1383,23 @@ class FpJob(models.Model): """Determine if a node should be included based on opt-in/out logic, per-job overrides, and start-at-node filter. + + Override map (per-job override rows) ALWAYS wins + regardless of the node's opt_in_out setting. This is the + escape hatch the Express Orders flow relies on to opt + out of 'disabled' (mandatory) nodes like masking + bake. + Without this, overrides on disabled nodes were silently + ignored. """ nid = node.id if allowed_ids is not None and nid not in allowed_ids: return False + # Explicit per-job override wins over any default behaviour. + if nid in override_map: + return override_map[nid] opt = node.opt_in_out or 'disabled' if opt == 'disabled': return True - if nid in override_map: - return override_map[nid] if opt == 'opt_in': return False # Default excluded return True # opt_out → default included diff --git a/fusion_plating/fusion_plating_jobs/models/sale_order.py b/fusion_plating/fusion_plating_jobs/models/sale_order.py index a44ade67..1454738c 100644 --- a/fusion_plating/fusion_plating_jobs/models/sale_order.py +++ b/fusion_plating/fusion_plating_jobs/models/sale_order.py @@ -361,15 +361,17 @@ class SaleOrder(models.Model): return super().unlink() def _fp_resolve_recipe_for_line(self, line): - """4-tier recipe resolution. Used BOTH for grouping (Task 6 - recipe-driven WO splits) AND for the per-job vals construction. + """Recipe resolution with Express-Orders SO header fallback. Priority (most-specific first): - 1. line.x_fc_process_variant_id — Sarah explicitly picked a - part-scoped variant on this order line. Always wins. - 2. part.default_process_id — part's flagged default + 1. line.x_fc_process_variant_id — explicit per-line variant + (always wins; this is where G3 propagation lands a value). + 2. self.x_fc_material_process — Express Orders order-level + recipe. Catches the case where G3 propagation failed to + reach the line but the header has the recipe. + 3. part.default_process_id — part's flagged default variant. Customer-and-part-tuned recipe. - 3. part.recipe_id — legacy fallback. + 4. part.recipe_id — legacy fallback. Returns the recipe record or an empty recordset. """ Node = self.env['fusion.plating.process.node'] @@ -384,6 +386,9 @@ class SaleOrder(models.Model): ) or False if picked: return picked + # Express Orders header recipe (2026-05-27 fallback) + if 'x_fc_material_process' in self._fields and self.x_fc_material_process: + return self.x_fc_material_process if part and 'default_process_id' in part._fields and part.default_process_id: return part.default_process_id if part and 'recipe_id' in part._fields and part.recipe_id: