docs(fusion_plating): racking multi-rack + WO grouping design spec & Phase 1 plan

Approved design for splitting a WO's parts across multiple racks + grouping
multiple WOs on one rack, plus the Phase 1 implementation plan (split +
independent movement). Phases 2 (grouping + Station screen) and 3 (Plant
Kanban rollup) are noted for follow-up plans.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-03 08:37:18 -04:00
parent 5424c785d9
commit acd1fc9f8f
2 changed files with 759 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
# Multi-Rack Splitting + Work-Order Grouping at Racking — Design
**Date:** 2026-06-03
**Status:** Approved (design sign-off 2026-06-03)
**Modules touched:** `fusion_plating` (core: rack-load models), `fusion_plating_jobs` (movement / partial-order integration), `fusion_plating_shopfloor` (UI surfaces + controllers), `fusion_plating_reports` (rack travel ticket reuse)
## 1. Problem / Goal
At the **Racking** step, operators load a job's parts onto physical racks before plating. Today a step links to exactly **one** rack (`fp.job.step.rack_id`, single Many2one) and there is **no model for partial parts-per-rack** or **multiple work orders sharing a rack**. Operators need to:
1. **Split a job across multiple racks.** Default: all parts on one rack. An **"+ Add Rack"** button divides the quantity equally (100 → 50/50 → 34/33/33 → 25×4…). The operator can then **manually override** any individual rack's quantity.
2. **Move racks independently** through the rest of the line (Plating → Baking → De-Racking) — partial-order flow, but rack-aware. The operator chooses which rack(s) advance.
3. **Group multiple work orders on one rack** when they run the **identical recipe + spec** (any customer), for line efficiency — e.g. WO-A (20 ENP parts) + WO-B (10 ENP parts) on one rack, processed together, then separated at De-Racking.
## 2. Locked Decisions (from brainstorm 2026-06-03)
| # | Decision |
|---|----------|
| D1 | **Rack movement = independent, operator's choice.** Each rack is its own trackable unit; it can move ahead on its own, or the operator can move several at once. |
| D2 | **Grouping eligibility = identical process + spec.** Only WOs with the same resolved recipe AND same coating spec / thickness target may share a rack. Different customers are allowed. Mismatched recipe/spec is **blocked**. |
| D3 | **Two UI surfaces.** (a) A per-WO **Racking panel** on the Job Workspace (the split case). (b) A dedicated **Racking Station** shop-floor screen listing all WOs at Racking, with split controls *and* cross-WO grouping. Both drive the same model + endpoints. |
| D4 | **Division remainder** goes to the first rack(s): `base = total // N`, the first `total % N` racks get `base + 1`. Total always equals the parts available. |
| D5 | **Capacity = soft warning.** Each rack shows `assigned / capacity`; over-capacity is an amber warning, never a hard block. |
| D6 | **Plant Kanban = one card per job** with a small **rack rollup** ("3 racks · 1 Baking, 2 Plating"). The job card sits in the column of its **least-advanced** rack-load (a WO isn't "done" until every rack clears). Per-rack detail lives on the Racking screen / a card drill-down — NOT as separate board cards. |
## 3. Data Model
### 3.1 `fp.rack.load` (new, in `fusion_plating`)
"Parts loaded on one physical rack." First-class, moves through the workflow independently.
| Field | Type | Notes |
|---|---|---|
| `name` | Char | Sequence `RACKLOAD/YYYY/NNNN` |
| `rack_id` | Many2one `fusion.plating.rack` | The physical rack |
| `line_ids` | One2many `fp.rack.load.line` (inverse `load_id`) | Per-WO allocation (1 line = single WO; 2+ = grouped) |
| `qty_total` | Integer (compute, stored) | `sum(line_ids.qty)` |
| `recipe_id` | Many2one (recipe ref) | The shared recipe (all lines must match) — for grouping eligibility + display |
| `spec_key` | Char (compute, stored) | Normalised spec/thickness signature used to enforce D2 grouping |
| `current_step_id` | Many2one `fp.job.step` | The step the rack-load is parked at (drives independent position) |
| `current_area_kind` | Char (compute, stored) | From `current_step_id.area_kind` — for the Plant Kanban column |
| `state` | Selection | `loading``loaded``running``unracked` (→ `cancelled`) |
| `tag_ids` | Many2many `fp.rack.tag` | Reuse existing rack tags (Rush / Hold for QC) |
| `company_id` | Many2one | Standard |
| chatter | mail.thread | Audit |
Constraints: a rack-load's `line_ids` must all share `recipe_id` + `spec_key` (D2); `qty_total` must be ≥ 1; `rack_id` unique among non-unracked loads (a physical rack holds one active load at a time).
### 3.2 `fp.rack.load.line` (new, in `fusion_plating`)
| Field | Type | Notes |
|---|---|---|
| `load_id` | Many2one `fp.rack.load`, required, ondelete cascade | |
| `job_id` | Many2one `fp.job`, required | The work order whose parts are on this rack |
| `qty` | Integer, required | Parts of this job on this rack |
| `part_catalog_id` | Many2one (related from job) | Display |
| `recipe_id` / `spec_key` | related/compute from job | Used to enforce D2 |
### 3.3 Job ↔ rack-load relationships (on `fp.job`, in `fusion_plating_jobs`)
- `rack_load_line_ids` (One2many to `fp.rack.load.line`) — all loads carrying this job's parts.
- `qty_racked` (compute) = sum of this job's load-line qtys — how many of the job's parts are on racks.
- `qty_unracked` (compute) = `qty_at_racking_step qty_racked` — parts not yet assigned to a rack (the "Unassigned" counter).
## 4. Division Math (the "+ Add Rack" behaviour)
- Default state: **1 rack-load, line.qty = full racking quantity**.
- **+ Add Rack** → create one more rack-load and **re-divide equally** across all current loads (D4): `base = total // N`; first `total % N` loads get `base + 1`. This overwrites all line qtys (the simple behaviour: "add 4th rack → divide by 4").
- **Divide Equally** button → same as above without adding a rack (re-balance current N).
- **Manual qty edit** on a rack → updates that load's line qty; the **Unassigned: N** counter recomputes (`total Σ assigned`). Manual edits persist until the next *Add Rack* / *Divide Equally*. Sum may not exceed `total` (validation). Sum < total is allowed (operator may rack in waves) and shown as Unassigned.
- **Remove Rack** → only when its load hasn't moved past Racking; its qty returns to Unassigned.
## 5. Independent Movement + Partial-Order Integration
- Movement reuses the existing **move log** `fp.job.step.move`. When a rack-load advances from step A → B, create **one move row per line** (per job): `from_step_id`, `to_step_id`, `qty_moved = line.qty`, `rack_id = load.rack_id`, `transfer_type = 'step'`. This keeps the existing `qty_at_step` partial-order compute correct and rack-aware.
- The rack-load's `current_step_id` is set to the destination on commit (explicit position for the independent-movement UI), and `state` flips `loaded → running`.
- The operator can move **one** load or **select several** to move together (D1). Reuse / extend the existing **Move Rack** tablet dialog (`move_rack_dialog.js` + `/fp/tablet/move_rack/*`) so a rack-load moves as a unit; the multi-select batch move is a thin wrapper.
- **De-Racking** = unrack. When a rack-load reaches the De-Racking step and is unracked: set `state = unracked`, free the physical rack (`rack.racking_state = 'empty'`), and each line's `qty` returns to **its own** job's downstream flow (inspection → cert → shipping). Grouped WOs separate cleanly here — each job continues with its own parts/qty.
## 6. Work-Order Grouping (D2)
- On the Racking Station screen, eligible WOs at Racking (same `recipe_id` + `spec_key`, any customer) can be **pulled onto a shared rack-load** → adds a `fp.rack.load.line` for the second job.
- Eligibility is enforced server-side: adding a line whose job's recipe/spec differs from the load's is rejected with a clear message.
- A grouped rack-load moves as one unit (§5); at De-Racking each line returns to its job (§5).
## 7. UI Surfaces
### 7.1 Job Workspace → Racking panel (per-WO) — `fusion_plating_shopfloor`
- Appears on the Job Workspace when the WO is at the Racking step (mirrors the existing Receiving card pattern).
- Shows: total parts, **Unassigned: N**, a list of rack-loads each with `[rack picker] [qty input] [assigned/capacity bar] [remove]`, **+ Add Rack** and **Divide Equally** buttons.
- Split / qty-edit only (single WO). Grouping is not done here.
### 7.2 Racking Station screen (new) — `fusion_plating_shopfloor`
- New OWL client action + menu under Shop Floor.
- Lists all WOs currently at the Racking step (grouped by recipe/spec for grouping visibility).
- Per-WO split controls (same as 7.1) **plus** "Combine onto rack" to pull an eligible WO onto another's rack-load.
- Shows rack capacity bars + over-capacity warnings.
### 7.3 Shared controller endpoints — `fusion_plating_shopfloor/controllers`
- `/fp/racking/load` (GET context for a WO or the station)
- `/fp/racking/add_rack` / `divide_equally` / `set_qty` / `remove_rack`
- `/fp/racking/assign_rack` (pick/scan the physical rack for a load — reuse `/rack/list_empty` + `/rack/scan_qr`)
- `/fp/racking/group` (add an eligible WO's line to a load) / `ungroup`
- `/fp/racking/move` (advance one or more rack-loads to the next step — wraps the move-log writes)
All run as `request.env.user` (the technician) reusing existing rack/move ACLs.
## 8. Plant Kanban Representation (D6)
- One card per job. Card column = area of the job's **least-advanced** rack-load (`min` over `rack_load_line_ids.load_id.current_area_kind` by column sequence), falling back to today's `active_step_id.area_kind` when the job has no rack-loads.
- Card shows a compact **rack rollup** chip ("3 racks · 1 Baking, 2 Plating"). Tapping the chip / card opens a per-rack drill-down (or routes to the Racking screen).
- No new board columns; no per-rack board cards.
## 9. Phasing (single spec, built in order)
1. **Phase 1 — Split + independent movement.** `fp.rack.load` + `fp.rack.load.line`, division math, move-log integration, De-Racking unrack, Job Workspace Racking panel. Single-WO only (one line per load).
2. **Phase 2 — WO grouping + Racking Station screen.** Multi-line loads, eligibility enforcement, the dedicated cross-WO surface.
3. **Phase 3 — Plant Kanban rollup + drill-down.**
## 10. Integration Points / Reuse
- `fusion.plating.rack` (capacity, racking_state, tags) — reused; rack-load references it.
- `fp.job.step.move` / `qty_at_step` partial-order compute — reused, now rack-aware.
- `move_rack_dialog.js` + `/fp/tablet/move_rack/*` + `/rack/list_empty` + `/rack/scan_qr` — reused/extended.
- Rack Travel Ticket PDF (`report_fp_rack_travel`) — reused (print a load's ticket).
- `_fp_is_racking_step` / racking inspection gate — unchanged; rack-loads are created at the racking step.
## 11. Edge Cases / Rules
- Sum of load qtys may be **< total** (rack in waves); the remainder shows as Unassigned and can be racked later.
- A load can't be removed/edited once it has moved past Racking.
- One physical rack = one active (non-unracked) load at a time.
- Over-capacity = soft amber warning only.
- Cancelling a job cascades its load lines; a load with no remaining lines is cancelled.
- Migration: existing single `fp.job.step.rack_id` assignments are left as-is (legacy); new flow uses rack-loads. No destructive backfill.
## 12. Out of Scope (this spec)
- Auto-suggesting which WOs to group (operator-driven only).
- Rack capacity *planning*/optimisation.
- Changing the De-Racking inspection model.
- Reworking the legacy `rack_id`-on-step flow (kept for back-compat).