diff --git a/fusion_plating/docs/superpowers/plans/2026-05-13-sticker-multi-part-and-variants-plan.md b/fusion_plating/docs/superpowers/plans/2026-05-13-sticker-multi-part-and-variants-plan.md new file mode 100644 index 00000000..d6faa923 --- /dev/null +++ b/fusion_plating/docs/superpowers/plans/2026-05-13-sticker-multi-part-and-variants-plan.md @@ -0,0 +1,1048 @@ +# Sticker — Multi-part, Per-box, Internal/External Variants — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix multi-thickness/multi-SN grouping in `_fp_auto_create_job`, add per-box rendering to the sticker, and add a parallel Internal variant of each sticker that shows the internal description in the Notes column. + +**Architecture:** Three changes touching three files. Backend: extend the fp.job grouping key to include thickness + serial. Frontend: inner sticker template gains a `range(int(_qty_total))` loop and a fallback chain on `_notes_content`. New outer templates per variant pre-set `_notes_content` and `_qty_total`. Existing action XML IDs unchanged; new actions added for the Internal variants. + +**Tech Stack:** Odoo 19 (Python/QWeb XML), wkhtmltopdf 0.12, native fp.job model (post Sub 11 — no MRP), entech LXC 111 deployment. + +**Spec:** `docs/superpowers/specs/2026-05-13-sticker-multi-part-and-variants-design.md` + +--- + +## File Structure + +| File | Responsibility | Touch | +|---|---|---| +| `fusion_plating_jobs/models/sale_order.py` | `_fp_auto_create_job()` — owns the line→job grouping key | Modify ~lines 415–441 | +| `fusion_plating_jobs/tests/test_fp_job_extensions.py` | Existing test suite for SO→fp.job creation | Modify (add multi-thickness test) | +| `fusion_plating_reports/report/report_fp_wo_sticker.xml` | Inner template (shared by SO+Job stickers), SO outer templates, defaults block | Modify inner + SO outer + add SO Internal outer | +| `fusion_plating_reports/report/report_actions.xml` | Action records for sale.order-bound reports | Modify SO action label + add SO Internal action | +| `fusion_plating_jobs/report/report_fp_job_sticker.xml` | Job outer template + action record | Modify Job outer + Job action label + add Job Internal outer + Job Internal action | +| `fusion_plating_jobs/__manifest__.py` | Version | Bump | +| `fusion_plating_reports/__manifest__.py` | Version | Bump | + +--- + +## Deployment helpers + +Every task that changes code pushes to entech LXC 111 + upgrades. Reusable command blocks: + +**Push a file to entech:** +```bash +cat LOCAL_PATH | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > REMOTE_PATH'" +``` +where `REMOTE_PATH` mirrors the repo structure under `/mnt/extra-addons/custom/`. + +**Upgrade a module + restart:** +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u MODULE_NAME --stop-after-init\" && systemctl start odoo'" +``` + +**Clear asset cache after a template/XML change:** +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'" +``` + +**Render a sticker PDF via odoo shell** (substitute `TEMPLATE_REF` and `RECORD_ID`): +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/out.pdf +``` + +--- + +### Task 1: Backend — Add thickness + serial to fp.job grouping key + +**Goal:** Lines that differ in thickness or serial number now spawn separate fp.jobs instead of silently collapsing. + +**Files:** +- Modify: `fusion_plating_jobs/models/sale_order.py` (lines 415–441 in `_fp_auto_create_job`) +- Modify: `fusion_plating_jobs/tests/test_fp_job_extensions.py` (add new test method) +- Modify: `fusion_plating_jobs/__manifest__.py` (version bump) + +- [ ] **Step 1: Add failing test for multi-thickness split** + +Open `fusion_plating_jobs/tests/test_fp_job_extensions.py`. Find the class `TestSOConfirmCreatesJob` (contains `test_so_confirm_creates_job` ~line 289). Add this method to the class: + +```python +def test_so_confirm_splits_by_thickness(self): + """Two lines with same part+coating but DIFFERENT thicknesses + must produce TWO fp.jobs — silent merge was a compliance bug + (the second thickness's CoC would carry the first thickness).""" + SOL = self.env['sale.order.line'] + if 'x_fc_part_catalog_id' not in SOL._fields or 'x_fc_thickness_id' not in SOL._fields: + self.skipTest('plating fields not present') + partner_for_part = self.env['res.partner'].create({'name': 'SplitTestPartner'}) + part = self.env['fp.part.catalog'].create({ + 'name': 'SplitTestPart', + 'part_number': 'STP-1', + 'partner_id': partner_for_part.id, + }) + thickness_a = self.env['fp.coating.thickness'].create({ + 'value_min': 0.3, 'value_max': 0.5, 'uom': 'mils', + }) + thickness_b = self.env['fp.coating.thickness'].create({ + 'value_min': 0.5, 'value_max': 1.0, 'uom': 'mils', + }) + so = self.env['sale.order'].create({'partner_id': self.partner.id}) + SOL.create({ + 'order_id': so.id, 'product_id': self.product.id, + 'product_uom_qty': 2.0, 'price_unit': 10.0, + 'x_fc_part_catalog_id': part.id, + 'x_fc_thickness_id': thickness_a.id, + }) + SOL.create({ + 'order_id': so.id, 'product_id': self.product.id, + 'product_uom_qty': 1.0, 'price_unit': 10.0, + 'x_fc_part_catalog_id': part.id, + 'x_fc_thickness_id': thickness_b.id, + }) + so.action_confirm() + jobs = self.env['fp.job'].search([('sale_order_id', '=', so.id)]) + self.assertEqual(len(jobs), 2, + 'Lines with different thicknesses must spawn separate fp.jobs') + thicknesses = jobs.sale_order_line_ids.mapped('x_fc_thickness_id') + self.assertEqual(len(thicknesses), 2, + 'Each job should carry its own thickness via its linked SO line') +``` + +- [ ] **Step 2: Run test, expect FAIL** + +Push the test file to entech, then run: +```bash +cat fusion_plating_jobs/tests/test_fp_job_extensions.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/tests/test_fp_job_extensions.py'" + +ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags=/fusion_plating_jobs:TestSOConfirmCreatesJob.test_so_confirm_splits_by_thickness --stop-after-init\"'" 2>&1 | tail -30 +``` +Expected: `AssertionError: 1 != 2` (the current code collapses both lines into one job). + +- [ ] **Step 3: Modify the grouping key in `_fp_auto_create_job`** + +Open `fusion_plating_jobs/models/sale_order.py`. Find the comment block starting at line 415 (`# Group by (recipe, part, coating).`) and the loop body starting at line 426 (`for line in plating_lines:`). + +Replace this block: + +```python + # Group by (recipe, part, coating). Lines that share ALL THREE + # collapse into one WO. Sharing only the recipe is not enough — + # the WO header captures part_id and coating_config_id from + # first_line, and downstream the CoC prints the WO header's + # part_number on the customer-facing cert. Bundling Part A + + # Part B under one WO because they happen to share a recipe + # would put Part A's number on a cert covering both, which is + # a compliance bug (silent mis-attestation). + # No-recipe lines get their own group each. + groups = {} + unrecipe_idx = 0 + for line in plating_lines: + recipe = self._fp_resolve_recipe_for_line(line) + part_id = ( + 'x_fc_part_catalog_id' in line._fields + and line.x_fc_part_catalog_id.id + ) or False + coating_id = ( + 'x_fc_coating_config_id' in line._fields + and line.x_fc_coating_config_id.id + ) or False + if recipe: + key = (recipe.id, part_id, coating_id) + else: + unrecipe_idx += 1 + key = ('no_recipe', unrecipe_idx) + groups[key] = groups.get(key, self.env['sale.order.line']) | line +``` + +with: + +```python + # Group by (recipe, part, coating, thickness, serial). Lines that + # share ALL FIVE collapse into one WO. Same compliance reasoning + # as part_id + coating_id: bundling lines with different thicknesses + # or different serials under one WO would carry the first line's + # values onto the cert + sticker — silent mis-attestation. Sub 5 + # added thickness_id + serial_id; this fix extends the grouping + # logic to honour them. No-recipe lines get their own group each. + groups = {} + unrecipe_idx = 0 + for line in plating_lines: + recipe = self._fp_resolve_recipe_for_line(line) + part_id = ( + 'x_fc_part_catalog_id' in line._fields + and line.x_fc_part_catalog_id.id + ) or False + coating_id = ( + 'x_fc_coating_config_id' in line._fields + and line.x_fc_coating_config_id.id + ) or False + 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 + if recipe: + key = (recipe.id, part_id, coating_id, thickness_id, serial_id) + else: + unrecipe_idx += 1 + key = ('no_recipe', unrecipe_idx) + groups[key] = groups.get(key, self.env['sale.order.line']) | line +``` + +- [ ] **Step 4: Bump fusion_plating_jobs version** + +Open `fusion_plating_jobs/__manifest__.py`, find the `'version'` line, increment the patch component. e.g. `'19.0.8.22.10'` → `'19.0.8.23.0'`. + +- [ ] **Step 5: Push to entech + run test, expect PASS** + +```bash +cat fusion_plating_jobs/models/sale_order.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/models/sale_order.py'" +cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'" + +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_jobs --test-tags=/fusion_plating_jobs:TestSOConfirmCreatesJob --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -30 +``` +Expected: tests pass (both `test_so_confirm_creates_job`, `test_so_confirm_idempotent`, `test_so_confirm_splits_by_thickness`). + +- [ ] **Step 6: Commit** + +```bash +git add fusion_plating_jobs/models/sale_order.py \ + fusion_plating_jobs/tests/test_fp_job_extensions.py \ + fusion_plating_jobs/__manifest__.py +git commit -m "$(cat <<'EOF' +fix(jobs): split fp.jobs by thickness + serial on SO confirm + +The _fp_auto_create_job grouping key was (recipe, part, coating). +Lines that shared all three but differed in thickness (or serial) +silently collapsed into one fp.job — the second line's thickness/SN +was lost, and any downstream cert printed the first line's values +across both batches. Silent mis-attestation = compliance hole. + +Extended the key tuple to (recipe, part, coating, thickness, serial). +Single-line SOs and same-(thickness, SN) multi-line SOs collapse +identically to before. Only lines that previously merged when they +shouldn't have now split into their own fp.jobs. + +Added test_so_confirm_splits_by_thickness covering the case. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 2: Sticker template — defaults + Notes fallback + per-box loop + +**Goal:** Inner template renders N pages when `_qty_total > 1`, accepts an outer-supplied `_notes_content` override, defaults block initializes the new variables. + +**Files:** +- Modify: `fusion_plating_reports/report/report_fp_wo_sticker.xml` +- Modify: `fusion_plating_reports/__manifest__.py` + +- [ ] **Step 1: Add `_notes_content` + `_qty_total` to the defaults template** + +Open `fusion_plating_reports/report/report_fp_wo_sticker.xml`. Find the `report_fp_wo_sticker_defaults` template (around line 360). Inside it, after the last `` line, add: + +```xml + + +``` + +- [ ] **Step 2: Convert `_notes_content` to override-or-fallback in the inner template** + +Same file. Find the inner template's existing line: + +```xml + +``` + +Replace with: + +```xml + + +``` + +- [ ] **Step 3: Wrap the sticker body in a per-box loop** + +Same file. Find the `
` opening tag inside the inner template (after the `` close). Wrap the entire div in a `t-foreach` loop: + +```xml + + +
+ ... existing structure unchanged ... +
+
+``` + +The closing `
` for the foreach goes immediately after the closing `
` of the sticker. + +- [ ] **Step 4: Change Qty row to show "X / N" when qty>1** + +Same file. Find the Qty row in the inner template body: + +```xml + + Qty: + + + + + + +``` + +Replace with: + +```xml + + Qty: + + + + / + + + + + + + +``` + +- [ ] **Step 5: Bump fusion_plating_reports version** + +Open `fusion_plating_reports/__manifest__.py`, increment patch component. e.g. `'19.0.10.11.0'` → `'19.0.10.12.0'`. + +- [ ] **Step 6: Push + upgrade + clear cache** + +```bash +cat fusion_plating_reports/report/report_fp_wo_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_fp_wo_sticker.xml'" +cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'" + +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3 + +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'" +``` + +- [ ] **Step 7: Verify regression-free on existing single-line SO** + +Render the existing fp.job 2635 sticker (SO-30019, 1 line, qty 1): +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/sticker_t2.pdf +``` + +Read the PDF (`/tmp/sticker_t2.pdf`) — expected: single page, Qty row shows `1` (no slash), Notes shows `_line.name` content. Identical to the pre-Task-2 baseline. The job is qty=1 so `_qty_total = 1` → falls into the `t-else` branch. + +- [ ] **Step 8: Commit** + +```bash +git add fusion_plating_reports/report/report_fp_wo_sticker.xml \ + fusion_plating_reports/__manifest__.py +git commit -m "$(cat <<'EOF' +feat(sticker): per-box render loop + Notes override hook + +Inner sticker template gains two parameters that outer templates +pre-set: + + _qty_total — total qty for the line/job. Inner wraps the body + in t-foreach="range(int(_qty_total or 1))" so a qty=5 line + produces 5 consecutive single-box stickers. Qty row in the + body switches from "5" to "1 / 5", "2 / 5", ... "5 / 5". + When _qty_total is missing/0/1, the Qty row keeps showing + the plain integer (regression-free). + + _notes_content — Notes column source. Existing inner code + hard-read _line.name; new code accepts an outer override + and falls back to _line.name. External outers don't set it + (unchanged behaviour); the new Internal outers (Task 4+5) + pre-set it to x_fc_internal_description. + +Defaults template initialises both new vars to False so the +inner's "outer-supplied OR fallback" pattern doesn't NameError +when called from existing outers that haven't been updated yet. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 3: Wire `_qty_total` in External outer templates + +**Goal:** SO External + Job External outers now pass `_qty_total` so the inner's box loop activates. + +**Files:** +- Modify: `fusion_plating_reports/report/report_fp_wo_sticker.xml` (SO External outer) +- Modify: `fusion_plating_jobs/report/report_fp_job_sticker.xml` (Job External outer) +- Modify: `fusion_plating_reports/__manifest__.py` (version bump) +- Modify: `fusion_plating_jobs/__manifest__.py` (version bump) + +- [ ] **Step 1: Add `_qty_total` to SO outer template** + +Open `fusion_plating_reports/report/report_fp_wo_sticker.xml`. Find the `report_fp_so_sticker` outer template (around line 427). After the line: + +```xml + +``` + +Insert immediately below: + +```xml + +``` + +- [ ] **Step 2: Add `_qty_total` to Job outer template** + +Open `fusion_plating_jobs/report/report_fp_job_sticker.xml`. Find the `report_fp_job_sticker_template` template (around line 43). After the line: + +```xml + +``` + +Insert immediately below: + +```xml + +``` + +- [ ] **Step 3: Bump both module versions** + +`fusion_plating_reports/__manifest__.py`: increment patch (e.g. `19.0.10.12.0` → `19.0.10.13.0`). +`fusion_plating_jobs/__manifest__.py`: increment patch (e.g. `19.0.8.23.0` → `19.0.8.24.0`). + +- [ ] **Step 4: Push + upgrade both modules + clear cache** + +```bash +cat fusion_plating_reports/report/report_fp_wo_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_fp_wo_sticker.xml'" +cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'" +cat fusion_plating_jobs/report/report_fp_job_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/report/report_fp_job_sticker.xml'" +cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'" + +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports,fusion_plating_jobs --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3 + +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'" +``` + +- [ ] **Step 5: Verify multi-box rendering via odoo shell** + +Bump fp.job 2635's `qty` to 3 temporarily, render, then restore. Use odoo shell: +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/sticker_t3_multibox.pdf +``` + +Read the PDF. Expected: 3 pages. Each shows `Qty: 1 / 3`, `Qty: 2 / 3`, `Qty: 3 / 3` respectively. WO# header identical on all 3 (same fp.job, different physical box). + +- [ ] **Step 6: Commit** + +```bash +git add fusion_plating_reports/report/report_fp_wo_sticker.xml \ + fusion_plating_reports/__manifest__.py \ + fusion_plating_jobs/report/report_fp_job_sticker.xml \ + fusion_plating_jobs/__manifest__.py +git commit -m "$(cat <<'EOF' +feat(sticker): wire _qty_total in SO + Job External outers + +Activates the per-box loop landed in the prior commit. SO External +reads line.product_uom_qty; Job External reads job.qty. Inner +template now renders one sticker per physical box, marking each +with "X / N" in the Qty row. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 4: SO Internal variant — template + action + +**Goal:** New "Internal Sticker" action on `sale.order` Print menu. Same layout as External, Notes shows `x_fc_internal_description`. + +**Files:** +- Modify: `fusion_plating_reports/report/report_fp_wo_sticker.xml` (add new outer template at end) +- Modify: `fusion_plating_reports/report/report_actions.xml` (add new action record) +- Modify: `fusion_plating_reports/__manifest__.py` (version bump) + +- [ ] **Step 1: Add SO Internal outer template** + +Open `fusion_plating_reports/report/report_fp_wo_sticker.xml`. Find the existing `report_fp_so_sticker` template. After its closing `` tag (before the final `` close), insert: + +```xml + + +``` + +- [ ] **Step 2: Add SO Internal action record** + +Open `fusion_plating_reports/report/report_actions.xml`. Find the existing `action_report_fp_so_sticker` record (around line 326). After its closing `` tag, insert: + +```xml + + + 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 + + +``` + +- [ ] **Step 3: Bump fusion_plating_reports version** + +`fusion_plating_reports/__manifest__.py`: increment patch (e.g. `19.0.10.13.0` → `19.0.10.14.0`). + +- [ ] **Step 4: Push + upgrade + clear cache** + +```bash +cat fusion_plating_reports/report/report_fp_wo_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_fp_wo_sticker.xml'" +cat fusion_plating_reports/report/report_actions.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_actions.xml'" +cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'" + +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3 + +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'" +``` + +- [ ] **Step 5: Verify SO Internal renders with internal description** + +Find an SO line with `x_fc_internal_description` populated (or seed one): +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/sticker_t4_internal.pdf +``` + +Read the PDF. Expected: single page, Notes column shows `INTERNAL: rework if any dings on flange. Buff per WI-104.` (NOT the customer-facing description). Header `SO-30019`, body fields identical to External variant. + +- [ ] **Step 6: Commit** + +```bash +git add fusion_plating_reports/report/report_fp_wo_sticker.xml \ + fusion_plating_reports/report/report_actions.xml \ + fusion_plating_reports/__manifest__.py +git commit -m "$(cat <<'EOF' +feat(sticker): add Internal Sticker variant on sale.order Print menu + +Same 3-cell + body layout as External; Notes column reads +x_fc_internal_description (Sub 2 internal-description field on the +SO line) instead of line.name. Shop floor gets ops-facing notes +without leaking them to the customer-facing variant. + +New action record action_report_fp_so_sticker_internal — binds to +sale.order, appears in the Print menu next to the existing External +sticker. New template report_fp_so_sticker_internal that pre-sets +_notes_content before t-calling the shared inner. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 5: Job Internal variant — template + action + +**Goal:** New "Internal Job Sticker" action on `fp.job` Print menu. Same layout as External Job Sticker, Notes shows `x_fc_internal_description` from the first linked SO line. + +**Files:** +- Modify: `fusion_plating_jobs/report/report_fp_job_sticker.xml` (add new outer template + action) +- Modify: `fusion_plating_jobs/__manifest__.py` (version bump) + +- [ ] **Step 1: Add Job Internal action record** + +Open `fusion_plating_jobs/report/report_fp_job_sticker.xml`. Find the existing `action_report_fp_job_sticker` record. After its closing `` tag (still inside ``), insert: + +```xml + + 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 + + +``` + +- [ ] **Step 2: Add Job Internal template** + +Same file. After the existing `report_fp_job_sticker_template` template's closing `` (still inside ``), insert: + +```xml + +``` + +- [ ] **Step 3: Bump fusion_plating_jobs version** + +`fusion_plating_jobs/__manifest__.py`: increment patch (e.g. `19.0.8.24.0` → `19.0.8.25.0`). + +- [ ] **Step 4: Push + upgrade + clear cache** + +```bash +cat fusion_plating_jobs/report/report_fp_job_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/report/report_fp_job_sticker.xml'" +cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'" + +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_jobs --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3 + +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'" +``` + +- [ ] **Step 5: Verify Job Internal renders with internal description** + +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/sticker_t5_job_internal.pdf +``` + +Read the PDF. Expected: single page (job qty=1), Notes shows `INTERNAL JOB: handle with care, no rework on this batch`. Header `WO-30019`, body fields identical to External Job sticker. + +- [ ] **Step 6: Commit** + +```bash +git add fusion_plating_jobs/report/report_fp_job_sticker.xml \ + fusion_plating_jobs/__manifest__.py +git commit -m "$(cat <<'EOF' +feat(sticker): add Internal Job Sticker variant on fp.job Print menu + +Mirror of the SO Internal variant for fp.job. Same body fields, +same per-box loop; Notes column reads x_fc_internal_description +from the first linked SO line (job.sale_order_line_ids[:1]). +Operator on the shop floor sees ops-internal notes without those +ever appearing on the customer-facing External sticker. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 6: Rename External action labels + +**Goal:** Print menu shows "External Sticker" (was "WO Box Sticker") on `sale.order`, "External Job Sticker" (was "Job Sticker") on `fp.job`. XML IDs unchanged — bookmarks/binding records keep working. + +**Files:** +- Modify: `fusion_plating_reports/report/report_actions.xml` +- Modify: `fusion_plating_jobs/report/report_fp_job_sticker.xml` +- Modify: `fusion_plating_reports/__manifest__.py` +- Modify: `fusion_plating_jobs/__manifest__.py` + +- [ ] **Step 1: Rename SO External action label** + +Open `fusion_plating_reports/report/report_actions.xml`. Find `action_report_fp_so_sticker`. Change: + +```xml + WO Box Sticker +``` + +to: + +```xml + External Sticker +``` + +Also update the `print_report_name` if it references the old label. The existing print_report_name is `'WO Sticker - %s' % ...` — change to: + +```xml + 'External Sticker - %s' % (object.name or '').replace('/', '-') +``` + +- [ ] **Step 2: Rename Job External action label** + +Open `fusion_plating_jobs/report/report_fp_job_sticker.xml`. Find `action_report_fp_job_sticker`. Change: + +```xml + Job Sticker +``` + +to: + +```xml + External Job Sticker +``` + +And update `print_report_name` from `'Job Sticker - %s' % ...` to: + +```xml + 'External Job Sticker - %s' % (object.name or '').replace('/', '-') +``` + +- [ ] **Step 3: Bump both module versions** + +`fusion_plating_reports/__manifest__.py`: increment patch (e.g. `19.0.10.14.0` → `19.0.10.15.0`). +`fusion_plating_jobs/__manifest__.py`: increment patch (e.g. `19.0.8.25.0` → `19.0.8.26.0`). + +- [ ] **Step 4: Push + upgrade + clear cache** + +```bash +cat fusion_plating_reports/report/report_actions.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_actions.xml'" +cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'" +cat fusion_plating_jobs/report/report_fp_job_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/report/report_fp_job_sticker.xml'" +cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'" + +ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports,fusion_plating_jobs --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3 + +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'" +``` + +- [ ] **Step 5: Verify label rename in DB** + +```bash +ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -t -A -c \\\"SELECT id, name, model FROM ir_act_report_xml WHERE id IN (SELECT res_id FROM ir_model_data WHERE module IN ('\''fusion_plating_reports'\'', '\''fusion_plating_jobs'\'') AND name LIKE '\''action_report_fp_%sticker%'\'') ORDER BY model, id;\\\"\"'" +``` + +Expected output: 4 rows. +- `External Sticker | sale.order` +- `Internal Sticker | sale.order` +- `External Job Sticker | fp.job` +- `Internal Job Sticker | fp.job` + +- [ ] **Step 6: Commit** + +```bash +git add fusion_plating_reports/report/report_actions.xml \ + fusion_plating_reports/__manifest__.py \ + fusion_plating_jobs/report/report_fp_job_sticker.xml \ + fusion_plating_jobs/__manifest__.py +git commit -m "$(cat <<'EOF' +chore(sticker): rename External action labels for the variant split + +Print menu now shows External + Internal as paired entries: + + sale.order: External Sticker / Internal Sticker + fp.job: External Job Sticker / Internal Job Sticker + +XML IDs unchanged (action_report_fp_so_sticker / +action_report_fp_job_sticker) so existing bookmarks and +binding_model_id records keep working. print_report_name strings +also updated so the downloaded filename matches the new label. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 7: End-to-end verification on a multi-line test SO + +**Goal:** All six spec scenarios pass on a fresh test SO with multi-line + multi-thickness + qty>1. Catch interaction bugs. + +**Files:** none modified (verification only). + +- [ ] **Step 1: Create test SO via odoo shell** + +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/sticker_t7_so_ext.pdf +``` + +Read PDF. Expected: 3 pages. +- Page 1: line_a box 1 of 2 — Qty `1 / 2`, Notes `TEST EXTERNAL A: VALVE BODY Rev A`, Thickness shows thick_a +- Page 2: line_a box 2 of 2 — Qty `2 / 2`, same other fields +- Page 3: line_b box 1 of 1 — Qty `1` (plain), Notes `TEST EXTERNAL B: VALVE BODY Rev B`, Thickness shows thick_b + +- [ ] **Step 3: Render SO Internal — expect 3 pages, different Notes** + +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/sticker_t7_so_int.pdf +``` + +Read PDF. Expected: 3 pages, same headers + body fields, but Notes: +- Pages 1+2: `TEST INTERNAL A: rework cell-1` +- Page 3: `TEST INTERNAL B: extra dip cell-3` + +- [ ] **Step 4: Render Job External + Internal for each spawned job** + +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/$base +done +ls /tmp/sticker_t7_job_*.pdf +``` + +Read each PDF. Expected: +- Job 1 (line_a, qty 2): External + Internal each render 2 pages with `1 / 2`, `2 / 2`. External Notes = `TEST EXTERNAL A: ...`, Internal Notes = `TEST INTERNAL A: ...`. +- Job 2 (line_b, qty 1): External + Internal each render 1 page with `Qty: 1`. Notes differ per variant. + +- [ ] **Step 5: Verify SO-30019 regression-free** + +Pre-existing SO-30019 (1 line, qty 1) should render identically to its pre-Task baseline. Render and verify: +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /tmp/sticker_t7_regression.pdf +``` + +Read PDF. Expected: single page, `Qty: 1` (plain, no slash), Notes shows the existing customer-facing description. Layout identical to last-known-good state. + +- [ ] **Step 6: Clean up the test SO + jobs** + +```bash +ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <`) | +| Migration: none needed | Confirmed — Tasks 4–6 don't touch existing fp.job/sticker DB state | +| Testing scenarios | Task 7 covers Scenarios 1–6 from the spec | + +No gaps. + +**Placeholder scan:** No TBDs, no "implement appropriate", no "similar to Task N" — every step has the exact code or the exact command. + +**Type consistency:** Variable names used consistently: `_qty_total` (introduced in Task 2, used in Tasks 3/4/5), `_notes_content` (introduced in Task 2, used in Tasks 4/5), `_box_idx` (introduced in Task 2's inner template, referenced in the same step). XML IDs: `action_report_fp_so_sticker_internal` + `report_fp_so_sticker_internal` (consistent in Task 4); `action_report_fp_job_sticker_internal` + `report_fp_job_sticker_internal_template` (consistent in Task 5; trailing `_template` matches existing `report_fp_job_sticker_template` convention).