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>
66 KiB
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 olddescriptioncolumn, 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.0fusion_plating_reports:19.0.4.9.0→19.0.5.0.0fusion_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— nodefault_codesyncfusion_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_qualityorfusion_plating_iotfiles — 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 theactivefield) -
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:
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:
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
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:
# 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
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/fp_sale_description_template.py').read()); print('OK')"
Expected: OK
- Step 3 — Commit
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
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:
# 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
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/sale_order_line.py').read()); print('OK')"
Expected: OK
- Step 4 — Commit
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:
'version': '19.0.8.0.0',
to:
'version': '19.0.9.0.0',
- Step 2 — Commit
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
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:
# -*- 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)
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
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
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
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:
-- 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
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
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
docker restart odoo-dev-app
- Step 3 — Run the verification SQL
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 1–5 each return zero rows; query 6 prints population counts.
- Step 4 — If any anomaly, investigate and fix migration script
If queries 1–5 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:
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):
@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
_orderto sort by part_number instead of name
Find _order = 'name' or similar line (near line 22). Replace with:
_order = 'partner_id, part_number, revision desc'
- Step 4 — Upgrade + spot-check
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
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
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:
# ------------------------------------------------------------------
# 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
python3 -c "import ast; ast.parse(open('fusion_plating_bridge_mrp/models/mrp_production.py').read()); print('OK')"
Expected: OK
- Step 4 — Commit
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
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):
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:
# 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
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
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
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:
"""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
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
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 1–3 QC suite still green
Files: none
- Step 1 — Run the existing QC smoke test
# 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 1–3 work.
Expected: 9-step QC smoke passes green.
- Step 2 — Run the QC E2E test
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
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
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:
<group string="Quality & Delivery" name="quality_delivery">
<field name="certificate_requirement"/>
</group>
- Step 3 — Relabel "SKU" if any occurrence exists in this file
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
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
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
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
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:
<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:
<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
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
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
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
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:
<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:
@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
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
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
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
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:
<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:
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
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
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
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 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
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
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
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
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:
<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:
<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
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
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
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
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:
<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:
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:
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
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
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
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
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:
<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
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
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:
<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
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
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
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
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
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
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
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
- Step 3 — Commit
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
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
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
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
descriptionfield (should be zero after Task 14, but verify) -
Step 1 — Remove the
descriptionfield from the Python model
Delete the line:
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:
# 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
descriptioncolumn
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
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
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
'version': '19.0.9.0.0', # was 19.0.8.0.0
- Step 2 — Commit
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:
"""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
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 1–3 QC smoke + E2E again (Task 12 pattern):
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
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
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
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)
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
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
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 1–5 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
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 18–22 |
| §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 unpackwant_coc, want_thickness = .... Consistent.customer_line_headermacro (Task 17) readsline.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_requirementSelection 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 1–3 QC suite) run in Tasks 12 and 29.
- Deploy-to-entech is explicit (Task 30) — follows the CLAUDE.md deployment pattern.