# Sticker — Multi-part, Per-box, Internal/External Variants **Date:** 2026-05-13 **Module(s):** `fusion_plating_jobs`, `fusion_plating_reports` **Author:** Gurpreet (Nexa Systems Inc.) **Status:** Approved — ready for implementation plan ## Summary The box sticker (printed at SO level and at fp.job level) currently mishandles three real-world scenarios on multi-line orders: 1. **Silent thickness/SN merge bug.** When two SO lines share `(recipe, part, coating)` but differ in thickness or serial, the current `_create_fp_jobs` grouping collapses them into one `fp.job`. The job inherits the FIRST line's thickness/SN — the other line's values are silently dropped from the sticker (and eventually from the CoC). 2. **No per-box stickers.** A line with `qty = 5` prints one sticker showing `Qty: 5`. Operators want one physical label per box, with a `1 / 5`, `2 / 5`, ... indicator. 3. **No Internal variant.** The sticker always prints the customer-facing description (`_line.name`) in the Notes column. The shop floor wants a parallel variant that shows the internal ops description (`_line.x_fc_internal_description`, from Sub 2) instead. This spec covers all three as a single piece of work — they touch the same files and ship together. ## Goals / non-goals **Goals** - Multi-thickness / multi-SN lines split into separate `fp.job` records with correct WO-XXXXX-NN naming. - SO sticker and Job sticker render one page per physical box, with a `Box X / N` indicator replacing the current `Qty: N`. - New "Internal" variant for each sticker that prints the internal description in the Notes column. Existing variant becomes "External". - Both variants share the same inner template — only the Notes source differs. - Existing action XML IDs unchanged so bookmarks and binding records keep working. **Non-goals** - Per-physical-box serial number tracking (today's `x_fc_serial_id` is one per line, shared across all boxes in that line — that's fine). - Box-count override (today: 1 sticker per qty unit; if the shop packs 5 parts into 1 box, that's an operational choice the sticker doesn't try to encode). - Migration of pre-existing single-line, single-thickness jobs — they remain as-is. ## Current state (post Sub 11) ### Backend — `fusion_plating_jobs/models/sale_order.py` ```python # Inside _create_fp_jobs(), the grouping key: key = (recipe.id, part_id, coating_id) groups[key] = groups.get(key, ...) | line ``` Lines that share ALL THREE collapse into one `fp.job`. Sub 11's comment explicitly calls out the part_id+coating_id check ("sharing only the recipe is not enough — would put Part A's number on a cert covering both") but doesn't extend the same reasoning to thickness or SN. The thickness Many2one (`x_fc_thickness_id`) and serial Many2one (`x_fc_serial_id`) were added in Sub 5, after the grouping logic was last touched. ### Sticker — `fusion_plating_reports/report/report_fp_wo_sticker.xml` Two outer templates wrap a shared inner: - `report_fp_so_sticker` (bound to `sale.order` via `action_report_fp_so_sticker`) — iterates `so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)`, renders one inner per line. - `report_fp_job_sticker_template` (in `fusion_plating_jobs/report/report_fp_job_sticker.xml`, bound to `fp.job` via `action_report_fp_job_sticker`) — iterates `docs`, renders one inner per job. Neither outer accounts for `qty > 1` — each line/job produces exactly one inner render. The inner template `report_fp_wo_sticker_inner` sets variables and renders one page. The Notes content is fixed: ```xml ``` There is no way for an outer to override this — it's a hard read of `_line.name`. ## Architecture — the three changes ### Change 1 — Backend split: extend grouping key In `fusion_plating_jobs/models/sale_order.py`, in the method that builds the `groups` dict (currently `_create_fp_jobs` around line 424–441), extend the key tuple: ```python # Before key = (recipe.id, part_id, coating_id) # After thickness_id = ( 'x_fc_thickness_id' in line._fields and line.x_fc_thickness_id.id ) or False serial_id = ( 'x_fc_serial_id' in line._fields and line.x_fc_serial_id.id ) or False key = (recipe.id, part_id, coating_id, thickness_id, serial_id) ``` **Effect:** Lines that previously merged silently across different thicknesses or SNs now split into separate fp.jobs. WO-XXXXX-NN suffixes apply normally (driven by the existing `ordered_keys = sorted(...)` block — no change needed there). **Backwards compat:** Single-line SOs and same-(thickness, SN) multi-line SOs collapse identically to before. No data migration required. ### Change 2 — Per-box render in the inner template `fusion_plating_reports/report/report_fp_wo_sticker.xml`, in the `report_fp_wo_sticker_inner` template: 1. Move the variable-resolution + style block OUT of the per-page render (these don't change per box, so they don't need to repeat). 2. Wrap the `
` body in a box loop: ```xml
... existing structure ...
``` 3. Change the Qty row's value column to show `X / N` when `_qty_total > 1`: ```xml Qty: / ``` **Outer templates supply `_qty_total`:** - SO outer: `_qty_total = line.product_uom_qty` - Job outer: `_qty_total = job.qty` If `_qty_total` is missing/zero, fall back to `1` so single-box behavior is unchanged. ### Change 3 — Internal/External variants #### 3a. Inner template: override-or-fallback on `_notes_content` In `report_fp_wo_sticker_inner`, change the `_notes_content` set from a hard read to override-or-fallback (matches the existing pattern for `_so`, `_part`, etc.): ```xml ``` External outer templates don't set `_notes_content` → falls through to `_line.name` (unchanged External behavior). Internal outer templates pre-set `_notes_content` before t-calling the inner: ```xml ``` #### 3b. New outer templates + action records **SO Internal** — in `fusion_plating_reports/report/report_fp_wo_sticker.xml`: ```xml ``` **SO External** — existing `report_fp_so_sticker` template gets one addition: ``. No other logic change (no `_notes_content` set = External default). **Job Internal** — in `fusion_plating_jobs/report/report_fp_job_sticker.xml`: ```xml ``` **Job External** — existing `report_fp_job_sticker_template` template gets one addition: ``. **Action records — labels + new XML IDs** In `fusion_plating_reports/report/report_actions.xml`: ```xml External Sticker ... Internal Sticker sale.order qweb-pdf fusion_plating_reports.report_fp_so_sticker_internal fusion_plating_reports.report_fp_so_sticker_internal 'Internal Sticker - %s' % (object.name or '').replace('/', '-') report ``` In `fusion_plating_jobs/report/report_fp_job_sticker.xml`: ```xml External Job Sticker ... Internal Job Sticker fp.job qweb-pdf fusion_plating_jobs.report_fp_job_sticker_internal_template fusion_plating_jobs.report_fp_job_sticker_internal_template 'Internal Job Sticker - %s' % (object.name or '').replace('/', '-') report ``` ## Files touched | # | File | Change | |---|------|--------| | 1 | `fusion_plating_jobs/models/sale_order.py` | Extend grouping key in `_create_fp_jobs` (+5 lines) | | 2 | `fusion_plating_reports/report/report_fp_wo_sticker.xml` | Inner template: box loop, Qty row logic, `_notes_content` fallback chain. SO outer: add `_qty_total`. NEW: SO Internal outer template. | | 3 | `fusion_plating_reports/report/report_actions.xml` | Rename existing SO action label. NEW: SO Internal action record. | | 4 | `fusion_plating_jobs/report/report_fp_job_sticker.xml` | Job outer: add `_qty_total`. Rename existing job action label. NEW: Job Internal outer template + action record. | | 5 | `fusion_plating_jobs/__manifest__.py` | Version bump | | 6 | `fusion_plating_reports/__manifest__.py` | Version bump | ## Migration None required. - **New grouping key (`_create_fp_jobs`)** is purely additive — existing jobs are protected by the existing `if existing: return` idempotency guard. Single-line and same-(thickness, SN) multi-line SOs collapse identically to before. - **Existing XML IDs unchanged** — bookmarks / `binding_model_id` records keep working. Only the visible label flips. - **New variants** appear in the Print menu on next module upgrade with no data work. ## Testing ### Scenario 1 — Multi-thickness split (new fp.jobs) Create a new SO with two lines: - Line 10: Part A, Coating X, Thickness 0.3-0.5 mils, qty 2 - Line 20: Part A, Coating X, Thickness 0.5-1.0 mils, qty 1 Confirm SO → 2 fp.jobs are created: - `WO-XXXXX-01`: qty 2, thickness 0.3-0.5 - `WO-XXXXX-02`: qty 1, thickness 0.5-1.0 Print each job's External sticker → confirm correct thickness on each. ### Scenario 2 — Per-box rendering Take Scenario 1's SO, click "Print → External Sticker" on the SO. Confirm: 3-page PDF. - Page 1: Line 10 box 1 → Qty row shows `1 / 2` - Page 2: Line 10 box 2 → Qty row shows `2 / 2` - Page 3: Line 20 box 1 → Qty row shows `1` ### Scenario 3 — Internal variant On the same SO, click "Print → Internal Sticker". Confirm: same 3 pages, same WO#/PO#/Customer/Part#/SN/Thickness/Qty, but the Notes column shows `x_fc_internal_description` from each line instead of `name`. If `x_fc_internal_description` is blank on a line, Notes shows `-`. ### Scenario 4 — Regression check (existing single-line) Re-print SO-30019 (1 line, qty 1) → External sticker prints single-page, no `X / N` indicator, Notes shows `_line.name` as before. Internal variant: single-page, Notes shows `x_fc_internal_description` or `-`. ### Scenario 5 — Job-level multi-box Take any existing fp.job with `qty = 3`. Print External Job Sticker. Confirm: 3 pages, `1/3`, `2/3`, `3/3`. Internal Job Sticker also 3 pages with the line's internal description in Notes. ### Scenario 6 — Action menu visibility On a sale order Print menu: both "External Sticker" and "Internal Sticker" appear. On an fp.job Print menu: both "External Job Sticker" and "Internal Job Sticker" appear. ## Out-of-scope items (deferred) - **Per-box SN registry.** Today `x_fc_serial_id` is one per line. If the customer needs unique SNs per physical box (5 parts = 5 SNs), build out an `fp.box.serial` registry that links to the line. Out of scope for this spec — would need workflow design (UI for assigning, where SNs print, etc.). - **Box count ≠ qty.** Some shops pack multiple parts per box. Today this spec assumes 1 sticker per qty unit. If needed, add an `x_fc_box_count` field on the line that defaults to qty but can be overridden, and the sticker loops over box_count instead. Defer until requested. - **Sticker preview UI in the form view.** No live preview today; operators print + visually verify. Defer. ## Open questions None — all decisions locked at spec time: | Q | Decision | |---|---| | Add SN to grouping key? | **Yes.** Same reasoning as thickness — silent merge of different SNs is a compliance hole. | | Per-box indicator location? | **Replace Qty row value.** Operator's confirmation: "we can use the quantity field portion for the box, there is room we can use rather than creating another line below and making everything smaller." | | Box indicator format? | **`1 / 5`** (slash, spaces around for legibility at 50pt). When qty=1, show plain `1` (no slash) — matches current behavior. | | Label naming convention? | **Prefix.** `External Sticker` / `Internal Sticker` (SO Print menu), `External Job Sticker` / `Internal Job Sticker` (fp.job Print menu). | | Migration for existing jobs? | **None.** Idempotency guard in `_create_fp_jobs` protects them. | | Existing action XML IDs? | **Unchanged.** Only labels rename — bookmarks/binding records survive. | | Fractional qty? | Cast to `int(qty)` — current behavior preserved. | | Qty=0 line? | Already filtered out by `lambda l: l.x_fc_part_catalog_id` (no part → no sticker). |