7 tasks, bite-sized steps with exact code + commands. TDD on the backend grouping change (new test_so_confirm_splits_by_thickness); deploy-and-render-PDF on the QWeb template changes. Each task self-contained, pushes to entech LXC 111 via the standard pct exec + cat-pipe path, bumps the module version, and commits. Task 7 is verification-only — creates a multi-line test SO with two different thicknesses, renders External + Internal stickers on both the SO and each spawned fp.job, confirms the box loop and the Notes variant pattern both work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
49 KiB
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:
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:
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:
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):
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\" <<EOF
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''TEMPLATE_REF'\'', [RECORD_ID])
with open('\''/tmp/out.pdf'\'', '\''wb'\'') as f:
f.write(pdf)
print('\''SIZE:'\'', len(pdf))
EOF"'
Pull rendered PDF locally:
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/out.pdf'" > /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:
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:
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:
# 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:
# 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
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
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) <noreply@anthropic.com>
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_totalto 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 <t t-set="..." t-value="False"/> line, add:
<t t-set="_notes_content" t-value="False"/>
<t t-set="_qty_total" t-value="False"/>
- Step 2: Convert
_notes_contentto override-or-fallback in the inner template
Same file. Find the inner template's existing line:
<t t-set="_notes_content" t-value="(_line and _line.name)
or (_part and _part.name)
or '-'"/>
Replace with:
<!-- _notes_content: outer can pre-set this (e.g. Internal variant
passes line.x_fc_internal_description). Otherwise falls back
to the customer-facing description on the SO line. -->
<t t-set="_notes_content" t-value="_notes_content
or (_line and _line.name)
or (_part and _part.name)
or '-'"/>
- Step 3: Wrap the sticker body in a per-box loop
Same file. Find the <div class="fp-sticker"> opening tag inside the inner template (after the </style> close). Wrap the entire div in a t-foreach loop:
<t t-foreach="range(int(_qty_total or 1))" t-as="_box_idx0">
<t t-set="_box_idx" t-value="_box_idx0 + 1"/>
<div class="fp-sticker">
... existing structure unchanged ...
</div>
</t>
The closing </t> for the foreach goes immediately after the closing </div> 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:
<tr>
<td class="fp-sticker-label">Qty:</td>
<td class="fp-sticker-value">
<span class="fp-sticker-strong">
<span t-esc="int(_qty) if _qty == int(_qty) else _qty"/>
</span>
</td>
</tr>
Replace with:
<tr>
<td class="fp-sticker-label">Qty:</td>
<td class="fp-sticker-value">
<span class="fp-sticker-strong">
<t t-if="_qty_total and int(_qty_total) > 1">
<span t-esc="_box_idx"/> / <span t-esc="int(_qty_total)"/>
</t>
<t t-else="">
<span t-esc="int(_qty) if _qty == int(_qty) else _qty"/>
</t>
</span>
</td>
</tr>
- 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
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):
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\" <<EOF
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_jobs.report_fp_job_sticker_template'\'', [2635])
with open('\''/tmp/sticker_t2.pdf'\'', '\''wb'\'') as f:
f.write(pdf)
print('\''SIZE:'\'', len(pdf))
EOF"'
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/sticker_t2.pdf'" > /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
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) <noreply@anthropic.com>
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_totalto 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:
<t t-set="_qty" t-value="line.product_uom_qty"/>
Insert immediately below:
<t t-set="_qty_total" t-value="line.product_uom_qty"/>
- Step 2: Add
_qty_totalto 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:
<t t-set="_qty" t-value="job.qty"/>
Insert immediately below:
<t t-set="_qty_total" t-value="job.qty"/>
- 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
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:
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\" <<EOF
job = env['\''fp.job'\''].browse(2635)
original_qty = job.qty
job.write({'\''qty'\'': 3.0})
env.cr.commit()
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_jobs.report_fp_job_sticker_template'\'', [2635])
with open('\''/tmp/sticker_t3_multibox.pdf'\'', '\''wb'\'') as f:
f.write(pdf)
print('\''PDF SIZE:'\'', len(pdf))
job.write({'\''qty'\'': original_qty})
env.cr.commit()
print('\''restored qty to'\'', original_qty)
EOF"'
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/sticker_t3_multibox.pdf'" > /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
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) <noreply@anthropic.com>
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 </template> tag (before the final </odoo> close), insert:
<!-- ========== Outer template — sale.order Internal variant ==========
Same layout + iteration as report_fp_so_sticker, but pre-sets
_notes_content from x_fc_internal_description (Sub 2 internal
description field) so the Notes column shows the ops-facing
description instead of line.name. ===================================-->
<template id="report_fp_so_sticker_internal">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="so">
<t t-foreach="so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)"
t-as="line">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="so.name"/>
<t t-set="_scan_id" t-value="line.id"/>
<t t-set="_scan_path" t-value="'/fp/so-line/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="so"/>
<t t-set="_line" t-value="line"/>
<t t-set="_part" t-value="line.x_fc_part_catalog_id"/>
<t t-set="_coating" t-value="line.x_fc_coating_config_id"/>
<t t-set="_due" t-value="line.x_fc_part_deadline or so.commitment_date or False"/>
<t t-set="_qty" t-value="line.product_uom_qty"/>
<t t-set="_qty_total" t-value="line.product_uom_qty"/>
<t t-set="_partner_name" t-value="so.partner_id.name"/>
<t t-set="_mo_ref" t-value="''"/>
<!-- Internal override: read x_fc_internal_description -->
<t t-set="_notes_content" t-value="('x_fc_internal_description' in line._fields
and line.x_fc_internal_description) or '-'"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</t>
</template>
- 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 </record> tag, insert:
<!-- SO Internal sticker — same layout, prints internal description
instead of the customer-facing line.name. -->
<record id="action_report_fp_so_sticker_internal" model="ir.actions.report">
<field name="name">Internal Sticker</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_reports.report_fp_so_sticker_internal</field>
<field name="report_file">fusion_plating_reports.report_fp_so_sticker_internal</field>
<field name="print_report_name">'Internal Sticker - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_wo_sticker"/>
</record>
- 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
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):
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\" <<EOF
so = env['\''sale.order'\''].search([('\''name'\'', '\''='\'', '\''SO-30019'\'')], limit=1)
line = so.order_line.filtered(lambda l: l.x_fc_part_catalog_id)[:1]
if '\''x_fc_internal_description'\'' in line._fields:
original_internal = line.x_fc_internal_description
line.write({'\''x_fc_internal_description'\'': '\''INTERNAL: rework if any dings on flange. Buff per WI-104.'\'' })
env.cr.commit()
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_reports.report_fp_so_sticker_internal'\'', [so.id])
with open('\''/tmp/sticker_t4_internal.pdf'\'', '\''wb'\'') as f:
f.write(pdf)
print('\''PDF SIZE:'\'', len(pdf))
line.write({'\''x_fc_internal_description'\'': original_internal or False})
env.cr.commit()
print('\''restored'\'')
else:
print('\''x_fc_internal_description not on sale.order.line — Sub 2 not landed?'\'')
EOF"'
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/sticker_t4_internal.pdf'" > /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
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) <noreply@anthropic.com>
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 </record> tag (still inside <odoo>), insert:
<record id="action_report_fp_job_sticker_internal" model="ir.actions.report">
<field name="name">Internal Job Sticker</field>
<field name="model">fp.job</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_plating_jobs.report_fp_job_sticker_internal_template</field>
<field name="report_file">fusion_plating_jobs.report_fp_job_sticker_internal_template</field>
<field name="print_report_name">'Internal Job Sticker - %s' % (object.name or '').replace('/', '-')</field>
<field name="binding_model_id" ref="fusion_plating.model_fp_job"/>
<field name="binding_type">report</field>
<field name="paperformat_id" ref="paperformat_fp_job_sticker"/>
</record>
- Step 2: Add Job Internal template
Same file. After the existing report_fp_job_sticker_template template's closing </template> (still inside <odoo>), insert:
<template id="report_fp_job_sticker_internal_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="job">
<t t-call="fusion_plating_reports.report_fp_wo_sticker_defaults"/>
<t t-set="_order_id" t-value="job.name"/>
<t t-set="_scan_id" t-value="job.id"/>
<t t-set="_scan_path" t-value="'/fp/job/'"/>
<t t-set="_mo" t-value="False"/>
<t t-set="_so" t-value="job.sale_order_id"/>
<t t-set="_line" t-value="job.sale_order_line_ids[:1]"/>
<t t-set="_part" t-value="('part_catalog_id' in job._fields and job.part_catalog_id) or False"/>
<t t-set="_coating" t-value="('coating_config_id' in job._fields and job.coating_config_id) or False"/>
<t t-set="_process" t-value="job.recipe_id or False"/>
<t t-set="_due" t-value="job.date_deadline or False"/>
<t t-set="_qty" t-value="job.qty"/>
<t t-set="_qty_total" t-value="job.qty"/>
<t t-set="_partner_name" t-value="job.partner_id.name"/>
<t t-set="_mo_ref" t-value="''"/>
<!-- Internal override: read x_fc_internal_description from
the first linked SO line (jobs are 1:N with SO lines
but Sub 5 thickness+serial grouping means same-x_fc
lines share a job — first line's internal_description
is representative). -->
<t t-set="_notes_content" t-value="(job.sale_order_line_ids[:1]
and 'x_fc_internal_description' in job.sale_order_line_ids[:1]._fields
and job.sale_order_line_ids[:1].x_fc_internal_description) or '-'"/>
<t t-call="fusion_plating_reports.report_fp_wo_sticker_inner"/>
</t>
</t>
</template>
- 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
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
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\" <<EOF
job = env['\''fp.job'\''].browse(2635)
line = job.sale_order_line_ids[:1]
original_internal = line.x_fc_internal_description if '\''x_fc_internal_description'\'' in line._fields else False
if '\''x_fc_internal_description'\'' in line._fields:
line.write({'\''x_fc_internal_description'\'': '\''INTERNAL JOB: handle with care, no rework on this batch'\'' })
env.cr.commit()
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_jobs.report_fp_job_sticker_internal_template'\'', [2635])
with open('\''/tmp/sticker_t5_job_internal.pdf'\'', '\''wb'\'') as f:
f.write(pdf)
print('\''PDF SIZE:'\'', len(pdf))
if '\''x_fc_internal_description'\'' in line._fields:
line.write({'\''x_fc_internal_description'\'': original_internal or False})
env.cr.commit()
EOF"'
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/sticker_t5_job_internal.pdf'" > /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
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) <noreply@anthropic.com>
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:
<field name="name">WO Box Sticker</field>
to:
<field name="name">External Sticker</field>
Also update the print_report_name if it references the old label. The existing print_report_name is 'WO Sticker - %s' % ... — change to:
<field name="print_report_name">'External Sticker - %s' % (object.name or '').replace('/', '-')</field>
- Step 2: Rename Job External action label
Open fusion_plating_jobs/report/report_fp_job_sticker.xml. Find action_report_fp_job_sticker. Change:
<field name="name">Job Sticker</field>
to:
<field name="name">External Job Sticker</field>
And update print_report_name from 'Job Sticker - %s' % ... to:
<field name="print_report_name">'External Job Sticker - %s' % (object.name or '').replace('/', '-')</field>
- 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
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
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
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) <noreply@anthropic.com>
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
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\" <<EOF
# Reuse an existing partner + product
partner = env['\''res.partner'\''].search([('\''name'\'', '\''ilike'\'', '\''ABC'\'')], limit=1)
product = env['\''product.product'\''].search([], limit=1)
# Get an existing part + coating + 2 thicknesses
part = env['\''fp.part.catalog'\''].search([], limit=1)
coating = env['\''fp.coating.config'\''].search([], limit=1)
thick_a = env['\''fp.coating.thickness'\''].search([], order='\''id'\'', limit=1)
thick_b = env['\''fp.coating.thickness'\''].search([('\''id'\'', '\''!='\'', thick_a.id)], order='\''id'\'', limit=1)
print('\''partner='\'', partner.name, '\''product='\'', product.name)
print('\''part='\'', part.name, '\''coating='\'', coating.name)
print('\''thick_a='\'', thick_a.display_name, '\''thick_b='\'', thick_b.display_name)
# Create the SO with two lines, same part+coating, different thicknesses
so = env['\''sale.order'\''].create({'\''partner_id'\'': partner.id})
SOL = env['\''sale.order.line'\'']
line_a = SOL.create({
'\''order_id'\'': so.id, '\''product_id'\'': product.id,
'\''product_uom_qty'\'': 2.0, '\''price_unit'\'': 10.0,
'\''x_fc_part_catalog_id'\'': part.id,
'\''x_fc_coating_config_id'\'': coating.id,
'\''x_fc_thickness_id'\'': thick_a.id,
'\''x_fc_internal_description'\'': '\''TEST INTERNAL A: rework cell-1'\'' ,
'\''name'\'': '\''TEST EXTERNAL A: VALVE BODY Rev A'\'',
})
line_b = SOL.create({
'\''order_id'\'': so.id, '\''product_id'\'': product.id,
'\''product_uom_qty'\'': 1.0, '\''price_unit'\'': 10.0,
'\''x_fc_part_catalog_id'\'': part.id,
'\''x_fc_coating_config_id'\'': coating.id,
'\''x_fc_thickness_id'\'': thick_b.id,
'\''x_fc_internal_description'\'': '\''TEST INTERNAL B: extra dip cell-3'\'' ,
'\''name'\'': '\''TEST EXTERNAL B: VALVE BODY Rev B'\'',
})
so.action_confirm()
env.cr.commit()
print('\''SO created:'\'', so.name, '\''id='\'', so.id)
jobs = env['\''fp.job'\''].search([('\''sale_order_id'\'', '\''='\'', so.id)])
print('\''Jobs spawned:'\'', jobs.mapped('\''name'\''))
for j in jobs:
print('\'' '\'', j.name, '\''qty='\'', j.qty, '\''thickness='\'', j.sale_order_line_ids[:1].x_fc_thickness_id.display_name)
EOF"'
Expected console output: 2 jobs created (e.g. WO-30020-01, WO-30020-02), each carrying its own thickness via its linked SO line.
- Step 2: Render SO External — expect 3 pages
Substitute the SO id from Step 1:
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\" <<EOF
so = env['\''sale.order'\''].search([], order='\''id desc'\'', limit=1)
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_reports.report_fp_so_sticker'\'', [so.id])
with open('\''/tmp/sticker_t7_so_ext.pdf'\'', '\''wb'\'') as f: f.write(pdf)
print('\''SO='\'', so.name, '\''SIZE='\'', len(pdf))
EOF"'
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/sticker_t7_so_ext.pdf'" > /tmp/sticker_t7_so_ext.pdf
Read PDF. Expected: 3 pages.
-
Page 1: line_a box 1 of 2 — Qty
1 / 2, NotesTEST 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), NotesTEST EXTERNAL B: VALVE BODY Rev B, Thickness shows thick_b -
Step 3: Render SO Internal — expect 3 pages, different Notes
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\" <<EOF
so = env['\''sale.order'\''].search([], order='\''id desc'\'', limit=1)
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_reports.report_fp_so_sticker_internal'\'', [so.id])
with open('\''/tmp/sticker_t7_so_int.pdf'\'', '\''wb'\'') as f: f.write(pdf)
print('\''SIZE='\'', len(pdf))
EOF"'
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/sticker_t7_so_int.pdf'" > /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
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\" <<EOF
so = env['\''sale.order'\''].search([], order='\''id desc'\'', limit=1)
jobs = env['\''fp.job'\''].search([('\''sale_order_id'\'', '\''='\'', so.id)])
for j in jobs:
pdf_ext, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_jobs.report_fp_job_sticker_template'\'', [j.id])
pdf_int, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_jobs.report_fp_job_sticker_internal_template'\'', [j.id])
fn_ext = '\''/tmp/sticker_t7_job_'\'' + j.name.replace('\''/'\'', '\''_'\'') + '\''_ext.pdf'\''
fn_int = '\''/tmp/sticker_t7_job_'\'' + j.name.replace('\''/'\'', '\''_'\'') + '\''_int.pdf'\''
with open(fn_ext, '\''wb'\'') as f: f.write(pdf_ext)
with open(fn_int, '\''wb'\'') as f: f.write(pdf_int)
print(j.name, '\''ext='\'', len(pdf_ext), '\''int='\'', len(pdf_int))
EOF"'
For each job, pull both PDFs:
for fn in $(ssh pve-worker5 "pct exec 111 -- bash -c 'ls /tmp/sticker_t7_job_*.pdf'"); do
base=$(basename "$fn")
ssh pve-worker5 "pct exec 111 -- bash -c 'cat $fn'" > /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:
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\" <<EOF
so = env['\''sale.order'\''].search([('\''name'\'', '\''='\'', '\''SO-30019'\'')], limit=1)
pdf, _ = env['\''ir.actions.report'\''].sudo()._render_qweb_pdf('\''fusion_plating_reports.report_fp_so_sticker'\'', [so.id])
with open('\''/tmp/sticker_t7_regression.pdf'\'', '\''wb'\'') as f: f.write(pdf)
print('\''SIZE='\'', len(pdf))
EOF"'
ssh pve-worker5 "pct exec 111 -- bash -c 'cat /tmp/sticker_t7_regression.pdf'" > /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
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\" <<EOF
so = env['\''sale.order'\''].search([], order='\''id desc'\'', limit=1)
# Only delete if name carries the expected pattern (defensive)
if so.partner_id.name.startswith('\''ABC'\'') and so.amount_total < 100:
jobs = env['\''fp.job'\''].search([('\''sale_order_id'\'', '\''='\'', so.id)])
print('\''deleting jobs:'\'', jobs.mapped('\''name'\''))
jobs.with_context(force_delete=True).unlink()
so.with_context(force_delete=True).unlink()
env.cr.commit()
print('\''cleanup done'\'')
else:
print('\''skipping cleanup — most recent SO doesn'\\\\\\''t match test pattern'\'')
EOF"'
If cleanup is blocked (e.g. by parent-number unlink guard), document the test SO id for manual cleanup later — not a blocker for the spec.
- Step 7: Push commits to GitHub
git push origin main
All six commits (Tasks 1–6) land on main. No further commits needed for Task 7 — it's verification-only.
Self-Review
Spec coverage check:
| Spec section | Implemented by |
|---|---|
| Backend grouping fix (thickness + serial) | Task 1 |
| Per-box sticker render | Task 2 (inner) + Task 3 (outer wiring) |
| Internal variant on SO | Task 4 |
| Internal variant on fp.job | Task 5 |
| Action label rename | Task 6 |
| Existing XML IDs unchanged | Confirmed in Tasks 4–6 (new actions get _internal suffix; existing actions only rename <field name="name">) |
| 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).