# 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).