fix(receiving): propagate qty_received to fp.job + drop duplicate carrier field
Bug surfaced on WO-30043 (2026-05-20): operator walked every step
including a fully closed receiving record, then hit
"Quantity Received is blank — close the receiving record for
SO SO-30043 before completing this job." Receiving WAS closed.
Root cause: the 2026-05-18 cert-creation gate
(fp.job.button_mark_done) blocks on job.qty_received but nothing
populated it. fp.receiving carried the qty on its line records,
fp.job stayed at 0 indefinitely. Two disconnected records on the
same SO.
Fix: when fp.receiving._update_so_receiving_status runs (i.e. on
every state transition — counted / staged / closed / accepted /
resolved), also mirror each line's received_qty onto the matching
fp.job by (sale_order_id + part_catalog_id). Single-part SOs map
1-to-1; multi-part SOs spawn one job per line so the same join
still works.
Two defensive guards in the hook:
- Skip silently when fusion_plating_jobs not installed
(Job = env.get('fp.job') returns None).
- Skip silently when fp.job doesn't yet carry part_catalog_id /
qty_received (test scope, unusual install topology).
Drive-by during cleanup:
- fp_parent_numbered_mixin._fp_assign_parent_name: guard
so.x_fc_parent_number access with field-existence check. The
column lives in fusion_plating_jobs; downstream modules that
inherit the mixin (receiving) but don't depend on jobs were
hitting AttributeError on every fp.receiving.create at test
time. Falls through to the legacy sequence when the column
isn't there.
- fp_receiving_views.xml: legacy carrier_name Char field rendered
as a second carrier row labeled "Legacy Carrier" alongside the
proper x_fc_carrier_id M2O — operators saw two carrier fields
and got confused. Hide the legacy display (data stays in DB for
audit; migration 19.0.3.10.0 already matched it to a real
delivery.carrier).
Migration 19.0.3.19.0/post-migrate.py backfills qty_received from
closed receiving lines for any job stuck at 0 — fixes WO-30043
and two sibling jobs on entech.
Modules: fusion_plating 19.0.20.2.0, fusion_plating_receiving
19.0.3.19.0, fusion_plating_jobs 19.0.10.15.0.
All 19 tests green (TestCarrierFields 6, TestQtyReceivedPropagation 5
new, TestReceivingGate 8). Direct verification on entech: WO-30043
qty_received = 1, mark_done succeeds, delivery + cert auto-created.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -882,3 +882,61 @@ class FpReceiving(models.Model):
|
||||
rec.sale_order_id.x_fc_receiving_status = 'partial'
|
||||
elif rec.state == 'draft':
|
||||
rec.sale_order_id.x_fc_receiving_status = 'not_received'
|
||||
# Propagate the per-part qty onto the matching fp.job records
|
||||
# so the 2026-05-18 mark_done gate can see what was received.
|
||||
rec._update_job_qty_received()
|
||||
|
||||
def _update_job_qty_received(self):
|
||||
"""Push received qty from this receiving's lines onto fp.job.
|
||||
|
||||
The 2026-05-18 cert-creation gate (fp.job.button_mark_done)
|
||||
blocks completion until ``job.qty_received`` is non-zero, but
|
||||
nothing was writing the field — receiving and job were two
|
||||
disconnected records on the same SO. Operators completed
|
||||
receiving, then hit "Quantity Received is blank" with no
|
||||
obvious next step.
|
||||
|
||||
Match rule: one fp.job ←→ one fp.receiving.line within the same
|
||||
sale order, joined by ``part_catalog_id``. Multi-part SOs spawn
|
||||
one job per line + one receiving line per part, so this gives a
|
||||
1-to-1 mapping. Single-line SOs work too because the only line
|
||||
matches the only job.
|
||||
|
||||
Best-effort: if no job matches (e.g. receiving without a
|
||||
spawned job, or part-catalog mismatch), skip silently — the
|
||||
receiving record itself still has the qty for audit.
|
||||
"""
|
||||
Job = self.env.get('fp.job')
|
||||
if Job is None:
|
||||
return # fusion_plating_jobs not installed
|
||||
# Match criteria depend on fields owned by fusion_plating_jobs.
|
||||
# Bail out cleanly if the registry doesn't have them — the same
|
||||
# hook then becomes a no-op in any install topology that
|
||||
# doesn't ship the jobs module (and in test scope where the
|
||||
# field may not be materialised on fp.job yet).
|
||||
if 'sale_order_id' not in Job._fields \
|
||||
or 'part_catalog_id' not in Job._fields \
|
||||
or 'qty_received' not in Job._fields:
|
||||
return
|
||||
for rec in self:
|
||||
so = rec.sale_order_id
|
||||
if not so:
|
||||
continue
|
||||
for line in rec.line_ids:
|
||||
if not line.part_catalog_id:
|
||||
continue
|
||||
domain = [
|
||||
('sale_order_id', '=', so.id),
|
||||
('part_catalog_id', '=', line.part_catalog_id.id),
|
||||
]
|
||||
jobs = Job.sudo().search(domain)
|
||||
if not jobs:
|
||||
continue
|
||||
# Only sync the integer qty, don't touch state. Skip writes
|
||||
# when the value already matches so we don't churn chatter.
|
||||
qty = int(line.received_qty or 0)
|
||||
jobs_to_update = jobs.filtered(
|
||||
lambda j: (j.qty_received or 0) != qty
|
||||
)
|
||||
if jobs_to_update:
|
||||
jobs_to_update.sudo().write({'qty_received': qty})
|
||||
|
||||
Reference in New Issue
Block a user