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

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

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

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

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

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

66 KiB
Raw Blame History

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.019.0.9.0.0
  • fusion_plating_reports: 19.0.4.9.019.0.5.0.0
  • fusion_plating_bridge_mrp: 19.0.8.0.019.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:

    certificate_requirement = fields.Selection(
        [
            ('inherit', 'Inherit from Customer'),
            ('none', 'No Certificate'),
            ('coc', 'CoC Only'),
            ('coc_thickness', 'CoC + Thickness Report'),
        ],
        string='Certificate Requirement',
        default='inherit',
        required=True,
        tracking=True,
        help='Determines which quality documents ship with this part. '
             '"Inherit" reads the customer\'s default on the partner form.',
    )
  • Step 2 — Syntax check

Run:

cd /Users/gurpreet/Github/Odoo-Modules/fusion_plating
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/fp_part_catalog.py').read()); print('OK')"

Expected: OK

  • Step 3 — Commit
git add fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py
git commit -m "feat(configurator): add certificate_requirement field to fp.part.catalog (Sub 2 Task 1)"

Task 2 — Add dual-description fields to fp.sale.description.template

Files:

  • Modify: fusion_plating_configurator/models/fp_sale_description_template.py

  • Step 1 — Add the two new fields alongside the old

Open the file. Find the description = fields.Text(...) line (around line 33). Keep it. Add these two fields after it:

    # Sub 2 — dual descriptions. Added alongside the legacy `description`
    # field; migration copies old value into both, then old column dropped
    # in Phase C. Nullable during Phase A so existing records don't fail.
    internal_description = fields.Text(
        string='Internal Description',
        help='What the shop floor sees on the WO / traveler. Never on '
             'customer documents.',
    )
    customer_facing_description = fields.Text(
        string='Customer-Facing Description',
        help='Prints on the SO, invoice, packing slip, and BoL.',
    )
  • Step 2 — Syntax check
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/fp_sale_description_template.py').read()); print('OK')"

Expected: OK

  • Step 3 — Commit
git add fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py
git commit -m "feat(configurator): add internal + customer-facing description fields (Sub 2 Task 2)"

Task 3 — Add SO-line dual description fields

Files:

  • Modify: fusion_plating_configurator/models/sale_order_line.py

  • Step 1 — Scan the existing class

grep -n "class SaleOrderLine\|_inherit\|x_fc_part_catalog_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/sale_order_line.py | head -10

Expected: a class inheriting sale.order.line, with x_fc_part_catalog_id already defined.

  • Step 2 — Add the fields

Inside the class, right after x_fc_part_catalog_id:

    # Sub 2 — dual descriptions captured from a template row at order
    # entry. `name` remains Odoo's standard customer-facing line
    # description; x_fc_internal_description is ops-only (prints on WO).
    # Nullable during Phase A; flipped to required in Phase C.
    x_fc_internal_description = fields.Text(
        string='Internal Description',
        help='Shop-floor instructions. Prints on WO / traveler. Never on customer docs.',
    )
    x_fc_description_template_id = fields.Many2one(
        'fp.sale.description.template',
        string='Description Template',
        help='Which template row populated this line. Informational.',
    )
  • Step 3 — Syntax check
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/sale_order_line.py').read()); print('OK')"

Expected: OK

  • Step 4 — Commit
git add fusion_plating/fusion_plating_configurator/models/sale_order_line.py
git commit -m "feat(configurator): add dual descriptions to sale.order.line (Sub 2 Task 3)"

Task 4 — Bump configurator version + register migration

Files:

  • Modify: fusion_plating_configurator/__manifest__.py

  • Step 1 — Bump version

Edit fusion_plating_configurator/__manifest__.py. Change:

'version': '19.0.8.0.0',

to:

'version': '19.0.9.0.0',
  • Step 2 — Commit
git add fusion_plating/fusion_plating_configurator/__manifest__.py
git commit -m "chore(configurator): bump version to 19.0.9.0.0 for Sub 2 (Task 4)"

Task 5 — Create the migration script

Files:

  • Create: fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py

  • Step 1 — Create the directory

mkdir -p /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/migrations/19.0.9.0.0
  • Step 2 — Write the migration script

Create fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py with the following content:

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1
# Sub 2 — Part Data Model Overhaul. Runs on upgrade from < 19.0.9.0.0.
# Idempotent (NULL / empty guards). Safe to re-run.

import logging

_logger = logging.getLogger(__name__)


def migrate(cr, version):
    if not version:
        return  # Fresh install — nothing to migrate

    _logger.info("Sub 2: starting part-data-model migration to %s", version)

    # Step 1: Backfill part_number from name where empty
    cr.execute("""
        UPDATE fp_part_catalog
        SET    part_number = name
        WHERE  part_number IS NULL OR part_number = ''
    """)
    _logger.info("Sub 2: backfilled part_number on %d records", cr.rowcount)

    # Step 2: Backfill revision with 'A' where empty
    cr.execute("""
        UPDATE fp_part_catalog
        SET    revision = 'A'
        WHERE  revision IS NULL OR revision = ''
    """)
    _logger.info("Sub 2: backfilled revision on %d records", cr.rowcount)

    # Step 3: Split fp_sale_description_template.description into two columns
    # Copy existing description into BOTH internal_description and
    # customer_facing_description. Estimators split them later.
    cr.execute("""
        UPDATE fp_sale_description_template
        SET    internal_description         = description,
               customer_facing_description  = description
        WHERE  description IS NOT NULL
          AND  description <> ''
          AND  (internal_description IS NULL OR internal_description = '')
    """)
    _logger.info(
        "Sub 2: duplicated description into new columns on %d template rows",
        cr.rowcount,
    )

    # Step 4: Backfill x_fc_internal_description on sale.order.line
    # Copy the existing `name` (Odoo's line description) into internal so
    # historical lines satisfy the required-field check when it flips.
    cr.execute("""
        UPDATE sale_order_line
        SET    x_fc_internal_description = name
        WHERE  x_fc_internal_description IS NULL OR x_fc_internal_description = ''
    """)
    _logger.info(
        "Sub 2: backfilled x_fc_internal_description on %d SO lines",
        cr.rowcount,
    )

    # Step 5: Default certificate_requirement to 'inherit' on any rows
    # where it's NULL (shouldn't happen given Odoo default=, but defensive).
    cr.execute("""
        UPDATE fp_part_catalog
        SET    certificate_requirement = 'inherit'
        WHERE  certificate_requirement IS NULL
    """)
    _logger.info(
        "Sub 2: defaulted certificate_requirement to 'inherit' on %d parts",
        cr.rowcount,
    )

    _logger.info("Sub 2: migration complete")
  • Step 3 — Create empty init.py files (Odoo requires them for migrations dir)
