# 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 1. **Simplify `fp.receiving` semantics** — "receiving" is now box count only. The state machine becomes `draft → 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). 2. **New model `fp.racking.inspection`** — per-MO inspection record captured by the racking crew when they open the customer's boxes. State `draft → inspecting → done | discrepancy_flagged`. Child lines record per-part count + condition. 3. **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. 4. **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 `draft` or `inspecting`, a confirmation dialog warns them. Plating Manager override available. 5. **Box-parity check on `fp.delivery`** — new `box_count_out` Integer field. On `action_mark_delivered`, if the linked receiving's `box_count_in` differs, the delivery chatter posts a non-blocking warning ("Shipped 3 boxes, received 4 — verify consolidation was intended"). ### Out of scope - Renaming `fp.receiving` to 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 ```python 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) ```python 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` ```python 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 ```python 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`: ```python 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: ```python 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_in` field to the header. - Keep `receiving_line_ids` + `damage_ids` tabs but demote the inspection-style fields to `readonly=True` if 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_out` next to existing delivery fields. - Show `x_fc_box_parity_warning` as an info banner when populated. --- ## 5. Security - `fp.racking.inspection` ACL: - Operator: create + write (racking crew does the inspection) - Supervisor: read/write/create/close - Manager: full + override state transitions - Admin: full + delete - `fp.racking.inspection.line` matches parent. --- ## 6. Defensive Measures 1. **Legacy state values preserved** — old `inspecting/discrepancy/resolved/accepted` stay in the Selection so existing records don't raise on upgrade. 2. **Inspection auto-create is idempotent** — guard on `search_count` for an existing inspection before creating one, so re-confirming an MO doesn't pile up duplicates. 3. **Soft gate (warning, not block)** — first-WO-start raises `UserError` for non-managers but Plating Manager can override. No hard block on production. 4. **Box parity is non-blocking** — warnings only, never blocks shipping. 5. **No migration** — new fields only; existing `fp.receiving` records keep their state, visible under the "legacy" tags. --- ## 7. Testing ### 7.1 Racking inspection lifecycle - Create SO → confirm → assert MO created → assert `fp.racking.inspection` draft created. - Open inspection → state → `inspecting`, record `inspector_id`, `inspection_started`. - Add 2 lines, one OK, one `major` condition → state → `discrepancy_flagged`. - Close inspection → `inspection_completed` stamped. ### 7.2 First-WO soft gate - MO with draft racking inspection → non-manager tries `button_start` on 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 with `x_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: 1. `fusion_plating_receiving` (new models + state update) 2. `fusion_plating_logistics` (box parity) 3. `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.*