Files
Odoo-Modules/fusion_plating/docs/superpowers/plans/2026-05-13-sticker-multi-part-and-variants-plan.md
gsinghpal 135cbd3a5c docs: implementation plan — sticker multi-part / per-box / Internal+External
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>
2026-05-13 07:47:40 -04:00

49 KiB
Raw Blame History

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 415441
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 415441 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_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 <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_content to 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) &gt; 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_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:

                    <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_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:

                <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.019.0.10.13.0). fusion_plating_jobs/__manifest__.py: increment patch (e.g. 19.0.8.23.019.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.019.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.019.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.019.0.10.15.0). fusion_plating_jobs/__manifest__.py: increment patch (e.g. 19.0.8.25.019.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, 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

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 &lt; 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 16) 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 46 (new actions get _internal suffix; existing actions only rename <field name="name">)
Migration: none needed Confirmed — Tasks 46 don't touch existing fp.job/sticker DB state
Testing scenarios Task 7 covers Scenarios 16 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).