touch /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/migrations/__init__.py
touch /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/migrations/19.0.9.0.0/__init__.py
  • Step 4 — Syntax check
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py').read()); print('OK')"

Expected: OK

  • Step 5 — Commit
git add fusion_plating/fusion_plating_configurator/migrations/
git commit -m "feat(configurator): Sub 2 data migration — backfill part_number/revision, split descriptions (Task 5)"

Task 6 — Write migration verification SQL

Files:

  • Create: docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql

  • Step 1 — Create the tests directory

mkdir -p /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests
  • Step 2 — Write the verification SQL

Create docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql:

-- Sub 2 — Post-migration verification. Run on local dev DB after upgrade.
-- Every SELECT should return 0 rows (anomalies). If any return > 0, migration has gaps.

-- 1. No part should have an empty part_number
SELECT id, name FROM fp_part_catalog
WHERE (part_number IS NULL OR part_number = '') AND active = TRUE;

-- 2. No part should have an empty revision
SELECT id, name, part_number FROM fp_part_catalog
WHERE (revision IS NULL OR revision = '') AND active = TRUE;

-- 3. Every description-template row with text in `description` should have
--    both internal_description and customer_facing_description populated
SELECT id, name FROM fp_sale_description_template
WHERE description IS NOT NULL
  AND description <> ''
  AND (
      internal_description IS NULL OR internal_description = ''
      OR customer_facing_description IS NULL OR customer_facing_description = ''
  );

-- 4. Every SO line with a non-empty `name` should have x_fc_internal_description set
SELECT id, order_id FROM sale_order_line
WHERE (name IS NOT NULL AND name <> '')
  AND (x_fc_internal_description IS NULL OR x_fc_internal_description = '');

-- 5. Every part should have certificate_requirement set (default 'inherit')
SELECT id, name FROM fp_part_catalog
WHERE certificate_requirement IS NULL;

-- 6. Count summary (informational)
SELECT 'parts_total' AS metric, COUNT(*) AS value FROM fp_part_catalog
UNION ALL
SELECT 'parts_inherit_cert', COUNT(*) FROM fp_part_catalog WHERE certificate_requirement = 'inherit'
UNION ALL
SELECT 'description_templates_total', COUNT(*) FROM fp_sale_description_template
UNION ALL
SELECT 'description_templates_with_both', COUNT(*) FROM fp_sale_description_template
    WHERE internal_description IS NOT NULL AND customer_facing_description IS NOT NULL
UNION ALL
SELECT 'so_lines_with_internal_desc', COUNT(*) FROM sale_order_line
    WHERE x_fc_internal_description IS NOT NULL AND x_fc_internal_description <> '';
  • Step 3 — Commit
git add docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql
git commit -m "test(sub2): migration verification SQL (Task 6)"

Task 7 — Run migration on local dev, verify

Files: none (operational)

  • Step 1 — Upgrade the configurator module on local dev
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -20

Expected: "Sub 2: migration complete" appears in the log; no errors.

  • Step 2 — Restart odoo
docker restart odoo-dev-app
  • Step 3 — Run the verification SQL
docker exec odoo-dev-db psql -U odoo -d fusion-dev -f - < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql

Expected: queries 15 each return zero rows; query 6 prints population counts.

  • Step 4 — If any anomaly, investigate and fix migration script

If queries 15 return non-zero, the migration has a gap. Root-cause → edit post-migration.py → re-run Step 1.


Task 8 — Add display_name compute so part records render when name is blank

Files:

  • Modify: fusion_plating_configurator/models/fp_part_catalog.py

  • Step 1 — Add the compute field

