diff --git a/fusion_plating/docs/superpowers/specs/2026-04-27-simple-recipe-editor-steelhead-screens.md b/fusion_plating/docs/superpowers/specs/2026-04-27-simple-recipe-editor-steelhead-screens.md new file mode 100644 index 00000000..dc22ec78 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-04-27-simple-recipe-editor-steelhead-screens.md @@ -0,0 +1,973 @@ +# Steelhead "Move Parts" Screen Inventory — Simple Recipe Editor + +Working notes captured during brainstorming. Each screenshot the user +provides is logged here so we can fold the field requirements into the +final design. Do NOT lose these notes — they drive the data shape on +`fp.step.template.transition.input` and the eventual transition dialog +on the tablet. + +## Common dialog header (all screens) + +- **Title bar**: "Move Parts" +- **Cancel** + red **MOVE (n)** button at footer (n = part count being moved) + +## Screenshot 1 — node→node move (no station) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Part Count | Integer (with stepper) | 1 | "Available: 1" hint shown beneath the input | +| Part Number | Read-only link | TEST225451 | resolves back to part record | +| From Node | Read-only link | Contract Review | current node operator is leaving | +| Transfer Type | Selection | Step | (other values likely: Step / Hold / Scrap / Return — TBD as more screens come in) | +| To Node | Read-only link | Ready for Incoming Inspection | destination node | +| To Location | Selection + camera icon | Global | location picker; camera icon = take photo evidence inline | +| Number of Customer WOs | Char | (blank) | optional; only present on this screen | +| Billed Labor | section with timer icon + "Reset All Edits" button | — | per-operator timer breakdown follows | +| **Per-operator labor row** | composite | Kris Pathinather, Timer Duration: 56s (100.0% billed), WO #4521 PN: TEST225451 Qty: 1, hrs/min/sec edit fields | reconcilable timer vs. billed split; the small icon top-right of the row appears to be "edit notes" | + +## Screenshot 2 — node→node move with station picker (between two real shop steps) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Part Count | Integer (stepper) | 49 | Available: 49 | +| Part Number | Read-only link | TEST225451 | | +| From Node | Read-only link | Ready for Incoming Inspection | | +| Transfer Type | Selection | Step | | +| To Node | Read-only link | Incoming Inspection | | +| **To Station** | Selection | Incoming Inspection | NEW field on this variant — appears when destination node has multiple stations to choose from. Defaults to a sensible value but is editable. | +| To Location | Selection + camera | Global | | +| Billed Labor | section | — | (not expanded on this screen — implies optional / collapsed by default when timer hasn't accrued) | + +## Screenshot 3 — second node→node move with From Station + +| Field | Type | Example | Notes | +|---|---|---|---| +| Part Count | Integer (stepper) | 49 | Available: 49 | +| Part Number | Read-only link | TEST225451 | | +| From Node | Read-only link | Incoming Inspection | | +| **From Station** | Read-only link | Incoming Inspection | NEW — appears when the source node had a station selected previously | +| Transfer Type | Selection | Step | | +| To Node | Read-only link | Adhesion Test Coupon | | +| To Location | Selection + camera | Global | | +| Billed Labor | section with "Reset All Edits" button | — | | +| Per-operator labor row | composite | Kris Pathinather, Timer Duration: 4s (100.0% billed), WO #4521 PN: TEST225451 Qty: 49, hrs/min/sec edit fields | same shape as screenshot 1 | + +## Patterns emerging across screenshots so far + +1. **Variable field set per transition.** Some moves show "Number of Customer WOs", some don't. Some show "To Station" / "From Station", some don't. The recipe author needs to be able to declare which fields appear on which transitions — that's what `fp.step.template.transition.input` is for. + +2. **Read-only context fields are always present** (Part Count, Part Number, From Node, To Node, From/To Station when applicable, To Location, Transfer Type). These are *system-derived* — the recipe author doesn't author them, they come from the runtime context. Our model only needs to capture the *author-defined* prompts (extra compliance fields). + +3. **Camera icon next to To Location** = inline photo capture, attached to the transition log. Implies the runtime needs a `photo` input type that can either upload a file or trigger device camera (mobile / tablet). + +4. **Billed Labor is a separate concern** — it's a labor reconciliation widget, not a recipe-defined input. Operator can edit hrs/min/sec to reconcile timer vs. actual time billed. Per-operator row with Timer Duration + "100.0% billed" indicator. "Reset All Edits" button reverts all manual reconciliations to timer values. This is its own sub-system; goes outside `fp.step.template` (it's a runtime feature, not authored on a recipe). + +5. **Transfer Type values seen so far**: `Step`. More variants expected (Hold, Scrap, Return, Rework). Each variant likely has its own required field subset. + +## Screenshot 4 — Adhesion Test Coupon → Ready for racking (no station, no labor block) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Part Count | Integer (stepper) | 49 | Available: 49 | +| Part Number | Read-only link | TEST225451 | | +| From Node | Read-only link | Adhesion Test Coupon | | +| Transfer Type | Selection | Step | | +| To Node | Read-only link | Ready for racking | | +| To Location | Selection + camera | Global | | +| Billed Labor | section icon only | — | not expanded — implies no timer accrued on this leg | + +Pattern: short transitions between QA-style intermediate nodes don't +expand the labor reconciliation panel. Labor block expands only when a +non-zero timer has accrued OR when an operator has a row to reconcile. + +## Screenshot 5 — Ready for racking → Racking (no station, no labor) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Part Count | Integer (stepper) | 49 | Available: 49 | +| Part Number | Read-only link | TEST225451 | | +| From Node | Read-only link | Ready for racking | | +| Transfer Type | Selection | Step | | +| To Node | Read-only link | Racking | | +| To Location | Selection + camera | Global | | +| Billed Labor | section icon only | — | collapsed | + +Pattern: same as screenshot 4. "Ready for X" nodes are gating-only +(parts wait there) — moving past one accrues no measurable labor. + +## Screenshot 6 — Racking → Ready For Plating + amber **rack-required warning** + +| Field | Type | Example | Notes | +|---|---|---|---| +| Part Count | Integer (stepper) | 49 | Available: 49 | +| Part Number | Read-only link | TEST225451 | | +| From Node | Read-only link | Racking | | +| Transfer Type | Selection | Step | | +| To Node | Read-only link | Ready For Plating | | +| **To Station** | Selection | SP Line | destination has multiple plating lines; SP Line is the auto-pick | +| To Location | Selection + camera | Global | | +| **WARNING BLOCK** (amber, with ⚠ icon) | banner | "Parts are currently at a RACKING node and are not racked." | Soft block — informs operator the parts haven't been associated with a physical rack yet | +| Billed Labor | section icon only | — | collapsed | +| Footer | THREE buttons | Cancel · **RACK PARTS** (red secondary) · **MOVE (49)** (red primary) | RACK PARTS opens a separate Rack Parts dialog (screenshots 7–8) | + +**Critical pattern**: a node's **type** (e.g. Racking) can require +auxiliary records (rack assignment) BEFORE the move is allowed. The UI +warns but doesn't hard-block — operator can still hit MOVE if they +choose, presumably escalating an audit flag. RACK PARTS is the +"resolve the warning" path. + +This implies the recipe author needs a way to declare: +> "This step's destination requires a rack assignment before move." — +> a node-type-level rule, not a transition-input rule. + +→ NEW field on `fp.step.template` and `fusion.plating.process.node`: +`requires_rack_assignment` Boolean. When True and the operator hasn't +linked a rack to this batch of parts yet, the tablet shows the amber +warning + RACK PARTS button. + +## Screenshot 7 — Rack Parts dialog (overlay on top of Move Parts) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Title | "Rack Parts" with QR-scanner icon top-right | — | scanner icon = scan rack QR to auto-fill To Rack | +| To Rack | Search-and-select dropdown | (blank → "Search Racks…") | M2O picker against a rack registry | +| Per-line row | composite | TEST225451 on WO 4521 (49) — Unit: **Count** dropdown — Amount: **49** | shows the part being racked + qty in the chosen unit | +| Unit | Selection | Count | (other values likely: Count / Pieces / Lbs / Kg / Sheets — TBD) | +| Amount | Number | 49 | usually = Part Count from Move Parts but editable | +| Billed Labor | section icon only | — | collapsed; same widget as Move Parts | +| Footer | THREE buttons | Cancel · **SAVE** (disabled until To Rack set) · **SAVE + PRINT** (disabled until To Rack set) | SAVE + PRINT prints rack travel ticket / barcode | + +## Screenshot 8 — Rack Parts dropdown expanded + +| Element | Notes | +|---|---| +| Searchable dropdown | Free-text "Search Racks…" filters list | +| Shows list of named racks: Rack 3, Rack 4, Rack 5, Rack 6, Rack 7, Rack 9, Rack 11 ... | Gaps in numbering (no Rack 8, no Rack 10) imply **active filter** — dropdown only shows racks currently empty / available. Numbers persist; full list is sparser than the index range. | +| Highlighted on hover | Rack 3 shown highlighted, indicating standard combobox UX | + +**Implication for our model**: a `fp.rack` registry already has the +shape we need (`name`, `active`, `state` for empty/in-use). Need a +M2O on the racking transition log: `rack_id`. Selection is filtered +to `state='empty'` by default, with an override to show all. + +The QR scanner icon implies a scan-to-fill flow on the tablet — same +mechanism we already use elsewhere (`/fp/shopfloor/scan` endpoint +resolves a scanned QR to a tank/job/etc.). For racks, we'd extend the +scan endpoint with a `fp-rack:` token resolver. + +## Patterns updated after screens 4–8 + +6. **Three transition-time prompt families now visible**: + - **Author-defined compliance prompts** (per-step on + `fp.step.template.transition.input`) — variable per step. + - **Always-on context fields** (Part Count, From/To Node, etc.) — + system-derived, not authored. + - **Step-type-driven side dialogs** — declared by Boolean flags on + the step (e.g. `requires_rack_assignment`) — open a separate + mini-dialog (Rack Parts) before the main move can complete. + Other likely flags: `requires_bake_window`, `requires_qc_check`, + `requires_signature`. Each maps to an existing or new sub-dialog. + +7. **Move button label reflects qty**: `MOVE (49)` vs `MOVE (1)`. + Operator confidence cue — they see exactly how many parts they're + committing to move before tapping. + +8. **Soft-block vs hard-block UI language**: + - **Amber warning + clickable resolution button** = soft block + (operator may proceed with audit log). + - **Red error + disabled MOVE** = hard block (operator cannot + proceed). Not yet seen in any screenshot, but pattern is implied + by the amber design. + +## Screenshot 9 — Compact part-row card (paused timer) + +| Element | Notes | +|---|---| +| Checkbox (left) | bulk-select for cross-row actions | +| Red **paused** icon (∥∥) | visual cue: timer is currently paused on this row | +| Inline summary | "1 TEST225451 \| Rack 3 \| Racking" — Qty + part link + rack link + node link, all clickable navigations | +| Sub-line | "Kris Pathinather: 27s" — operator name + accrued timer duration on this row | + +Pattern: this is a Plant Overview / Tablet row — a **per-batch +position card** showing a single chunk of parts at a single node, with +the operator who currently owns its labor timer + its rack assignment +inline. Clicking any blue link drills into that record. + +## Screenshot 10 — Stop User Labor Timer dialog + +| Field | Type | Example | Notes | +|---|---|---|---| +| Title | "Stop User Labor Timer" | — | Distinct dialog from Move Parts; fires when operator pauses without moving | +| Billed Labor | section | Kris Pathinather, Timer Duration: 32s (100.0% billed) | with **RESET ALL EDITS** button | +| Per-row | composite | "WO #4521 PN: TEST225451 Qty: 1" + Product Select + hrs/min/sec inputs | NEW: **Product** dropdown — operator can split timer time onto multiple products. Defaults to current PN. | +| Footer | THREE buttons | Cancel · **SAVE** · **SAVE & START NEW TIMER** | Distinguished from Move Parts which is Cancel/MOVE — labor reconciliation is its own action | + +**Critical insight**: labor reconciliation is a **standalone flow**, +not embedded in Move Parts. Operator can stop the timer without moving +parts. This implies our model needs: +- A persistent **labor timer record** (`fp.labor.timer`?) per + (operator × WO × part × node), independent of the move log. +- A **labor reconciliation** action that closes a timer with a billed + hrs/min/sec breakdown, optionally splitting across multiple products. + +## Screenshot 11 — Move Rack: tyut (full-rack move dialog) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Title | "Move Rack: tyut" | — | rack name (here "tyut") in the title — ties move to a specific rack record | +| **Rack Labels** | M2M with `+` button | (empty) | tag rack with shop labels (priority colours, customer codes, etc.) | +| **Parts** section | static list of contained parts | "49 TEST225451 Parts on WO 4521" + "1 TEST225451 Parts on WO 4521" | shows ALL parts on the rack at once — moving a rack moves everything on it as one unit | +| Type | Selection | Step | same as Move Parts | +| **To Node** | Selection (read-only here, "Soak Clean (SP-1)") | Soak Clean (SP-1) | greyed-out — auto-derived from current step's recipe path | +| **To Station** | Selection | Soak Clean (SP-1) | editable; defaults to first compatible station | +| Billed Labor | section + RESET ALL EDITS | — | per-batch row breakdown: each contained part qty gets its own hrs/min/sec inputs | +| Per-row | composite (×2 in this rack) | "WO #4521 PN: TEST225451 Qty: 49" → 0/0/6 + "WO #4521 PN: TEST225451 Qty: 1" → 0/0/0 | individual labor split per chunk on the rack | +| Footer | TWO buttons | Cancel · **SAVE** | no separate MOVE button — SAVE commits the move | + +**Critical pattern**: Move Rack is a **rack-as-unit move**, distinct +from Move Parts (parts-as-unit). When parts are racked, they MUST move +as a rack — Steelhead enforces this by not surfacing the per-part Move +button (see screenshot 12: those buttons are disabled/greyed when +racked). The recipe author doesn't author move-rack rules; they're +runtime-enforced based on whether the parts are currently on a rack. + +## Screenshot 12 — Plant overview: Racks vs Parts panes (rack collapsed UI) + +| Section | Notes | +|---|---| +| **Racks** header | with red "UNRACK MULTIPLE" button — bulk unrack action | +| Rack row | red ∥∥ pause icon · "50 Parts" · rack name "tyut" (Rack 3) · current node breadcrumb "Soak Clean (SP-1) / Soak Clean (SP-1)" · "Kris Pathinather: 14s" labor stamp · **MOVE RACK** primary button on the right | +| **Parts** header | with `+ ADD NEW PARTS` button + filters (Part Number ▾, Part Account ▾) + search box "Search by PN or group" | +| Parts row(s) | qty + PN + rack + node breadcrumb + operator/timer + per-row buttons: **MOVE PARTS** (greyed out / disabled because racked) + QR icon + ribbon icon + ⋮ kebab + ⌄ expand | + +**Critical UI rule emerging**: when parts are racked, **the per-part +Move Parts button greys out**. The only way to move racked parts is +via **MOVE RACK**. This is enforced by the disabled button state, not +by error message. Operator UX is: "You can't accidentally move just +some of these parts — they're racked together, move the rack." + +This implies a runtime guard on `fp.job.step` (or wherever the move +controller lives): if `rack_id` is set on the part-batch, reject +move-parts calls and require move-rack. + +## Screenshot 13 — Move Rack: tyut (different destination, station picker shows non-default station) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Title | "Move Rack: tyut" | — | same dialog shape as screenshot 11 | +| Rack Labels | M2M + button | (empty) | | +| Parts list | static | "49 TEST225451 Parts on WO 4521" + "1 TEST225451 Parts on WO 4521" | same contents as 11 | +| Type | Selection | Step | | +| To Node | Selection (read-only) | Rinse (SP-2) | rack has advanced one step from screenshot 11 (Soak Clean → Rinse) | +| **To Station** | Selection | "Cold Water Rinse …" (truncated) | NEW: shows that one node can have **multiple stations** with descriptive names, not just SP-numbered codes. The SP-2 prefix is the tank code; "Cold Water Rinse" is the station's display name. | +| Billed Labor | section + RESET ALL EDITS | — | per-batch labor split as before; total Timer Duration: 10s | +| Footer | TWO buttons | Cancel · **SAVE** | | + +**New insight**: stations have both a **code** (SP-2) AND a **friendly +name** (Cold Water Rinse). Our model already has `name` + `code` on +`fusion.plating.tank` — confirming our existing design matches +Steelhead's naming model 1:1. + +## Patterns updated after screens 9–13 + +9. **Labor timer is a first-class persistent entity**, not a UI + ephemeral. It has a stop-without-move flow (screen 10), it can be + reset, it splits across products. Probably its own model + `fp.labor.timer` with states: `running / paused / stopped / + reconciled`. + +10. **Rack-vs-parts move duality**: parts-not-racked → Move Parts + dialog; parts-racked → Move Rack dialog (MOVE PARTS greys out). + Move Rack moves all rack contents at once with per-chunk labor + breakdown. Need `rack_id` on the move-controller decision. + +11. **Rack Labels** as a tagging surface — M2M against a tag-like + registry. Not yet authored on `fp.step.template` (it's a runtime + rack metadata feature). Goes on `fp.rack` directly when we build + that registry. + +12. **Plant Overview shape**: top section "Racks" with rack-level + primary action (MOVE RACK) + bulk action (UNRACK MULTIPLE); + bottom section "Parts" with per-part actions (greyed when + racked) + filters + search. This is the layout the simple-mode + customer probably also wants on their plant overview — but + that's a separate sub-project from the recipe editor. + +13. **"To Node" vs "To Station" hierarchy is consistent**: To Node is + read-only (auto-derived from recipe sequence), To Station is + editable (operator picks among compatible stations on that + node). For our simple recipe editor, this means each step's + `tank_ids` M2M is the **authoritative compatible-station list** + that the runtime uses to populate the To Station dropdown. The + recipe author's job is to declare the ALLOWED set; the operator + picks among them at run time. + +## Screenshot 14 — Move Rack: tyut → Ready For DeRack (no station picker) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Title | "Move Rack: tyut" | — | rack name in title | +| Rack Labels | M2M + button | (empty) | | +| Parts list | static | "49 TEST225451 Parts on WO 4521" + "1 TEST225451 Parts on WO 4521" | full rack contents | +| Type | Selection | Step | | +| **To Node** | Selection (read-only, greyed) | "Ready For DeRac…" (truncated; full = "Ready For DeRack") | gating-only node — no station to choose, no plating action | +| **No To Station field** | — | — | confirms: when destination is a single-station / gating node, the To Station row does not render | +| Billed Labor | section + RESET ALL EDITS | — | per-batch breakdown | +| Per-row labor | composite | "WO #4521 PN: TEST225451 Qty: 49" → 0/0/4 + "WO #4521 PN: TEST225451 Qty: 1" → 0/0/0 | | +| Footer | TWO buttons | Cancel · **SAVE** | | + +**Pattern**: gating nodes (anything starting with "Ready For…") are +single-station and skip the To Station selector. Move Rack and Move +Parts both honour this. Our model should treat any node with exactly +one tank in `tank_ids` (or none — implying "any global location") as a +node that hides the station picker. + +## Screenshot 15 — Move Parts with **soft-block: missing spec measurements** (MOVE button DISABLED) + +| Field | Type | Example | Notes | +|---|---|---|---| +| Part Count | Integer (stepper) | 49 | Available: 49 | +| Part Number | Read-only link | TEST225451 | | +| From Node | Read-only link | Bake | | +| **From Station** | Read-only link | Bake | | +| Transfer Type | Selection | Step | | +| To Node | Read-only link | Adhesion Testing | | +| To Location | Selection + camera | Global | | +| **WARNING BLOCK** (amber, ⚠ icon) | banner | "Additional Spec Measurements are required for this part." | distinct from the rack-required warning | +| Billed Labor | section + RESET ALL EDITS | — | Timer Duration: 7s | +| Per-row labor | composite | "WO #4521 PN: TEST225451 Qty: 49" → 0/0/7 | | +| Footer | TWO buttons | Cancel · **MOVE (49)** (DISABLED / greyed) | **HARD BLOCK** — operator can't proceed until spec measurements are recorded | + +**Critical pattern — first hard block we've seen**: +- Steelhead shows the same amber colour for both soft-block (rack + warning) and hard-block (missing spec). The DIFFERENCE is the + primary button state: soft-block leaves MOVE enabled (audit and + proceed); hard-block greys MOVE out (must resolve first). +- Steelhead does NOT explain HOW to record the missing measurements + in this dialog — the operator is left to figure out where to enter + them. This is what the user means by "Steelhead is limited." + +**Where we improve over Steelhead**: +- The amber banner should have a **clickable resolution button** like + the rack warning's "RACK PARTS" button — e.g. **"RECORD MEASUREMENTS"** + that opens the spec-measurement input dialog inline. +- After recording, the banner clears + MOVE re-enables. No screen + hunting. +- Distinguish soft vs hard visually: amber for soft (proceed-with- + audit), red for hard (must-resolve). + +## Patterns updated after screens 14–15 + +14. **Gating nodes (Ready For …)** are single-station / no-station + nodes — UI hides the To Station selector for them. Our recipe + editor's step-template form should let the author mark a step as + "gating" (no tanks/stations needed) and the runtime auto-hides + the station picker. + +15. **Soft-block vs hard-block protocol**: + - **Soft-block**: amber banner + resolution button + MOVE stays + enabled (rack-required warning, screen 6). + - **Hard-block**: amber banner + MOVE disabled until resolved + (missing spec measurements, screen 15). + - **Steelhead's gap**: hard-block doesn't tell operator where to + go to resolve. **Our improvement**: every blocker (hard or + soft) gets an inline resolution button. + +16. **Spec measurements vs operation measurements vs transition + inputs**: three distinct concepts now visible: + - **Operation measurements** (`input_template_ids` on + `fp.step.template`) — recorded *during* a step, e.g. "Actual # + of parts" mid-bake. + - **Transition inputs** (`transition_input_ids`) — recorded + *when leaving* a step, e.g. "Customer WO #" or photo evidence. + - **Spec measurements** (NEW from screen 15) — part-level + specs that are required *for the part itself* across multiple + steps, e.g. "thickness reading" required after Adhesion + Testing. These don't belong on the step template — they belong + on the **part** record (or on a part × step rule). The amber + block fires when the part hasn't satisfied a spec rule. + + → Implies a separate authoring surface: per part (or per + customer × part), a list of "required spec measurements" with + their trigger node. Out of scope for this sub-project but worth + flagging in the design as a hand-off point. + +17. **MOVE button label DISABLED state**: greyed background, greyed + text, still shows count "MOVE (49)". Steelhead does NOT show a + tooltip explaining why — another improvement opportunity for us. + +## Screenshots 16, 17, 18 — Paper Job Traveller (3-page WO #023633-1, Amphenol Canada, ENP-Aluminum) + +These are the **manual paper job travellers** the client fills in by +pen as the parts move. They're the gold-standard spec of what data +flows through a real plating job today. Every column on these sheets +is a candidate for digital capture in our recipe → step-template → +runtime stack. + +### Header (repeats on every page) + +| Column | Example | Where it lives in our model | +|---|---|---| +| WO # | 023633-1 | `fp.job.name` (already exists) | +| Barcode | (1D barcode of WO #) | `fp.job.qr_code` (already auto-generated; switch to 1D Code 128 for traveller print) | +| Date In | 29-11-2024 | `fp.job.date_received` (already exists, comes from `fp.receiving`) | +| Due Date | 11-12-2024 | `fp.job.date_due` (already exists, from SO commitment date) | +| Type | ENP-ALUMINUM | `fp.job.coating_config_id.name` (already exists) | +| Order No. | 023633 | `fp.job.sale_order_id.name` (already exists) | +| P.O. No. | 731830 | `fp.job.sale_order_id.client_order_ref` (already exists) | +| Customer | AMPHENOL CANADA + address + phone | `fp.job.partner_id.*` (already exists) | +| WO Generated By | RIYA | `fp.job.create_uid.name` (already exists) | + +→ **No new model fields needed for the header.** Existing job fields +cover everything. The traveller-print report just needs to pull them. + +### Item Information block (page 1, top) + +| Column | Example | Where it lives | +|---|---|---| +| Item informations / Part # | VS-E0443220025 | `fp.part.catalog.part_number` (already exists) | +| Rev. | 1F | `fp.part.catalog.revision` (already exists) | +| Mat. (material) | 6061-T6511 | NEW field on `fp.part.catalog`: `base_material` Char (e.g. "6061-T6511 aluminum"). Currently we have implicit material via coating config — making it explicit per-part is small. | +| Catg. (category) | ENP-ALUMINUM | derived from `fp.part.catalog.coating_config_id.name` | +| S/N (serial #) | (blank for batch) | already exists as `x_fc_serial_number` (Sub 5) | +| Item-Name / Process Description | "SHELL RECEPTACLE / 01 - ELECTROLESS NICKEL PLATING PER E499-303-00-002 OF AMPHENOL SPEC # E499-303-00-XXX REV : 1F" | `fp.part.catalog.name` + `fp.part.catalog.customer_facing_description` (already exist; just need to merge in the report) | +| Qty Rec. | 5850 → 5839, 5835 (multiple counted lines) | NEW: traveller needs to show **received qty** AND **per-stage running counts**. Stages: Received → Inspected → Racked → Plated → Final-counted. Capture per-stage. | +| VIS INSP. | 0 (visual inspection rejects/holds during incoming) | NEW: integer column on `fp.job` for "qty rejected at incoming inspection" | +| Rework | (blank) | NEW: integer column for "qty sent to rework" | +| Special Requirements | "MID PHOS / PANEL THICKNESS: 0.0224"-0.0228" (PLATING THICKNESS: 0.0005"-0.0007") / BAKE @ 250 DEG F FOR 1 HOUR / *****RUN EACH LOAD WITH 3 TEST PANELS AND VERIFY THE PLATING THICKNESS USING XRF PRIOR TO REMOVING PARTS FROM THE TANK..." | NEW: `fp.job.special_requirements` Text (free-form). Today this lives in customer specs but isn't pulled onto the traveller. | +| Stamp + Date | initials "LY" + "06.12.24" | runtime sign-off — captured per-section by the inspector | + +### Process-Sheet header (page 1, middle) + +| Column | Example | Where it lives | +|---|---|---| +| Process name 1 | "ENIP (A) BAKE (GENERIC)" | `fp.job.process_node_id.name` (root recipe) | +| Process name 2 | "ENIP (A) BAKE" | sub-process node name (already exists in tree) | +| Catg. | ELECTROLESS NICKEL | from coating config | +| Special Req. | (blank in this WO) | `fp.job.special_requirements` | +| Spec / Info block | (blank, reserved for sticker / inspector notes) | print-only blank | + +### Step rows (the heart of the traveller) + +Each step row carries: + +| Column | Type / format | Notes | +|---|---|---| +| **Step #** | Integer (1, 2, 3, ... 25) | already on our model as `sequence` (auto-numbered in the OWL editor) | +| **Tank** | Char code (A-1, A-2, A-13, blank) | comes from `tank_ids` M2M chosen at runtime → `fp.job.step.tank_id` | +| **Operation** | Operation name (e.g. "Soak clean", "Etch", "E-Nickel Plate", "Inspection") | `fp.step.template.name` | +| **Operation actual-data column** (multi-row, hand-written) | Free-form actuals filled in pen — varies per step. Examples: "Actual Qty: 5839", "Actual Time: 04 min", "Actual Temperature: 170 ºF", "Actual time: 01 min / Actual temperature: 190 ºF / Plating thickness: 0.0005"", "PASS: ✓ FAIL: ___", "Actual thickness: 0.00057" | THIS IS THE OPERATION-MEASUREMENT INPUT LIST. Each value the operator pencils in is a `fp.step.template.input` row. | +| **Instruction** | Free-form instructions tied to a Work Instruction reference | "Immerse parts in alkaline soak cleaner for 4-6 minutes @ 150-170 ºF as per WI 10.07." / "Verify plating thickness is as per customer requirements and as per WI 10.09" / "Verify the job traveler was filled out completely and the information is correct." | maps to `fp.step.template.description` (Html) | +| **Unit** | Char (each / Minutes / Seconds / mils / min / ºF / blank) | NEW: `fp.step.template.unit` Char or Selection. Currently NOT in our model. Operators need to know what unit the actual-time / actual-thickness etc. should be in. | +| **Material** | mostly N/A; some steps have "MID PHOS" written | this column is for **chemistry callout** when the step has a material-specific bath. Maps to `fp.step.template.process_type_id` (already exists) — the process type's name prints here. | +| **Voltage** | mostly N/A | electrolytic steps only. NEW: optional `voltage_target` Float on step template. | +| **Viscosity** | mostly N/A | bath-quality callout. NEW: optional `viscosity_target` Float. | +| **Time (min)** | range, e.g. "4 - 6" / "55 - 65" / "25 - 35" | the **target range** the operator must hit. NEW: `time_min_target` + `time_max_target` Float on step template. | +| **Temp.** | (mostly empty in target column; pencilled in actual) | the **target range**. NEW: `temp_min_target` + `temp_max_target` + `temp_unit` Selection (F/C). | +| **Stamp** | initials column | runtime per-step sign-off. Captured as `fp.job.step.signoff_user_id` + signoff datetime. | +| **Date** | dd.mm.yy format | per-step completion date (auto-stamped on `Mark Done`). Already exists as `fp.job.step.date_finished`. | + +### Concrete examples from the WO that show the data shape + +| Step # | Tank | Operation | Targets (printed) | Actuals (handwritten) | +|---|---|---|---|---| +| 1 | — | Part verification and quantity check | unit=each | "Actual Qty: 5839" | +| 2 | — | Issue Panels | — | (no actual; just stamp/date) | +| 3 | — | Rack | — | "Actual Qty: 5839" | +| 4 | A-1 | Soak clean | 4-6 min @ 150-170 °F | "Actual time: 04 min, Actual Temperature: 170 °F" | +| 5 | A-2 | Rinse | — | (stamp only) | +| 6 | — | Water Break Test | — | "PASS: ✓" | +| 7 | A-3 | Etch | 55-65 sec | "Actual Time: 60 sec." | +| 9 | A-5 | Desmut | 55-65 sec | "Actual Time: 60 sec." | +| 11 | A-7 | Zincate | 25-35 sec | "Actual Time: 25 sec." | +| 13 | A-5 | Strip Zincate | 15-25 sec | "Actual Time: 25 sec." | +| 17 | A-13 | E-Nickel Plate | 185-190 °F | "Actual time: 01 min, Actual temperature: 190 °F, Plating thickness: 0.0005"" | +| 22 | — | Baking | (Time In / Time Out) °F | "Time In: 10:00, Time Out: 11:00, Actual temperature: 250 °F" | +| 23 | — | Inspection | mils | "Actual thickness: 0.00057, PASS: ✓" | +| 24 | — | Final Inspection | — | "Verify the job traveler was filled out completely…" | +| 25 | — | Shipping | — | "Actual Qty: 5839" | + +## What the traveller tells us about our data model gaps + +### Step template — NEW fields needed + +| Field | Type | Reason | +|---|---|---| +| `unit` | Char or Selection | Print "each / Minutes / Seconds / mils / ºF / N/A" so operator knows units | +| `time_min_target` | Float | Lower bound of operation time | +| `time_max_target` | Float | Upper bound; runtime warns if outside | +| `time_unit` | Selection (`sec / min / hr`) | how to interpret time fields | +| `temp_min_target` | Float | Lower bound of operation temperature | +| `temp_max_target` | Float | Upper bound | +| `temp_unit` | Selection (`F / C`) | how to interpret temp fields | +| `voltage_target` | Float (optional) | electrolytic steps | +| `viscosity_target` | Float (optional) | bath-quality | +| `material_callout` | Char (optional) | "MID PHOS" — short string printed in the Material column. Defaults to `process_type_id.name` if not set. | + +### Step template input list (`fp.step.template.input`) — likely defaults + +For the traveller to render a useful actual-data column, each step +template should pre-populate its input list with the right entries: + +- **Cleaning / etch / desmut / zincate / acid steps**: `Actual Time + (sec or min)` + (optional) `Actual Temperature (°F)`. +- **Plating step**: `Actual time (min)` + `Actual temperature (°F)` + + `Plating thickness (")`. +- **Bake step**: `Time In (HH:MM)` + `Time Out (HH:MM)` + `Actual + temperature (°F)`. +- **Receiving / racking / shipping**: `Actual Qty`. +- **Inspection (visual)**: `PASS / FAIL` selection. +- **Inspection (thickness measurement)**: `Actual thickness + (mils)` + `PASS / FAIL`. +- **Water break test**: `PASS / FAIL` selection. +- **Rinse / dry**: no input — sign-off only. + +These defaults come "out of the box" when the customer drops a +library step into a recipe — no need for them to author the input +list every time. They can override / add later. + +### Job-level NEW fields + +| Field | Type | Reason | +|---|---|---| +| `qty_received` | Integer | "Qty Rec." column on traveller header | +| `qty_visual_inspection_rejects` | Integer | "VIS INSP." column | +| `qty_rework` | Integer | "Rework" column | +| `special_requirements` | Text | "Special Requirements" block (long free text from customer spec) | +| `qty_at_stage` (computed One2many?) | per-step running count | NEW — would let "Qty Rec.: 5850 → 5839 → 5835" auto-render on the traveller. Computed from `fp.job.step.qty_done` chain. | + +### Part catalog NEW fields + +| Field | Type | Reason | +|---|---|---| +| `base_material` | Char | "6061-T6511 aluminum" — currently implicit | + +### Traveller report (new QWeb template) + +`fusion_plating_reports/report/report_fp_job_traveller_v2.xml` — landscape A4 multi-page: + +1. **Page 1**: header + Item Information block + Process-Sheet header + first 6–8 steps. +2. **Pages 2..N**: continuation rows for the remaining steps. +3. **Last page**: Footer / final inspection / shipping rows + room for stamps / dates. + +The current `report_fp_job_traveller.xml` (in `fusion_plating_jobs`, +shipped in S5/S18) is portrait and minimal. We rebuild it to match +this paper format. + +### What we'll auto-capture instead of pencilling in + +| Pencilled today | Auto-captured by us | +|---|---| +| Actual Qty | comes from `fp.job.qty_done` chain (already exists) | +| Actual Time | timer on `fp.job.step` (already exists, S1/S2 stuff) | +| Actual Temperature | IoT sensor reading (already exists in `fusion_iot/fusion_plating_iot/`) — ties to step's tank, snapshot saved at sign-off | +| Plating thickness | Fischerscope auto-extract (already exists in `fusion_plating_certificates/fp.thickness.reading`, S19) | +| Time In / Time Out (bake) | bake-window record (already exists, S6/S15) | +| PASS/FAIL | QC checklist (already exists in `fusion_plating_quality`, S18/S19) | +| Stamp (initials) | per-step `signoff_user_id` (already exists) | +| Date | per-step `date_finished` (already exists) | + +Most of the runtime capture machinery already exists. **The gap is +making the recipe editor expose target ranges + units + per-step +input list defaults** so the traveller can render the targets next to +the actuals. + +## Patterns updated after screens 16–18 + +18. **Paper traveller is the spec for what's authored on a step + template.** Every column on the paper sheet maps to either: + - an authored field on `fp.step.template` (target range, unit, + material callout, voltage, viscosity), OR + - a runtime-captured field on `fp.job.step` (actual time, actual + temp, sign-off, date), OR + - a runtime-captured input value (`fp.job.step.input.value`) + tied to a `fp.step.template.input` definition. + +19. **Step "Actual" data is multi-field per step**, not just one + "actual" value. E.g. an etch step has Actual Time only; a + plating step has Actual Time + Actual Temp + Plating Thickness; + a bake has Time In + Time Out + Actual Temp. The input list per + step template must be flexible enough to model this — which it + already is via `input_template_ids`. We just need sane defaults + so the customer doesn't have to author them from scratch. + +20. **Target ranges (e.g. 4-6 minutes, 150-170 °F)** are first-class + authored data. They drive: traveller print, runtime overrun + warnings (S7), out-of-spec alerts on IoT readings (existing), + and the operator UX cue ("you're at 7 min, target is 4-6 — log + a deviation?"). Currently we have `estimated_duration` (single + value) on `process.node` — needs to extend to min/max per step. + +21. **Customer-spec callout** ("MID PHOS", "BAKE @ 250 DEG F FOR 1 + HOUR", "RUN EACH LOAD WITH 3 TEST PANELS…") is per-job, not + per-step. It's the **job-header special-requirements** field + (free text, copied from customer spec library). Already covered + by `fusion.plating.customer.spec` model — needs to be pulled + onto the traveller print. + +22. **Multi-pass through same tank** (e.g. step 11 = A-7 Zincate, + step 13 = A-5 Strip Zincate, step 15 = A-7 Zincate AGAIN). Our + recipe model handles this fine — each step is its own node + instance. Worth confirming the simple editor renders this + visually (e.g. "Zincate" appears twice in the ordered list, + each with its own SP-7 station tag). The screenshots from the + customer's other system in the original brainstorm already + showed this pattern (Primary Rinse appeared twice in the + Selected list). + +## Screenshots 19–22 — Steelhead's auto-generated CoC traveller PDF (7-page report) + +These pages are the **digital output** Steelhead produces after a job +completes — a multi-page PDF traveller showing the full chain of +custody, every step transition, every captured measurement, the +operator who recorded each value, and the timestamp. This is what +gets sent to the customer with the parts. Functionally equivalent to +our own `fp.certificate` flow + the new traveller report we plan to +build. + +**Critical context**: this is the OUTPUT that Steelhead generates from +its captured data. Everything on these pages is something we MUST be +able to capture + render to match feature parity. Job: +- Customer: (printed at top, e.g. cert-style header) +- Part: 2144A6201-5 OUTER CYLINDER ASSY +- Description: "ELECTROLESS NICKEL PLATING & BAKE PER LGPS 1104G & IAW + TECHNIQUE#: QA-016-36 REV.1, PLATING THICKNESS: 0.0019", + HYDROGEN EMBRITTLEMENT RELIEF BAKING @ 375ºF FOR 23 HOURS" +- Quantity: 1 +- WO#: 620, PO#: 980806214, Date: 2025-09-23 +- Specification(s): "Electroless Nickel Plating" +- Footer: "Cert Created At: 2025-09-23", page #/total, **Nadcap Accredited** logo, **ENTECH** logo + +### Page 1 — Header + first 3 step transitions (no captured data yet) + +Steps shown: +1. **Ready for Incoming Inspection** — Moved By: Riya Bhatt, Time: Sep 18, 2025 07:22:31 AM +2. **Ready For Plating** — Moved By: Kris Pathinather, Time: Sep 18, 2025 07:25:00 AM +3. **Soak Clean (S-3)** — Moved By: Kris Pathinather, Time: Sep 18, 2025 07:31:26 AM + +**Pattern**: each step heading shows `Step Name (station code)` — +"Soak Clean (S-3)" combines the step's name with the chosen station. +Below the heading, "Part Number" and "Moved By: Time: +" are always rendered. Then if the step has captured +measurements, a sub-table follows. + +### Page 2 — Step transitions with captured measurement tables + +Each step that captured operator inputs renders a 4-column table: +**Name | Description | Value | Recorded By** + +Sample tables: + +**Soak Clean** sub-table (between page 1 step heading and page 2 next +step heading): +| Name | Description | Value | Recorded By | +|---|---|---|---| +| Soak Clean Time (5-10 min.) | (blank) | 00:05:22 | Kris Pathinather | +| Soak Clean Temp 165-195 (ºF) | (blank) | 170 | Kris Pathinather | + +**ElectroClean (S-3)** sub-table: +| Name | Description | Value | Recorded By | +|---|---|---|---| +| ElectroClean Time 30-90 Seconds | (blank) | 00:00:55 | Kris Pathinather | +| ElectroClean Amperage (A) | (blank) | 280 | Kris Pathinather | +| Surface Area (FT2) | (blank) | 7 | Kris Pathinather | +| ElectroClean (SP-1) Temperature (ºF) | (blank) | 170 | Kris Pathinather | + +**Water Break Free Test** sub-table (test-style measurement): +| Name | Description | Value | Recorded By | +|---|---|---|---| +| Water Break Free Test | "Perform water break test on parts as per WI 10.08. Observe parts for 30 – 60 seconds after removing from the soap rinse and observe if the production parts exhibit a water break free surface. If test fails, repeat from step Soak Clean." | PASS | Kris Pathinather | + +### Page 3 — Mid-job step transitions (HCl Activation, Rinses, Plating, Porosity) + +Sample tables: + +**Acid Dip** sub-table (HCl Activation step — the input description +includes the WI reference): +| Name | Description | Value | Recorded By | +|---|---|---|---| +| Acid Dip Time | "Immerse parts in HCl tank for 20-40 seconds as per WI 10.07." | 00:00:30 | Kris Pathinather | + +**Electroless Nickel Plating (S-10)** sub-table — KEY step, multiple +measurements: +| Name | Description | Value | Recorded By | +|---|---|---|---| +| E-Nickel Plate Temp (187-193ºF) | (blank) | 190 | Kris Pathinather | +| Final Panel Thickness | (blank) | 0.0248 | Kris Pathinather | +| Actual Thickness (") | "Immerse parts with test panels in E-Nickel tank as per WI 10.07." | 0.0018 | Kris Pathinather | + +**Porosity Test** — minimal heading-only entry with one inline +"Results: PASS" text under the step name (instead of a table). + +### Pages 4–5 — More transitions (DeRacking, Ready for De-Masking, De-Masking, Ready for bake, Bake) + +Heading-only step transitions (no captured data table): +- **DeRacking** — Moved By: Ryan Persaud, Time: Sep 18, 2025 02:16:45 PM +- **Ready for De-Masking** — Moved By: Ryan Persaud, Time: Sep 18, 2025 02:20 PM +- **De-Masking** — Moved By: Ryan Persaud, Time: Sep 18, 2025 02:21:05 PM +- **Ready for bake** — Moved By: Ryan Persaud, Time: Sep 18, 2025 02:44:52 PM +- **Bake** — Moved By: Ryan Persaud, Time: Sep 18, 2025 2:45:27 PM + +**Pattern**: gating / no-input steps still render a heading + "Moved +By + Time" but no measurement table. They contribute to the +chain-of-custody trail without measurements. + +### Page 6/7 — Bake captured data + Adhesion Test + EN Final Inspection (final pass/fail) + +**Bake** sub-table: +| Name | Description | Value | Recorded By | +|---|---|---|---| +| Hydrogen Embrittlement Time | (blank) | 23:48:19 | Ryan Persaud | +| Hydrogen Embrittlement Relief Temp (ºF) | (blank) | 375 | Ryan Persaud | + +**Post Plate Inspection** (heading-only, gating) +- Moved By: Brett Kinzett, Time: Sep 22, 2025 08:22:01 AM + +**Final EN inspection** sub-table — pass/fail summary: +| Name | Description | Value | Recorded By | +|---|---|---|---| +| Adhesion Test | (blank) | PASS | Brett Kinzett | +| EN Final Inspection | (blank) | PASS | Brett Kinzett | + +## What this PDF tells us (key insights) + +1. **Steelhead's CoC traveller is a chronological audit log**, NOT a + recipe traveller. It walks the actual transition history (not the + authored recipe order) and prints captured data per step. Useful + for AS9100 / Nadcap audit because it shows EXACTLY what happened, + in time order, by whom. + +2. **Every captured input has 4 attributes**: + - **Name** (e.g. "Soak Clean Time (5-10 min.)") — the input + definition's display name. The target range is **embedded in + the name** ("(5-10 min.)") rather than a separate column. This + is a Steelhead UX choice — we should do better and put min/max + in their own columns so the auditor can see authored vs actual + side-by-side. + - **Description** (long-form WI reference) — pulled from the + step's `description` (Html) field. Sometimes blank. Usually + populated for inspection-type steps (where the operator needs + the procedure text), not for routine measurements. + - **Value** (the recorded value) — type varies: time HH:MM:SS, + number (with implicit unit), PASS/FAIL, etc. + - **Recorded By** — operator's full name. + +3. **Time values are always in HH:MM:SS** even for sub-minute + readings ("00:00:55"). Steelhead uses one consistent time + format. We should do the same on the report — it's noise for + the operator entering the data ("how do I type 55 seconds?") + but clean on the report. + +4. **Step heading has the station baked in** — "Soak Clean (S-3)", + "Electroless Nickel Plating (S-10)". This means our report needs + to render the **station code** chosen at runtime, not just the + step name. Already in our model (`fp.job.step.tank_id.code`). + +5. **Sub-tables only appear when there's captured data**. Heading- + only steps (Ready For X, gating nodes, racking moves) just show + the chain-of-custody line. Our report should follow the same + pattern — clean PDF with no empty tables. + +6. **Same input name with different runtime captures** — "Rinse + (S-11 / S-13)" is a single transition heading covering BOTH a + rinse pass through tank S-11 AND tank S-13 (two physical rinse + tanks operators dipped through in sequence). Steelhead collapses + them into one heading. Our model has each as a separate step; + we'd render two headings unless we add a "merge consecutive + identical steps" report option. **Decision later** — for now, + keep them separate. + +7. **Multi-day jobs are normal**. The bake spans 23h 48min, post- + plate inspection happens days later (Sep 19 / Sep 22). The + traveller knows because each transition has its own timestamp. + Our chain-of-custody log already captures this. + +8. **Two operators on the same job** — Riya Bhatt does receiving, + Kris Pathinather does the wet-line, Ryan Persaud handles + masking/bake, Brett Kinzett does final inspection. The "Moved By" + field changes per step, reflecting hand-offs. Already supported + by our model. + +9. **Footer branding** — "Nadcap Accredited (Administered by PRI)" + logo + ENTECH company logo. Per-page. Page numbering "n/total". + Plus "Cert Created At: ". Our existing `fp.certificate` + PDF flow can render this footer; we just need to add the Nadcap + logo asset + company logo configurable. + +## What this means for our recipe editor + traveller report + +### Step template input list — render targets in their OWN columns + +Steelhead embeds "(5-10 min.)" in the input name. We should split +this into two authored fields: + +| Field | Type | Notes | +|---|---|---| +| `target_min` | Float | Lower bound (e.g. 5) | +| `target_max` | Float | Upper bound (e.g. 10) | +| `target_unit` | Char | "min" / "ºF" / "A" / "FT2" / "in" / "%" / "(blank for pass-fail)" | +| `display_label` | Char (compute) | "Soak Clean Time" or "Soak Clean Temp" — clean name without the embedded range | + +Report renders 5 columns per measurement table: +**Name** | **Description** | **Target** | **Actual** | **Recorded By** + +The Target column shows "5-10 min" (or "165-195 ºF" or "30-90 sec" +auto-formatted from the three new fields). + +### Job traveller report = chronological audit, NOT step-order + +Build the report to walk `fp.job.step` records ordered by +`date_started` (or `date_finished`), not by `sequence`. This matches +Steelhead's chain-of-custody ordering and is what auditors expect. + +### Heading format for each step transition + +` ()` — already supported by our model. No +gap. + +### Time format + +Use HH:MM:SS for all duration values on the report, even sub-minute. +On entry (tablet input), accept "55 sec" or "00:00:55" — convert to +canonical HH:MM:SS for storage. + +### Pass/Fail value rendering + +For boolean inputs, render "PASS" / "FAIL" in caps. Already +straightforward. + +### Step description field carries the WI reference + +Already supported by our `fp.step.template.description` Html field. +The report just needs to render it stripped of HTML tags inline +(plain-text in the table cell). + +### Each operator's name comes from `res.users.name` + +Our existing pattern. No gap. + +## Patterns updated after screens 19–22 + +23. **The CoC PDF is a chronological audit, not a recipe-order + print.** Different report from the operator-facing traveller + (which prints in recipe order so the operator knows what's + next). We need both: + - **Operator traveller** (recipe-order, paper-style, A4 + landscape, blank actual columns) — what Riya prints when + Carlos starts a job. + - **Customer CoC** (chronological, captured-data, formal, + portrait, branded) — what gets attached to the cert and sent + to the customer. + Today we ship the CoC via `fp.certificate` (S18/S19). Need to + extend it to walk the chain-of-custody and render measurement + sub-tables. + +24. **Target ranges should be authored as min/max + unit, not + embedded in the name string.** Steelhead embeds them which is + a UX bug — auditors can't filter by "all jobs where Soak Clean + Temp was out of range" because the range only exists as text. + By splitting into structured fields, we get free queryability + AND can colour-code the value cell on the report + (green if in range, red if not). + +25. **Step description vs input description**: Steelhead uses the + "Description" column on the input table to show the WI + reference. That's the **step-level** description repeated on + the input row. Our model already has `description` on the step + template — we just render it once at the input table header + instead of per-row. Saves report space. + +26. **Inputs can be multi-valued per step** — ElectroClean has 4 + inputs, E-Nickel Plate has 3, Soak Clean has 2. Our + `input_template_ids` One2many already handles this. Confirmed + by real data. + +27. **"PASS" entries can be standalone (no table)** — Porosity Test + just prints "Results: PASS" inline, no input table. This is a + Steelhead simplification. We should normalize: every step with + a captured input renders a table, even if it's a single + pass/fail row. Cleaner audit trail. + +28. **Move-By trail is the operator chain-of-custody. CRITICAL for + aerospace / Nadcap.** Our existing `fp.job.step.signoff_user_id` + + `date_finished` already provides this. The report just walks + them in time order. + +## Screenshot 23 — Page 7/7 of CoC: Final Inspection / Packaging + Ready For Shipping + +Step transitions on this page: +- **Ready For Final Inspection / Packaging** — Moved By: Brett Kinzett, Sep 22, 2025 08:42:22 AM (heading-only, gating) +- **Final Inspection / Packaging** — Moved By: Brett Kinzett, Sep 23, 2025 11:39:47 AM (heading + measurement table) +- **Ready For Shipping** — Moved By: Brett Kinzett, Sep 24, 2025 01:45:49 PM (heading-only, gating) +- (unnamed transitions) — Sep 24, 2025 02:03:29 PM and 02:06:16 PM + +**Final Inspection / Packaging** sub-table: +| Name | Description | Value | Recorded By | +|---|---|---|---| +| Outgoing Part Count | (blank) | PASS | Brett Kinzett | +| Thickness Test Pass / Fail | (blank) | PASS | Brett Kinzett | +| Qty Accepted | (blank) | 1 | Brett Kinzett | +| Qty Rejected | (blank) | 0 | Brett Kinzett | +| Actual Coating Thickness - Final | (blank) | 0.0018 | Brett Kinzett | + +Notes: +- "Outgoing Part Count" with value "PASS" is odd — looks like Steelhead conflated a count check with a pass/fail input. Our model should keep them separate (`Qty Accepted` integer + `Outgoing Part Count Verified` boolean). +- "Qty Accepted" + "Qty Rejected" are the **final disposition split** at packaging time. Non-negotiable for AS9100 shipment audit. +- "Actual Coating Thickness - Final" duplicates the mid-job thickness reading captured at E-Nickel Plate. Steelhead re-takes it at final inspection as a sanity-check before sign-off. +- The two unnamed transition rows at the bottom are likely "Shipped" / "Closed" gating moves whose headings got cut off the screenshot. + +## Screenshot 24 — Cert sign-off footer (last block on the CoC) + +Two side-by-side panels: + +**Left panel — "Certified By:"** +- Embedded signature image (handwritten "K Pathinathn" — Kris Pathinather signed) +- Printed name below: "Name: Kris Pathinather" + +**Right panel — "Certification Statement: Ref. WO# 620"** (top half) + **"Other Comments:"** (bottom half), both blank-but-bordered for handwritten or template-rendered cert language. + +This is the **certificate of conformance attestation block** — a +named individual takes legal responsibility for the cert. Required +for Nadcap, AS9100, CGP. Already supported by our existing +`fp.certificate.signoff_user_id` + signature image (S18). Just need +to surface the signed image + the printed name + the cert statement +in our PDF footer. + +The "Certification Statement" body in our existing CoC is already +authored on the certificate template — Steelhead leaves it blank in +this screenshot suggesting the full statement was on a different +page. We DO render it (boilerplate "We certify that the parts +conform to specification…" language). + +## Patterns updated after screens 23–24 + +29. **Final inspection captures the disposition split** — Qty + Accepted / Qty Rejected as integer fields at the last QC step. + These should appear as `input_template_ids` on a "Final + Inspection" library step (sane defaults). Plus a redundant + "final coating thickness" reading. We model these as standard + operation-measurement inputs. + +30. **Certificate sign-off block is a 2-column footer**: + left = signature image + name, right = cert statement + other + comments. Already supported by `fp.certificate` (S18). Just + confirm our PDF template arranges them this way. + +31. **CoC renders gating "Shipped / Closed" transitions** as + chain-of-custody entries with no measurement tables — same + treatment as "Ready For X" gating nodes. + +## Screenshot inventory closed (24 screenshots logged total) + +All Steelhead screens captured. The data model + UI implications +have been folded into the Pending-Questions section below. We can +now resume the brainstorm with the full picture in hand. + +## Pending questions to ask user once screenshots are done + +- What other Transfer Type values exist? (Hold / Scrap / Rework / Return / Splits?) +- Does Billed Labor show up on every screen or only certain ones? +- Are From Station / To Station always editable, or read-only when single-station nodes? +- What does the camera icon save against — the part record, the transition log, or the job? +- Is "Number of Customer WOs" a free-text count or a multi-select of existing customer POs? +- Is there a way to **split** a move (e.g. send 40 to next step, hold 9)? Steelhead's Part Count selector hints at it. diff --git a/fusion_plating/docs/superpowers/specs/2026-04-27-sub12-simple-recipe-editor-design.md b/fusion_plating/docs/superpowers/specs/2026-04-27-sub12-simple-recipe-editor-design.md new file mode 100644 index 00000000..cb3da6a3 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-04-27-sub12-simple-recipe-editor-design.md @@ -0,0 +1,827 @@ +# Sub 12 — Simple Recipe Editor + Step Library + Tablet Move/Rack + Reports + +**Status**: design ready for implementation planning +**Date**: 2026-04-27 +**Sliced into 3 sequential sub-projects**: 12a → 12b → 12c +**Companion file**: [Steelhead screen inventory](2026-04-27-simple-recipe-editor-steelhead-screens.md) — 24 screenshots logged with field-by-field notes that drove these decisions. + +--- + +## 1. Why this sub-project exists + +Two facts forced the design: + +1. **The customer has two operator personas with opposite preferences.** "Tree-loving" engineers like our existing OWL tree editor (`fusion.plating.process.node`, hierarchical recipe → sub-process → operation → step, drag-drop tree). "Simple-loving" foremen find the tree intimidating and want a flat ordered drag-drop list with a step library on the side. Forcing one persona to use the other's tool blocks adoption. + +2. **The customer is migrating off Steelhead** and brought 24 screenshots showing what their team is used to: a flat step library + a 2-column drag-drop recipe builder + Move-Parts / Move-Rack / Stop-Timer dialogs at the tablet + a chronological CoC traveller PDF. Their authoring UX is simpler than ours; their runtime UX is roughly equivalent but has gaps we can improve on (every blocker should have an inline resolution button). + +Plus: the existing tree editor + 205+ live `fp.job` records + 1800+ `fp.job.step` records + the entire shopfloor runtime + the S14 predecessor lock + the S19 Fischerscope merge + the Sub 11 MRP cutout — all already shipped, all working — must keep working unchanged. + +The design satisfies both personas without forking the data model. **Same recipe data, two editor views, same tablet runtime + reports.** + +--- + +## 2. Locked decisions (Q1–Q8 from the brainstorming session) + +| Q | Decision | +|---|---| +| Q1 — Editor strategy | **Hybrid.** Keep the existing OWL tree editor, build the simple editor alongside. Both edit the same recipes. | +| Q2 — Data model fork | **No fork.** Both editors operate on the same `fusion.plating.process.node` records. Edits in either editor update the same recipe. | +| Q3 — Step library | **New model `fp.step.template`** as a dedicated reusable step library. Surfaced under Plating → Configuration → Step Library. | +| Q4 — Library import semantics | **Snapshot copy.** Dragging a library step into a recipe creates a fresh `fusion.plating.process.node` with the template's fields copied in. Editing the library template later does **not** mutate recipes already built. `source_template_id` carries the trace. | +| Q5 — Recipe templates ("starter recipes") | **`is_template` Boolean on the existing recipe node.** A recipe flagged as a template appears in the simple editor's "Import starter from template" dropdown. Importing snapshot-copies all child nodes. | +| Q6 — What's on a step template | **Mirror Steelhead screen 1 + Advanced expander with high-value extras.** Visible by default: Title, Stations, Operation Measurements, Instructions, Require QA. Advanced: icon, time/temp targets, voltage, viscosity, material callout, predecessor lock, rack/transition flags, transition inputs. | +| Q7 — Editor toggle | **Per-recipe `preferred_editor` (tree/simple/auto) + company-level `default_recipe_editor` setting + header buttons "Open in Tree Editor" / "Open in Simple Editor".** Authoring lead can build in tree, hand off to a foreman who edits in simple. | +| Q8 — Slicing strategy | **Three sequential sub-projects (12a → 12b → 12c).** Each independently shippable, each closes with a smoke test on entech. | + +--- + +## 3. Architecture overview + +``` + ┌────────────────────┐ + Tree Editor (existing OWL) ─────reads/writes──▶│ │ + │ Recipe data: │ + Simple Editor (12a, new OWL) ─────reads/writes──▶│ fusion.plating. │ + │ process.node │ + Step Library (12a, new model) ─────snapshots─────▶│ (hierarchical, │ + │ _parent_store) │ + │ │ + Recipe Template (12a, is_template)─────snapshots─────▶│ + new fields: │ + │ is_template │ + │ source_template_id│ + │ tank_ids │ + │ target ranges, │ + │ units, kind, │ + │ transition flags │ + └─────────┬──────────┘ + │ + ▼ + Same job-creation flow + (no runtime change) + │ + ▼ + ┌────────────────────┐ + Tablet (existing OWL) ─────operates on───▶│ fp.job.step │ + + Move Parts / Move Rack (12b) │ + new fields: │ + + Rack Parts sub-dialog (12b) │ current_rack_id │ + + Stop Timer dialog (12b) │ is_racked │ + + Soft/Hard block UX (12b) │ qty_at_step_* │ + └─────────┬──────────┘ + │ + ▼ + ┌────────────────────┐ + │ fp.job.step.move │ + │ (12b, NEW) │ + │ fp.rack (12b, NEW)│ + │ fp.labor.timer │ + │ (12b, NEW) │ + └─────────┬──────────┘ + │ + ▼ + Operator Traveller PDF (12c) ◀─renders from ──── recipe-order step list + Customer CoC PDF (12c) ◀─renders from ──── chronological move log + Labor History screen (12c) ◀─lists ─────────── fp.labor.timer +``` + +**Hard rule preserved**: every change is additive at the data layer. No FK drops, no model deletions, no ACL relaxations. Tree editor + every existing battle-test scenario + the Sub 11 MRP cutout + the Sub 12 (Quality / RMA) work all keep working unchanged. + +--- + +## 4. Sub 12a — Simple Recipe Editor + Step Library + +### 4.1 Scope + +Recipe authoring only. No runtime/tablet/report changes. **Estimated 4 days.** + +Customer outcome: authors can build / edit / clone recipes via a flat drag-drop editor with a step library on the side. They can flag any recipe as a starter template and import its full step list into a new recipe (snapshot copy). Tree editor untouched. + +### 4.2 Data model + +**New model: `fp.step.template`** (the reusable step library) + +| Field | Type | Notes | +|---|---|---| +| `name` | Char, required, translate | "Solvent Clean" | +| `code` | Char | optional short code, auto-uppercased | +| `description` | Html | rich-text instructions / WI reference | +| `icon` | Selection | reuses the 24-icon list from `fusion.plating.process.node` | +| `tank_ids` | M2M `fusion.plating.tank` | allowed stations ("Stations" column in screen 1) | +| `process_type_id` | M2O `fusion.plating.process.type` | bath / chemistry tie | +| `material_callout` | Char | "MID PHOS" — short string for traveller print; defaults to `process_type_id.name` | +| `time_min_target` | Float | lower bound (`time_unit`-aware) | +| `time_max_target` | Float | upper bound | +| `time_unit` | Selection (`sec` / `min` / `hr`) | default `min` | +| `temp_min_target` | Float | lower bound (`temp_unit`-aware) | +| `temp_max_target` | Float | upper bound | +| `temp_unit` | Selection (`F` / `C`) | default `F` | +| `voltage_target` | Float (optional) | electrolytic | +| `viscosity_target` | Float (optional) | bath quality | +| `requires_signoff` | Boolean | "Require QA" | +| `requires_predecessor_done` | Boolean | S14 lock support | +| `requires_rack_assignment` | Boolean | step-type flag → triggers Rack Parts sub-dialog at runtime | +| `requires_transition_form` | Boolean | step-type flag → opens transition form before Mark Done | +| `input_template_ids` | O2M `fp.step.template.input` | operation measurements | +| `transition_input_ids` | O2M `fp.step.template.transition.input` | compliance fields collected at move-time | +| `default_kind` | Selection | `cleaning / etch / rinse / plate / bake / inspect / racking / derack / mask / demask / dry / wbf_test / final_inspect / ship / gating` — drives sane-defaults seeding | +| `active`, `sequence`, `company_id` | (standard) | | + +**New model: `fp.step.template.input`** — operation measurements (recorded *during* a step) + +| Field | Type | Notes | +|---|---|---| +| `name` | Char, required | e.g. "Soak Clean Time" | +| `template_id` | M2O `fp.step.template` | parent | +| `input_type` | Selection | `text / number / boolean / selection / date / signature / time_hms / time_seconds / temperature / thickness / pass_fail` (typed inputs auto-format on the report) | +| `target_min` | Float | structured target lower bound | +| `target_max` | Float | structured target upper bound | +| `target_unit` | Char | "min" / "ºF" / "A" / "FT2" / "in" | +| `required` | Boolean | hard-block sign-off if blank | +| `hint` | Char | inline help | +| `selection_options` | Text | comma-separated when `input_type='selection'` | +| `sequence` | Integer | render order | + +**New model: `fp.step.template.transition.input`** — compliance fields collected *when leaving* a step + +| Field | Type | Notes | +|---|---|---| +| `name` | Char, required | "Customer WO #" / "Photo Evidence" / "Scrap Reason" | +| `template_id` | M2O `fp.step.template` | parent | +| `input_type` | Selection | `text / number / boolean / selection / date / signature / photo / location_picker / customer_wo` | +| `required` | Boolean | hard-block move if blank | +| `hint` | Char | | +| `selection_options` | Text | | +| `sequence` | Integer | | +| `compliance_tag` | Selection | `none / as9100 / nadcap / cgp / nuclear` — drives audit report filter | + +**Changes to existing `fusion.plating.process.node`** (all additive — zero impact on tree editor / runtime) + +| Field | Type | Notes | +|---|---|---| +| `is_template` | Boolean, default False | marks a recipe (when `node_type='recipe'`) as a starter template | +| `source_template_id` | M2O `fp.step.template`, optional, indexed | snapshot trace; set when a node is created by dragging a library step in | +| `tank_ids` | M2M `fusion.plating.tank` | mirrors what library steps carry | +| `material_callout` | Char | mirrors library | +| `time_min_target`, `time_max_target`, `time_unit` | (mirrors library) | | +| `temp_min_target`, `temp_max_target`, `temp_unit` | (mirrors library) | | +| `voltage_target`, `viscosity_target` | (mirrors library) | | +| `requires_rack_assignment`, `requires_transition_form` | Boolean | mirrors library | +| `default_kind` | Selection | mirrors library; auto-set when imported | +| `transition_input_ids` | O2M to existing `fusion.plating.process.node.input` (filtered by new `kind` field) | one model, two roles | +| `preferred_editor` | Selection (`tree` / `simple` / `auto`) | per-recipe editor choice (only meaningful when `node_type='recipe'`) | + +**Changes to `fusion.plating.process.node.input`**: +- Add `kind` Selection (`step_input` / `transition_input`), default `step_input` (existing rows backfill via post_init_hook). +- Add the same target-range fields as `fp.step.template.input`. + +**New: `res.config.settings.default_recipe_editor`** — Selection (`tree` / `simple`), default `tree`. Drives "New Recipe" button's editor choice. + +### 4.3 Simple Recipe Editor UI (OWL client action) + +Registered as `fp_simple_recipe_editor` in `web.client_actions`. Single-page, full-screen, tablet-friendly. + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ ← Back Recipe: ENP-ALUM-BASIC [Tree Editor] [Save] [More ▾]│ +├──────────────────────────────────────────────────────────────────────┤ +│ Title [ ENP-ALUM-BASIC ] │ +│ Code [ ENP_ALUM ] │ +│ Coating Config [ Electroless Nickel Mid-Phos ▾ ] │ +│ Part [ — none — ▾ ] │ +│ ☐ Use as starter template │ +│ │ +│ Import starter from template: [ Select template ▾ ] [Import] │ +├──────────────────────────────────────────────────────────────────────┤ +│ ┌─ Selected (drag to reorder) ─────────┐ ┌─ Step Library ──────────┐ │ +│ │ ⠿ 1. Part Verification [Edit ▾] │ │ 🔍 [Search…] │ │ +│ │ ⠿ 2. Solvent Clean SP-1 [Edit ▾] │ │ Acid Dip │ │ +│ │ ⠿ 3. Soak Clean SP-1 [Edit ▾] │ │ Bake │ │ +│ │ ⠿ 4. Rinse SP-2 [Edit ▾] │ │ E-Nickel Plate │ │ +│ │ ... │ │ ... │ │ +│ │ [+ Add Inline Step] │ │ [+ New Library Step] │ │ +│ └──────────────────────────────────────┘ └──────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**Behavior**: + +- **Drag from Library → Selected**: snapshot-copies the library step into the recipe as a new `fusion.plating.process.node` (`node_type=step`, `source_template_id` set, all author-defined fields copied, sane-default `input_template_ids` copied, all `transition_input_ids` copied). +- **Drag within Selected**: reorders by updating `sequence`. +- **Drag out / X button**: removes the step from the recipe (unlinks the node). +- **Station picker** per Selected row: dropdown of the step's `tank_ids` M2M. +- **Edit ▾** per row: expands inline panel with Title, Stations, Operation Measurements, Instructions, Require QA + an **Advanced** expander (icon, time/temp targets, voltage, viscosity, material callout, requires_predecessor_done, requires_rack_assignment, requires_transition_form, transition inputs). +- **+ Add Inline Step**: creates a one-off step in the recipe without touching the library. +- **+ New Library Step**: side panel to author a new `fp.step.template`. +- **Import starter from template**: dropdown of `fusion.plating.process.node` where `is_template=True AND node_type='recipe'`. Snapshot-copies all child steps preserving `sequence`. Confirms before replacing existing steps. +- **[Tree Editor]** button: switches to the existing tree editor on the same recipe. +- **Auto-save** every 5s when dirty + explicit Save. +- **Mobile/tablet responsive**: <900px width stacks columns. + +**Search/filter**: case-insensitive substring match against `name + code + description (plain-text)`. + +**Drag-drop**: HTML5 native dragstart/dragend, reuses helpers from existing tree editor. + +**Soft-validation** for predecessor lock: a Selected row with `requires_predecessor_done=True` placed before its predecessor highlights amber with a tooltip. Doesn't block save (S14 enforces at runtime), informs the author. + +### 4.4 Backend controller endpoints + +New file: `fusion_plating/controllers/simple_recipe_controller.py`. JSONRPC routes: + +| Route | Purpose | +|---|---| +| `POST /fp/simple_recipe/load` | recipe header + ordered step list | +| `POST /fp/simple_recipe/library/list` | all `fp.step.template` (search-filtered, company-scoped) | +| `POST /fp/simple_recipe/library/create` | new template | +| `POST /fp/simple_recipe/library/write` | update | +| `POST /fp/simple_recipe/library/delete` | unlink (soft if any node references it via `source_template_id`, hard otherwise) | +| `POST /fp/simple_recipe/step/insert` | insert (from library or blank) at position N | +| `POST /fp/simple_recipe/step/write` | inline edit | +| `POST /fp/simple_recipe/step/remove` | unlink from recipe | +| `POST /fp/simple_recipe/step/reorder` | bulk sequence update after drag-drop | +| `POST /fp/simple_recipe/template/list` | recipes where `is_template=True` (for Import dropdown) | +| `POST /fp/simple_recipe/template/import` | snapshot-copy all child nodes of a template recipe | + +All endpoints honor company multi-tenancy + ACL (`group_fusion_plating_supervisor` for write, `_operator` for read). + +### 4.5 Recipe form integration + +**On `fusion.plating.process.node` form view** (when `node_type='recipe'`): +- Header buttons: **Open in Simple Editor** + **Open in Tree Editor**. +- New "Editor Preference" Selection field (`tree` / `simple` / `auto`). +- Clicking a recipe in the menu list routes through `preferred_editor` (falls back to company `default_recipe_editor` if `auto`). +- "Use as starter template" checkbox surfaces `is_template` (visible to supervisors only). + +**Menu integration**: +- Plating → Operations → Process Recipes — existing list view; clicks route through preferred editor. +- Plating → Configuration → **Step Library** — NEW; CRUD on `fp.step.template`. + +### 4.6 Sane-default input seeding per `default_kind` + +| `default_kind` | Suggested `input_template_ids` | +|---|---| +| `cleaning` | Actual Time (time_seconds, "sec") + Actual Temperature (temperature, "°F") | +| `etch` | Actual Time + Actual Temperature | +| `rinse` | (none — sign-off only) | +| `plate` | Actual Time (time_hms, "min") + Actual Temperature + Plating Thickness (thickness, "in") | +| `bake` | Time In (text "HH:MM") + Time Out (text "HH:MM") + Actual Temperature | +| `racking` | Actual Qty (number, "each") | +| `derack` | Actual Qty | +| `inspect` | PASS/FAIL (pass_fail) | +| `final_inspect` | Outgoing Part Count Verified (boolean) + Qty Accepted (number, "each") + Qty Rejected (number, "each") + Actual Coating Thickness (thickness, "in") + Pass/Fail | +| `wbf_test` | PASS/FAIL | +| `mask` | Actual Qty | +| `demask` | (none) | +| `dry` | (none) | +| `ship` | Outgoing Qty (number, "each") | +| `gating` | (none) | + +Implemented via server method `fp.step.template._seed_default_inputs(self)`, idempotent, exposed as a "Seed Defaults" button on the library form. + +### 4.7 Migration / install + +**Module**: extend `fusion_plating` core (no new module). Bump to `19.0.10.0.0`. + +`post_init_hook` for 12a: +- Backfills `kind='step_input'` on all existing `fusion.plating.process.node.input` rows. +- Seeds `fp.step.template` with **18 starter templates** copied from the existing `ENP-ALUM-BASIC` recipe's child steps (Soak Clean, Rinse, Etch, Desmut, Zincate, Strip Zincate, Electroclean, Acid Dip, Water Break Test, Issue Panels, Racking, E-Nickel Plate, Hot Rinse, Drying, De-rack, Inspection, Final Inspection, Shipping). Each gets `default_kind` set + sane-default inputs seeded. Idempotent — won't re-seed if any `fp.step.template` rows already exist. + +### 4.8 Verification (smoke test on entech staging) + +1. Install module → step library auto-seeds 18 templates. +2. Plating → Configuration → Step Library — confirm 18 entries with sane-default inputs. +3. Plating → Operations → Process Recipes → New Recipe → Simple Editor. +4. Drag 5 library steps in, reorder via drag-drop, pick stations. +5. Save, close, reopen — data persists, sequence correct. +6. Click [Tree Editor] — same recipe opens in tree editor with all 5 steps under root. Edit step 3's name in tree editor, save, return to Simple Editor — change visible. +7. Mark a recipe as template, build a new recipe, "Import starter from template" — all steps copy in, snapshot. +8. Edit the original library step's name → previously-imported recipe steps DO NOT change (snapshot decoupling). +9. Run `bt_s2_*` battle tests on a job built from a Simple-Editor recipe — confirm runtime unaffected. + +--- + +## 5. Sub 12b — Move Parts / Move Rack Dialogs + Tablet Transition Capture + +### 5.1 Scope + +Tablet UX + transition-time data capture. Uses 12a's authored data; no new recipe-authoring UI. **Estimated 3–4 days.** + +Customer outcome: operators on the tablet get the Steelhead-style Move Parts / Move Rack flow with author-defined compliance prompts, station picker, photo evidence, and the soft/hard block UX — but with our improvement: **every blocker has a clickable resolution button**. + +### 5.2 Data model + +**New: `fp.rack`** — physical rack registry + +| Field | Type | Notes | +|---|---|---| +| `name` | Char, required | "Rack 3" | +| `code` | Char, required | "R-03" | +| `qr_code` | Char | scannable; defaults to `FP-RACK:` | +| `state` | Selection | `empty / loading / loaded / in_use / awaiting_unrack / out_of_service` | +| `current_part_count` | Integer (compute) | sum of part-batches currently on rack | +| `current_job_step_id` | M2O `fp.job.step` (compute) | current location | +| `current_tank_id` | M2O `fusion.plating.tank` (compute) | derived from current step | +| `tag_ids` | M2M `fp.rack.tag` | rack labels | +| `facility_id`, `work_center_id` | M2O | location | +| `material` | Selection (`steel / titanium / polypro / pvc / plastic / other`) | construction | +| `capacity_count` | Integer | max parts (soft warn) | +| `notes` | Text | maintenance / damage notes | +| `active`, `company_id` | (standard) | | + +**New: `fp.rack.tag`** — rack labels (M2M tag registry) + +| Field | Type | Notes | +|---|---|---| +| `name` | Char, required | "Rush" / "Customer-Amphenol" / "Hold-for-QC" | +| `color` | Integer | kanban color | + +**New: `fp.job.step.move`** — chain-of-custody transition log (one row per move) + +| Field | Type | Notes | +|---|---|---| +| `name` | Char, sequence `FP/MOVE/YYYY/NNNN` | | +| `job_id` | M2O `fp.job` | | +| `from_step_id` | M2O `fp.job.step` | source | +| `to_step_id` | M2O `fp.job.step` | destination | +| `from_tank_id` | M2O `fusion.plating.tank` | derived | +| `to_tank_id` | M2O `fusion.plating.tank` | operator's choice on multi-station node | +| `transfer_type` | Selection | `step / hold / scrap / rework / split / return` | +| `qty_moved` | Integer | partial-qty supported | +| `qty_available_at_move` | Integer | snapshot | +| `to_location` | Selection (`global / quarantine / staging_a / staging_b / shipping_dock / scrap_bin`) | | +| `photo_evidence_id` | M2O `ir.attachment` | inline-captured photo | +| `customer_wo_count` | Integer | optional | +| `rack_id` | M2O `fp.rack` | populated on rack-aware moves | +| `unrack_after_move` | Boolean | for derack steps | +| `moved_by_user_id` | M2O `res.users` | | +| `move_datetime` | Datetime | | +| `transition_input_value_ids` | O2M `fp.job.step.move.input.value` | compliance values captured | +| `chatter` | mail.thread | yes | + +**New: `fp.job.step.move.input.value`** — recorded transition-input values + +| Field | Type | Notes | +|---|---|---| +| `move_id` | M2O `fp.job.step.move` | parent | +| `template_input_id` | M2O `fp.step.template.transition.input` | what was asked (template-level) | +| `node_input_id` | M2O `fusion.plating.process.node.input` | snapshot of the authored prompt at job-creation time | +| `value_text` | Char | for text/selection | +| `value_number` | Float | for number | +| `value_boolean` | Boolean | for boolean | +| `value_date` | Datetime | for date | +| `value_attachment_id` | M2O `ir.attachment` | for photo/signature | + +**New: `fp.labor.timer`** — persistent labor timer (lifted from screens 9, 10) + +| Field | Type | Notes | +|---|---|---| +| `name` | Char, sequence `FP/TIMER/YYYY/NNNN` | | +| `user_id` | M2O `res.users` | operator | +| `job_id` | M2O `fp.job` | | +| `step_id` | M2O `fp.job.step` | step at start | +| `state` | Selection | `running / paused / stopped / reconciled` | +| `started_at`, `last_paused_at`, `stopped_at` | Datetime | | +| `total_paused_duration` | Float (compute) | sum of pauses | +| `accrued_seconds` | Integer (compute) | live for `running`, frozen otherwise | +| `billed_hrs` / `billed_min` / `billed_sec` | Integer | reconciled, default = accrued, editable on stop | +| `billed_pct` | Float (compute) | billed / accrued | +| `product_id` | M2O `product.product` | optional split-target product | +| `notes` | Text | | +| `chatter` | mail.thread | yes | + +Lifecycle: `running → paused → running → stopped → reconciled`. Stop Timer dialog (screen 10) opens on stop and lets the operator reconcile billed hrs/min/sec + optional product split (creating sibling timer rows). + +**Changes to existing `fp.job.step`**: + +| Field | Type | Notes | +|---|---|---| +| `move_ids` | O2M `fp.job.step.move` (inverse `from_step_id`) | history of moves out of this step | +| `current_rack_id` | M2O `fp.rack` | snapshot of rack on this step (when racked) | +| `is_racked` | Boolean (compute, stored) | `current_rack_id != False` | +| `qty_at_step_start` | Integer | sum of incoming move qty | +| `qty_at_step_finish` | Integer | sum of outgoing move qty | + +**Changes to `fp.job`**: + +| Field | Type | Notes | +|---|---|---| +| `qty_received` | Integer | from screen 16 traveller header | +| `qty_visual_inspection_rejects` | Integer | | +| `qty_rework` | Integer | | +| `special_requirements` | Text | from customer spec | +| `active_timer_ids` | O2M `fp.labor.timer` (inverse `job_id`, filtered by state) | for live displays | + +### 5.3 Move Parts dialog + +Trigger: operator taps `Move Parts` on a part-batch row in the tablet, OR scans a part QR while at a step. + +``` +┌────────────────────────────────────────────────────────┐ +│ Move Parts │ +├────────────────────────────────────────────────────────┤ +│ Part Count [ 49 ] Available: 49 │ +│ Part Number TEST225451 (link) │ +│ From Node Bake (link) │ +│ From Station Bake (link) ← shown if applicable│ +│ Transfer Type [ Step ▾ ] │ +│ To Node Adhesion Testing (link) │ +│ To Station [ Adhesion Testing ▾ ] ← shown if multi │ +│ To Location [ Global ▾ ] 📷 ← inline camera │ +│ │ +│ ── Compliance Prompts (author-defined) ── │ +│ • Customer WO # [ 731830 ] │ +│ • Photo Evidence [ Attach 📷 ] │ +│ • Scrap Reason [ none ▾ ] │ +│ │ +│ ─── Blockers ────────────────────────────────── │ +│ ⚠ Additional Spec Measurements required. │ +│ [ RECORD MEASUREMENTS ] ← OUR IMPROVEMENT │ +│ │ +│ Billed Labor ⏱ [Reset All Edits] │ +│ Kris Pathinather Timer: 7s (100% billed) │ +│ WO #4521 PN: TEST225451 Qty: 49 0 hrs 0 min 7 sec │ +│ │ +│ [ Cancel ] [ MOVE (49) ] │ +└────────────────────────────────────────────────────────┘ +``` + +**Behavior**: +- **System-derived top section**: Part Count (editable, max=available), Part Number, From Node, From Station (only if source step has a tank), Transfer Type, To Node, To Station (only if destination step's `tank_ids` count > 1), To Location. +- **Camera button next to To Location**: launches `getUserMedia` on tablet → captures photo → uploads as `ir.attachment` → links to the move. +- **Compliance Prompts section**: renders the destination step's `transition_input_ids` (snapshot from authored template). `required=True` prompts hard-block MOVE. +- **Blockers section** (NEW pattern, our improvement over Steelhead): a list of resolvable issues, each with an inline action button: + - "Spec measurements required" → opens spec input dialog inline → re-evaluates → clears. + - "Parts not racked" → opens **Rack Parts** sub-dialog (5.4). + - "Predecessor not done" (S14) → opens predecessor step's checklist. + - "Operation measurements missing" → opens operation input form on source step. +- **MOVE button state**: enabled only when all hard-blockers cleared + all required transition inputs filled. Disabled state shows tooltip listing blockers (improvement over Steelhead's silent disabled state). +- **Billed Labor section**: surfaces the operator's active `fp.labor.timer` for this WO with editable hrs/min/sec. +- **MOVE click**: creates `fp.job.step.move`, copies transition-input values, advances the part-batch, stops the active timer, advances `qty_done` on source step + `qty_at_step_start` on dest step. + +### 5.4 Rack Parts sub-dialog + +Trigger: from Move Parts when destination has `requires_rack_assignment=True` and operator hasn't picked a rack → "RACK PARTS" button. + +``` +┌────────────────────────────────────────────┐ +│ Rack Parts [QR scan]│ +├────────────────────────────────────────────┤ +│ To Rack [ Search Racks… ▾ ] │ +│ │ +│ Part Number Unit Amount │ +│ TEST225451 [ Count ▾] [ 49 ] Count │ +│ on WO 4521 (49) │ +│ │ +│ Billed Labor ⏱ │ +│ │ +│ [Cancel] [Save] [Save + Print] │ +└────────────────────────────────────────────┘ +``` + +- **Rack picker** filters to `state='empty'` by default; "show all" toggle bypasses. +- **QR scan button**: tablet camera + parses `FP-RACK:` → auto-fills "To Rack". +- **Save**: marks rack `state='loaded'`, sets `current_job_step_id`, returns to Move Parts dialog with rack populated, blocker cleared. +- **Save + Print**: same + prints rack travel ticket. +- **Unit + Amount**: defaults `Count` + Move Parts' Part Count. Editable for partial racking. + +### 5.5 Move Rack dialog + +Trigger: operator taps `MOVE RACK` on a rack row in the tablet. + +``` +┌────────────────────────────────────────────────────────┐ +│ Move Rack: tyut │ +├────────────────────────────────────────────────────────┤ +│ Rack Labels [ Rush ✕ ] [ + ] │ +│ Parts │ +│ • 49 TEST225451 Parts on WO 4521 │ +│ • 1 TEST225451 Parts on WO 4521 │ +│ │ +│ Type [ Step ▾ ] │ +│ To Node [ Soak Clean (SP-1) ▾ greyed ] │ +│ To Station [ Soak Clean (SP-1) ▾ ] │ +│ │ +│ Billed Labor ⏱ [Reset All Edits] │ +│ Kris Pathinather Timer: 6s (100% billed) │ +│ WO #4521 PN: TEST225451 Qty: 49 0 hrs 0 min 6 sec │ +│ WO #4521 PN: TEST225451 Qty: 1 0 hrs 0 min 0 sec │ +│ │ +│ [Cancel] [Save] │ +└────────────────────────────────────────────────────────┘ +``` + +- **Rack name in title** from `fp.rack.name`. +- **Parts list** read-only. +- **To Node** auto-derived (greyed); To Station shown only if multi-station. +- **Per-batch billed-labor split** for each batch. +- **Save** atomically creates one `fp.job.step.move` per batch, all linked by the same `rack_id`. + +### 5.6 Stop User Labor Timer dialog + +Trigger: operator taps the timer pause icon on the tablet without moving parts. + +Mirror screen 10. Reconcile billed hrs/min/sec + optional product split. Footer: Cancel / **Save** / **Save & Start New Timer**. + +### 5.7 Tablet runtime guards + +**Per-batch row in tablet station view**: +- `current_rack_id` set → MOVE PARTS button **disabled / greyed** with tooltip "Racked — use Move Rack instead." +- `current_rack_id` empty → MOVE PARTS enabled. + +**Plant overview gets two panes** (mirror screen 12): +- Top: **Racks** with `MOVE RACK` per row + bulk **UNRACK MULTIPLE**. +- Bottom: **Parts** with `MOVE PARTS` per row (greyed when racked) + **+ ADD NEW PARTS** + filters + search. + +**Soft/hard block protocol** (our protocol — improves on Steelhead): +- Amber banner + button enabled = soft (proceed with audit). +- Amber banner + button disabled + tooltip listing blockers = hard. +- Every blocker carries an **inline resolution button** that opens a form to resolve without leaving the dialog. +- Dialog re-evaluates blockers reactively after each resolution. + +### 5.8 Backend controller endpoints + +Extend `fusion_plating_jobs/controllers/tablet_controller.py`: + +| Route | Purpose | +|---|---| +| `POST /fp/tablet/move_parts/preview` | dialog payload (system fields + author prompts + blockers) | +| `POST /fp/tablet/move_parts/commit` | creates `fp.job.step.move`, advances qty, handles timer | +| `POST /fp/tablet/move_rack/preview` | multi-batch dialog payload | +| `POST /fp/tablet/move_rack/commit` | atomic multi-batch move tied to a rack | +| `POST /fp/tablet/rack_parts/commit` | assigns parts to a rack | +| `POST /fp/tablet/rack_parts/print` | prints rack travel ticket | +| `POST /fp/tablet/labor_timer/start` | start | +| `POST /fp/tablet/labor_timer/pause` | pause | +| `POST /fp/tablet/labor_timer/resume` | resume | +| `POST /fp/tablet/labor_timer/stop` | stop and open reconciliation | +| `POST /fp/tablet/labor_timer/reconcile` | save reconciled billed time + optional product split | + +ACL: `group_fusion_plating_operator` minimum. + +### 5.9 Migration / install + +Same module: extend `fusion_plating` core. Bump to `19.0.10.1.0`. + +`post_init_hook` for 12b: +- Seeds `fp.rack.tag` with 4 starter tags: "Rush", "Hold for QC", "Damaged", "Customer Sample". +- Backfills `fp.job.step.qty_at_step_start` from existing `qty_done` chain (idempotent). + +No data destruction. No FK drops. + +### 5.10 Verification (smoke test on entech staging) + +1. Open tablet, scan a part QR → tablet shows the part-batch row at its current step. +2. Tap `Move Parts` → dialog opens with system fields populated, dest step's authored transition prompts rendered. +3. Try MOVE with required prompt blank → button disabled, tooltip lists the blank prompt. +4. Fill prompt → MOVE re-enables. +5. Move to a step with `requires_rack_assignment=True` → amber blocker + RACK PARTS button. +6. Click RACK PARTS → sub-dialog → pick rack → save → blocker clears, MOVE re-enables. +7. Complete MOVE → `fp.job.step.move` row created, part-batch advanced, rack state updated. +8. Confirm tablet now shows MOVE PARTS button greyed out + MOVE RACK shown on the rack row. +9. Tap MOVE RACK → dialog with all batches → save → all advance atomically. +10. Tap timer pause → Stop Timer dialog → reconcile billed time → save → state moves to `reconciled`. +11. Run `bt_s1_*` through `bt_s17_*` battle tests — confirm no regressions on existing flows. + +--- + +## 6. Sub 12c — Reports + Persistent Labor Audit + +### 6.1 Scope + +Two PDF templates + a labor history screen. No new model surface — uses 12a + 12b data. **Estimated 3–4 days.** + +Customer outcome: +- **Operator Traveller PDF**: recipe-order, paper-style A4 landscape. Equivalent to Amphenol paper sheets (screens 16–18). +- **Customer CoC Traveller PDF**: chronological audit, branded, Nadcap stamped. Equivalent to Steelhead's CoC output (screens 19–24). +- **Labor History screen**: surfaces `fp.labor.timer` for billing audit, payroll reconciliation, and "who-was-on-what-when" forensics. + +### 6.2 Operator Traveller Report + +**File**: `fusion_plating_jobs/report/report_fp_job_traveller_v2.xml` +**Layout**: A4 landscape, multi-page, table-driven. +**Replaces**: existing `report_fp_job_traveller.xml` (S5/S18 minimal portrait — kept as fallback). + +Page structure mirrors Amphenol paper sheets: +- Header (every page): logo, WO# + barcode (Code 128), Date In, Due Date, Type, Order #, P.O. #, Customer + address. +- Item Information (page 1): Part #, Rev., Mat., Catg., S/N, Item-Name / Process Description, Qty Rec., Vis Insp, Rework, Special Requirements, Stamp/Date. +- Process-Sheet Header (page 1): recipe name, sub-process name, category, special req. +- Step Table (continues page-to-page): Step | Tank | Operation + Actual | Instruction | Unit | Material | Voltage | Viscosity | Time(min) | Temp | Stamp | Date. +- Footer (every page): WO# + Page n of total. + +**Data source**: walks `fp.job.step` ordered by `sequence` (recipe order). Each row pulls from the step's authored fields (target ranges, units, material callout) + leaves blank lines for the operator to pencil in actuals. + +**ir.actions.report**: `action_report_fp_job_traveller_v2`, paperformat A4 landscape. Smart button on `fp.job` form: **"Print Operator Traveller"**. + +### 6.3 Customer CoC Traveller Report + +**File**: `fusion_plating_certificates/report/report_fp_certificate_coc_v2.xml` +**Layout**: A4 portrait, multi-page, table-driven. +**Extends**: existing `fp.certificate` flow (S18/S19) — replaces minimal CoC body with chronological audit body. + +Page structure mirrors Steelhead's CoC (screens 19–24): +- Header (page 1): Part Number, Description, Quantity, WO#, PO#, Packing List, Date, Specification(s). +- Body: chronological list of step transitions, each rendered as: + - Step heading: ` ()`. + - "Part Number / Moved By / Time" line. + - If the step has captured input values, a 5-column table: **Name | Description | Target | Actual | Recorded By**. +- Last page: 2-column sign-off block (left = signed image + name, right = cert statement + comments). Footer: Nadcap logo + ENTECH logo + "Cert Created At: " + page n/total. + +**Critical improvements over Steelhead**: +1. **Target column is its own column** (Steelhead embeds "(5-10 min.)" in the input name). +2. **Out-of-range Actual values colour-coded red**, in-range green. +3. **Heading-only steps** (Ready For X, gating) render as compact 1-line transitions — no empty tables. +4. **Multi-day jobs**: each transition carries its own datetime; the report walks chronologically. +5. **Signature image** from `fp.certificate.signoff_user_id.x_fc_signature`. +6. **Configurable per-company**: Nadcap logo + brand logo as `res.company.x_fc_nadcap_logo` + `x_fc_company_brand_logo` (fall back to Fusion Plating logo). + +**Data source**: walks `fp.job.step.move` records ordered by `move_datetime`, NOT `fp.job.step` records ordered by `sequence`. Chain-of-custody view auditors expect. + +For each move: +- Render heading: ` ()` or ` ()`. +- Render "Part Number / Moved By / Time". +- If destination step had `input_template_ids` filled at move time, render the measurement table. + +**ir.actions.report**: `action_report_fp_certificate_coc_v2`, paperformat A4 portrait. Smart button on `fp.certificate` form: **"Generate Customer CoC PDF"**. + +Existing CoC merge with Fischerscope thickness PDF (S19) is preserved — `_fp_render_and_attach_pdf` keeps appending the Fischerscope page. + +### 6.4 Labor History Screen + +**Menu**: Plating → Operations → **Labor History**. + +**View**: list view on `fp.labor.timer`, grouped by `user_id` then `job_id`. + +**Columns**: Operator | Job | Step | State (badge) | Started | Stopped | Accrued (HH:MM:SS) | Billed | Billed % (bar) | Product. + +**Filters**: My timers / Today / This week / Reconciled / Pending reconciliation / By operator / By customer. + +**Group-by**: Operator / Job / Customer / Date. + +**Form view**: read-only timer history with chatter for operator notes. Manager-only fields: +- `billed_hrs / billed_min / billed_sec` editable (audit-logged). +- "Re-open for re-reconciliation" button — moves a `reconciled` timer back to `stopped`. + +**ACL**: +- Operator: read own timers, write own running/paused/stopped timers. +- Supervisor: read all team timers, write reconciliations. +- Manager: full edit including re-open. + +### 6.5 Backend support + +Extend `fusion_plating_certificates/models/fp_certificate.py`: +- New method `_fp_build_chronological_payload(self)` returns the ordered list of moves + measurement values for the QWeb template. +- Existing `_fp_render_and_attach_pdf` calls it and assembles the multi-page PDF. +- Existing Fischerscope merge logic (S19) untouched. + +Extend `fusion_plating_jobs/models/fp_job.py`: +- Property `traveller_v2_step_payload` returns ordered step list with target ranges + author-defined inputs for the operator traveller QWeb template. + +No new endpoints. + +### 6.6 Migration / install + +Bumps: +- `fusion_plating` → `19.0.10.2.0` +- `fusion_plating_jobs` → `19.0.7.0.0` +- `fusion_plating_certificates` → `19.0.6.0.0` + +`post_init_hook` for 12c: +- Creates `paperformat.fp_a4_landscape_traveller`. +- Sets `fp.certificate.report_template_id = action_report_fp_certificate_coc_v2` so existing certs auto-use the new layout. Old `report_fp_certificate_coc.xml` stays in place as fallback. +- Bumps `fp.certificate.version` field on existing rows (drives "regenerate PDF" prompt). +- Adds `res.company.x_fc_nadcap_logo` + `x_fc_company_brand_logo` placeholders. + +### 6.7 Verification (smoke test on entech staging) + +1. Open an in-flight job. Click **Print Operator Traveller** → A4 landscape PDF renders with header, item info, process-sheet header, step table with target ranges + blank actual lines + tank codes. +2. Verify Code 128 barcode on header reads correctly with a phone scanner. +3. Take a completed job (ENP-ALUM-BASIC, ~25 transitions). Open its `fp.certificate`. Click **Generate Customer CoC PDF** → portrait PDF renders chronologically with WO header, transitions in time order, per-step measurement tables with Target + Actual columns, sign-off block at end with signature + Nadcap + ENTECH logos. +4. For a step where actual was out-of-spec (soak clean ran 12 min, target 4-6) → confirm Actual cell renders red. +5. Run `bt_s19_fischer_merge.py` → confirm Fischerscope PDF appends as page N+1. +6. Plating → Operations → Labor History → see all timers from smoke-test jobs. Group by Operator → expand → see hrs/min/sec breakdown. Verify reconciliation form opens for a `stopped` timer. +7. Run all existing battle tests (`bt_s1` through `bt_s17`) — no regressions. + +### 6.8 Things to NOT do in 12c + +- Don't replace `report_fp_certificate_coc.xml` (legacy) — keep as fallback. +- Don't touch `fp.certificate.action_issue` flow — only the rendering layer changes. +- Don't add new model fields — 12a + 12b shipped everything we need. +- Don't try to merge Operator Traveller and Customer CoC into one report — different audiences, different layouts. +- Don't bake the cert statement into the CoC template — read from `fp.certificate.cert_statement` so it stays per-customer configurable. + +--- + +## 7. Cross-cutting concerns + +### 7.1 Manager-bypass context flags (preserved) + +The existing manager-bypass context-flag protocol stays as-is. New flags added by 12b: + +| Flag | Skips | +|------|-------| +| `fp_skip_transition_form=True` | required transition input check on Move Parts | +| `fp_skip_rack_assignment=True` | rack-assignment check on `requires_rack_assignment` steps | + +All bypasses post to chatter with user name for audit (consistent with existing flags). + +### 7.2 ACL changes + +- `group_fusion_plating_supervisor` gets write on `fp.step.template` + `fp.rack` + `fp.rack.tag`. +- `group_fusion_plating_operator` gets read on `fp.step.template` + `fp.rack`, write on `fp.job.step.move` + `fp.labor.timer` (own only). +- `group_fusion_plating_manager` gets full access on all new models, plus the Re-open Reconciled Timer action. + +### 7.3 Multi-company + +All new models carry `company_id`. All new controllers honor `request.env.company`. Step library is **company-scoped** by default — a multi-shop customer authoring a recipe in Shop A doesn't see Shop B's library. A "shared library" cross-company toggle is **out of scope** (YAGNI). + +### 7.4 Performance + +- `fp.step.template` is a small table (< 1000 rows expected). +- `fp.rack` is small (< 100 rows expected). +- `fp.job.step.move` will be the biggest new table (~5–10 rows per job × 1000s of jobs/year = 50k rows/year). All key queries index on `(job_id, move_datetime)`. +- `fp.labor.timer` similar volume. Index on `(user_id, state, started_at)`. +- Simple Editor's `library/list` endpoint paginates server-side at 50 rows + supports search (no client-side filter on full library). + +### 7.5 Backwards compatibility + +- Tree editor: 100% unchanged. +- Existing `ENP-ALUM-BASIC` recipe: keeps working, retroactively gets `is_template=True` if customer wants (via post-install patch). +- Existing battle tests S1–S17 + S18/S19 cert flow: all keep working. +- Existing CoC report: stays as fallback. New CoC report is opt-in per customer. +- Existing tablet flows: keep working. New Move dialogs are opt-in via `requires_transition_form` flag on step templates (default False = legacy one-tap behavior). + +--- + +## 8. Build order (single-session executable checklist) + +1. Read this design + the [screen inventory](2026-04-27-simple-recipe-editor-steelhead-screens.md). +2. Confirm Sub 11 (MRP cutout) + Sub 12 quality-native work + Subs 1–10 fine-tuning all shipped. Check `fusion_plating` version ≥ `19.0.9.3.0` (after the tank state-control work shipped this session). +3. **Sub 12a** (recipe authoring): + 1. Bump `fusion_plating/__manifest__.py` to `19.0.10.0.0`. + 2. Add models: `fp.step.template` + `fp.step.template.input` + `fp.step.template.transition.input`. + 3. Extend `fusion.plating.process.node` with the additive fields. + 4. Extend `fusion.plating.process.node.input` with `kind` + target-range fields. + 5. Build OWL `fp_simple_recipe_editor` client action + SCSS + XML template. + 6. Build `simple_recipe_controller.py` JSONRPC endpoints. + 7. Recipe form: header buttons + `preferred_editor` field + `is_template` checkbox. + 8. Menu: Plating → Configuration → Step Library. + 9. `res.config.settings.default_recipe_editor` field. + 10. `post_init_hook`: backfill `kind='step_input'` + seed 18 starter library templates from ENP-ALUM-BASIC. + 11. Smoke test → deploy → verify on entech. +4. **Sub 12b** (tablet move/rack/timer): + 1. Bump `fusion_plating/__manifest__.py` to `19.0.10.1.0`. + 2. Add models: `fp.rack` + `fp.rack.tag` + `fp.job.step.move` + `fp.job.step.move.input.value` + `fp.labor.timer`. + 3. Extend `fp.job.step` + `fp.job` with the new fields. + 4. Build OWL Move Parts dialog + Move Rack dialog + Rack Parts sub-dialog + Stop Timer dialog (extend tablet OWL). + 5. Extend tablet plant-overview pane: Racks section + Parts section. + 6. Build `tablet_controller.py` extension: 11 new endpoints. + 7. Implement runtime guards (rack-vs-parts duality, soft/hard block). + 8. `post_init_hook`: seed 4 starter rack tags + backfill `qty_at_step_start`. + 9. Smoke test → deploy → verify on entech. + 10. Run `bt_s1_*` through `bt_s17_*` — confirm no regressions. +5. **Sub 12c** (reports + labor history): + 1. Bump `fusion_plating` → `19.0.10.2.0`, `fusion_plating_jobs` → `19.0.7.0.0`, `fusion_plating_certificates` → `19.0.6.0.0`. + 2. Build `report_fp_job_traveller_v2.xml` + `ir.actions.report` + `paperformat.fp_a4_landscape_traveller` + smart button on `fp.job`. + 3. Build `report_fp_certificate_coc_v2.xml` + extend `_fp_render_and_attach_pdf` to use chronological payload + extend `_fp_build_chronological_payload`. + 4. Build Labor History screen: list + form + filters + group-by + ACL. + 5. Add `res.company.x_fc_nadcap_logo` + `x_fc_company_brand_logo` placeholders. + 6. `post_init_hook`: paperformat creation + cert template wiring. + 7. Smoke test → deploy → verify on entech. + 8. Run `bt_s19_fischer_merge.py` — confirm Fischerscope PDF appends. + +Each sub-project deploys independently with its own version bump and `-u` command. If 12b reveals issues, 12a stays shipped. If 12c reveals issues, 12a and 12b stay shipped. + +--- + +## 9. Things to NOT do (cross-cutting) + +- **Don't touch the existing tree editor**, its OWL file, or its 7 endpoints. Sub 12a's simple editor lives alongside, not on top. +- **Don't introduce a new module.** All work extends existing modules: `fusion_plating`, `fusion_plating_jobs`, `fusion_plating_certificates`. +- **Don't fork the recipe data model.** Both editors operate on the same `fusion.plating.process.node` records. +- **Don't make library imports live references.** Every drag-drop creates an independent snapshot. Editing the library never mutates an in-flight recipe. +- **Don't break the S14 predecessor lock, S15 bake gate, S17 scrap auto-hold, S18 cert flow, or S19 Fischerscope merge.** All of these continue to fire on jobs created from Simple Editor recipes. +- **Don't auto-install `requires_rack_assignment=True` or `requires_transition_form=True` on existing step templates.** New flags default to False so existing tablet flows are unaffected. Customer opts in step-by-step. +- **Don't hardcode any company branding** in PDFs. Logos read from `res.company` configurable fields with sensible fallbacks. +- **Don't introduce `'mrp'` or `'quality_control'` as a manifest dep.** Sub 11 and Sub 12-quality removed them; this work doesn't bring them back. + +--- + +## 10. Open items deferred to later sub-projects + +- **Spec measurements as a standalone authoring surface** (per part × step rule library) — surfaced by screen 15's hard-block warning. Belongs to a future "QC Spec Library" sub-project. For now, hard-block resolution opens whatever the existing spec input form is. +- **Multi-day audit chain on multi-shift jobs** — chain-of-custody report renders correctly today. Future work: shift-aware grouping. +- **Bulk operations** in Step Library (batch-edit station list across multiple steps) — out of scope for 12a. Customer can edit one at a time. +- **Step library import/export as YAML/JSON** — useful for cross-environment promotion but out of scope. Customer copies templates by re-creating in target env. +- **First-off / last-off QC** — already deferred from earlier sub-projects (S28-style); still deferred. +- **VEC machine auto-ingest** — already deferred; still deferred. +- **Spec measurement dialog UI** for hard-block resolution — built in a future sub-project. For now the resolution button opens the existing spec form. + +--- + +## 11. References + +- [Steelhead screen inventory](2026-04-27-simple-recipe-editor-steelhead-screens.md) — 24 screenshots, field-by-field +- Existing tree editor: `fusion_plating/static/src/js/recipe_tree_editor.js` (649 lines), `recipe_controller.py` (367 lines) +- Existing process node model: `fusion_plating/models/fp_process_node.py` (531 lines) +- Battle test scenarios driving constraints: S5, S6, S7, S14, S15, S17, S18, S19, S20 in `CLAUDE.md` +- Sub 11 cutout (MRP removal) decisions in `CLAUDE.md` § Sub 11 +- Sub 12 native quality work decisions in `CLAUDE.md` § Sub 12