feat(fusion_plating): box-level tracking (fp.box) + thermal job-sticker redesign

Box registry: new fp.box model (fusion_plating_receiving), one record per
received box, auto-created when a receiving is marked Counted (idempotent
_fp_sync_boxes — grows/shrinks with box_count_in, never touches an advanced
box). Status received -> racked -> in_process -> packed -> shipped, per-box
scannable QR (/fp/box/<id> controller). Backfill migration for receivings
counted before tracking shipped. Boxes list/kanban/form + receiving smart
button.

Job stickers redesigned (thermal label, 6x4 in / 152x102mm, mm layout @
paperformat dpi=96 so mm maps 1:1 in wkhtmltopdf — see rule 14):
- Internal Job Sticker = Layout A, ONE per job (shop notes from
  x_fc_internal_description, job QR).
- External Job Sticker = Layout B, ONE per fp.box (BOX n/N, per-box QR,
  factory company logo, customer-facing notes). Dynamic MASK badge
  (x_fc_masking_enabled) + BAKE block (x_fc_bake_instructions), length-tiered
  notes font. Display logic in fp.job._fp_sticker_data().

Also retains the SO/WO box-sticker MemoryError fix in report_fp_wo_sticker.xml
(per-box loop sourced from fp.receiving.box_count_in + 100-label safety cap).

Verified live on entech: 111 boxes backfilled (31 receivings), External renders
one page per box, Internal one per job, scan endpoint 303->login.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-03 13:21:54 -04:00
parent 951cad0f81
commit d531faad12
17 changed files with 827 additions and 83 deletions

View File