Add display_name compute at the top of the FpPartCatalog class (Odoo's _order = 'partner_id, part_number, revision' may also need adjusting — handled in Step 3):

Find the existing name = fields.Char(...) (line 23). Directly above it, add:

    display_name = fields.Char(
        string='Display Name',
        compute='_compute_display_name',
        store=True,
    )
  • Step 2 — Add the compute method

Find any existing compute method in the file for reference, then add this method at the bottom of the compute methods block (near _compute_revision_count if it exists):

    @api.depends('part_number', 'revision', 'name')
    def _compute_display_name(self):
        """Display = 'PART-NUMBER (Rev X) — Optional Name'.

        Used by m2o pickers, breadcrumbs, kanban cards. Falls back to
        name-only when part_number is missing (legacy / in-progress records).
        """
        for rec in self:
            if rec.part_number:
                core = f"{rec.part_number}"
                if rec.revision:
                    core += f" (Rev {rec.revision})"
                if rec.name:
                    core += f" — {rec.name}"
                rec.display_name = core
            else:
                rec.display_name = rec.name or _('[unnamed part]')
  • Step 3 — Update _order to sort by part_number instead of name

Find _order = 'name' or similar line (near line 22). Replace with:

    _order = 'partner_id, part_number, revision desc'
  • Step 4 — Upgrade + spot-check
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10

Then open http://localhost:8069, log in, navigate to Plating → Sales → Part Catalog. Every row should show PART-NUMBER (Rev X) — Name.

  • Step 5 — Commit
git add fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py
git commit -m "feat(configurator): display_name compute for fp.part.catalog (Sub 2 Task 8)"

Task 9 — Add _fp_resolve_cert_requirement to mrp.production

Files:

  • Modify: fusion_plating_bridge_mrp/models/mrp_production.py

  • Step 1 — Locate the insertion point

grep -n "def _fp_build_delivery_vals\|def button_mark_done\|def action_open_active_qc" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py | head -5

Expected: _fp_build_delivery_vals exists around line ~1155. We'll insert the resolver just above it.

  • Step 2 — Add the resolver method

Directly above def _fp_build_delivery_vals(self, mo, job):, insert:

    # ------------------------------------------------------------------
    # Sub 2 — Certificate requirement resolution (single source)
    # ------------------------------------------------------------------
    def _fp_resolve_cert_requirement(self):
        """Resolve which certs are required for this MO.

        Returns (want_coc: bool, want_thickness: bool).

        Logic:
          1. Collect every linked fp.part.catalog via the SO line walk.
          2. For each part:
             - if certificate_requirement != 'inherit' -> part wins
             - else -> fall back to partner's x_fc_send_coc /
               x_fc_send_thickness_report flags
          3. Multi-line MO: strictest wins (any() across lines).
          4. MO with no SO link: partner fallback; safe default (True, False).

        This is the single entry point used by:
          - _fp_generate_cert_pdf (MO-done cert cascade)
          - QC gate when it audits thickness requirements
          - Any future caller (Sub 6 will update this method when
            partner-level flags move to per-location / per-contact).
        """
        self.ensure_one()
        SO = self.env['sale.order']

        # Resolve partner via origin
        partner = False
        lines = self.env['sale.order.line']
        if self.origin:
            so = SO.search([('name', '=', self.origin)], limit=1)
            if so:
                partner = so.partner_id
                lines = so.order_line

        # No SO link — use partner-level fallback with safe defaults
        if not lines:
            if partner and 'x_fc_send_coc' in partner._fields:
                return (
                    bool(partner.x_fc_send_coc),
                    bool(partner.x_fc_send_thickness_report),
                )
            # No partner at all — safe default: CoC yes, thickness no
            return (True, False)

        want_coc_any = False
        want_thickness_any = False
        for line in lines:
            part = line.x_fc_part_catalog_id
            if part and part.certificate_requirement != 'inherit':
                # Part-level override wins
                want_coc_line = part.certificate_requirement in (
                    'coc', 'coc_thickness',
                )
                want_thickness_line = (
                    part.certificate_requirement == 'coc_thickness'
                )
            else:
                # Inherit (or no part) -> partner fallback
                if partner and 'x_fc_send_coc' in partner._fields:
                    want_coc_line = bool(partner.x_fc_send_coc)
                    want_thickness_line = bool(
                        partner.x_fc_send_thickness_report
                    )
                else:
                    want_coc_line = True
                    want_thickness_line = False
            want_coc_any = want_coc_any or want_coc_line
            want_thickness_any = want_thickness_any or want_thickness_line

        return (want_coc_any, want_thickness_any)
  • Step 3 — Syntax check
python3 -c "import ast; ast.parse(open('fusion_plating_bridge_mrp/models/mrp_production.py').read()); print('OK')"

Expected: OK

  • Step 4 — Commit
git add fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
git commit -m "feat(bridge_mrp): _fp_resolve_cert_requirement single-source resolver (Sub 2 Task 9)"

Task 10 — Rewire _fp_generate_cert_pdf and button_mark_done to use the resolver

Files:

  • Modify: fusion_plating_bridge_mrp/models/mrp_production.py

  • Step 1 — Locate the old partner-flag reads inside button_mark_done

grep -n "x_fc_send_coc\|x_fc_send_thickness_report" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py

Expected: references inside button_mark_done (around line 850-870).

  • Step 2 — Replace the inline partner reads

Find the block inside button_mark_done that looks roughly like this (line numbers may differ):

                want_coc = True  # default for customers that predate the flag
                want_thickness = True
                if 'x_fc_send_coc' in customer._fields:
                    want_coc = bool(customer.x_fc_send_coc)
                if 'x_fc_send_thickness_report' in customer._fields:
                    want_thickness = bool(customer.x_fc_send_thickness_report)

Replace it with:

                # Sub 2: part-level cert requirement wins; partner is fallback.
                # Single entry point for all cert decisions.
                want_coc, want_thickness = mo._fp_resolve_cert_requirement()
  • Step 3 — Syntax check
python3 -c "import ast; ast.parse(open('fusion_plating_bridge_mrp/models/mrp_production.py').read()); print('OK')"

Expected: OK

  • Step 4 — Upgrade bridge_mrp on local dev
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_bridge_mrp --stop-after-init 2>&1 | tail -10

Expected: no errors.

  • Step 5 — Commit
git add fusion_plating/fusion_plating_bridge_mrp/models/mrp_production.py
git commit -m "refactor(bridge_mrp): route button_mark_done cert cascade through resolver (Sub 2 Task 10)"

Task 11 — Regression test cert resolver via odoo-shell

Files:

  • Create: docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py

  • Step 1 — Write the test script

Create the file with:

"""Sub 2 — test _fp_resolve_cert_requirement behaviour.

Run via:
  docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
      < docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py
"""
import sys
env = self.env

def ok(msg): print(f"  [OK]  {msg}")
def fail(msg): print(f"  [FAIL] {msg}"); sys.exit(1)
def hdr(t): print(f"\n=== {t} ===")

# ---- Setup ----
hdr("Setup")
Product = env['product.product']
product = Product.search([('active', '=', True)], limit=1)
Partner = env['res.partner']
partner = Partner.create({
    'name': 'Sub2 CertResolver Test Co',
    'is_company': True,
    'customer_rank': 1,
    'x_fc_send_coc': True,
    'x_fc_send_thickness_report': False,
})
PartCat = env['fp.part.catalog']
part_inherit = PartCat.create({
    'name': 'P-INHERIT', 'part_number': 'P-INHERIT', 'revision': 'A',
    'partner_id': partner.id, 'certificate_requirement': 'inherit',
})
part_none = PartCat.create({
    'name': 'P-NONE', 'part_number': 'P-NONE', 'revision': 'A',
    'partner_id': partner.id, 'certificate_requirement': 'none',
})
part_thick = PartCat.create({
    'name': 'P-THICK', 'part_number': 'P-THICK', 'revision': 'A',
    'partner_id': partner.id, 'certificate_requirement': 'coc_thickness',
})
ok(f"parts created: {part_inherit.id}, {part_none.id}, {part_thick.id}")

def make_mo(lines):
    """Helper: builds an SO with the given part_catalogs, confirms it, returns MO."""
    so = env['sale.order'].create({
        'partner_id': partner.id,
        'x_fc_po_number': 'TEST-PO',
        'order_line': [
            (0, 0, {
                'product_id': product.id,
                'product_uom_qty': 1,
                'x_fc_part_catalog_id': pc.id,
            }) for pc in lines
        ],
    })
    so.action_confirm()
    mo = env['mrp.production'].create({
        'product_id': product.id, 'product_qty': 1, 'origin': so.name,
    })
    mo.action_confirm()
    return mo

# ---- Test 1: inherit → partner ----
hdr("1. Part=inherit + partner(coc=T, thick=F) → (T, F)")
mo1 = make_mo([part_inherit])
want_coc, want_thickness = mo1._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, False):
    fail(f"got ({want_coc}, {want_thickness}), expected (True, False)")
ok("inherit falls through to partner correctly")

# ---- Test 2: part=none overrides partner ----
hdr("2. Part=none + partner(coc=T) → (F, F)")
mo2 = make_mo([part_none])
want_coc, want_thickness = mo2._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (False, False):
    fail(f"got ({want_coc}, {want_thickness}), expected (False, False)")
