From 418dabc68800fca4ee7d1aac6debc9ab8b0b9d9c Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 21 Apr 2026 20:02:34 -0400 Subject: [PATCH] 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) --- .../plans/2026-04-21-sub2-part-data-model.md | 1842 +++++++++++++++++ 1 file changed, 1842 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-21-sub2-part-data-model.md diff --git a/docs/superpowers/plans/2026-04-21-sub2-part-data-model.md b/docs/superpowers/plans/2026-04-21-sub2-part-data-model.md new file mode 100644 index 00000000..58d5f4ef --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-sub2-part-data-model.md @@ -0,0 +1,1842 @@ +# 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 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: + +```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 1–3 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 1–3 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 "` block that contains `name`, `part_number`, `revision`. After that group, add a new group: + +```xml + + + +``` + +- [ ] **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 `` block. Inside it replace the list view with: + +```xml + + + + + + + + + + + +``` + +Add a hint `
` above the field: + +```xml + + + ... + +``` + +- [ ] **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\|` on the SO line form. Directly after it, add: + +```xml + + +``` + +- [ ] **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\| + +``` + +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 + + + + + +``` + +- [ ] **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 + +``` + +AND the adjacent cell that renders the line name (look for the `` containing `` or product display name nearby), REPLACE the two cells with ONE cell that uses the macro: + +```xml + + + +``` + +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 `` cells that render `default_code` / product name / line name with a single cell: + +```xml + + + +``` + +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 + + +``` + +- [ ] **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 ``. + +- [ ] 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 +
+ Part: + + + (Rev ) + + + + +
+
+ Customer-Facing Description: + +
+
+ Internal Description / Workflow: + +
+
+ Service SKU: + +
+``` + +- [ ] **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 1–3 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 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** + +```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 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 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 1–3 QC suite) run in Tasks 12 and 29. +- Deploy-to-entech is explicit (Task 30) — follows the CLAUDE.md deployment pattern.