# 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.