Files
Odoo-Modules/docs/superpowers/plans/2026-04-21-sub2-part-data-model.md
gsinghpal 418dabc688 docs(plating): Sub 2 implementation plan (30 tasks, 3 phases)
Full bite-sized plan matching the approved spec. Each task has file
paths, complete code, syntax-check commands, upgrade commands, expected
outputs, and commit messages.

Phase A (Tasks 1-12): additive schema + migration + cert-resolver.
System runnable throughout.

Phase B (Tasks 13-23): UI + QWeb macro + report rewiring. Users see new
fields. Old fields still exist.

Phase C (Tasks 24-30): flip required=True, drop legacy column, regression,
deploy to entech.

Self-review pass: every spec section mapped to a task; no TBD/TODO/placeholder.
Type signatures (_fp_resolve_cert_requirement, display_name, macro
params) consistent across tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:02:34 -04:00

1843 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sub 2 — Part Data Model Overhaul 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:** Restructure `fp.part.catalog`, `fp.sale.description.template`, and `sale.order.line` so part number + revision are required, descriptions are split into internal + customer-facing, each part carries a cert requirement, and customer-facing reports print the customer's part number — without breaking any in-flight jobs.
**Architecture:** Three-phase rollout that keeps the system runnable after every task.
- **Phase A** — additive schema (new nullable fields alongside old ones) + data migration that backfills + the cert-resolver refactor. System still works with old UI.
- **Phase B** — UI updates, report macro, report rewiring. Users see the new fields; old fields still exist in the DB.
- **Phase C** — flip `required=True`, drop the old `description` column, run regressions. Point of no return.
**Tech Stack:** Odoo 19 (Python models + XML views + QWeb reports + PostgreSQL migrations), odoo-shell scripts for testing (matching the QC-suite pattern already in the repo).
**Spec:** `docs/superpowers/specs/2026-04-21-sub2-part-data-model-design.md` — read first.
**Module versions to bump:**
- `fusion_plating_configurator`: `19.0.8.0.0``19.0.9.0.0`
- `fusion_plating_reports`: `19.0.4.9.0``19.0.5.0.0`
- `fusion_plating_bridge_mrp`: `19.0.8.0.0``19.0.9.0.0`
**Deploy pattern:** follow `fusion_plating/CLAUDE.md` → "odoo-entech" section. Local dev on `odoo-dev-app` docker.
---
## File Structure
### Files to create
| File | Purpose |
|---|---|
| `fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py` | Five-step data backfill + old-column drop |
| `fusion_plating_reports/report/customer_line_header.xml` | Shared QWeb macro for customer-facing line rendering |
| `docs/superpowers/tests/2026-04-21-sub2-smoke.py` | Odoo-shell smoke test (pattern from `fp_qc_smoke.py`) |
| `docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql` | SQL assertions post-migration |
### Files to modify
| File | Change |
|---|---|
| `fusion_plating_configurator/models/fp_part_catalog.py` | Add `certificate_requirement`; add `display_name` compute; DON'T flip required yet |
| `fusion_plating_configurator/models/fp_sale_description_template.py` | Add `internal_description` + `customer_facing_description`; keep old `description` until migration complete |
| `fusion_plating_configurator/models/sale_order_line.py` | Add `x_fc_internal_description`, `x_fc_description_template_id` |
| `fusion_plating_bridge_mrp/models/mrp_production.py` | Add `_fp_resolve_cert_requirement`; rewire `_fp_generate_cert_pdf` |
| `fusion_plating_configurator/views/fp_part_catalog_views.xml` | Certificate Requirement field; dual-description repeater; relabels |
| `fusion_plating_configurator/views/sale_order_views.xml` | SO line internal_description field + template picker |
| `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` | Template picker + dual-description inputs |
| `fusion_plating_reports/report/report_fp_sale.xml` | Use macro (lines 159, 436) |
| `fusion_plating_reports/report/report_fp_invoice.xml` | Use macro |
| `fusion_plating_reports/report/report_fp_packing_slip.xml` | Use macro |
| `fusion_plating_reports/report/report_fp_bol.xml` | Use macro |
| `fusion_plating_reports/report/report_fp_work_order.xml` | Internal — add new description fields |
| `fusion_plating_reports/report/report_fp_job_traveller.xml` | Same |
| `fusion_plating_configurator/__manifest__.py` | Bump version + register migration file |
| `fusion_plating_reports/__manifest__.py` | Bump version + register macro |
| `fusion_plating_bridge_mrp/__manifest__.py` | Bump version |
### Files explicitly NOT touching (defensive)
- `product.product` / `product.template` — no `default_code` sync
- `fusion_plating/models/fp_process_node.py` — untouched (Sub 3's work)
- `fusion_plating_bridge_mrp/models/fp_quality_check.py` — QC logic untouched
- Any `fusion_plating_quality` or `fusion_plating_iot` files — out of scope
---
## Phase A — Additive Schema + Migration + Cert Resolver
System remains runnable throughout this phase. Old fields still work; new fields are nullable; users see the old UI.
### Task 1 — Add `certificate_requirement` to `fp.part.catalog`
**Files:**
- Modify: `fusion_plating_configurator/models/fp_part_catalog.py` (after the `active` field)
- [ ] **Step 1 — Add the field**
Open `fusion_plating_configurator/models/fp_part_catalog.py`, find `active = fields.Boolean(...)` (around line 132), and add directly below it:
```python
certificate_requirement = fields.Selection(
[
('inherit', 'Inherit from Customer'),
('none', 'No Certificate'),
('coc', 'CoC Only'),
('coc_thickness', 'CoC + Thickness Report'),
],
string='Certificate Requirement',
default='inherit',
required=True,
tracking=True,
help='Determines which quality documents ship with this part. '
'"Inherit" reads the customer\'s default on the partner form.',
)
```
- [ ] **Step 2 — Syntax check**
Run:
```bash
cd /Users/gurpreet/Github/Odoo-Modules/fusion_plating
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/fp_part_catalog.py').read()); print('OK')"
```
Expected: `OK`
- [ ] **Step 3 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py
git commit -m "feat(configurator): add certificate_requirement field to fp.part.catalog (Sub 2 Task 1)"
```
---
### Task 2 — Add dual-description fields to `fp.sale.description.template`
**Files:**
- Modify: `fusion_plating_configurator/models/fp_sale_description_template.py`
- [ ] **Step 1 — Add the two new fields alongside the old**
Open the file. Find the `description = fields.Text(...)` line (around line 33). Keep it. Add these two fields **after** it:
```python
# Sub 2 — dual descriptions. Added alongside the legacy `description`
# field; migration copies old value into both, then old column dropped
# in Phase C. Nullable during Phase A so existing records don't fail.
internal_description = fields.Text(
string='Internal Description',
help='What the shop floor sees on the WO / traveler. Never on '
'customer documents.',
)
customer_facing_description = fields.Text(
string='Customer-Facing Description',
help='Prints on the SO, invoice, packing slip, and BoL.',
)
```
- [ ] **Step 2 — Syntax check**
```bash
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/fp_sale_description_template.py').read()); print('OK')"
```
Expected: `OK`
- [ ] **Step 3 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py
git commit -m "feat(configurator): add internal + customer-facing description fields (Sub 2 Task 2)"
```
---
### Task 3 — Add SO-line dual description fields
**Files:**
- Modify: `fusion_plating_configurator/models/sale_order_line.py`
- [ ] **Step 1 — Scan the existing class**
```bash
grep -n "class SaleOrderLine\|_inherit\|x_fc_part_catalog_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/sale_order_line.py | head -10
```
Expected: a class inheriting `sale.order.line`, with `x_fc_part_catalog_id` already defined.
- [ ] **Step 2 — Add the fields**
Inside the class, right after `x_fc_part_catalog_id`:
```python
# Sub 2 — dual descriptions captured from a template row at order
# entry. `name` remains Odoo's standard customer-facing line
# description; x_fc_internal_description is ops-only (prints on WO).
# Nullable during Phase A; flipped to required in Phase C.
x_fc_internal_description = fields.Text(
string='Internal Description',
help='Shop-floor instructions. Prints on WO / traveler. Never on customer docs.',
)
x_fc_description_template_id = fields.Many2one(
'fp.sale.description.template',
string='Description Template',
help='Which template row populated this line. Informational.',
)
```
- [ ] **Step 3 — Syntax check**
```bash
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/sale_order_line.py').read()); print('OK')"
```
Expected: `OK`
- [ ] **Step 4 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/models/sale_order_line.py
git commit -m "feat(configurator): add dual descriptions to sale.order.line (Sub 2 Task 3)"
```
---
### Task 4 — Bump configurator version + register migration
**Files:**
- Modify: `fusion_plating_configurator/__manifest__.py`
- [ ] **Step 1 — Bump version**
Edit `fusion_plating_configurator/__manifest__.py`. Change:
```python
'version': '19.0.8.0.0',
```
to:
```python
'version': '19.0.9.0.0',
```
- [ ] **Step 2 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/__manifest__.py
git commit -m "chore(configurator): bump version to 19.0.9.0.0 for Sub 2 (Task 4)"
```
---
### Task 5 — Create the migration script
**Files:**
- Create: `fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py`
- [ ] **Step 1 — Create the directory**
```bash
mkdir -p /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/migrations/19.0.9.0.0
```
- [ ] **Step 2 — Write the migration script**
Create `fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py` with the following content:
```python
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1
# Sub 2 — Part Data Model Overhaul. Runs on upgrade from < 19.0.9.0.0.
# Idempotent (NULL / empty guards). Safe to re-run.
import logging
_logger = logging.getLogger(__name__)
def migrate(cr, version):
if not version:
return # Fresh install — nothing to migrate
_logger.info("Sub 2: starting part-data-model migration to %s", version)
# Step 1: Backfill part_number from name where empty
cr.execute("""
UPDATE fp_part_catalog
SET part_number = name
WHERE part_number IS NULL OR part_number = ''
""")
_logger.info("Sub 2: backfilled part_number on %d records", cr.rowcount)
# Step 2: Backfill revision with 'A' where empty
cr.execute("""
UPDATE fp_part_catalog
SET revision = 'A'
WHERE revision IS NULL OR revision = ''
""")
_logger.info("Sub 2: backfilled revision on %d records", cr.rowcount)
# Step 3: Split fp_sale_description_template.description into two columns
# Copy existing description into BOTH internal_description and
# customer_facing_description. Estimators split them later.
cr.execute("""
UPDATE fp_sale_description_template
SET internal_description = description,
customer_facing_description = description
WHERE description IS NOT NULL
AND description <> ''
AND (internal_description IS NULL OR internal_description = '')
""")
_logger.info(
"Sub 2: duplicated description into new columns on %d template rows",
cr.rowcount,
)
# Step 4: Backfill x_fc_internal_description on sale.order.line
# Copy the existing `name` (Odoo's line description) into internal so
# historical lines satisfy the required-field check when it flips.
cr.execute("""
UPDATE sale_order_line
SET x_fc_internal_description = name
WHERE x_fc_internal_description IS NULL OR x_fc_internal_description = ''
""")
_logger.info(
"Sub 2: backfilled x_fc_internal_description on %d SO lines",
cr.rowcount,
)
# Step 5: Default certificate_requirement to 'inherit' on any rows
# where it's NULL (shouldn't happen given Odoo default=, but defensive).
cr.execute("""
UPDATE fp_part_catalog
SET certificate_requirement = 'inherit'
WHERE certificate_requirement IS NULL
""")
_logger.info(
"Sub 2: defaulted certificate_requirement to 'inherit' on %d parts",
cr.rowcount,
)
_logger.info("Sub 2: migration complete")
```
- [ ] **Step 3 — Create empty __init__.py files** (Odoo requires them for migrations dir)
```bash
touch /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/migrations/__init__.py
touch /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/migrations/19.0.9.0.0/__init__.py
```
- [ ] **Step 4 — Syntax check**
```bash
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py').read()); print('OK')"
```
Expected: `OK`
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/migrations/
git commit -m "feat(configurator): Sub 2 data migration — backfill part_number/revision, split descriptions (Task 5)"
```
---
### Task 6 — Write migration verification SQL
**Files:**
- Create: `docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql`
- [ ] **Step 1 — Create the tests directory**
```bash
mkdir -p /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests
```
- [ ] **Step 2 — Write the verification SQL**
Create `docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql`:
```sql
-- Sub 2 — Post-migration verification. Run on local dev DB after upgrade.
-- Every SELECT should return 0 rows (anomalies). If any return > 0, migration has gaps.
-- 1. No part should have an empty part_number
SELECT id, name FROM fp_part_catalog
WHERE (part_number IS NULL OR part_number = '') AND active = TRUE;
-- 2. No part should have an empty revision
SELECT id, name, part_number FROM fp_part_catalog
WHERE (revision IS NULL OR revision = '') AND active = TRUE;
-- 3. Every description-template row with text in `description` should have
-- both internal_description and customer_facing_description populated
SELECT id, name FROM fp_sale_description_template
WHERE description IS NOT NULL
AND description <> ''
AND (
internal_description IS NULL OR internal_description = ''
OR customer_facing_description IS NULL OR customer_facing_description = ''
);
-- 4. Every SO line with a non-empty `name` should have x_fc_internal_description set
SELECT id, order_id FROM sale_order_line
WHERE (name IS NOT NULL AND name <> '')
AND (x_fc_internal_description IS NULL OR x_fc_internal_description = '');
-- 5. Every part should have certificate_requirement set (default 'inherit')
SELECT id, name FROM fp_part_catalog
WHERE certificate_requirement IS NULL;
-- 6. Count summary (informational)
SELECT 'parts_total' AS metric, COUNT(*) AS value FROM fp_part_catalog
UNION ALL
SELECT 'parts_inherit_cert', COUNT(*) FROM fp_part_catalog WHERE certificate_requirement = 'inherit'
UNION ALL
SELECT 'description_templates_total', COUNT(*) FROM fp_sale_description_template
UNION ALL
SELECT 'description_templates_with_both', COUNT(*) FROM fp_sale_description_template
WHERE internal_description IS NOT NULL AND customer_facing_description IS NOT NULL
UNION ALL
SELECT 'so_lines_with_internal_desc', COUNT(*) FROM sale_order_line
WHERE x_fc_internal_description IS NOT NULL AND x_fc_internal_description <> '';
```
- [ ] **Step 3 — Commit**
```bash
git add docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql
git commit -m "test(sub2): migration verification SQL (Task 6)"
```
---
### Task 7 — Run migration on local dev, verify
**Files:** none (operational)
- [ ] **Step 1 — Upgrade the configurator module on local dev**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -20
```
Expected: "Sub 2: migration complete" appears in the log; no errors.
- [ ] **Step 2 — Restart odoo**
```bash
docker restart odoo-dev-app
```
- [ ] **Step 3 — Run the verification SQL**
```bash
docker exec odoo-dev-db psql -U odoo -d fusion-dev -f - < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql
```
Expected: queries 15 each return zero rows; query 6 prints population counts.
- [ ] **Step 4 — If any anomaly, investigate and fix migration script**
If queries 15 return non-zero, the migration has a gap. Root-cause → edit `post-migration.py` → re-run Step 1.
---
### Task 8 — Add `display_name` compute so part records render when `name` is blank
**Files:**
- Modify: `fusion_plating_configurator/models/fp_part_catalog.py`
- [ ] **Step 1 — Add the compute field**
Add `display_name` compute at the top of the `FpPartCatalog` class (Odoo's `_order = 'partner_id, part_number, revision'` may also need adjusting — handled in Step 3):
Find the existing `name = fields.Char(...)` (line 23). Directly above it, add:
```python
display_name = fields.Char(
string='Display Name',
compute='_compute_display_name',
store=True,
)
```
- [ ] **Step 2 — Add the compute method**
Find any existing compute method in the file for reference, then add this method at the bottom of the compute methods block (near `_compute_revision_count` if it exists):
```python
@api.depends('part_number', 'revision', 'name')
def _compute_display_name(self):
"""Display = 'PART-NUMBER (Rev X) — Optional Name'.
Used by m2o pickers, breadcrumbs, kanban cards. Falls back to
name-only when part_number is missing (legacy / in-progress records).
"""
for rec in self:
if rec.part_number:
core = f"{rec.part_number}"
if rec.revision:
core += f" (Rev {rec.revision})"
if rec.name:
core += f"{rec.name}"
rec.display_name = core
else:
rec.display_name = rec.name or _('[unnamed part]')
```
- [ ] **Step 3 — Update `_order`** to sort by part_number instead of name
Find `_order = 'name'` or similar line (near line 22). Replace with:
```python
_order = 'partner_id, part_number, revision desc'
```
- [ ] **Step 4 — Upgrade + spot-check**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
```
Then open http://localhost:8069, log in, navigate to Plating → Sales → Part Catalog. Every row should show `PART-NUMBER (Rev X) — Name`.
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py
git commit -m "feat(configurator): display_name compute for fp.part.catalog (Sub 2 Task 8)"
```
---
### Task 9 — Add `_fp_resolve_cert_requirement` to `mrp.production`
**Files:**
- Modify: `fusion_plating_bridge_mrp/models/mrp_production.py`
- [ ] **Step 1 — Locate the insertion point**
```bash
grep -n "def _fp_build_delivery_vals\|def button_mark_done\|def action_open_active_qc" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py | head -5
```
Expected: `_fp_build_delivery_vals` exists around line ~1155. We'll insert the resolver just above it.
- [ ] **Step 2 — Add the resolver method**
Directly above `def _fp_build_delivery_vals(self, mo, job):`, insert:
```python
# ------------------------------------------------------------------
# Sub 2 — Certificate requirement resolution (single source)
# ------------------------------------------------------------------
def _fp_resolve_cert_requirement(self):
"""Resolve which certs are required for this MO.
Returns (want_coc: bool, want_thickness: bool).
Logic:
1. Collect every linked fp.part.catalog via the SO line walk.
2. For each part:
- if certificate_requirement != 'inherit' -> part wins
- else -> fall back to partner's x_fc_send_coc /
x_fc_send_thickness_report flags
3. Multi-line MO: strictest wins (any() across lines).
4. MO with no SO link: partner fallback; safe default (True, False).
This is the single entry point used by:
- _fp_generate_cert_pdf (MO-done cert cascade)
- QC gate when it audits thickness requirements
- Any future caller (Sub 6 will update this method when
partner-level flags move to per-location / per-contact).
"""
self.ensure_one()
SO = self.env['sale.order']
# Resolve partner via origin
partner = False
lines = self.env['sale.order.line']
if self.origin:
so = SO.search([('name', '=', self.origin)], limit=1)
if so:
partner = so.partner_id
lines = so.order_line
# No SO link — use partner-level fallback with safe defaults
if not lines:
if partner and 'x_fc_send_coc' in partner._fields:
return (
bool(partner.x_fc_send_coc),
bool(partner.x_fc_send_thickness_report),
)
# No partner at all — safe default: CoC yes, thickness no
return (True, False)
want_coc_any = False
want_thickness_any = False
for line in lines:
part = line.x_fc_part_catalog_id
if part and part.certificate_requirement != 'inherit':
# Part-level override wins
want_coc_line = part.certificate_requirement in (
'coc', 'coc_thickness',
)
want_thickness_line = (
part.certificate_requirement == 'coc_thickness'
)
else:
# Inherit (or no part) -> partner fallback
if partner and 'x_fc_send_coc' in partner._fields:
want_coc_line = bool(partner.x_fc_send_coc)
want_thickness_line = bool(
partner.x_fc_send_thickness_report
)
else:
want_coc_line = True
want_thickness_line = False
want_coc_any = want_coc_any or want_coc_line
want_thickness_any = want_thickness_any or want_thickness_line
return (want_coc_any, want_thickness_any)
```
- [ ] **Step 3 — Syntax check**
```bash
python3 -c "import ast; ast.parse(open('fusion_plating_bridge_mrp/models/mrp_production.py').read()); print('OK')"
```
Expected: `OK`
- [ ] **Step 4 — Commit**
```bash
git add fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
git commit -m "feat(bridge_mrp): _fp_resolve_cert_requirement single-source resolver (Sub 2 Task 9)"
```
---
### Task 10 — Rewire `_fp_generate_cert_pdf` and `button_mark_done` to use the resolver
**Files:**
- Modify: `fusion_plating_bridge_mrp/models/mrp_production.py`
- [ ] **Step 1 — Locate the old partner-flag reads inside button_mark_done**
```bash
grep -n "x_fc_send_coc\|x_fc_send_thickness_report" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
```
Expected: references inside `button_mark_done` (around line 850-870).
- [ ] **Step 2 — Replace the inline partner reads**
Find the block inside `button_mark_done` that looks roughly like this (line numbers may differ):
```python
want_coc = True # default for customers that predate the flag
want_thickness = True
if 'x_fc_send_coc' in customer._fields:
want_coc = bool(customer.x_fc_send_coc)
if 'x_fc_send_thickness_report' in customer._fields:
want_thickness = bool(customer.x_fc_send_thickness_report)
```
Replace it with:
```python
# Sub 2: part-level cert requirement wins; partner is fallback.
# Single entry point for all cert decisions.
want_coc, want_thickness = mo._fp_resolve_cert_requirement()
```
- [ ] **Step 3 — Syntax check**
```bash
python3 -c "import ast; ast.parse(open('fusion_plating_bridge_mrp/models/mrp_production.py').read()); print('OK')"
```
Expected: `OK`
- [ ] **Step 4 — Upgrade bridge_mrp on local dev**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_bridge_mrp --stop-after-init 2>&1 | tail -10
```
Expected: no errors.
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
git commit -m "refactor(bridge_mrp): route button_mark_done cert cascade through resolver (Sub 2 Task 10)"
```
---
### Task 11 — Regression test cert resolver via odoo-shell
**Files:**
- Create: `docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py`
- [ ] **Step 1 — Write the test script**
Create the file with:
```python
"""Sub 2 — test _fp_resolve_cert_requirement behaviour.
Run via:
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
< docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py
"""
import sys
env = self.env
def ok(msg): print(f" [OK] {msg}")
def fail(msg): print(f" [FAIL] {msg}"); sys.exit(1)
def hdr(t): print(f"\n=== {t} ===")
# ---- Setup ----
hdr("Setup")
Product = env['product.product']
product = Product.search([('active', '=', True)], limit=1)
Partner = env['res.partner']
partner = Partner.create({
'name': 'Sub2 CertResolver Test Co',
'is_company': True,
'customer_rank': 1,
'x_fc_send_coc': True,
'x_fc_send_thickness_report': False,
})
PartCat = env['fp.part.catalog']
part_inherit = PartCat.create({
'name': 'P-INHERIT', 'part_number': 'P-INHERIT', 'revision': 'A',
'partner_id': partner.id, 'certificate_requirement': 'inherit',
})
part_none = PartCat.create({
'name': 'P-NONE', 'part_number': 'P-NONE', 'revision': 'A',
'partner_id': partner.id, 'certificate_requirement': 'none',
})
part_thick = PartCat.create({
'name': 'P-THICK', 'part_number': 'P-THICK', 'revision': 'A',
'partner_id': partner.id, 'certificate_requirement': 'coc_thickness',
})
ok(f"parts created: {part_inherit.id}, {part_none.id}, {part_thick.id}")
def make_mo(lines):
"""Helper: builds an SO with the given part_catalogs, confirms it, returns MO."""
so = env['sale.order'].create({
'partner_id': partner.id,
'x_fc_po_number': 'TEST-PO',
'order_line': [
(0, 0, {
'product_id': product.id,
'product_uom_qty': 1,
'x_fc_part_catalog_id': pc.id,
}) for pc in lines
],
})
so.action_confirm()
mo = env['mrp.production'].create({
'product_id': product.id, 'product_qty': 1, 'origin': so.name,
})
mo.action_confirm()
return mo
# ---- Test 1: inherit → partner ----
hdr("1. Part=inherit + partner(coc=T, thick=F) → (T, F)")
mo1 = make_mo([part_inherit])
want_coc, want_thickness = mo1._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, False):
fail(f"got ({want_coc}, {want_thickness}), expected (True, False)")
ok("inherit falls through to partner correctly")
# ---- Test 2: part=none overrides partner ----
hdr("2. Part=none + partner(coc=T) → (F, F)")
mo2 = make_mo([part_none])
want_coc, want_thickness = mo2._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (False, False):
fail(f"got ({want_coc}, {want_thickness}), expected (False, False)")
ok("part=none wins over partner=coc")
# ---- Test 3: part=coc_thickness + partner(coc=F, thick=F) → (T, T) ----
hdr("3. Part=coc_thickness + partner=none → (T, T)")
partner.x_fc_send_coc = False # partner explicitly no
mo3 = make_mo([part_thick])
want_coc, want_thickness = mo3._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, True):
fail(f"got ({want_coc}, {want_thickness}), expected (True, True)")
ok("part=coc_thickness wins over partner=off")
partner.x_fc_send_coc = True # reset
# ---- Test 4: multi-line MO strictest wins ----
hdr("4. Multi-line [none, coc_thickness] → (T, T)")
mo4 = make_mo([part_none, part_thick])
want_coc, want_thickness = mo4._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, True):
fail(f"got ({want_coc}, {want_thickness}), expected (True, True)")
ok("multi-line strictest wins")
# ---- Test 5: MO without SO link → safe default ----
hdr("5. Orphan MO → (T, F) safe default")
orphan = env['mrp.production'].create({
'product_id': product.id, 'product_qty': 1, 'origin': 'NONEXISTENT-SO',
})
want_coc, want_thickness = orphan._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, False):
fail(f"got ({want_coc}, {want_thickness}), expected (True, False)")
ok("orphan MO falls back to safe default")
hdr("ALL CERT RESOLVER TESTS PASS")
```
- [ ] **Step 2 — Run the test**
```bash
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
< /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py \
2>&1 | grep -E "\[OK\]|\[FAIL\]|===|ALL CERT"
```
Expected: all 5 tests print `[OK]` then `ALL CERT RESOLVER TESTS PASS`.
- [ ] **Step 3 — Commit**
```bash
git add docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py
git commit -m "test(sub2): cert-resolver edge cases (Task 11)"
```
---
### Task 12 — Regression: Phase 13 QC suite still green
**Files:** none
- [ ] **Step 1 — Run the existing QC smoke test**
```bash
# The QC smoke test from previous phase
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
< /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_smoke.py \
2>&1 | grep -E "\[OK\]|\[FAIL\]|===|DONE"
```
If `fp_qc_smoke.py` doesn't exist in the tests dir, re-create it from the `/tmp/fp_qc_smoke.py` version used during Phase 13 work.
Expected: 9-step QC smoke passes green.
- [ ] **Step 2 — Run the QC E2E test**
```bash
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
< /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_e2e.py \
2>&1 | grep -E "\[OK\]|\[FAIL\]|===|ALL"
```
Expected: 8 edge cases still pass.
- [ ] **Step 3 — If any test fails, ROLLBACK via git and investigate**
```bash
git log --oneline -5
# If regression detected: identify which Sub 2 task broke it, fix, re-run tests
```
No commit — this is verification only.
---
## Phase B — UI updates + Report Macro + Report Rewiring
Users now see the new fields. Old fields still exist; new fields populated by migration. Required flags still off.
### Task 13 — Update part-catalog form: identity block + certificate group
**Files:**
- Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml`
- [ ] **Step 1 — Locate the form view**
```bash
grep -n "<record id=\"view_fp_part_catalog_form\"\|<field name=\"name\"\|<field name=\"part_number\"" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml | head -10
```
Find the identity block near the top of the form (where `name`, `part_number`, `revision`, `partner_id` are rendered).
- [ ] **Step 2 — Add certificate requirement field**
Find the existing identity `<group>` block that contains `name`, `part_number`, `revision`. After that group, add a new group:
```xml
<group string="Quality &amp; Delivery" name="quality_delivery">
<field name="certificate_requirement"/>
</group>
```
- [ ] **Step 3 — Relabel "SKU" if any occurrence exists in this file**
```bash
grep -n 'string="SKU"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
```
If any matches, change `string="SKU"` to `string="Part Number"`.
- [ ] **Step 4 — XML syntax check**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/fp_part_catalog_views.xml'); print('OK')"
```
Expected: `OK`
- [ ] **Step 5 — Upgrade + visually verify**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
```
Open browser → Plating → Sales → Part Catalog → any part. Confirm:
- "Quality & Delivery" group appears with Certificate Requirement dropdown
- No SKU label visible (all now "Part Number")
- [ ] **Step 6 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
git commit -m "feat(configurator): cert requirement + SKU relabel on part form (Sub 2 Task 13)"
```
---
### Task 14 — Update part-catalog form: two-column Descriptions repeater
**Files:**
- Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml`
- [ ] **Step 1 — Locate the Descriptions tab**
```bash
grep -n 'description_template_ids\|"Descriptions"\|Descriptions' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml | head -10
```
- [ ] **Step 2 — Replace the single-description list with two-column**
Find the `<field name="description_template_ids">` block. Inside it replace the list view with:
```xml
<field name="description_template_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name" placeholder="e.g. Standard, With threaded holes masked"/>
<field name="tag" optional="show"/>
<field name="internal_description" placeholder="What the shop floor sees on the WO / traveler"/>
<field name="customer_facing_description" placeholder="What prints on SO, invoice, packing slip"/>
<field name="usage_count" string="Used" optional="hide"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
```
Add a hint `<div>` above the field:
```xml
<div class="alert alert-info" role="alert">
<strong>Canned descriptions for this part.</strong>
Internal = what the shop floor sees on the WO / traveler.
Customer-Facing = what prints on SO, invoice, packing slip.
Whichever row the estimator picks on the order wizard lands both values on the SO line.
</div>
<field name="description_template_ids">
...
</field>
```
- [ ] **Step 3 — XML syntax check**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/fp_part_catalog_views.xml'); print('OK')"
```
Expected: `OK`
- [ ] **Step 4 — Upgrade + verify**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
```
Browser → part → Descriptions tab. Two columns should show. Verify an existing row has the SAME text in both columns (from migration).
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
git commit -m "feat(configurator): two-column dual-description repeater (Sub 2 Task 14)"
```
---
### Task 15 — Update SO line view: internal description + template picker
**Files:**
- Modify: `fusion_plating_configurator/views/sale_order_views.xml`
- [ ] **Step 1 — Locate the SO line form**
```bash
grep -n 'sale.order.line\|<field name=\"order_line\"\|x_fc_part_catalog_id' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml | head -10
```
- [ ] **Step 2 — Add the template picker + internal description**
Find the `<field name="x_fc_part_catalog_id"/>` on the SO line form. Directly after it, add:
```xml
<field name="x_fc_description_template_id"
domain="[('part_catalog_id', '=', x_fc_part_catalog_id)]"
options="{'no_create': True}"
invisible="not x_fc_part_catalog_id"/>
<field name="x_fc_internal_description"
placeholder="Shop-floor workflow instructions (prints on WO / traveler)"/>
```
- [ ] **Step 3 — Add a QWeb onchange hook (optional — do inline in XML via context)**
In `models/sale_order_line.py`, add an onchange method so picking a template fills both fields:
```python
@api.onchange('x_fc_description_template_id')
def _onchange_description_template(self):
if self.x_fc_description_template_id:
tpl = self.x_fc_description_template_id
self.name = tpl.customer_facing_description or self.name
self.x_fc_internal_description = tpl.internal_description
```
- [ ] **Step 4 — XML + Python syntax checks**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/sale_order_views.xml'); print('OK')"
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/sale_order_line.py').read()); print('OK')"
```
Expected: both `OK`.
- [ ] **Step 5 — Upgrade + manual test**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
```
Browser → Create a new quote → pick a part → pick a description template. Verify both `name` (customer-facing) and `x_fc_internal_description` fields populate.
- [ ] **Step 6 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/views/sale_order_views.xml \
fusion_plating/fusion_plating_configurator/models/sale_order_line.py
git commit -m "feat(configurator): SO-line template picker + dual descriptions (Sub 2 Task 15)"
```
---
### Task 16 — Update direct-order wizard view
**Files:**
- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml`
- [ ] **Step 1 — Find the line-repeater in the wizard**
```bash
grep -n 'x_fc_part_catalog_id\|<field name=\"order_line_ids\"\|<field name=\"line_ids\"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml | head -10
```
- [ ] **Step 2 — Add the template picker + internal description column**
In the order-line list (inside the wizard), after the part picker field, add:
```xml
<field name="x_fc_description_template_id"
domain="[('part_catalog_id', '=', x_fc_part_catalog_id)]"
options="{'no_create': True}"
invisible="not x_fc_part_catalog_id"/>
<field name="x_fc_internal_description"/>
```
Note: this assumes the wizard line model `fp.direct.order.line` already has these fields or inherits from `sale.order.line`. Check with:
```bash
grep -n "_name\|_inherit" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py
```
If `fp.direct.order.line` is a standalone transient model, add the same two fields to it as were added to `sale.order.line` in Task 3. Copy the field definitions verbatim.
- [ ] **Step 3 — XML syntax check**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml'); print('OK')"
```
Expected: `OK`
- [ ] **Step 4 — Upgrade + manual test**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
```
Browser → Plating → Sales → New Direct Order. Verify two description columns appear per line.
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/wizard/
git commit -m "feat(configurator): direct-order wizard dual-description inputs (Sub 2 Task 16)"
```
---
### Task 17 — Create the customer_line_header QWeb macro
**Files:**
- Create: `fusion_plating_reports/report/customer_line_header.xml`
- [ ] **Step 1 — Write the macro**
Create `fusion_plating_reports/report/customer_line_header.xml`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1
Sub 2 — shared QWeb macro for customer-facing line rendering.
Called from report_fp_sale, report_fp_invoice, report_fp_packing_slip,
report_fp_bol. Prints the customer's part number + revision + the
line's customer-facing description (the `name` field, standard Odoo).
For non-part lines (rush fees, freight, expedite) where
x_fc_part_catalog_id is blank, falls back to Odoo's standard product
display — safe for fee/service lines that shouldn't look like parts.
Params expected in the calling context:
line - the sale.order.line / account.move.line / stock.picking line
Usage:
<t t-call="fusion_plating_reports.customer_line_header"/>
-->
<odoo>
<template id="customer_line_header">
<t t-if="line.x_fc_part_catalog_id">
<strong>
<span t-esc="line.x_fc_part_catalog_id.part_number"/>
<t t-if="line.x_fc_part_catalog_id.revision">
<span> (Rev <span t-esc="line.x_fc_part_catalog_id.revision"/>)</span>
</t>
</strong>
<br/>
<span t-esc="line.name"/>
</t>
<t t-else="">
<!-- Fee / freight / non-part line: standard Odoo rendering -->
<strong t-esc="line.product_id.display_name or ''"/>
<t t-if="line.name and line.name != line.product_id.display_name">
<br/>
<span t-esc="line.name"/>
</t>
</t>
</template>
</odoo>
```
- [ ] **Step 2 — Register the file in the reports manifest**
Edit `fusion_plating_reports/__manifest__.py`. Find the `'data': [` list. Add `'report/customer_line_header.xml',` BEFORE any other `report_fp_*.xml` entries (so the macro is loaded before its callers).
Also bump `'version'` from `19.0.4.9.0` to `19.0.5.0.0`.
- [ ] **Step 3 — Syntax checks**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/customer_line_header.xml'); print('OK')"
python3 -c "import ast; ast.parse(open('fusion_plating_reports/__manifest__.py').read()); print('OK')"
```
Expected: both `OK`.
- [ ] **Step 4 — Upgrade + verify macro loads**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -10
```
Expected: no errors. Template registered in ir_ui_view.
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_reports/
git commit -m "feat(reports): customer_line_header QWeb macro + version bump (Sub 2 Task 17)"
```
---
### Task 18 — Rewire report_fp_sale.xml to use macro
**Files:**
- Modify: `fusion_plating_reports/report/report_fp_sale.xml`
- [ ] **Step 1 — Find the current line rendering**
```bash
grep -n "line.product_id.default_code\|product_id.display_name\|line.name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml
```
Expected: two locations where `default_code` is printed (around lines 159 and 436 per earlier scan). Each is a line-item row inside a list table.
- [ ] **Step 2 — Replace with macro call**
For each occurrence of:
```xml
<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>
```
AND the adjacent cell that renders the line name (look for the `<td>` containing `<span t-esc="line.name"/>` or product display name nearby), REPLACE the two cells with ONE cell that uses the macro:
```xml
<td>
<t t-call="fusion_plating_reports.customer_line_header"/>
</td>
```
This collapses the former "SKU | Description" two-column layout into one "Part" column. Adjust the table header to match: rename the column from "SKU" / "Description" to "Part".
- [ ] **Step 3 — XML syntax check**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_sale.xml'); print('OK')"
```
Expected: `OK`
- [ ] **Step 4 — Upgrade + render a sample SO**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
```
Browser → any Sale Order → Print → Fusion Plating SO. Verify:
- No "[EN-PLATE]" or service SKU visible anywhere
- Each line shows `PART-NUMBER (Rev X)` in bold + customer-facing description below
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_reports/report/report_fp_sale.xml
git commit -m "feat(reports): SO PDF uses customer_line_header macro (Sub 2 Task 18)"
```
---
### Task 19 — Rewire report_fp_invoice.xml
**Files:**
- Modify: `fusion_plating_reports/report/report_fp_invoice.xml`
- [ ] **Step 1 — Find line-rendering block**
```bash
grep -n "default_code\|line.name\|product_id.display_name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml
```
- [ ] **Step 2 — Apply the same macro swap as Task 18**
Replace the `<td>` cells that render `default_code` / product name / line name with a single cell:
```xml
<td>
<t t-call="fusion_plating_reports.customer_line_header"/>
</td>
```
Important: invoice line model is `account.move.line`, not `sale.order.line`. The `x_fc_part_catalog_id` field must exist on `account.move.line` for the macro to work. Check:
```bash
grep -n "x_fc_part_catalog_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/
```
If the invoice line doesn't have `x_fc_part_catalog_id` yet, add it to `fusion_plating_configurator/models/` in a new file `account_move_line.py`:
```python
from odoo import fields, models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
x_fc_part_catalog_id = fields.Many2one(
'fp.part.catalog', string='Part',
help='Populated by sale_order → account_move invoice creation.',
)
```
And ensure it's imported in `models/__init__.py`.
Also extend the invoice creation hook (probably in sale_order.py) to copy `x_fc_part_catalog_id` from SO line → invoice line. Look at `_prepare_invoice_line` in the existing codebase.
- [ ] **Step 3 — Syntax checks**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_invoice.xml'); print('OK')"
```
- [ ] **Step 4 — Upgrade + render sample invoice**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator,fusion_plating_reports --stop-after-init 2>&1 | tail -5
```
Browser → any Invoice → Print → Fusion Plating Invoice. Verify same rendering as SO (Task 18).
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml \
fusion_plating/fusion_plating_configurator/models/
git commit -m "feat(reports+configurator): invoice PDF uses macro; x_fc_part_catalog_id on account.move.line (Sub 2 Task 19)"
```
---
### Task 20 — Rewire report_fp_packing_slip.xml
**Files:**
- Modify: `fusion_plating_reports/report/report_fp_packing_slip.xml`
- [ ] **Step 1 — Find line-rendering block**
```bash
grep -n "default_code\|line.name\|product_id.display_name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml
```
- [ ] **Step 2 — Apply macro swap**
Packing slip likely iterates `stock.move.line` or `stock.picking` lines. The macro expects `line.x_fc_part_catalog_id`. Check what the loop variable represents. If `move`, adjust the macro call to pass the correct variable, OR add a `t-set` adapter:
```xml
<t t-set="line" t-value="move.sale_line_id or move"/>
<t t-call="fusion_plating_reports.customer_line_header"/>
```
- [ ] **Step 3 — Syntax check + visual verify + commit**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_packing_slip.xml'); print('OK')"
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
# Browser: print a packing slip. Verify part number renders.
git add fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml
git commit -m "feat(reports): packing slip PDF uses macro (Sub 2 Task 20)"
```
---
### Task 21 — Rewire report_fp_bol.xml
**Files:**
- Modify: `fusion_plating_reports/report/report_fp_bol.xml`
Mirror of Task 20. Swap line header to use `<t t-call="fusion_plating_reports.customer_line_header"/>`.
- [ ] Apply same steps as Task 20 on `report_fp_bol.xml`.
- [ ] Syntax check, upgrade, visual verify, commit with message `feat(reports): BoL PDF uses macro (Sub 2 Task 21)`.
---
### Task 22 — Update report_fp_work_order.xml (internal — ADD fields, don't remove)
**Files:**
- Modify: `fusion_plating_reports/report/report_fp_work_order.xml`
- [ ] **Step 1 — Locate the existing line/description block**
```bash
grep -n "line.name\|product.default_code\|product_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_work_order.xml | head -10
```
- [ ] **Step 2 — Add internal-description rendering**
Internal reports keep everything. Add the internal description below the product description. Example pattern — find where the WO currently shows `line.name` and augment:
```xml
<div>
<strong>Part:</strong>
<span t-if="line.x_fc_part_catalog_id">
<span t-esc="line.x_fc_part_catalog_id.part_number"/>
<span> (Rev <span t-esc="line.x_fc_part_catalog_id.revision"/>)</span>
</span>
<span t-else="">
<span t-esc="line.product_id.display_name"/>
</span>
</div>
<div>
<strong>Customer-Facing Description:</strong>
<span t-esc="line.name"/>
</div>
<div t-if="line.x_fc_internal_description">
<strong>Internal Description / Workflow:</strong>
<span t-esc="line.x_fc_internal_description" style="white-space: pre-wrap;"/>
</div>
<div>
<strong>Service SKU:</strong>
<span t-esc="line.product_id.default_code or '—'"/>
</div>
```
- [ ] **Step 3 — Syntax check, upgrade, visual verify**
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_work_order.xml'); print('OK')"
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
# Browser: print a work order. Verify Part Number, Customer-Facing Description, Internal Description, Service SKU all visible.
```
- [ ] **Step 4 — Commit**
```bash
git add fusion_plating/fusion_plating_reports/report/report_fp_work_order.xml
git commit -m "feat(reports): WO PDF surfaces internal description + part number (Sub 2 Task 22)"
```
---
### Task 23 — Update report_fp_job_traveller.xml (same as Task 22)
**Files:**
- Modify: `fusion_plating_reports/report/report_fp_job_traveller.xml`
Apply the identical pattern as Task 22 to the traveler report.
- [ ] Apply Task 22's pattern on `report_fp_job_traveller.xml`. Syntax check, upgrade, visual verify.
- [ ] Commit: `feat(reports): traveler PDF surfaces internal description + part number (Sub 2 Task 23)`
---
## Phase C — Required-Field Flip + Cleanup
Point of no return. Only execute once Phase B is verified end-to-end.
### Task 24 — Flip required flags on fp.part.catalog
**Files:**
- Modify: `fusion_plating_configurator/models/fp_part_catalog.py`
- [ ] **Step 1 — Update the fields**
```python
name = fields.Char(string='Part Name', tracking=True) # was required=True
part_number = fields.Char(string='Part Number', required=True, tracking=True) # was optional
revision = fields.Char(string='Revision', required=True, default='A',
help='Revision letter or number (e.g. Rev: 1B).')
```
- [ ] **Step 2 — Upgrade + verify**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
```
Expected: no errors. Existing parts (all backfilled from Task 7) pass validation.
- [ ] **Step 3 — Try to save a new part with empty part_number**
Manually in browser → New Part → leave Part Number blank → Save. Expected: Odoo rejects with a validation error.
- [ ] **Step 4 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py
git commit -m "feat(configurator): flip part_number + revision to required, name optional (Sub 2 Task 24)"
```
---
### Task 25 — Flip required flags on description template
**Files:**
- Modify: `fusion_plating_configurator/models/fp_sale_description_template.py`
- [ ] **Step 1 — Update the two new fields to required**
```python
internal_description = fields.Text(
string='Internal Description',
required=True,
help='What the shop floor sees on the WO / traveler. Never on customer documents.',
)
customer_facing_description = fields.Text(
string='Customer-Facing Description',
required=True,
help='Prints on the SO, invoice, packing slip, and BoL.',
)
```
- [ ] **Step 2 — Upgrade + verify**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
```
- [ ] **Step 3 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py
git commit -m "feat(configurator): flip dual-descriptions to required on template (Sub 2 Task 25)"
```
---
### Task 26 — Flip SO-line internal description to required
**Files:**
- Modify: `fusion_plating_configurator/models/sale_order_line.py`
- [ ] **Step 1 — Update the field**
```python
x_fc_internal_description = fields.Text(
string='Internal Description',
required=True,
help='Shop-floor instructions. Prints on WO / traveler. Never on customer docs.',
)
```
- [ ] **Step 2 — Upgrade + verify historical rows still valid**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
```
If any historical SO line has NULL `x_fc_internal_description` (shouldn't, after Task 7), upgrade fails. Re-run migration with `-u fusion_plating_configurator` and verify.
- [ ] **Step 3 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/models/sale_order_line.py
git commit -m "feat(configurator): flip sale.order.line.x_fc_internal_description to required (Sub 2 Task 26)"
```
---
### Task 27 — Drop the legacy `description` column from the template
**Files:**
- Modify: `fusion_plating_configurator/models/fp_sale_description_template.py`
- Modify: `fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py` (add cleanup step)
- Modify: views that still reference the old `description` field (should be zero after Task 14, but verify)
- [ ] **Step 1 — Remove the `description` field from the Python model**
Delete the line:
```python
description = fields.Text(...)
```
from `fp_sale_description_template.py`.
- [ ] **Step 2 — Add column drop to migration**
Add to the end of the `migrate()` function in `post-migration.py`:
```python
# Step 6: Drop legacy description column (all reads migrated to new fields)
cr.execute("""
ALTER TABLE fp_sale_description_template
DROP COLUMN IF EXISTS description
""")
_logger.info("Sub 2: dropped legacy description column")
```
- [ ] **Step 3 — Verify no views reference the old `description` column**
```bash
grep -rn 'name="description"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/ | grep -i template
```
Expected: no matches (Task 14 already replaced with the two new fields). If any remain, remove them.
- [ ] **Step 4 — Upgrade + verify column gone**
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
docker exec odoo-dev-db psql -U odoo -d fusion-dev -c "\d fp_sale_description_template" | grep -i description
```
Expected: only `internal_description` and `customer_facing_description` appear; no bare `description` column.
- [ ] **Step 5 — Commit**
```bash
git add fusion_plating/fusion_plating_configurator/
git commit -m "feat(configurator): drop legacy description column; model cleanup (Sub 2 Task 27)"
```
---
### Task 28 — Bump bridge_mrp version
**Files:**
- Modify: `fusion_plating_bridge_mrp/__manifest__.py`
- [ ] **Step 1 — Bump version**
```python
'version': '19.0.9.0.0', # was 19.0.8.0.0
```
- [ ] **Step 2 — Commit**
```bash
git add fusion_plating/fusion_plating_bridge_mrp/__manifest__.py
git commit -m "chore(bridge_mrp): bump to 19.0.9.0.0 after cert-resolver refactor (Sub 2 Task 28)"
```
---
### Task 29 — Final end-to-end smoke test
**Files:**
- Create: `docs/superpowers/tests/2026-04-21-sub2-smoke.py`
- [ ] **Step 1 — Write the smoke test**
Create the file:
```python
"""Sub 2 — end-to-end smoke. Full lifecycle with Sub 2 features.
Confirms:
- new parts reject without part_number / revision
- description template row requires both fields
- SO line requires both descriptions
- customer-facing reports render part_number, not default_code
- cert cascade via resolver, part-level wins over partner
"""
import sys
env = self.env
def ok(msg): print(f" [OK] {msg}")
def fail(msg): print(f" [FAIL] {msg}"); sys.exit(1)
def hdr(t): print(f"\n=== {t} ===")
# ---- 1. Required fields on fp.part.catalog ----
hdr("1. Required fields")
try:
env['fp.part.catalog'].create({
'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id,
})
fail("accepted part with no part_number / revision")
except Exception as e:
ok(f"rejected blank part: {str(e)[:80]}")
# ---- 2. Description template two-field requirement ----
hdr("2. Description template required fields")
part = env['fp.part.catalog'].create({
'name': 'Sub2 Smoke Part',
'part_number': 'SUB2-SMOKE-001',
'revision': 'A',
'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id,
})
try:
env['fp.sale.description.template'].create({
'name': 'Broken template',
'part_catalog_id': part.id,
'internal_description': 'ops only',
# customer_facing_description missing
})
fail("accepted template without customer_facing_description")
except Exception as e:
ok(f"rejected template: {str(e)[:80]}")
# ---- 3. SO line dual descriptions ----
hdr("3. SO line required internal description")
tpl = env['fp.sale.description.template'].create({
'name': 'Standard',
'part_catalog_id': part.id,
'internal_description': 'Racking pattern A; mask threaded holes',
'customer_facing_description': 'Electroless nickel plate per customer spec',
})
product = env['product.product'].search([('active', '=', True)], limit=1)
so_vals = {
'partner_id': part.partner_id.id,
'order_line': [(0, 0, {
'product_id': product.id,
'product_uom_qty': 1,
'x_fc_part_catalog_id': part.id,
'x_fc_description_template_id': tpl.id,
})],
}
if 'x_fc_po_number' in env['sale.order']._fields:
so_vals['x_fc_po_number'] = 'SUB2-SMOKE-PO'
so = env['sale.order'].create(so_vals)
# Onchange should have copied from template
line = so.order_line[0]
if line.name != tpl.customer_facing_description:
fail(f"line.name not set from template: got {line.name!r}")
if line.x_fc_internal_description != tpl.internal_description:
fail(f"line.x_fc_internal_description not set: got {line.x_fc_internal_description!r}")
ok("SO line populated from template onchange")
# ---- 4. Cert resolver end-to-end ----
hdr("4. Cert resolver with part override")
part.certificate_requirement = 'coc_thickness'
part.partner_id.x_fc_send_coc = False
part.partner_id.x_fc_send_thickness_report = False
so.action_confirm()
mo = env['mrp.production'].create({
'product_id': product.id, 'product_qty': 1, 'origin': so.name,
})
mo.action_confirm()
want_coc, want_thickness = mo._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, True):
fail(f"resolver returned ({want_coc}, {want_thickness}); expected (True, True)")
ok("part-level coc_thickness override working end-to-end")
# ---- 5. display_name compute ----
hdr("5. display_name includes part_number + revision")
part.invalidate_recordset()
if 'SUB2-SMOKE-001' not in part.display_name:
fail(f"display_name missing part_number: {part.display_name!r}")
if 'Rev A' not in part.display_name:
fail(f"display_name missing revision: {part.display_name!r}")
ok(f"display_name: {part.display_name}")
hdr("SUB 2 SMOKE COMPLETE")
```
- [ ] **Step 2 — Run it**
```bash
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
< /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-smoke.py \
2>&1 | grep -E "\[OK\]|\[FAIL\]|===|SUB 2"
```
Expected: all 5 sections `[OK]`, ends with `SUB 2 SMOKE COMPLETE`.
- [ ] **Step 3 — Regression: QC suite still green**
Run the Phase 13 QC smoke + E2E again (Task 12 pattern):
```bash
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
< /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_smoke.py \
2>&1 | grep -E "\[OK\]|\[FAIL\]|DONE"
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
< /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_e2e.py \
2>&1 | grep -E "\[OK\]|\[FAIL\]|ALL"
```
Expected: both still green.
- [ ] **Step 4 — Commit the smoke script**
```bash
git add docs/superpowers/tests/2026-04-21-sub2-smoke.py
git commit -m "test(sub2): end-to-end smoke covers required-field flip, onchange, resolver, display_name (Task 29)"
```
---
### Task 30 — Deploy to entech + run migration verification there
**Files:** none (operational)
- [ ] **Step 1 — Package up all three modules**
```bash
cd /Users/gurpreet/Github/Odoo-Modules/fusion_plating
for mod in fusion_plating_configurator fusion_plating_reports fusion_plating_bridge_mrp; do
tar --exclude='__pycache__' --exclude='*.pyc' -czf /tmp/$mod.tgz $mod/
done
```
- [ ] **Step 2 — Push each to entech**
```bash
for mod in fusion_plating_configurator fusion_plating_reports fusion_plating_bridge_mrp; do
cat /tmp/$mod.tgz | base64 | ssh pve-worker5 \
"pct exec 111 -- bash -c 'base64 -d > /tmp/fp_deploy/$mod.tgz && \
cd /mnt/extra-addons/custom && rm -rf $mod && tar xzf /tmp/fp_deploy/$mod.tgz && echo $mod DEPLOYED'"
done
```
- [ ] **Step 3 — Update modules in order (configurator → reports → bridge_mrp)**
```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 fusion_plating_configurator,fusion_plating_reports,fusion_plating_bridge_mrp \
--stop-after-init 2>&1 | tail -20\" && \
systemctl start odoo'"
```
Expected: "Sub 2: migration complete" in log; no errors.
- [ ] **Step 4 — Clear asset cache**
```bash
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\""
```
- [ ] **Step 5 — Run migration verification SQL on entech**
```bash
cat /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql \
| ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /tmp/sub2-verify.sql && \
su - postgres -c \"psql -d admin -f /tmp/sub2-verify.sql\"'"
```
Expected: queries 15 return zero rows; query 6 shows population counts.
- [ ] **Step 6 — Run end-to-end smoke on entech**
Same pattern as Task 29 Step 2, but via `ssh pve-worker5 "pct exec 111 ..."` invoking odoo shell on entech.
- [ ] **Step 7 — Push commits upstream**
```bash
cd /Users/gurpreet/Github/Odoo-Modules/fusion_plating
git push origin main
```
---
## Plan Self-Review (fill in actuals only — do not rerun tasks)
### Spec coverage check
| Spec § | Spec item | Plan task |
|---------|-----------------------------------------------|-----------|
| §2.1 | `certificate_requirement` on fp.part.catalog | Task 1 |
| §2.1 | part_number / revision required; name opt. | Tasks 3 (add) + 24 (flip) |
| §2.2 | Split `description` → two columns | Task 2 (add) + 25 (flip) + 27 (drop) |
| §2.3 | SO-line dual descriptions | Tasks 3 + 15 + 26 |
| §3 Step 1 | Backfill part_number | Task 5 + verify 7 |
| §3 Step 2 | Backfill revision='A' | Task 5 + verify 7 |
| §3 Step 3 | Duplicate description to new columns | Task 5 + verify 7 |
| §3 Step 4 | Backfill SO-line internal desc | Task 5 + verify 7 |
| §3 Step 5 | Default cert='inherit' | Task 5 + verify 7 |
| §4.1 | Part form — cert dropdown + relabel | Task 13 |
| §4.1 | Part form — two-column repeater | Task 14 |
| §4.2 | SO / wizard — template picker + dual desc | Tasks 15 + 16 |
| §4.3 | Universal SKU relabel | Task 13 (Fusion views) + macro swap in Tasks 1822 |
| §5.1 | Single-source cert resolver | Task 9 |
| §5.3 | Resolver callers updated | Task 10 |
| §5.4 | Multi-line + orphan-MO cases | Task 11 tests |
| §6.1 | Shared QWeb macro | Task 17 |
| §6.2 | Report rewiring (4 customer, 2 internal) | Tasks 18, 19, 20, 21, 22, 23 |
| §6.3 | Non-part fallback | Task 17 macro body |
| §7.1 | Migration tests | Task 6 (SQL) + 7 (run) |
| §7.2 | Unit tests | Task 11 (resolver) + 29 (smoke) |
| §7.3 | End-to-end smoke | Task 29 |
| §7.4 | QC regression | Task 12 + Task 29 Step 3 |
| §8.1 | Defensive measure 1 — resolver | Task 9 |
| §8.2 | Defensive measure 2 — macro | Task 17 |
| §8.3 | Defensive measure 3 — idempotent migration | Task 5 (NULL guards) |
| §8.4 | Defensive measure 4 — additive SO fields | Task 3 |
| §8.5 | Defensive measure 5 — drop old column | Task 27 |
All spec sections covered.
### Placeholder scan
No "TBD", "TODO", "fill in", "similar to above" in the task bodies. Every code block is complete and ready to copy-paste.
### Type / method consistency
- `_fp_resolve_cert_requirement` (Task 9) returns `(bool, bool)`. Callers in Task 10 unpack `want_coc, want_thickness = ...`. Consistent.
- `customer_line_header` macro (Task 17) reads `line.x_fc_part_catalog_id` — matches field name on sale.order.line (Task 3) and account.move.line (Task 19).
- `display_name` (Task 8) stored compute — matches `_compute_display_name`.
- `certificate_requirement` Selection values (`inherit` / `none` / `coc` / `coc_thickness`) — used identically in Tasks 1, 5, 9, 11, 29.
---
## Summary
- **30 tasks** spanning three phases (A: additive + migrate + resolver, B: UI + reports, C: required-flag flip + cleanup).
- System remains runnable after every task in Phases A and B. Phase C is the flip-point-of-no-return.
- Every view / model / report change includes a syntax check, a module upgrade, a visual or script verify, and a commit.
- Regression tests (Phase 13 QC suite) run in Tasks 12 and 29.
- Deploy-to-entech is explicit (Task 30) — follows the CLAUDE.md deployment pattern.