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