docs(receiving): Sub 8 design spec — split receiving vs inspection + box parity

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>
This commit is contained in:
gsinghpal
2026-04-23 00:21:02 -04:00
parent 0342535b9f
commit 392359d2c4

View File

@@ -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.*