fix(jobs): override_map always wins in _is_node_included + 2 wiring fixes

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.
This commit is contained in:
gsinghpal
2026-05-27 00:11:25 -04:00
parent f5cee25299
commit 1d0d4afdbf
3 changed files with 31 additions and 10 deletions

View File

@@ -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):

View File

@@ -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

View File

@@ -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: