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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user