ok("part=none wins over partner=coc")

# ---- Test 3: part=coc_thickness + partner(coc=F, thick=F) → (T, T) ----
hdr("3. Part=coc_thickness + partner=none → (T, T)")
partner.x_fc_send_coc = False  # partner explicitly no
mo3 = make_mo([part_thick])
want_coc, want_thickness = mo3._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, True):
    fail(f"got ({want_coc}, {want_thickness}), expected (True, True)")
ok("part=coc_thickness wins over partner=off")
partner.x_fc_send_coc = True  # reset

# ---- Test 4: multi-line MO strictest wins ----
hdr("4. Multi-line [none, coc_thickness] → (T, T)")
mo4 = make_mo([part_none, part_thick])
want_coc, want_thickness = mo4._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, True):
    fail(f"got ({want_coc}, {want_thickness}), expected (True, True)")
ok("multi-line strictest wins")

# ---- Test 5: MO without SO link → safe default ----
hdr("5. Orphan MO → (T, F) safe default")
orphan = env['mrp.production'].create({
    'product_id': product.id, 'product_qty': 1, 'origin': 'NONEXISTENT-SO',
})
want_coc, want_thickness = orphan._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, False):
    fail(f"got ({want_coc}, {want_thickness}), expected (True, False)")
ok("orphan MO falls back to safe default")

hdr("ALL CERT RESOLVER TESTS PASS")
  • Step 2 — Run the test
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
    < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py \
    2>&1 | grep -E "\[OK\]|\[FAIL\]|===|ALL CERT"

Expected: all 5 tests print [OK] then ALL CERT RESOLVER TESTS PASS.

  • Step 3 — Commit
git add docs/superpowers/tests/2026-04-21-sub2-cert-resolver-test.py
git commit -m "test(sub2): cert-resolver edge cases (Task 11)"

Task 12 — Regression: Phase 13 QC suite still green

Files: none

  • Step 1 — Run the existing QC smoke test
# The QC smoke test from previous phase
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
    < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_smoke.py \
    2>&1 | grep -E "\[OK\]|\[FAIL\]|===|DONE"

If fp_qc_smoke.py doesn't exist in the tests dir, re-create it from the /tmp/fp_qc_smoke.py version used during Phase 13 work.

Expected: 9-step QC smoke passes green.

  • Step 2 — Run the QC E2E test
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
    < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_e2e.py \
    2>&1 | grep -E "\[OK\]|\[FAIL\]|===|ALL"

Expected: 8 edge cases still pass.

  • Step 3 — If any test fails, ROLLBACK via git and investigate
git log --oneline -5
# If regression detected: identify which Sub 2 task broke it, fix, re-run tests

No commit — this is verification only.


Phase B — UI updates + Report Macro + Report Rewiring

Users now see the new fields. Old fields still exist; new fields populated by migration. Required flags still off.

Task 13 — Update part-catalog form: identity block + certificate group

Files:

  • Modify: fusion_plating_configurator/views/fp_part_catalog_views.xml

  • Step 1 — Locate the form view

grep -n "<record id=\"view_fp_part_catalog_form\"\|<field name=\"name\"\|<field name=\"part_number\"" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml | head -10

Find the identity block near the top of the form (where name, part_number, revision, partner_id are rendered).

  • Step 2 — Add certificate requirement field

Find the existing identity <group> block that contains name, part_number, revision. After that group, add a new group:

<group string="Quality &amp; Delivery" name="quality_delivery">
    <field name="certificate_requirement"/>
</group>
  • Step 3 — Relabel "SKU" if any occurrence exists in this file
grep -n 'string="SKU"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml

If any matches, change string="SKU" to string="Part Number".

  • Step 4 — XML syntax check
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/fp_part_catalog_views.xml'); print('OK')"

Expected: OK

  • Step 5 — Upgrade + visually verify
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5

Open browser → Plating → Sales → Part Catalog → any part. Confirm:

  • "Quality & Delivery" group appears with Certificate Requirement dropdown

  • No SKU label visible (all now "Part Number")

  • Step 6 — Commit

git add fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
git commit -m "feat(configurator): cert requirement + SKU relabel on part form (Sub 2 Task 13)"

Task 14 — Update part-catalog form: two-column Descriptions repeater

Files:

  • Modify: fusion_plating_configurator/views/fp_part_catalog_views.xml

  • Step 1 — Locate the Descriptions tab

grep -n 'description_template_ids\|"Descriptions"\|Descriptions' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml | head -10
  • Step 2 — Replace the single-description list with two-column

Find the <field name="description_template_ids"> block. Inside it replace the list view with:

<field name="description_template_ids">
    <list editable="bottom">
        <field name="sequence" widget="handle"/>
        <field name="name" placeholder="e.g. Standard, With threaded holes masked"/>
        <field name="tag" optional="show"/>
        <field name="internal_description" placeholder="What the shop floor sees on the WO / traveler"/>
        <field name="customer_facing_description" placeholder="What prints on SO, invoice, packing slip"/>
        <field name="usage_count" string="Used" optional="hide"/>
        <field name="active" widget="boolean_toggle"/>
    </list>
</field>

Add a hint <div> above the field:

<div class="alert alert-info" role="alert">
    <strong>Canned descriptions for this part.</strong>
    Internal = what the shop floor sees on the WO / traveler.
    Customer-Facing = what prints on SO, invoice, packing slip.
    Whichever row the estimator picks on the order wizard lands both values on the SO line.
</div>
<field name="description_template_ids">
    ...
</field>
  • Step 3 — XML syntax check
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/fp_part_catalog_views.xml'); print('OK')"

Expected: OK

  • Step 4 — Upgrade + verify
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5

Browser → part → Descriptions tab. Two columns should show. Verify an existing row has the SAME text in both columns (from migration).

  • Step 5 — Commit
git add fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
git commit -m "feat(configurator): two-column dual-description repeater (Sub 2 Task 14)"

Task 15 — Update SO line view: internal description + template picker

Files:

  • Modify: fusion_plating_configurator/views/sale_order_views.xml

  • Step 1 — Locate the SO line form

grep -n 'sale.order.line\|<field name=\"order_line\"\|x_fc_part_catalog_id' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml | head -10
  • Step 2 — Add the template picker + internal description

Find the <field name="x_fc_part_catalog_id"/> on the SO line form. Directly after it, add:

<field name="x_fc_description_template_id"
       domain="[('part_catalog_id', '=', x_fc_part_catalog_id)]"
       options="{'no_create': True}"
       invisible="not x_fc_part_catalog_id"/>