@@ -0,0 +1,129 @@
# Box-Level Tracking + Job Sticker Redesign — Design Spec
Date: 2026-06-03
Status: Approved (brainstormed with client), implementation in progress.
## Summary
Two coupled deliverables:
1. **Job sticker redesign** (thermal-label-friendly, 6×4 in / 152×102 mm):
- **Internal Job Sticker → Layout A** (stacked: identity band + full-width
instructions), printed **one per job**.
- **External Job Sticker → Layout B** (left identity rail + tall instructions
column), printed **one per box**, carrying the **box identity** (BOX n/N)
and a **per-box QR**. Shows the **factory logo** (`env.company.logo`).
2. **Box-level tracking**: a new `fp.box` registry, one record per received box,
auto-created at receiving, with a status workflow and per-box scannable QR.
## Decisions (locked with client)
| Q | Decision |
|---|---|
| Label size | Keep 6×4 in (152×102 mm). |
| Redesign goals | Readability/scan-speed + thermal print quality (no grey fills — solid-black bands + knockout white text; thick rules; bold sans). |
| Masking on label | **MASK badge** (on/off flag) when `sale.order.line.x_fc_masking_enabled` is true. No detail text. |
| Baking on label | **BAKE block** showing `sale.order.line.x_fc_bake_instructions` text, only when present. Also a BAKE flag for at-a-glance. |
| Notes source | Internal = `x_fc_internal_description`; External = SO line `name` (customer-facing). |
| Long notes | Notes-dominant zone, **length-tiered font shrink** to keep to **one label**, clip with "…see traveller" only in the extreme. |
| Factory logo | On **External only** (header), from `env.company.logo``logo_web` → company partner image. Internal stays clean. Thermal caveat: prefer a mono/high-contrast logo. |
| Box tracking depth | **Box registry** — per-box record, status, scannable QR. (Not box-contents.) |
| Internal copies | **One per job.** |
| External copies | **One per box.** |
| Box QR | **Per-box** — encodes `/fp/box/<id>`. |
## Label layouts (approved mockups)
Both labels: outer 0.9 mm border, `overflow:hidden` single-page guard, dynamic
blocks render only when their field has content.
**Layout A (Internal, per job):** full-width stacked rows —
`[logo | WO# band + INTERNAL tag | QR]``Part# + MASK/BAKE flags`
one-line field strip `Customer · PO · Qty · Due · Thk``BAKE` block →
`NOTES` (full width, `x_fc_internal_description`, length-tiered, bottom padding).
**Layout B (External, per box):** absolute two-column —
- Left rail (50 mm): `logo` → black band `WORK ORDER <wo> | BOX n / N`
`MASK/BAKE` flags → per-box QR → `Part#``Customer``PO/Qty``Due/Thk`.
- Right column: `BAKE` block → `NOTES` (customer description, length-tiered).
- Full-height divider (rail `border-right`). CUSTOMER copy.
Reference mockups (Chrome-rendered, true 6×4):
`~/Downloads/fusion_sticker_concepts/Sticker-A-Internal-LongNotes.*`,
`Sticker-B-External.*`. Final proof renders through entech wkhtmltopdf.
## `fp.box` model (fusion_plating_receiving)
| Field | Type | Notes |
|---|---|---|
| `name` | Char | Sequence, e.g. `BOX/<wo-or-recv>/01`. |
| `box_number` | Integer | n (1..N). |
| `box_count` | Integer | N (related/snapshot of receiving `box_count_in`). |
| `receiving_id` | M2O `fp.receiving` | Origin. ondelete cascade. |
| `sale_order_id` | M2O `sale.order` | Related from receiving. |
| `job_id` | M2O `fp.job` | Resolved (single-job SO = that job; multi-job = first/SO-level, see edge cases). |
| `partner_id` | M2O `res.partner` | Related (customer). |
| `state` | Selection | `received → racked → in_process → packed → shipped` (+ `lost`/`cancelled`). |
| `qr` | Binary/compute | Encodes `<base_url>/fp/box/<id>`. |
| `location_note` | Char | Optional free text "where is it now". |
| `scan_event_ids` | (phase 2) | Per-scan log — deferred. |
Constraints: `(receiving_id, box_number)` unique. Append-only-ish; state advances.
## Auto-create at receiving
When `fp.receiving.box_count_in = N` is set and the receiving is confirmed
(state hook — reuse the existing box-count chatter point at
`fp_receiving.py:~1191`), create/sync N `fp.box` rows (1..N), linked to the
receiving + resolved job. **Idempotent**: changing N adds/removes trailing rows
(never renumbers existing tracked boxes). Manager can regenerate.
## Scanning
- Controller route `/fp/box/<int:box_id>` → resolves the box, shows its job /
status, allows advancing state (received→…→shipped). Tie into the existing
shopfloor scan wedge (`request.env.user` attribution — no `tablet_tech_id`).
- **Reconciliation**: helper flags a receiving/job whose boxes haven't all
reached `shipped` (so none are lost — matches the "ship back in the same
boxes" Sub-8 rule).
## Label binding
- **External job sticker** (`fusion_plating_jobs.report_fp_job_sticker_template`):
iterate the job's `fp.box` records → **one label per box** (Layout B), each
with its `box_number/box_count` + per-box QR (`/fp/box/<id>`). Replaces the
current `range(box_count_in)` loop in `report_fp_wo_sticker_inner`. When a job
has no `fp.box` rows yet, fall back to a single label (BOX 1/1).
- **Internal job sticker** (`report_fp_job_sticker_internal_template`): **one per
job** (Layout A), job QR (`/fp/job/<id>`), no box loop.
- Shared inner keeps the 100-label hard safety cap (defense-in-depth from the
WO-30072 OOM fix).
## UI
- Boxes list + kanban (group by `state`) under **Operations**; form with state
buttons + scan QR.
- Smart buttons: box count on `fp.receiving` and `fp.job` forms.
## Module placement
- Model + auto-create + views/menu/ACL → `fusion_plating_receiving`.
- Scan controller → `fusion_plating_receiving` (or shopfloor).
- Label templates → `fusion_plating_jobs` (job stickers) + shared inner in
`fusion_plating_reports`.
## Edge cases / open
- **Multi-job SO** (one SO line → multiple jobs via serial/thickness grouping):
boxes are physical (per shipment/receiving). MVP links a box to the SO's
primary job; the external sticker prints the SO's boxes. Revisit if a real
multi-job-per-box case appears.
- **Box ↔ part for multi-part SO**: out of MVP (registry, not contents).
- Per-box qty/contents = future "registry + contents" upgrade.
## Deploy / verify
entech (LXC 111 / pve-worker5), `-u fusion_plating_receiving fusion_plating_jobs
fusion_plating_reports` with the revert-on-failure guard. Verify: render both
stickers for a real job through wkhtmltopdf; confirm auto-create on a test
receiving; scan a box id.