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>
11 KiB
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:
- 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.
- 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.
- 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 tofp.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; firsttotal % Nloads getbase + 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 exceedtotal(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 existingqty_at_steppartial-order compute correct and rack-aware. - The rack-load's
current_step_idis set to the destination on commit (explicit position for the independent-movement UI), andstateflipsloaded → 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'sqtyreturns 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 afp.rack.load.linefor 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 asrequest.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 (
minoverrack_load_line_ids.load_id.current_area_kindby column sequence), falling back to today'sactive_step_id.area_kindwhen 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)
- 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). - Phase 2 — WO grouping + Racking Station screen. Multi-line loads, eligibility enforcement, the dedicated cross-WO surface.
- 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_steppartial-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_idassignments 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).