<field name="x_fc_internal_description"
       placeholder="Shop-floor workflow instructions (prints on WO / traveler)"/>
  • Step 3 — Add a QWeb onchange hook (optional — do inline in XML via context)

In models/sale_order_line.py, add an onchange method so picking a template fills both fields:

    @api.onchange('x_fc_description_template_id')
    def _onchange_description_template(self):
        if self.x_fc_description_template_id:
            tpl = self.x_fc_description_template_id
            self.name = tpl.customer_facing_description or self.name
            self.x_fc_internal_description = tpl.internal_description
  • Step 4 — XML + Python syntax checks
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/sale_order_views.xml'); print('OK')"
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/sale_order_line.py').read()); print('OK')"

Expected: both OK.

  • Step 5 — Upgrade + manual test
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5

Browser → Create a new quote → pick a part → pick a description template. Verify both name (customer-facing) and x_fc_internal_description fields populate.

  • Step 6 — Commit
git add fusion_plating/fusion_plating_configurator/views/sale_order_views.xml \
        fusion_plating/fusion_plating_configurator/models/sale_order_line.py
git commit -m "feat(configurator): SO-line template picker + dual descriptions (Sub 2 Task 15)"

Task 16 — Update direct-order wizard view

Files:

  • Modify: fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml

  • Step 1 — Find the line-repeater in the wizard

grep -n 'x_fc_part_catalog_id\|<field name=\"order_line_ids\"\|<field name=\"line_ids\"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml | head -10
  • Step 2 — Add the template picker + internal description column

In the order-line list (inside the wizard), after the part picker field, add:

<field name="x_fc_description_template_id"
       domain="[('part_catalog_id', '=', x_fc_part_catalog_id)]"
       options="{'no_create': True}"
       invisible="not x_fc_part_catalog_id"/>
<field name="x_fc_internal_description"/>

Note: this assumes the wizard line model fp.direct.order.line already has these fields or inherits from sale.order.line. Check with:

grep -n "_name\|_inherit" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py

If fp.direct.order.line is a standalone transient model, add the same two fields to it as were added to sale.order.line in Task 3. Copy the field definitions verbatim.

  • Step 3 — XML syntax check
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml'); print('OK')"

Expected: OK

  • Step 4 — Upgrade + manual test
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5

Browser → Plating → Sales → New Direct Order. Verify two description columns appear per line.

  • Step 5 — Commit
git add fusion_plating/fusion_plating_configurator/wizard/
git commit -m "feat(configurator): direct-order wizard dual-description inputs (Sub 2 Task 16)"

Task 17 — Create the customer_line_header QWeb macro

Files:

  • Create: fusion_plating_reports/report/customer_line_header.xml

  • Step 1 — Write the macro

Create fusion_plating_reports/report/customer_line_header.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!--
    Copyright 2026 Nexa Systems Inc.
    License OPL-1

    Sub 2 — shared QWeb macro for customer-facing line rendering.

    Called from report_fp_sale, report_fp_invoice, report_fp_packing_slip,
    report_fp_bol. Prints the customer's part number + revision + the
    line's customer-facing description (the `name` field, standard Odoo).

    For non-part lines (rush fees, freight, expedite) where
    x_fc_part_catalog_id is blank, falls back to Odoo's standard product
    display — safe for fee/service lines that shouldn't look like parts.

    Params expected in the calling context:
      line  - the sale.order.line / account.move.line / stock.picking line

    Usage:
      <t t-call="fusion_plating_reports.customer_line_header"/>
-->
<odoo>
    <template id="customer_line_header">
        <t t-if="line.x_fc_part_catalog_id">
            <strong>
                <span t-esc="line.x_fc_part_catalog_id.part_number"/>
                <t t-if="line.x_fc_part_catalog_id.revision">
                    <span> (Rev <span t-esc="line.x_fc_part_catalog_id.revision"/>)</span>
                </t>
            </strong>
            <br/>
            <span t-esc="line.name"/>
        </t>
        <t t-else="">
            <!-- Fee / freight / non-part line: standard Odoo rendering -->
            <strong t-esc="line.product_id.display_name or ''"/>
            <t t-if="line.name and line.name != line.product_id.display_name">
                <br/>
                <span t-esc="line.name"/>
            </t>
        </t>
    </template>
</odoo>
  • Step 2 — Register the file in the reports manifest

