feat(plating): Sub 8 — split receiving vs inspection + box parity

fp.receiving simplifies to box-count-only (new primary state
machine: draft → counted → staged → closed). Legacy
inspecting/accepted/discrepancy/resolved states stay in the
Selection so existing records load without error but are surfaced
behind a manager-only toggle. New box_count_in field + banner
that tells the receiver "count boxes only — parts are inspected
by the racking crew."

New fp.racking.inspection + fp.racking.inspection.line models —
one record per MO, auto-created by mrp.production.create() with
one line per contributing SO line (qty_expected seeded, qty_found
+ condition filled in by the racking crew when they open the boxes).
State: draft → inspecting → done | discrepancy_flagged (flagged
when any line has a non-ok condition or qty variance). Reopen
restricted to Plating Manager.

WO soft gate: first plating WO button_start raises a UserError
when the MO's racking inspection is still Draft or Inspecting.
Plating Manager bypasses; later WOs are not gated.

fp.delivery gains x_fc_box_count_out. action_mark_delivered calls
_fp_check_box_parity which posts a non-blocking chatter warning
when boxes out ≠ boxes in (resolved via job_ref → MO.origin → SO
→ receiving). Warning only — never blocks shipping.

Menu entry: Plating → Operations → Racking Inspection.

Module version bumps:
  fusion_plating_receiving  → 19.0.3.0.0
  fusion_plating_logistics  → 19.0.3.0.0
  fusion_plating_bridge_mrp → 19.0.12.0.0 (+depends receiving)

Smoke on entech: 12/12 assertions pass (one gate test skipped —
MO had no WOs to test) including box-count state machine, inspection
auto-create, lifecycle, discrepancy flag, and box-parity chatter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-23 00:30:36 -04:00
parent 392359d2c4
commit 2bfabfe135
15 changed files with 808 additions and 25 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Logistics',
'version': '19.0.2.0.0',
'version': '19.0.3.0.0',
'category': 'Manufacturing/Plating',
'summary': (
'Pickup & delivery for plating shops: vehicle master, driver '

View File

@@ -74,6 +74,17 @@ class FpDelivery(models.Model):
x_fc_revision_snapshot = fields.Char(
string='Revision (snapshot)',
)
# ---- Sub 8 — box parity ------------------------------------------------
# Shipping crew packs returns into the SAME boxes the parts arrived in
# (client requirement). Receiving captures box_count_in; we capture
# box_count_out here. action_mark_delivered posts a non-blocking
# chatter warning if they don't match.
x_fc_box_count_out = fields.Integer(
string='Boxes Out',
help='Number of boxes the shipping crew packed for return. '
'Should match the box count captured at receiving.',
)
scheduled_date = fields.Datetime(
string='Scheduled Date',
tracking=True,
@@ -226,6 +237,48 @@ class FpDelivery(models.Model):
or 'Driver'),
to_party=rec.partner_id.display_name,
)
# Sub 8 — box-parity warning. Non-blocking; just posts to
# chatter so the shipping supervisor sees it on the record.
rec._fp_check_box_parity()
def _fp_check_box_parity(self):
"""Compare this delivery's boxes-out count to the boxes-in count
captured at receiving. Post a chatter warning if they differ.
Never blocks — shipping has already happened by the time this
fires. The warning is for audit + shipping-supervisor review.
"""
self.ensure_one()
if not self.x_fc_box_count_out:
return
Receiving = self.env.get('fp.receiving')
if Receiving is None:
return
# Resolve SO via job_ref → MO.origin → SO.name
so_name = False
if self.job_ref:
mo = self.env['mrp.production'].search(
[('name', '=', self.job_ref)], limit=1,
)
if mo and mo.origin:
so_name = mo.origin
if not so_name:
return
so = self.env['sale.order'].search(
[('name', '=', so_name)], limit=1,
)
if not so:
return
recv = Receiving.search(
[('sale_order_id', '=', so.id)], limit=1,
)
if not recv or not recv.box_count_in:
return
if recv.box_count_in != self.x_fc_box_count_out:
self.message_post(body=_(
'Box parity check: shipped %(out)d box(es), received '
'%(in)d. Verify consolidation was intended.'
) % {'out': self.x_fc_box_count_out, 'in': recv.box_count_in})
def action_mark_refused(self):
self.write({'state': 'refused'})