diff --git a/fusion_plating/docs/superpowers/specs/2026-04-22-sub8-receiving-inspection-design.md b/fusion_plating/docs/superpowers/specs/2026-04-22-sub8-receiving-inspection-design.md new file mode 100644 index 00000000..d2c20c93 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-04-22-sub8-receiving-inspection-design.md @@ -0,0 +1,349 @@ +# 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.*