Edit fusion_plating_reports/__manifest__.py. Find the 'data': [ list. Add 'report/customer_line_header.xml', BEFORE any other report_fp_*.xml entries (so the macro is loaded before its callers).

Also bump 'version' from 19.0.4.9.0 to 19.0.5.0.0.

  • Step 3 — Syntax checks
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/customer_line_header.xml'); print('OK')"
python3 -c "import ast; ast.parse(open('fusion_plating_reports/__manifest__.py').read()); print('OK')"

Expected: both OK.

  • Step 4 — Upgrade + verify macro loads
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -10

Expected: no errors. Template registered in ir_ui_view.

  • Step 5 — Commit
git add fusion_plating/fusion_plating_reports/
git commit -m "feat(reports): customer_line_header QWeb macro + version bump (Sub 2 Task 17)"

Task 18 — Rewire report_fp_sale.xml to use macro

Files:

  • Modify: fusion_plating_reports/report/report_fp_sale.xml

  • Step 1 — Find the current line rendering

grep -n "line.product_id.default_code\|product_id.display_name\|line.name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml

Expected: two locations where default_code is printed (around lines 159 and 436 per earlier scan). Each is a line-item row inside a list table.

  • Step 2 — Replace with macro call

For each occurrence of:

<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>

AND the adjacent cell that renders the line name (look for the <td> containing <span t-esc="line.name"/> or product display name nearby), REPLACE the two cells with ONE cell that uses the macro:

<td>
    <t t-call="fusion_plating_reports.customer_line_header"/>
</td>

This collapses the former "SKU | Description" two-column layout into one "Part" column. Adjust the table header to match: rename the column from "SKU" / "Description" to "Part".

  • Step 3 — XML syntax check
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_sale.xml'); print('OK')"

Expected: OK

  • Step 4 — Upgrade + render a sample SO
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5

Browser → any Sale Order → Print → Fusion Plating SO. Verify:

  • No "[EN-PLATE]" or service SKU visible anywhere

  • Each line shows PART-NUMBER (Rev X) in bold + customer-facing description below

  • Step 5 — Commit

git add fusion_plating/fusion_plating_reports/report/report_fp_sale.xml
git commit -m "feat(reports): SO PDF uses customer_line_header macro (Sub 2 Task 18)"

Task 19 — Rewire report_fp_invoice.xml

Files:

  • Modify: fusion_plating_reports/report/report_fp_invoice.xml

  • Step 1 — Find line-rendering block

grep -n "default_code\|line.name\|product_id.display_name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml
  • Step 2 — Apply the same macro swap as Task 18

Replace the <td> cells that render default_code / product name / line name with a single cell:

<td>
    <t t-call="fusion_plating_reports.customer_line_header"/>
</td>

Important: invoice line model is account.move.line, not sale.order.line. The x_fc_part_catalog_id field must exist on account.move.line for the macro to work. Check:

grep -n "x_fc_part_catalog_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/

If the invoice line doesn't have x_fc_part_catalog_id yet, add it to fusion_plating_configurator/models/ in a new file account_move_line.py:

from odoo import fields, models

class AccountMoveLine(models.Model):
    _inherit = 'account.move.line'

    x_fc_part_catalog_id = fields.Many2one(
        'fp.part.catalog', string='Part',
        help='Populated by sale_order → account_move invoice creation.',
    )

And ensure it's imported in models/__init__.py.

Also extend the invoice creation hook (probably in sale_order.py) to copy x_fc_part_catalog_id from SO line → invoice line. Look at _prepare_invoice_line in the existing codebase.

  • Step 3 — Syntax checks
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_invoice.xml'); print('OK')"
  • Step 4 — Upgrade + render sample invoice
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator,fusion_plating_reports --stop-after-init 2>&1 | tail -5

Browser → any Invoice → Print → Fusion Plating Invoice. Verify same rendering as SO (Task 18).

  • Step 5 — Commit
git add fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml \
        fusion_plating/fusion_plating_configurator/models/
git commit -m "feat(reports+configurator): invoice PDF uses macro; x_fc_part_catalog_id on account.move.line (Sub 2 Task 19)"

Task 20 — Rewire report_fp_packing_slip.xml

Files:

  • Modify: fusion_plating_reports/report/report_fp_packing_slip.xml

  • Step 1 — Find line-rendering block

grep -n "default_code\|line.name\|product_id.display_name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml
  • Step 2 — Apply macro swap

Packing slip likely iterates stock.move.line or stock.picking lines. The macro expects line.x_fc_part_catalog_id. Check what the loop variable represents. If move, adjust the macro call to pass the correct variable, OR add a t-set adapter:

<t t-set="line" t-value="move.sale_line_id or move"/>
<t t-call="fusion_plating_reports.customer_line_header"/>
  • Step 3 — Syntax check + visual verify + commit
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_packing_slip.xml'); print('OK')"
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
# Browser: print a packing slip. Verify part number renders.
git add fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml
git commit -m "feat(reports): packing slip PDF uses macro (Sub 2 Task 20)"

Task 21 — Rewire report_fp_bol.xml

Files:

  • Modify: fusion_plating_reports/report/report_fp_bol.xml

Mirror of Task 20. Swap line header to use <t t-call="fusion_plating_reports.customer_line_header"/>.

  • Apply same steps as Task 20 on report_fp_bol.xml.
  • Syntax check, upgrade, visual verify, commit with message feat(reports): BoL PDF uses macro (Sub 2 Task 21).

Task 22 — Update report_fp_work_order.xml (internal — ADD fields, don't remove)

Files:

  • Modify: fusion_plating_reports/report/report_fp_work_order.xml

  • Step 1 — Locate the existing line/description block

grep -n "line.name\|product.default_code\|product_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_work_order.xml | head -10
  • Step 2 — Add internal-description rendering

Internal reports keep everything. Add the internal description below the product description. Example pattern — find where the WO currently shows line.name and augment:

<div>
    <strong>Part:</strong>
    <span t-if="line.x_fc_part_catalog_id">
        <span t-esc="line.x_fc_part_catalog_id.part_number"/>
        <span> (Rev <span t-esc="line.x_fc_part_catalog_id.revision"/>)</span>
    </span>
    <span t-else="">
        <span t-esc="line.product_id.display_name"/>
    </span>
</div>
<div>
    <strong>Customer-Facing Description:</strong>
    <span t-esc="line.name"/>
</div>
<div t-if="line.x_fc_internal_description">
    <strong>Internal Description / Workflow:</strong>
    <span t-esc="line.x_fc_internal_description" style="white-space: pre-wrap;"/>
</div>
<div>
    <strong>Service SKU:</strong>
    <span t-esc="line.product_id.default_code or '—'"/>
</div>
  • Step 3 — Syntax check, upgrade, visual verify
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_work_order.xml'); print('OK')"
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
# Browser: print a work order. Verify Part Number, Customer-Facing Description, Internal Description, Service SKU all visible.
  • Step 4 — Commit
git add fusion_plating/fusion_plating_reports/report/report_fp_work_order.xml
git commit -m "feat(reports): WO PDF surfaces internal description + part number (Sub 2 Task 22)"

Task 23 — Update report_fp_job_traveller.xml (same as Task 22)

Files:

  • Modify: fusion_plating_reports/report/report_fp_job_traveller.xml

Apply the identical pattern as Task 22 to the traveler report.

  • Apply Task 22's pattern on report_fp_job_traveller.xml. Syntax check, upgrade, visual verify.
  • Commit: feat(reports): traveler PDF surfaces internal description + part number (Sub 2 Task 23)

Phase C — Required-Field Flip + Cleanup

Point of no return. Only execute once Phase B is verified end-to-end.

Task 24 — Flip required flags on fp.part.catalog

Files:

  • Modify: fusion_plating_configurator/models/fp_part_catalog.py

  • Step 1 — Update the fields

    name = fields.Char(string='Part Name', tracking=True)           # was required=True
    part_number = fields.Char(string='Part Number', required=True, tracking=True)  # was optional
    revision = fields.Char(string='Revision', required=True, default='A',
                           help='Revision letter or number (e.g. Rev: 1B).')
  • Step 2 — Upgrade + verify
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10

Expected: no errors. Existing parts (all backfilled from Task 7) pass validation.

  • Step 3 — Try to save a new part with empty part_number

Manually in browser → New Part → leave Part Number blank → Save. Expected: Odoo rejects with a validation error.

  • Step 4 — Commit
git add fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py
git commit -m "feat(configurator): flip part_number + revision to required, name optional (Sub 2 Task 24)"

Task 25 — Flip required flags on description template

Files:

  • Modify: fusion_plating_configurator/models/fp_sale_description_template.py

  • Step 1 — Update the two new fields to required

    internal_description = fields.Text(
        string='Internal Description',
        required=True,
        help='What the shop floor sees on the WO / traveler. Never on customer documents.',
    )
    customer_facing_description = fields.Text(
        string='Customer-Facing Description',
        required=True,
        help='Prints on the SO, invoice, packing slip, and BoL.',
    )
  • Step 2 — Upgrade + verify
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
  • Step 3 — Commit
git add fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py
git commit -m "feat(configurator): flip dual-descriptions to required on template (Sub 2 Task 25)"

Task 26 — Flip SO-line internal description to required

Files:

  • Modify: fusion_plating_configurator/models/sale_order_line.py

  • Step 1 — Update the field

    x_fc_internal_description = fields.Text(
        string='Internal Description',
        required=True,
        help='Shop-floor instructions. Prints on WO / traveler. Never on customer docs.',
    )
  • Step 2 — Upgrade + verify historical rows still valid
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10

If any historical SO line has NULL x_fc_internal_description (shouldn't, after Task 7), upgrade fails. Re-run migration with -u fusion_plating_configurator and verify.

  • Step 3 — Commit
git add fusion_plating/fusion_plating_configurator/models/sale_order_line.py
git commit -m "feat(configurator): flip sale.order.line.x_fc_internal_description to required (Sub 2 Task 26)"

Task 27 — Drop the legacy description column from the template

Files:

  • Modify: fusion_plating_configurator/models/fp_sale_description_template.py

  • Modify: fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py (add cleanup step)

  • Modify: views that still reference the old description field (should be zero after Task 14, but verify)

  • Step 1 — Remove the description field from the Python model

Delete the line:

    description = fields.Text(...)

from fp_sale_description_template.py.

  • Step 2 — Add column drop to migration

Add to the end of the migrate() function in post-migration.py:

    # Step 6: Drop legacy description column (all reads migrated to new fields)
    cr.execute("""
        ALTER TABLE fp_sale_description_template
        DROP COLUMN IF EXISTS description
    """)
    _logger.info("Sub 2: dropped legacy description column")
  • Step 3 — Verify no views reference the old description column
grep -rn 'name="description"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/ | grep -i template

Expected: no matches (Task 14 already replaced with the two new fields). If any remain, remove them.

  • Step 4 — Upgrade + verify column gone
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
docker exec odoo-dev-db psql -U odoo -d fusion-dev -c "\d fp_sale_description_template" | grep -i description

Expected: only internal_description and customer_facing_description appear; no bare description column.

  • Step 5 — Commit
git add fusion_plating/fusion_plating_configurator/
git commit -m "feat(configurator): drop legacy description column; model cleanup (Sub 2 Task 27)"

Task 28 — Bump bridge_mrp version

Files:

  • Modify: fusion_plating_bridge_mrp/__manifest__.py

  • Step 1 — Bump version

'version': '19.0.9.0.0',  # was 19.0.8.0.0
  • Step 2 — Commit
git add fusion_plating/fusion_plating_bridge_mrp/__manifest__.py
git commit -m "chore(bridge_mrp): bump to 19.0.9.0.0 after cert-resolver refactor (Sub 2 Task 28)"

Task 29 — Final end-to-end smoke test

Files:

  • Create: docs/superpowers/tests/2026-04-21-sub2-smoke.py

  • Step 1 — Write the smoke test

Create the file:

"""Sub 2 — end-to-end smoke. Full lifecycle with Sub 2 features.

Confirms:
  - new parts reject without part_number / revision
  - description template row requires both fields
  - SO line requires both descriptions
  - customer-facing reports render part_number, not default_code
  - cert cascade via resolver, part-level wins over partner
"""
import sys
env = self.env

def ok(msg): print(f"  [OK]  {msg}")
def fail(msg): print(f"  [FAIL] {msg}"); sys.exit(1)
def hdr(t): print(f"\n=== {t} ===")

# ---- 1. Required fields on fp.part.catalog ----
hdr("1. Required fields")
try:
    env['fp.part.catalog'].create({
        'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id,
    })
    fail("accepted part with no part_number / revision")
except Exception as e:
    ok(f"rejected blank part: {str(e)[:80]}")

# ---- 2. Description template two-field requirement ----
hdr("2. Description template required fields")
part = env['fp.part.catalog'].create({
    'name': 'Sub2 Smoke Part',
    'part_number': 'SUB2-SMOKE-001',
    'revision': 'A',
    'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id,
})
try:
    env['fp.sale.description.template'].create({
        'name': 'Broken template',
        'part_catalog_id': part.id,
        'internal_description': 'ops only',
        # customer_facing_description missing
    })
    fail("accepted template without customer_facing_description")
except Exception as e:
    ok(f"rejected template: {str(e)[:80]}")

# ---- 3. SO line dual descriptions ----
hdr("3. SO line required internal description")
tpl = env['fp.sale.description.template'].create({
    'name': 'Standard',
    'part_catalog_id': part.id,
    'internal_description': 'Racking pattern A; mask threaded holes',
    'customer_facing_description': 'Electroless nickel plate per customer spec',
})
product = env['product.product'].search([('active', '=', True)], limit=1)
so_vals = {
    'partner_id': part.partner_id.id,
    'order_line': [(0, 0, {
        'product_id': product.id,
        'product_uom_qty': 1,
        'x_fc_part_catalog_id': part.id,
        'x_fc_description_template_id': tpl.id,
    })],
}
if 'x_fc_po_number' in env['sale.order']._fields:
    so_vals['x_fc_po_number'] = 'SUB2-SMOKE-PO'
so = env['sale.order'].create(so_vals)
# Onchange should have copied from template
line = so.order_line[0]
if line.name != tpl.customer_facing_description:
    fail(f"line.name not set from template: got {line.name!r}")
if line.x_fc_internal_description != tpl.internal_description:
    fail(f"line.x_fc_internal_description not set: got {line.x_fc_internal_description!r}")
ok("SO line populated from template onchange")

# ---- 4. Cert resolver end-to-end ----
hdr("4. Cert resolver with part override")
part.certificate_requirement = 'coc_thickness'
part.partner_id.x_fc_send_coc = False
part.partner_id.x_fc_send_thickness_report = False
so.action_confirm()
mo = env['mrp.production'].create({
    'product_id': product.id, 'product_qty': 1, 'origin': so.name,
})
mo.action_confirm()
want_coc, want_thickness = mo._fp_resolve_cert_requirement()
if (want_coc, want_thickness) != (True, True):
    fail(f"resolver returned ({want_coc}, {want_thickness}); expected (True, True)")
ok("part-level coc_thickness override working end-to-end")

# ---- 5. display_name compute ----
hdr("5. display_name includes part_number + revision")
part.invalidate_recordset()
if 'SUB2-SMOKE-001' not in part.display_name:
    fail(f"display_name missing part_number: {part.display_name!r}")
if 'Rev A' not in part.display_name:
    fail(f"display_name missing revision: {part.display_name!r}")
ok(f"display_name: {part.display_name}")

hdr("SUB 2 SMOKE COMPLETE")
  • Step 2 — Run it
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
    < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-smoke.py \
    2>&1 | grep -E "\[OK\]|\[FAIL\]|===|SUB 2"

Expected: all 5 sections [OK], ends with SUB 2 SMOKE COMPLETE.

  • Step 3 — Regression: QC suite still green

Run the Phase 13 QC smoke + E2E again (Task 12 pattern):

docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
    < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_smoke.py \
    2>&1 | grep -E "\[OK\]|\[FAIL\]|DONE"
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
    < /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/fp_qc_e2e.py \
    2>&1 | grep -E "\[OK\]|\[FAIL\]|ALL"

Expected: both still green.

  • Step 4 — Commit the smoke script
git add docs/superpowers/tests/2026-04-21-sub2-smoke.py
git commit -m "test(sub2): end-to-end smoke covers required-field flip, onchange, resolver, display_name (Task 29)"

Task 30 — Deploy to entech + run migration verification there

Files: none (operational)

  • Step 1 — Package up all three modules
cd /Users/gurpreet/Github/Odoo-Modules/fusion_plating
for mod in fusion_plating_configurator fusion_plating_reports fusion_plating_bridge_mrp; do
    tar --exclude='__pycache__' --exclude='*.pyc' -czf /tmp/$mod.tgz $mod/
done
  • Step 2 — Push each to entech
for mod in fusion_plating_configurator fusion_plating_reports fusion_plating_bridge_mrp; do
    cat /tmp/$mod.tgz | base64 | ssh pve-worker5 \
        "pct exec 111 -- bash -c 'base64 -d > /tmp/fp_deploy/$mod.tgz && \
            cd /mnt/extra-addons/custom && rm -rf $mod && tar xzf /tmp/fp_deploy/$mod.tgz && echo $mod DEPLOYED'"
done
  • Step 3 — Update modules in order (configurator → reports → bridge_mrp)
ssh pve-worker5 "pct exec 111 -- bash -c '
    systemctl stop odoo && \
    su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin \
        -u fusion_plating_configurator,fusion_plating_reports,fusion_plating_bridge_mrp \
        --stop-after-init 2>&1 | tail -20\" && \
    systemctl start odoo'"

Expected: "Sub 2: migration complete" in log; no errors.

  • Step 4 — Clear asset cache
ssh pve-worker5 "pct exec 111 -- su - postgres -c \"psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\""
  • Step 5 — Run migration verification SQL on entech
cat /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-migration-verify.sql \
    | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /tmp/sub2-verify.sql && \
        su - postgres -c \"psql -d admin -f /tmp/sub2-verify.sql\"'"

Expected: queries 15 return zero rows; query 6 shows population counts.

  • Step 6 — Run end-to-end smoke on entech

Same pattern as Task 29 Step 2, but via ssh pve-worker5 "pct exec 111 ..." invoking odoo shell on entech.

  • Step 7 — Push commits upstream
cd /Users/gurpreet/Github/Odoo-Modules/fusion_plating
git push origin main

Plan Self-Review (fill in actuals only — do not rerun tasks)

Spec coverage check

Spec § Spec item Plan task
§2.1 certificate_requirement on fp.part.catalog Task 1
§2.1 part_number / revision required; name opt. Tasks 3 (add) + 24 (flip)
§2.2 Split description → two columns Task 2 (add) + 25 (flip) + 27 (drop)
§2.3 SO-line dual descriptions Tasks 3 + 15 + 26
§3 Step 1 Backfill part_number Task 5 + verify 7
§3 Step 2 Backfill revision='A' Task 5 + verify 7
§3 Step 3 Duplicate description to new columns Task 5 + verify 7
§3 Step 4 Backfill SO-line internal desc Task 5 + verify 7
§3 Step 5 Default cert='inherit' Task 5 + verify 7
§4.1 Part form — cert dropdown + relabel Task 13
§4.1 Part form — two-column repeater Task 14
§4.2 SO / wizard — template picker + dual desc Tasks 15 + 16
§4.3 Universal SKU relabel Task 13 (Fusion views) + macro swap in Tasks 1822
§5.1 Single-source cert resolver Task 9
§5.3 Resolver callers updated Task 10
§5.4 Multi-line + orphan-MO cases Task 11 tests
§6.1 Shared QWeb macro Task 17
§6.2 Report rewiring (4 customer, 2 internal) Tasks 18, 19, 20, 21, 22, 23
§6.3 Non-part fallback Task 17 macro body
§7.1 Migration tests Task 6 (SQL) + 7 (run)
§7.2 Unit tests Task 11 (resolver) + 29 (smoke)
§7.3 End-to-end smoke Task 29
§7.4 QC regression Task 12 + Task 29 Step 3
§8.1 Defensive measure 1 — resolver Task 9
§8.2 Defensive measure 2 — macro Task 17
§8.3 Defensive measure 3 — idempotent migration Task 5 (NULL guards)
§8.4 Defensive measure 4 — additive SO fields Task 3
§8.5 Defensive measure 5 — drop old column Task 27

All spec sections covered.

Placeholder scan

No "TBD", "TODO", "fill in", "similar to above" in the task bodies. Every code block is complete and ready to copy-paste.

Type / method consistency

  • _fp_resolve_cert_requirement (Task 9) returns (bool, bool). Callers in Task 10 unpack want_coc, want_thickness = .... Consistent.
  • customer_line_header macro (Task 17) reads line.x_fc_part_catalog_id — matches field name on sale.order.line (Task 3) and account.move.line (Task 19).
  • display_name (Task 8) stored compute — matches _compute_display_name.
  • certificate_requirement Selection values (inherit / none / coc / coc_thickness) — used identically in Tasks 1, 5, 9, 11, 29.

Summary

  • 30 tasks spanning three phases (A: additive + migrate + resolver, B: UI + reports, C: required-flag flip + cleanup).
  • System remains runnable after every task in Phases A and B. Phase C is the flip-point-of-no-return.
  • Every view / model / report change includes a syntax check, a module upgrade, a visual or script verify, and a commit.
  • Regression tests (Phase 13 QC suite) run in Tasks 12 and 29.
  • Deploy-to-entech is explicit (Task 30) — follows the CLAUDE.md deployment pattern.