fix(jobs): split fp.jobs by thickness + serial on SO confirm

The _fp_auto_create_job grouping key was (recipe, part, coating).
Lines that shared all three but differed in thickness (or serial)
silently collapsed into one fp.job — the second line's thickness/SN
was lost, and any downstream cert printed the first line's values
across both batches. Silent mis-attestation = compliance hole.

Extended the key tuple to (recipe, part, coating, thickness, serial).
Single-line SOs and same-(thickness, SN) multi-line SOs collapse
identically to before. Only lines that previously merged when they
shouldn't have now split into their own fp.jobs.

TDD via test_so_confirm_splits_by_thickness:
  - seeds the part with default_process_id so both lines hit the
    `if recipe:` branch (where the bug lived — the no_recipe branch
    already split correctly per line)
  - confirms 2 jobs after action_confirm with each carrying its
    own thickness via the linked SO line

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-13 07:57:56 -04:00
parent 135cbd3a5c
commit e4681a58c6
3 changed files with 93 additions and 11 deletions

View File

@@ -412,15 +412,13 @@ class SaleOrder(models.Model):
_logger.info('SO %s: no plating lines, skipping job creation.', self.name)
return
# Group by (recipe, part, coating). Lines that share ALL THREE
# collapse into one WO. Sharing only the recipe is not enough —
# the WO header captures part_id and coating_config_id from
# first_line, and downstream the CoC prints the WO header's
# part_number on the customer-facing cert. Bundling Part A +
# Part B under one WO because they happen to share a recipe
# would put Part A's number on a cert covering both, which is
# a compliance bug (silent mis-attestation).
# No-recipe lines get their own group each.
# Group by (recipe, part, coating, thickness, serial). Lines that
# share ALL FIVE collapse into one WO. Same compliance reasoning
# as part_id + coating_id: bundling lines with different thicknesses
# or different serials under one WO would carry the first line's
# values onto the cert + sticker — silent mis-attestation. Sub 5
# added thickness_id + serial_id; this extends the grouping logic
# to honour them. No-recipe lines still get their own group each.
groups = {}
unrecipe_idx = 0
for line in plating_lines:
@@ -433,8 +431,16 @@ class SaleOrder(models.Model):
'x_fc_coating_config_id' in line._fields
and line.x_fc_coating_config_id.id
) or False
thickness_id = (
'x_fc_thickness_id' in line._fields
and line.x_fc_thickness_id.id
) or False
serial_id = (
'x_fc_serial_id' in line._fields
and line.x_fc_serial_id.id
) or False
if recipe:
key = (recipe.id, part_id, coating_id)
key = (recipe.id, part_id, coating_id, thickness_id, serial_id)
else:
unrecipe_idx += 1
key = ('no_recipe', unrecipe_idx)