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>
1049 lines
49 KiB
Markdown
1049 lines
49 KiB
Markdown
# Sticker — Multi-part, Per-box, Internal/External Variants — Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Fix multi-thickness/multi-SN grouping in `_fp_auto_create_job`, add per-box rendering to the sticker, and add a parallel Internal variant of each sticker that shows the internal description in the Notes column.
|
||
|
||
**Architecture:** Three changes touching three files. Backend: extend the fp.job grouping key to include thickness + serial. Frontend: inner sticker template gains a `range(int(_qty_total))` loop and a fallback chain on `_notes_content`. New outer templates per variant pre-set `_notes_content` and `_qty_total`. Existing action XML IDs unchanged; new actions added for the Internal variants.
|
||
|
||
**Tech Stack:** Odoo 19 (Python/QWeb XML), wkhtmltopdf 0.12, native fp.job model (post Sub 11 — no MRP), entech LXC 111 deployment.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-05-13-sticker-multi-part-and-variants-design.md`
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
| File | Responsibility | Touch |
|
||
|---|---|---|
|
||
| `fusion_plating_jobs/models/sale_order.py` | `_fp_auto_create_job()` — owns the line→job grouping key | Modify ~lines 415–441 |
|
||
| `fusion_plating_jobs/tests/test_fp_job_extensions.py` | Existing test suite for SO→fp.job creation | Modify (add multi-thickness test) |
|
||
| `fusion_plating_reports/report/report_fp_wo_sticker.xml` | Inner template (shared by SO+Job stickers), SO outer templates, defaults block | Modify inner + SO outer + add SO Internal outer |
|
||
| `fusion_plating_reports/report/report_actions.xml` | Action records for sale.order-bound reports | Modify SO action label + add SO Internal action |
|
||
| `fusion_plating_jobs/report/report_fp_job_sticker.xml` | Job outer template + action record | Modify Job outer + Job action label + add Job Internal outer + Job Internal action |
|
||
| `fusion_plating_jobs/__manifest__.py` | Version | Bump |
|
||
| `fusion_plating_reports/__manifest__.py` | Version | Bump |
|
||
|
||
---
|
||
|
||
## Deployment helpers
|
||
|
||
Every task that changes code pushes to entech LXC 111 + upgrades. Reusable command blocks:
|
||
|
||
**Push a file to entech:**
|
||
```bash
|
||
cat LOCAL_PATH | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > REMOTE_PATH'"
|
||
```
|
||
where `REMOTE_PATH` mirrors the repo structure under `/mnt/extra-addons/custom/`.
|
||
|
||
**Upgrade a module + restart:**
|
||
```bash
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u MODULE_NAME --stop-after-init\" && systemctl start odoo'"
|
||
```
|
||
|
||
**Clear asset cache after a template/XML change:**
|
||
```bash
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'"
|
||
```
|
||
|
||
**Render a sticker PDF via odoo shell** (substitute `TEMPLATE_REF` and `RECORD_ID`):
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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:**
|
||
```bash
|
||
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:
|
||
|
||
```python
|
||
def test_so_confirm_splits_by_thickness(self):
|
||
"""Two lines with same part+coating but DIFFERENT thicknesses
|
||
must produce TWO fp.jobs — silent merge was a compliance bug
|
||
(the second thickness's CoC would carry the first thickness)."""
|
||
SOL = self.env['sale.order.line']
|
||
if 'x_fc_part_catalog_id' not in SOL._fields or 'x_fc_thickness_id' not in SOL._fields:
|
||
self.skipTest('plating fields not present')
|
||
partner_for_part = self.env['res.partner'].create({'name': 'SplitTestPartner'})
|
||
part = self.env['fp.part.catalog'].create({
|
||
'name': 'SplitTestPart',
|
||
'part_number': 'STP-1',
|
||
'partner_id': partner_for_part.id,
|
||
})
|
||
thickness_a = self.env['fp.coating.thickness'].create({
|
||
'value_min': 0.3, 'value_max': 0.5, 'uom': 'mils',
|
||
})
|
||
thickness_b = self.env['fp.coating.thickness'].create({
|
||
'value_min': 0.5, 'value_max': 1.0, 'uom': 'mils',
|
||
})
|
||
so = self.env['sale.order'].create({'partner_id': self.partner.id})
|
||
SOL.create({
|
||
'order_id': so.id, 'product_id': self.product.id,
|
||
'product_uom_qty': 2.0, 'price_unit': 10.0,
|
||
'x_fc_part_catalog_id': part.id,
|
||
'x_fc_thickness_id': thickness_a.id,
|
||
})
|
||
SOL.create({
|
||
'order_id': so.id, 'product_id': self.product.id,
|
||
'product_uom_qty': 1.0, 'price_unit': 10.0,
|
||
'x_fc_part_catalog_id': part.id,
|
||
'x_fc_thickness_id': thickness_b.id,
|
||
})
|
||
so.action_confirm()
|
||
jobs = self.env['fp.job'].search([('sale_order_id', '=', so.id)])
|
||
self.assertEqual(len(jobs), 2,
|
||
'Lines with different thicknesses must spawn separate fp.jobs')
|
||
thicknesses = jobs.sale_order_line_ids.mapped('x_fc_thickness_id')
|
||
self.assertEqual(len(thicknesses), 2,
|
||
'Each job should carry its own thickness via its linked SO line')
|
||
```
|
||
|
||
- [ ] **Step 2: Run test, expect FAIL**
|
||
|
||
Push the test file to entech, then run:
|
||
```bash
|
||
cat fusion_plating_jobs/tests/test_fp_job_extensions.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/tests/test_fp_job_extensions.py'"
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin --test-tags=/fusion_plating_jobs:TestSOConfirmCreatesJob.test_so_confirm_splits_by_thickness --stop-after-init\"'" 2>&1 | tail -30
|
||
```
|
||
Expected: `AssertionError: 1 != 2` (the current code collapses both lines into one job).
|
||
|
||
- [ ] **Step 3: Modify the grouping key in `_fp_auto_create_job`**
|
||
|
||
Open `fusion_plating_jobs/models/sale_order.py`. Find the comment block starting at line 415 (`# Group by (recipe, part, coating).`) and the loop body starting at line 426 (`for line in plating_lines:`).
|
||
|
||
Replace this block:
|
||
|
||
```python
|
||
# Group by (recipe, part, coating). Lines that share ALL THREE
|
||
# collapse into one WO. Sharing only the recipe is not enough —
|
||
# the WO header captures part_id and coating_config_id from
|
||
# first_line, and downstream the CoC prints the WO header's
|
||
# part_number on the customer-facing cert. Bundling Part A +
|
||
# Part B under one WO because they happen to share a recipe
|
||
# would put Part A's number on a cert covering both, which is
|
||
# a compliance bug (silent mis-attestation).
|
||
# No-recipe lines get their own group each.
|
||
groups = {}
|
||
unrecipe_idx = 0
|
||
for line in plating_lines:
|
||
recipe = self._fp_resolve_recipe_for_line(line)
|
||
part_id = (
|
||
'x_fc_part_catalog_id' in line._fields
|
||
and line.x_fc_part_catalog_id.id
|
||
) or False
|
||
coating_id = (
|
||
'x_fc_coating_config_id' in line._fields
|
||
and line.x_fc_coating_config_id.id
|
||
) or False
|
||
if recipe:
|
||
key = (recipe.id, part_id, coating_id)
|
||
else:
|
||
unrecipe_idx += 1
|
||
key = ('no_recipe', unrecipe_idx)
|
||
groups[key] = groups.get(key, self.env['sale.order.line']) | line
|
||
```
|
||
|
||
with:
|
||
|
||
```python
|
||
# Group by (recipe, part, coating, thickness, serial). Lines that
|
||
# share ALL FIVE collapse into one WO. Same compliance reasoning
|
||
# as part_id + coating_id: bundling lines with different thicknesses
|
||
# or different serials under one WO would carry the first line's
|
||
# values onto the cert + sticker — silent mis-attestation. Sub 5
|
||
# added thickness_id + serial_id; this fix extends the grouping
|
||
# logic to honour them. No-recipe lines get their own group each.
|
||
groups = {}
|
||
unrecipe_idx = 0
|
||
for line in plating_lines:
|
||
recipe = self._fp_resolve_recipe_for_line(line)
|
||
part_id = (
|
||
'x_fc_part_catalog_id' in line._fields
|
||
and line.x_fc_part_catalog_id.id
|
||
) or False
|
||
coating_id = (
|
||
'x_fc_coating_config_id' in line._fields
|
||
and line.x_fc_coating_config_id.id
|
||
) or False
|
||
thickness_id = (
|
||
'x_fc_thickness_id' in line._fields
|
||
and line.x_fc_thickness_id.id
|
||
) or False
|
||
serial_id = (
|
||
'x_fc_serial_id' in line._fields
|
||
and line.x_fc_serial_id.id
|
||
) or False
|
||
if recipe:
|
||
key = (recipe.id, part_id, coating_id, thickness_id, serial_id)
|
||
else:
|
||
unrecipe_idx += 1
|
||
key = ('no_recipe', unrecipe_idx)
|
||
groups[key] = groups.get(key, self.env['sale.order.line']) | line
|
||
```
|
||
|
||
- [ ] **Step 4: Bump fusion_plating_jobs version**
|
||
|
||
Open `fusion_plating_jobs/__manifest__.py`, find the `'version'` line, increment the patch component. e.g. `'19.0.8.22.10'` → `'19.0.8.23.0'`.
|
||
|
||
- [ ] **Step 5: Push to entech + run test, expect PASS**
|
||
|
||
```bash
|
||
cat fusion_plating_jobs/models/sale_order.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/models/sale_order.py'"
|
||
cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'"
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_jobs --test-tags=/fusion_plating_jobs:TestSOConfirmCreatesJob --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -30
|
||
```
|
||
Expected: tests pass (both `test_so_confirm_creates_job`, `test_so_confirm_idempotent`, `test_so_confirm_splits_by_thickness`).
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add fusion_plating_jobs/models/sale_order.py \
|
||
fusion_plating_jobs/tests/test_fp_job_extensions.py \
|
||
fusion_plating_jobs/__manifest__.py
|
||
git commit -m "$(cat <<'EOF'
|
||
fix(jobs): split fp.jobs by thickness + serial on SO confirm
|
||
|
||
The _fp_auto_create_job grouping key was (recipe, part, coating).
|
||
Lines that shared all three but differed in thickness (or serial)
|
||
silently collapsed into one fp.job — the second line's thickness/SN
|
||
was lost, and any downstream cert printed the first line's values
|
||
across both batches. Silent mis-attestation = compliance hole.
|
||
|
||
Extended the key tuple to (recipe, part, coating, thickness, serial).
|
||
Single-line SOs and same-(thickness, SN) multi-line SOs collapse
|
||
identically to before. Only lines that previously merged when they
|
||
shouldn't have now split into their own fp.jobs.
|
||
|
||
Added test_so_confirm_splits_by_thickness covering the case.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <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:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```xml
|
||
<t t-set="_notes_content" t-value="(_line and _line.name)
|
||
or (_part and _part.name)
|
||
or '-'"/>
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```xml
|
||
<!-- _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:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```xml
|
||
<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**
|
||
|
||
```bash
|
||
cat fusion_plating_reports/report/report_fp_wo_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_fp_wo_sticker.xml'"
|
||
cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'"
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'"
|
||
```
|
||
|
||
- [ ] **Step 7: Verify regression-free on existing single-line SO**
|
||
|
||
Render the existing fp.job 2635 sticker (SO-30019, 1 line, qty 1):
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
git add fusion_plating_reports/report/report_fp_wo_sticker.xml \
|
||
fusion_plating_reports/__manifest__.py
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(sticker): per-box render loop + Notes override hook
|
||
|
||
Inner sticker template gains two parameters that outer templates
|
||
pre-set:
|
||
|
||
_qty_total — total qty for the line/job. Inner wraps the body
|
||
in t-foreach="range(int(_qty_total or 1))" so a qty=5 line
|
||
produces 5 consecutive single-box stickers. Qty row in the
|
||
body switches from "5" to "1 / 5", "2 / 5", ... "5 / 5".
|
||
When _qty_total is missing/0/1, the Qty row keeps showing
|
||
the plain integer (regression-free).
|
||
|
||
_notes_content — Notes column source. Existing inner code
|
||
hard-read _line.name; new code accepts an outer override
|
||
and falls back to _line.name. External outers don't set it
|
||
(unchanged behaviour); the new Internal outers (Task 4+5)
|
||
pre-set it to x_fc_internal_description.
|
||
|
||
Defaults template initialises both new vars to False so the
|
||
inner's "outer-supplied OR fallback" pattern doesn't NameError
|
||
when called from existing outers that haven't been updated yet.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <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:
|
||
|
||
```xml
|
||
<t t-set="_qty" t-value="line.product_uom_qty"/>
|
||
```
|
||
|
||
Insert immediately below:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```xml
|
||
<t t-set="_qty" t-value="job.qty"/>
|
||
```
|
||
|
||
Insert immediately below:
|
||
|
||
```xml
|
||
<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**
|
||
|
||
```bash
|
||
cat fusion_plating_reports/report/report_fp_wo_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_fp_wo_sticker.xml'"
|
||
cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'"
|
||
cat fusion_plating_jobs/report/report_fp_job_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/report/report_fp_job_sticker.xml'"
|
||
cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'"
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports,fusion_plating_jobs --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'"
|
||
```
|
||
|
||
- [ ] **Step 5: Verify multi-box rendering via odoo shell**
|
||
|
||
Bump fp.job 2635's `qty` to 3 temporarily, render, then restore. Use odoo shell:
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
git add fusion_plating_reports/report/report_fp_wo_sticker.xml \
|
||
fusion_plating_reports/__manifest__.py \
|
||
fusion_plating_jobs/report/report_fp_job_sticker.xml \
|
||
fusion_plating_jobs/__manifest__.py
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(sticker): wire _qty_total in SO + Job External outers
|
||
|
||
Activates the per-box loop landed in the prior commit. SO External
|
||
reads line.product_uom_qty; Job External reads job.qty. Inner
|
||
template now renders one sticker per physical box, marking each
|
||
with "X / N" in the Qty row.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <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:
|
||
|
||
```xml
|
||
<!-- ========== 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:
|
||
|
||
```xml
|
||
<!-- 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**
|
||
|
||
```bash
|
||
cat fusion_plating_reports/report/report_fp_wo_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_fp_wo_sticker.xml'"
|
||
cat fusion_plating_reports/report/report_actions.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_actions.xml'"
|
||
cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'"
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'"
|
||
```
|
||
|
||
- [ ] **Step 5: Verify SO Internal renders with internal description**
|
||
|
||
Find an SO line with `x_fc_internal_description` populated (or seed one):
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
git add fusion_plating_reports/report/report_fp_wo_sticker.xml \
|
||
fusion_plating_reports/report/report_actions.xml \
|
||
fusion_plating_reports/__manifest__.py
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(sticker): add Internal Sticker variant on sale.order Print menu
|
||
|
||
Same 3-cell + body layout as External; Notes column reads
|
||
x_fc_internal_description (Sub 2 internal-description field on the
|
||
SO line) instead of line.name. Shop floor gets ops-facing notes
|
||
without leaking them to the customer-facing variant.
|
||
|
||
New action record action_report_fp_so_sticker_internal — binds to
|
||
sale.order, appears in the Print menu next to the existing External
|
||
sticker. New template report_fp_so_sticker_internal that pre-sets
|
||
_notes_content before t-calling the shared inner.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <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:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```xml
|
||
<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**
|
||
|
||
```bash
|
||
cat fusion_plating_jobs/report/report_fp_job_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/report/report_fp_job_sticker.xml'"
|
||
cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'"
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_jobs --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'"
|
||
```
|
||
|
||
- [ ] **Step 5: Verify Job Internal renders with internal description**
|
||
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
git add fusion_plating_jobs/report/report_fp_job_sticker.xml \
|
||
fusion_plating_jobs/__manifest__.py
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(sticker): add Internal Job Sticker variant on fp.job Print menu
|
||
|
||
Mirror of the SO Internal variant for fp.job. Same body fields,
|
||
same per-box loop; Notes column reads x_fc_internal_description
|
||
from the first linked SO line (job.sale_order_line_ids[:1]).
|
||
Operator on the shop floor sees ops-internal notes without those
|
||
ever appearing on the customer-facing External sticker.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <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:
|
||
|
||
```xml
|
||
<field name="name">WO Box Sticker</field>
|
||
```
|
||
|
||
to:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```xml
|
||
<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:
|
||
|
||
```xml
|
||
<field name="name">Job Sticker</field>
|
||
```
|
||
|
||
to:
|
||
|
||
```xml
|
||
<field name="name">External Job Sticker</field>
|
||
```
|
||
|
||
And update `print_report_name` from `'Job Sticker - %s' % ...` to:
|
||
|
||
```xml
|
||
<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**
|
||
|
||
```bash
|
||
cat fusion_plating_reports/report/report_actions.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/report/report_actions.xml'"
|
||
cat fusion_plating_reports/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_reports/__manifest__.py'"
|
||
cat fusion_plating_jobs/report/report_fp_job_sticker.xml | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/report/report_fp_job_sticker.xml'"
|
||
cat fusion_plating_jobs/__manifest__.py | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating_jobs/__manifest__.py'"
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating_reports,fusion_plating_jobs --stop-after-init\" && systemctl start odoo'" 2>&1 | tail -3
|
||
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '\''/web/assets/%'\'';\\\"\"'"
|
||
```
|
||
|
||
- [ ] **Step 5: Verify label rename in DB**
|
||
|
||
```bash
|
||
ssh pve-worker5 "pct exec 111 -- bash -c 'su - postgres -c \"psql admin -t -A -c \\\"SELECT id, name, model FROM ir_act_report_xml WHERE id IN (SELECT res_id FROM ir_model_data WHERE module IN ('\''fusion_plating_reports'\'', '\''fusion_plating_jobs'\'') AND name LIKE '\''action_report_fp_%sticker%'\'') ORDER BY model, id;\\\"\"'"
|
||
```
|
||
|
||
Expected output: 4 rows.
|
||
- `External Sticker | sale.order`
|
||
- `Internal Sticker | sale.order`
|
||
- `External Job Sticker | fp.job`
|
||
- `Internal Job Sticker | fp.job`
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add fusion_plating_reports/report/report_actions.xml \
|
||
fusion_plating_reports/__manifest__.py \
|
||
fusion_plating_jobs/report/report_fp_job_sticker.xml \
|
||
fusion_plating_jobs/__manifest__.py
|
||
git commit -m "$(cat <<'EOF'
|
||
chore(sticker): rename External action labels for the variant split
|
||
|
||
Print menu now shows External + Internal as paired entries:
|
||
|
||
sale.order: External Sticker / Internal Sticker
|
||
fp.job: External Job Sticker / Internal Job Sticker
|
||
|
||
XML IDs unchanged (action_report_fp_so_sticker /
|
||
action_report_fp_job_sticker) so existing bookmarks and
|
||
binding_model_id records keep working. print_report_name strings
|
||
also updated so the downloaded filename matches the new label.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <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**
|
||
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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:
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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:
|
||
```bash
|
||
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:
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
ssh pve-worker5 'pct exec 111 -- bash -c "su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" <<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**
|
||
|
||
```bash
|
||
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).
|