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: