docs(sub12): simple recipe editor + library + tablet move/rack + reports

Three-part design (12a/12b/12c) for adding a flat drag-drop recipe
editor alongside the existing tree editor, with a reusable step
library, Steelhead-style Move Parts/Rack/Stop-Timer dialogs, and
recipe-order + chronological CoC PDF reports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-27 19:23:25 -04:00
parent 66cfe5f97f
commit 4e4ca2c9da
2 changed files with 1800 additions and 0 deletions

View File

@@ -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 78) |
**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:<id>` token resolver.
## Patterns updated after screens 48
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 913
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 1415
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 68 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 1618
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 1922 — 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: <user> Time:
<datetime>" 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 45 — 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: <date>". 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
`<Step Name> (<Tank code>)` — 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 1922
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 2324
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.

View File

@@ -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 (Q1Q8 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 34 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:<code>` |
| `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:<code>` → 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 34 days.**
Customer outcome:
- **Operator Traveller PDF**: recipe-order, paper-style A4 landscape. Equivalent to Amphenol paper sheets (screens 1618).
- **Customer CoC Traveller PDF**: chronological audit, branded, Nadcap stamped. Equivalent to Steelhead's CoC output (screens 1924).
- **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 1924):
- 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: `<step.name> (<tank.code>)`.
- "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: <date>" + 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: `<from_step.name> (<from_tank.code>)` or `<to_step.name> (<to_tank.code>)`.
- 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 (~510 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 S1S17 + 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 110 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