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 — MRP Bridge",
'version': '19.0.11.0.0',
'version': '19.0.12.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """
@@ -42,6 +42,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'fusion_plating_shopfloor',
'fusion_plating_configurator',
'fusion_plating_certificates',
'fusion_plating_receiving',
'hr',
# hr_attendance gives us the standard hr.attendance model
# (check_in / check_out). fusion_clock builds on the same model

View File

@@ -797,6 +797,45 @@ class MrpProduction(models.Model):
recipe.name, recipe.default_lead_time),
)
# ------------------------------------------------------------------
# Sub 8 — Auto-create a racking inspection alongside every new MO,
# regardless of how the MO came into being (bridge_mrp auto-create,
# Odoo sale_mrp procurement, manual create). One row per MO via the
# unique SQL constraint on fp.racking.inspection.production_id.
# ------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
mos = super().create(vals_list)
Insp = self.env.get('fp.racking.inspection')
if Insp is None:
return mos
for mo in mos:
if Insp.search_count([('production_id', '=', mo.id)]):
continue
so_lines = mo.x_fc_sale_order_line_ids if (
'x_fc_sale_order_line_ids' in mo._fields
) else self.env['sale.order.line']
if so_lines:
insp_lines = [
(0, 0, {
'part_catalog_id': ln.x_fc_part_catalog_id.id
if ln.x_fc_part_catalog_id else False,
'qty_expected': int(ln.product_uom_qty or 0),
'condition': 'ok',
})
for ln in so_lines
]
else:
insp_lines = [(0, 0, {
'qty_expected': int(mo.product_qty or 0),
'condition': 'ok',
})]
Insp.sudo().create({
'production_id': mo.id,
'line_ids': insp_lines,
})
return mos
# ------------------------------------------------------------------
# GAP 2: SO confirm → MO confirm → auto-create Portal Job + WOs
# ------------------------------------------------------------------

View File

@@ -889,6 +889,10 @@ class MrpWorkorder(models.Model):
required field for traceability is filled in."""
self._fp_check_required_fields_before_start()
self._fp_check_operator_certification()
# Sub 8 — soft gate: block the first plating WO if the MO's
# racking inspection is still Draft or Inspecting. Non-manager
# operators get a clear error; Plating Managers override.
self._fp_warn_if_racking_inspection_pending()
res = super().button_start()
# Capture audit AFTER the super call so we don't stamp WOs that
# the cert gate (or any other downstream check) rejected.
@@ -931,6 +935,40 @@ class MrpWorkorder(models.Model):
'Request certification from your supervisor before starting this WO.'
) % (employee.name, process_type.name))
def _fp_warn_if_racking_inspection_pending(self):
"""Sub 8 — block first plating WO start if racking inspection is still
Draft or Inspecting.
Only applies to the first-sequence WO of an MO. Later WOs assume
inspection was cleared earlier. Plating Manager bypasses.
"""
from odoo.exceptions import UserError
if self.env.user.has_group('fusion_plating.group_fusion_plating_manager'):
return
Insp = self.env.get('fp.racking.inspection')
if Insp is None:
return
for wo in self:
mo = wo.production_id
if not mo:
continue
first_wo = mo.workorder_ids.sorted('sequence')[:1]
if wo != first_wo:
continue
inspection = Insp.search(
[('production_id', '=', mo.id)], limit=1,
)
if not inspection or inspection.state in ('done', 'discrepancy_flagged'):
continue
state_label = dict(
inspection._fields['state'].selection
).get(inspection.state, inspection.state)
raise UserError(_(
'Racking inspection for MO %(mo)s is still "%(st)s". '
'Complete the inspection (or ask a Plating Manager to '
'override) before starting the first plating work order.'
) % {'mo': mo.name, 'st': state_label})
def _fp_check_required_fields_before_finish(self):
"""Block button_finish on:

View File

@@ -241,6 +241,11 @@ class SaleOrder(models.Model):
mo_vals['x_fc_revision_snapshot'] = primary.x_fc_revision_snapshot
mo = Production.create(mo_vals)
created.append((mo, tag, len(lines)))
# Sub 8 — the racking inspection is auto-created by
# mrp.production.create() (see mrp_production.py), so
# no extra work here. The hook there picks up the
# x_fc_sale_order_line_ids written above to seed the
# inspection lines correctly.
self.env.cr.execute('RELEASE SAVEPOINT %s' % savepoint_name)
except Exception as exc:
self.env.cr.execute('ROLLBACK TO SAVEPOINT %s' % savepoint_name)