Receiving simplifies to box-count-only (draft → counted → staged → closed). New fp.racking.inspection + line models capture the per-part inspection the racking crew does when they open the boxes, linked to MO not to receiving. Soft gate on first WO start when the inspection is still pending (manager override). fp.delivery gains box_count_out; action_mark_delivered posts a non-blocking chatter warning when boxes out ≠ boxes in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
Sub 8 — Receiving / Inspection / QC Flow Restructure
Date: 2026-04-22
Module scope: fusion_plating_receiving (owns both models), fusion_plating_logistics
(box-parity check on delivery), fusion_plating_bridge_mrp (MO → inspection wiring).
Status: Design approved; implementing this session.
Predecessor context: Fine-Tuning Initiative, entry in fusion_plating/CLAUDE.md (Sub 8
preview from client transcript E).
1. Scope
The current fp.receiving module conflates three distinct shop-floor activities:
| Activity | Who does it | When |
|---|---|---|
| Box count on arrival | Receiver | Moment the truck drops off |
| Part inspection | Racking crew | When boxes are opened to load racks |
| Return packing | Shipping crew | When the job is ready to go back |
Sub 8 separates the first two (receiving vs inspection) and adds a sanity check on the third (box parity in vs. out).
In scope
- Simplify
fp.receivingsemantics — "receiving" is now box count only. The state machine becomesdraft → counted → staged → closed. Part-level inspection fields stay on the record but are surfaced as read-only informational ("populated by the racking crew", computed from the linked inspection record). - New model
fp.racking.inspection— per-MO inspection record captured by the racking crew when they open the customer's boxes. Statedraft → inspecting → done | discrepancy_flagged. Child lines record per-part count + condition. - MO integration — on MO confirm, auto-create a draft
fp.racking.inspection. The MO form gains a smart button and a "Start Racking Inspection" action. - Soft gate on first WO start — when the shop operator clicks Start on the first
plating work order of an MO whose racking inspection is still
draftorinspecting, a confirmation dialog warns them. Plating Manager override available. - Box-parity check on
fp.delivery— newbox_count_outInteger field. Onaction_mark_delivered, if the linked receiving'sbox_count_indiffers, the delivery chatter posts a non-blocking warning ("Shipped 3 boxes, received 4 — verify consolidation was intended").
Out of scope
- Renaming
fp.receivingto anything else. The DB table keeps its name; the user-facing label remains "Receiving" — operators are trained on that word. - Physical box identifier tracking (barcodes, QR labels). Out-of-scope until the client has the hardware.
- Deleting the old
action_start_inspection/action_flag_discrepancy— kept as no-ops on the receiving model so any legacy button bindings don't explode. - Auto-creating NCRs from racking-inspection discrepancies. The racking inspection flags the problem; NCR creation remains a human decision via the existing Quality menu.
2. Data Model
2.1 fp.receiving state-machine simplification
state = fields.Selection([
('draft', 'Draft'),
('counted', 'Counted'), # receiver has counted the boxes
('staged', 'Staged'), # boxes are in the racking area
('closed', 'Closed'), # inspection complete; job has started
# Legacy values kept for DB compatibility — never written by new code
('inspecting', 'Inspecting (legacy)'),
('discrepancy', 'Discrepancy (legacy)'),
('resolved', 'Resolved (legacy)'),
('accepted', 'Accepted (legacy)'),
], default='draft', tracking=True, required=True)
New boolean on the receiving form: box_count_in (Integer) — replaces the former per-part
counting UI. Part-level qty_received on fp.receiving.line stays read-only,
auto-populated from the linked fp.racking.inspection.line.
2.2 fp.racking.inspection (new model)
class FpRackingInspection(models.Model):
_name = 'fp.racking.inspection'
_description = 'Racking-time Inspection'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc, id desc'
name = fields.Char(compute='_compute_name', store=True)
production_id = fields.Many2one('mrp.production', required=True, ondelete='cascade')
sale_order_id = fields.Many2one(related='production_id.sale_order_id', store=True)
partner_id = fields.Many2one(related='production_id.partner_id', store=True)
receiving_id = fields.Many2one('fp.receiving', string='Source Receiving',
compute='_compute_receiving_id', store=True)
state = fields.Selection([
('draft', 'Draft'),
('inspecting', 'Inspecting'),
('done', 'Done'),
('discrepancy_flagged', 'Discrepancy Flagged'),
], default='draft', required=True, tracking=True)
inspector_id = fields.Many2one('res.users', readonly=True)
inspection_started = fields.Datetime(readonly=True)
inspection_completed = fields.Datetime(readonly=True)
line_ids = fields.One2many('fp.racking.inspection.line',
'inspection_id', copy=True)
notes = fields.Text()
company_id = fields.Many2one('res.company', required=True,
default=lambda s: s.env.company)
2.3 fp.racking.inspection.line
class FpRackingInspectionLine(models.Model):
_name = 'fp.racking.inspection.line'
_description = 'Racking Inspection Line'
_order = 'inspection_id, sequence, id'
inspection_id = fields.Many2one('fp.racking.inspection', required=True,
ondelete='cascade')
sequence = fields.Integer(default=10)
part_catalog_id = fields.Many2one('fp.part.catalog')
part_number = fields.Char(related='part_catalog_id.part_number', store=True)
qty_expected = fields.Integer()
qty_found = fields.Integer()
qty_variance = fields.Integer(compute='_compute_qty_variance', store=True)
condition = fields.Selection([
('ok', 'OK'),
('minor', 'Minor Issue (scratches, oxidation)'),
('major', 'Major Issue (dented, bent)'),
('reject', 'Reject (unusable)'),
], default='ok')
notes = fields.Char()
@api.depends('qty_expected', 'qty_found')
def _compute_qty_variance(self):
for rec in self:
rec.qty_variance = (rec.qty_found or 0) - (rec.qty_expected or 0)
2.4 fp.delivery box-parity field
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.')
x_fc_box_count_in = fields.Integer(compute='_compute_box_count_in', store=False,
help='Box count from the originating receiving record (if linked).')
x_fc_box_parity_warning = fields.Char(compute='_compute_box_count_in')
3. Workflow
3.1 Auto-create racking inspection on MO confirm
In fusion_plating_bridge_mrp/models/sale_order.py, after an MO is created from an SO
line, also create a draft fp.racking.inspection linked to the MO with one
inspection line per SO line that feeds the MO (copying part_catalog_id and
qty_expected = product_uom_qty).
3.2 Soft gate on first WO start
On mrp.workorder.button_start:
def button_start(self):
for wo in self:
self._fp_warn_if_racking_inspection_pending()
return super().button_start()
def _fp_warn_if_racking_inspection_pending(self):
if self.env.user.has_group('fusion_plating.group_fusion_plating_manager'):
return # managers bypass
# Find first-sequence plating WO for this MO
mo = self.production_id
first_plating_wo = mo.workorder_ids.sorted('sequence')[:1]
if self != first_plating_wo:
return # only gates the first WO
inspection = self.env['fp.racking.inspection'].search(
[('production_id', '=', mo.id)], limit=1,
)
if inspection and inspection.state in ('draft', 'inspecting'):
raise UserError(_(
'Racking inspection for MO %s is still %s. Complete the '
'inspection (or ask a Plating Manager to override) before '
'starting the first plating work order.'
) % (mo.name, dict(inspection._fields['state'].selection)[inspection.state]))
3.3 Box-parity check on delivery
On fp.delivery.action_mark_delivered, after the existing notification dispatch:
def action_mark_delivered(self):
res = super().action_mark_delivered()
for rec in self:
rec._fp_check_box_parity()
return res
def _fp_check_box_parity(self):
recv = self.env['fp.receiving'].search([
('sale_order_id', '=', self._fp_resolve_sale_order().id),
], limit=1)
if not recv or not recv.box_count_in or not self.x_fc_box_count_out:
return # nothing to compare
if recv.box_count_in != self.x_fc_box_count_out:
self.message_post(body=_(
'Box parity check: shipped %(out)d boxes, received %(in)d. '
'Verify consolidation was intended.'
) % {'out': self.x_fc_box_count_out, 'in': recv.box_count_in})
Non-blocking — it posts to the chatter so the shipping supervisor sees it on the record but doesn't stop the workflow.
4. Views
4.1 fp.receiving form (simplified)
- Replace the current "inspection" section with a banner:
ℹ️ Receiving = box count only. Parts are inspected by the racking crew when boxes are opened. See the Racking Inspection section below for per-part detail.
- Add
box_count_infield to the header. - Keep
receiving_line_ids+damage_idstabs but demote the inspection-style fields toreadonly=Trueif populated by the racking crew.
4.2 fp.racking.inspection form + list + search
- Form: header with state bar (
draft → inspecting → done), body with part/qty/condition list, signature block (who, when), notes, chatter. - List: MO name, customer, part count, variance summary, state.
- Search: filter by
state(pending / in progress / complete / flagged), by partner. - Menu: Plating → Operations → Racking Inspection (between Batches and Chemistry Logs).
4.3 MO form smart button
- New smart button "Racking Inspection" → opens the linked inspection record.
- Badge decoration matching inspection state.
4.4 fp.delivery form
- Add
x_fc_box_count_outnext to existing delivery fields. - Show
x_fc_box_parity_warningas an info banner when populated.
5. Security
fp.racking.inspectionACL:- Operator: create + write (racking crew does the inspection)
- Supervisor: read/write/create/close
- Manager: full + override state transitions
- Admin: full + delete
fp.racking.inspection.linematches parent.
6. Defensive Measures
- Legacy state values preserved — old
inspecting/discrepancy/resolved/acceptedstay in the Selection so existing records don't raise on upgrade. - Inspection auto-create is idempotent — guard on
search_countfor an existing inspection before creating one, so re-confirming an MO doesn't pile up duplicates. - Soft gate (warning, not block) — first-WO-start raises
UserErrorfor non-managers but Plating Manager can override. No hard block on production. - Box parity is non-blocking — warnings only, never blocks shipping.
- No migration — new fields only; existing
fp.receivingrecords keep their state, visible under the "legacy" tags.
7. Testing
7.1 Racking inspection lifecycle
- Create SO → confirm → assert MO created → assert
fp.racking.inspectiondraft created. - Open inspection → state →
inspecting, recordinspector_id,inspection_started. - Add 2 lines, one OK, one
majorcondition → state →discrepancy_flagged. - Close inspection →
inspection_completedstamped.
7.2 First-WO soft gate
- MO with draft racking inspection → non-manager tries
button_starton first WO → UserError. - Same MO, manager tries → succeeds.
- Inspection marked done → non-manager succeeds.
7.3 Box parity
- Receiving with
box_count_in=4, delivery withx_fc_box_count_out=4→ no warning. - Same with
x_fc_box_count_out=3→ chatter message posted.
7.4 Legacy states
- Existing receiving record with
state='inspecting'loads without error. Form renders the "legacy" label.
8. File Manifest
fusion_plating_receiving/
├── __manifest__.py (version bump, data list)
├── models/
│ ├── __init__.py (+ fp_racking_inspection, _line)
│ ├── fp_receiving.py (state simplification, banner field)
│ ├── fp_racking_inspection.py NEW
│ └── fp_racking_inspection_line.py NEW
├── views/
│ ├── fp_receiving_views.xml (banner + box_count_in)
│ └── fp_racking_inspection_views.xml NEW
└── security/
└── ir.model.access.csv (+ inspection + line)
fusion_plating_logistics/
├── __manifest__.py (version bump)
├── models/
│ └── fp_delivery.py (+ box_count_out + _fp_check_box_parity)
└── views/
└── fp_delivery_views.xml (add box_count_out field)
fusion_plating_bridge_mrp/
├── __manifest__.py (version bump)
└── models/
├── sale_order.py (auto-create inspection on MO create)
└── mrp_workorder.py (soft gate on button_start)
Rough LOC: ~500 Python, ~200 XML.
9. Rollout
Update order on entech:
fusion_plating_receiving(new models + state update)fusion_plating_logistics(box parity)fusion_plating_bridge_mrp(MO wiring + WO gate)
Post-deploy verification:
- Menu: Plating → Operations → Racking Inspection appears.
- Confirm a new SO → racking inspection auto-created.
- Try to start a WO without completing the inspection → warning fires.
- Mark a delivery with mismatched box counts → chatter warning posted.
End of spec.