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>
1843 lines
66 KiB
Markdown
1843 lines
66 KiB
Markdown
# 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 "<record id=\"view_fp_part_catalog_form\"\|<field name=\"name\"\|<field name=\"part_number\"" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml | head -10
|
||
```
|
||
Find the identity block near the top of the form (where `name`, `part_number`, `revision`, `partner_id` are rendered).
|
||
|
||
- [ ] **Step 2 — Add certificate requirement field**
|
||
|
||
Find the existing identity `<group>` block that contains `name`, `part_number`, `revision`. After that group, add a new group:
|
||
|
||
```xml
|
||
<group string="Quality & Delivery" name="quality_delivery">
|
||
<field name="certificate_requirement"/>
|
||
</group>
|
||
```
|
||
|
||
- [ ] **Step 3 — Relabel "SKU" if any occurrence exists in this file**
|
||
|
||
```bash
|
||
grep -n 'string="SKU"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
|
||
```
|
||
If any matches, change `string="SKU"` to `string="Part Number"`.
|
||
|
||
- [ ] **Step 4 — XML syntax check**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/fp_part_catalog_views.xml'); print('OK')"
|
||
```
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 5 — Upgrade + visually verify**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
|
||
```
|
||
|
||
Open browser → Plating → Sales → Part Catalog → any part. Confirm:
|
||
- "Quality & Delivery" group appears with Certificate Requirement dropdown
|
||
- No SKU label visible (all now "Part Number")
|
||
|
||
- [ ] **Step 6 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
|
||
git commit -m "feat(configurator): cert requirement + SKU relabel on part form (Sub 2 Task 13)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 14 — Update part-catalog form: two-column Descriptions repeater
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml`
|
||
|
||
- [ ] **Step 1 — Locate the Descriptions tab**
|
||
|
||
```bash
|
||
grep -n 'description_template_ids\|"Descriptions"\|Descriptions' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml | head -10
|
||
```
|
||
|
||
- [ ] **Step 2 — Replace the single-description list with two-column**
|
||
|
||
Find the `<field name="description_template_ids">` block. Inside it replace the list view with:
|
||
|
||
```xml
|
||
<field name="description_template_ids">
|
||
<list editable="bottom">
|
||
<field name="sequence" widget="handle"/>
|
||
<field name="name" placeholder="e.g. Standard, With threaded holes masked"/>
|
||
<field name="tag" optional="show"/>
|
||
<field name="internal_description" placeholder="What the shop floor sees on the WO / traveler"/>
|
||
<field name="customer_facing_description" placeholder="What prints on SO, invoice, packing slip"/>
|
||
<field name="usage_count" string="Used" optional="hide"/>
|
||
<field name="active" widget="boolean_toggle"/>
|
||
</list>
|
||
</field>
|
||
```
|
||
|
||
Add a hint `<div>` above the field:
|
||
|
||
```xml
|
||
<div class="alert alert-info" role="alert">
|
||
<strong>Canned descriptions for this part.</strong>
|
||
Internal = what the shop floor sees on the WO / traveler.
|
||
Customer-Facing = what prints on SO, invoice, packing slip.
|
||
Whichever row the estimator picks on the order wizard lands both values on the SO line.
|
||
</div>
|
||
<field name="description_template_ids">
|
||
...
|
||
</field>
|
||
```
|
||
|
||
- [ ] **Step 3 — XML syntax check**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/fp_part_catalog_views.xml'); print('OK')"
|
||
```
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4 — Upgrade + verify**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
|
||
```
|
||
|
||
Browser → part → Descriptions tab. Two columns should show. Verify an existing row has the SAME text in both columns (from migration).
|
||
|
||
- [ ] **Step 5 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
|
||
git commit -m "feat(configurator): two-column dual-description repeater (Sub 2 Task 14)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 15 — Update SO line view: internal description + template picker
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_configurator/views/sale_order_views.xml`
|
||
|
||
- [ ] **Step 1 — Locate the SO line form**
|
||
|
||
```bash
|
||
grep -n 'sale.order.line\|<field name=\"order_line\"\|x_fc_part_catalog_id' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml | head -10
|
||
```
|
||
|
||
- [ ] **Step 2 — Add the template picker + internal description**
|
||
|
||
Find the `<field name="x_fc_part_catalog_id"/>` on the SO line form. Directly after it, add:
|
||
|
||
```xml
|
||
<field name="x_fc_description_template_id"
|
||
domain="[('part_catalog_id', '=', x_fc_part_catalog_id)]"
|
||
options="{'no_create': True}"
|
||
invisible="not x_fc_part_catalog_id"/>
|
||
<field name="x_fc_internal_description"
|
||
placeholder="Shop-floor workflow instructions (prints on WO / traveler)"/>
|
||
```
|
||
|
||
- [ ] **Step 3 — Add a QWeb onchange hook (optional — do inline in XML via context)**
|
||
|
||
In `models/sale_order_line.py`, add an onchange method so picking a template fills both fields:
|
||
|
||
```python
|
||
@api.onchange('x_fc_description_template_id')
|
||
def _onchange_description_template(self):
|
||
if self.x_fc_description_template_id:
|
||
tpl = self.x_fc_description_template_id
|
||
self.name = tpl.customer_facing_description or self.name
|
||
self.x_fc_internal_description = tpl.internal_description
|
||
```
|
||
|
||
- [ ] **Step 4 — XML + Python syntax checks**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/views/sale_order_views.xml'); print('OK')"
|
||
python3 -c "import ast; ast.parse(open('fusion_plating_configurator/models/sale_order_line.py').read()); print('OK')"
|
||
```
|
||
Expected: both `OK`.
|
||
|
||
- [ ] **Step 5 — Upgrade + manual test**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
|
||
```
|
||
|
||
Browser → Create a new quote → pick a part → pick a description template. Verify both `name` (customer-facing) and `x_fc_internal_description` fields populate.
|
||
|
||
- [ ] **Step 6 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/views/sale_order_views.xml \
|
||
fusion_plating/fusion_plating_configurator/models/sale_order_line.py
|
||
git commit -m "feat(configurator): SO-line template picker + dual descriptions (Sub 2 Task 15)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 16 — Update direct-order wizard view
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml`
|
||
|
||
- [ ] **Step 1 — Find the line-repeater in the wizard**
|
||
|
||
```bash
|
||
grep -n 'x_fc_part_catalog_id\|<field name=\"order_line_ids\"\|<field name=\"line_ids\"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml | head -10
|
||
```
|
||
|
||
- [ ] **Step 2 — Add the template picker + internal description column**
|
||
|
||
In the order-line list (inside the wizard), after the part picker field, add:
|
||
|
||
```xml
|
||
<field name="x_fc_description_template_id"
|
||
domain="[('part_catalog_id', '=', x_fc_part_catalog_id)]"
|
||
options="{'no_create': True}"
|
||
invisible="not x_fc_part_catalog_id"/>
|
||
<field name="x_fc_internal_description"/>
|
||
```
|
||
|
||
Note: this assumes the wizard line model `fp.direct.order.line` already has these fields or inherits from `sale.order.line`. Check with:
|
||
|
||
```bash
|
||
grep -n "_name\|_inherit" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py
|
||
```
|
||
|
||
If `fp.direct.order.line` is a standalone transient model, add the same two fields to it as were added to `sale.order.line` in Task 3. Copy the field definitions verbatim.
|
||
|
||
- [ ] **Step 3 — XML syntax check**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml'); print('OK')"
|
||
```
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4 — Upgrade + manual test**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -5
|
||
```
|
||
|
||
Browser → Plating → Sales → New Direct Order. Verify two description columns appear per line.
|
||
|
||
- [ ] **Step 5 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/wizard/
|
||
git commit -m "feat(configurator): direct-order wizard dual-description inputs (Sub 2 Task 16)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 17 — Create the customer_line_header QWeb macro
|
||
|
||
**Files:**
|
||
- Create: `fusion_plating_reports/report/customer_line_header.xml`
|
||
|
||
- [ ] **Step 1 — Write the macro**
|
||
|
||
Create `fusion_plating_reports/report/customer_line_header.xml`:
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!--
|
||
Copyright 2026 Nexa Systems Inc.
|
||
License OPL-1
|
||
|
||
Sub 2 — shared QWeb macro for customer-facing line rendering.
|
||
|
||
Called from report_fp_sale, report_fp_invoice, report_fp_packing_slip,
|
||
report_fp_bol. Prints the customer's part number + revision + the
|
||
line's customer-facing description (the `name` field, standard Odoo).
|
||
|
||
For non-part lines (rush fees, freight, expedite) where
|
||
x_fc_part_catalog_id is blank, falls back to Odoo's standard product
|
||
display — safe for fee/service lines that shouldn't look like parts.
|
||
|
||
Params expected in the calling context:
|
||
line - the sale.order.line / account.move.line / stock.picking line
|
||
|
||
Usage:
|
||
<t t-call="fusion_plating_reports.customer_line_header"/>
|
||
-->
|
||
<odoo>
|
||
<template id="customer_line_header">
|
||
<t t-if="line.x_fc_part_catalog_id">
|
||
<strong>
|
||
<span t-esc="line.x_fc_part_catalog_id.part_number"/>
|
||
<t t-if="line.x_fc_part_catalog_id.revision">
|
||
<span> (Rev <span t-esc="line.x_fc_part_catalog_id.revision"/>)</span>
|
||
</t>
|
||
</strong>
|
||
<br/>
|
||
<span t-esc="line.name"/>
|
||
</t>
|
||
<t t-else="">
|
||
<!-- Fee / freight / non-part line: standard Odoo rendering -->
|
||
<strong t-esc="line.product_id.display_name or ''"/>
|
||
<t t-if="line.name and line.name != line.product_id.display_name">
|
||
<br/>
|
||
<span t-esc="line.name"/>
|
||
</t>
|
||
</t>
|
||
</template>
|
||
</odoo>
|
||
```
|
||
|
||
- [ ] **Step 2 — Register the file in the reports manifest**
|
||
|
||
Edit `fusion_plating_reports/__manifest__.py`. Find the `'data': [` list. Add `'report/customer_line_header.xml',` BEFORE any other `report_fp_*.xml` entries (so the macro is loaded before its callers).
|
||
|
||
Also bump `'version'` from `19.0.4.9.0` to `19.0.5.0.0`.
|
||
|
||
- [ ] **Step 3 — Syntax checks**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/customer_line_header.xml'); print('OK')"
|
||
python3 -c "import ast; ast.parse(open('fusion_plating_reports/__manifest__.py').read()); print('OK')"
|
||
```
|
||
Expected: both `OK`.
|
||
|
||
- [ ] **Step 4 — Upgrade + verify macro loads**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -10
|
||
```
|
||
Expected: no errors. Template registered in ir_ui_view.
|
||
|
||
- [ ] **Step 5 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_reports/
|
||
git commit -m "feat(reports): customer_line_header QWeb macro + version bump (Sub 2 Task 17)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 18 — Rewire report_fp_sale.xml to use macro
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_reports/report/report_fp_sale.xml`
|
||
|
||
- [ ] **Step 1 — Find the current line rendering**
|
||
|
||
```bash
|
||
grep -n "line.product_id.default_code\|product_id.display_name\|line.name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml
|
||
```
|
||
Expected: two locations where `default_code` is printed (around lines 159 and 436 per earlier scan). Each is a line-item row inside a list table.
|
||
|
||
- [ ] **Step 2 — Replace with macro call**
|
||
|
||
For each occurrence of:
|
||
```xml
|
||
<td class="text-center"><span t-esc="line.product_id.default_code or ''"/></td>
|
||
```
|
||
|
||
AND the adjacent cell that renders the line name (look for the `<td>` containing `<span t-esc="line.name"/>` or product display name nearby), REPLACE the two cells with ONE cell that uses the macro:
|
||
|
||
```xml
|
||
<td>
|
||
<t t-call="fusion_plating_reports.customer_line_header"/>
|
||
</td>
|
||
```
|
||
|
||
This collapses the former "SKU | Description" two-column layout into one "Part" column. Adjust the table header to match: rename the column from "SKU" / "Description" to "Part".
|
||
|
||
- [ ] **Step 3 — XML syntax check**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_sale.xml'); print('OK')"
|
||
```
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4 — Upgrade + render a sample SO**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
|
||
```
|
||
|
||
Browser → any Sale Order → Print → Fusion Plating SO. Verify:
|
||
- No "[EN-PLATE]" or service SKU visible anywhere
|
||
- Each line shows `PART-NUMBER (Rev X)` in bold + customer-facing description below
|
||
|
||
- [ ] **Step 5 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_reports/report/report_fp_sale.xml
|
||
git commit -m "feat(reports): SO PDF uses customer_line_header macro (Sub 2 Task 18)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 19 — Rewire report_fp_invoice.xml
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_reports/report/report_fp_invoice.xml`
|
||
|
||
- [ ] **Step 1 — Find line-rendering block**
|
||
|
||
```bash
|
||
grep -n "default_code\|line.name\|product_id.display_name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml
|
||
```
|
||
|
||
- [ ] **Step 2 — Apply the same macro swap as Task 18**
|
||
|
||
Replace the `<td>` cells that render `default_code` / product name / line name with a single cell:
|
||
|
||
```xml
|
||
<td>
|
||
<t t-call="fusion_plating_reports.customer_line_header"/>
|
||
</td>
|
||
```
|
||
|
||
Important: invoice line model is `account.move.line`, not `sale.order.line`. The `x_fc_part_catalog_id` field must exist on `account.move.line` for the macro to work. Check:
|
||
|
||
```bash
|
||
grep -n "x_fc_part_catalog_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/
|
||
```
|
||
|
||
If the invoice line doesn't have `x_fc_part_catalog_id` yet, add it to `fusion_plating_configurator/models/` in a new file `account_move_line.py`:
|
||
|
||
```python
|
||
from odoo import fields, models
|
||
|
||
class AccountMoveLine(models.Model):
|
||
_inherit = 'account.move.line'
|
||
|
||
x_fc_part_catalog_id = fields.Many2one(
|
||
'fp.part.catalog', string='Part',
|
||
help='Populated by sale_order → account_move invoice creation.',
|
||
)
|
||
```
|
||
|
||
And ensure it's imported in `models/__init__.py`.
|
||
|
||
Also extend the invoice creation hook (probably in sale_order.py) to copy `x_fc_part_catalog_id` from SO line → invoice line. Look at `_prepare_invoice_line` in the existing codebase.
|
||
|
||
- [ ] **Step 3 — Syntax checks**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_invoice.xml'); print('OK')"
|
||
```
|
||
|
||
- [ ] **Step 4 — Upgrade + render sample invoice**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator,fusion_plating_reports --stop-after-init 2>&1 | tail -5
|
||
```
|
||
|
||
Browser → any Invoice → Print → Fusion Plating Invoice. Verify same rendering as SO (Task 18).
|
||
|
||
- [ ] **Step 5 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_reports/report/report_fp_invoice.xml \
|
||
fusion_plating/fusion_plating_configurator/models/
|
||
git commit -m "feat(reports+configurator): invoice PDF uses macro; x_fc_part_catalog_id on account.move.line (Sub 2 Task 19)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 20 — Rewire report_fp_packing_slip.xml
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_reports/report/report_fp_packing_slip.xml`
|
||
|
||
- [ ] **Step 1 — Find line-rendering block**
|
||
|
||
```bash
|
||
grep -n "default_code\|line.name\|product_id.display_name" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml
|
||
```
|
||
|
||
- [ ] **Step 2 — Apply macro swap**
|
||
|
||
Packing slip likely iterates `stock.move.line` or `stock.picking` lines. The macro expects `line.x_fc_part_catalog_id`. Check what the loop variable represents. If `move`, adjust the macro call to pass the correct variable, OR add a `t-set` adapter:
|
||
|
||
```xml
|
||
<t t-set="line" t-value="move.sale_line_id or move"/>
|
||
<t t-call="fusion_plating_reports.customer_line_header"/>
|
||
```
|
||
|
||
- [ ] **Step 3 — Syntax check + visual verify + commit**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_packing_slip.xml'); print('OK')"
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
|
||
# Browser: print a packing slip. Verify part number renders.
|
||
git add fusion_plating/fusion_plating_reports/report/report_fp_packing_slip.xml
|
||
git commit -m "feat(reports): packing slip PDF uses macro (Sub 2 Task 20)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 21 — Rewire report_fp_bol.xml
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_reports/report/report_fp_bol.xml`
|
||
|
||
Mirror of Task 20. Swap line header to use `<t t-call="fusion_plating_reports.customer_line_header"/>`.
|
||
|
||
- [ ] Apply same steps as Task 20 on `report_fp_bol.xml`.
|
||
- [ ] Syntax check, upgrade, visual verify, commit with message `feat(reports): BoL PDF uses macro (Sub 2 Task 21)`.
|
||
|
||
---
|
||
|
||
### Task 22 — Update report_fp_work_order.xml (internal — ADD fields, don't remove)
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_reports/report/report_fp_work_order.xml`
|
||
|
||
- [ ] **Step 1 — Locate the existing line/description block**
|
||
|
||
```bash
|
||
grep -n "line.name\|product.default_code\|product_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_work_order.xml | head -10
|
||
```
|
||
|
||
- [ ] **Step 2 — Add internal-description rendering**
|
||
|
||
Internal reports keep everything. Add the internal description below the product description. Example pattern — find where the WO currently shows `line.name` and augment:
|
||
|
||
```xml
|
||
<div>
|
||
<strong>Part:</strong>
|
||
<span t-if="line.x_fc_part_catalog_id">
|
||
<span t-esc="line.x_fc_part_catalog_id.part_number"/>
|
||
<span> (Rev <span t-esc="line.x_fc_part_catalog_id.revision"/>)</span>
|
||
</span>
|
||
<span t-else="">
|
||
<span t-esc="line.product_id.display_name"/>
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<strong>Customer-Facing Description:</strong>
|
||
<span t-esc="line.name"/>
|
||
</div>
|
||
<div t-if="line.x_fc_internal_description">
|
||
<strong>Internal Description / Workflow:</strong>
|
||
<span t-esc="line.x_fc_internal_description" style="white-space: pre-wrap;"/>
|
||
</div>
|
||
<div>
|
||
<strong>Service SKU:</strong>
|
||
<span t-esc="line.product_id.default_code or '—'"/>
|
||
</div>
|
||
```
|
||
|
||
- [ ] **Step 3 — Syntax check, upgrade, visual verify**
|
||
|
||
```bash
|
||
python3 -c "import xml.etree.ElementTree as ET; ET.parse('fusion_plating_reports/report/report_fp_work_order.xml'); print('OK')"
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports --stop-after-init 2>&1 | tail -5
|
||
# Browser: print a work order. Verify Part Number, Customer-Facing Description, Internal Description, Service SKU all visible.
|
||
```
|
||
|
||
- [ ] **Step 4 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_reports/report/report_fp_work_order.xml
|
||
git commit -m "feat(reports): WO PDF surfaces internal description + part number (Sub 2 Task 22)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 23 — Update report_fp_job_traveller.xml (same as Task 22)
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_reports/report/report_fp_job_traveller.xml`
|
||
|
||
Apply the identical pattern as Task 22 to the traveler report.
|
||
|
||
- [ ] Apply Task 22's pattern on `report_fp_job_traveller.xml`. Syntax check, upgrade, visual verify.
|
||
- [ ] Commit: `feat(reports): traveler PDF surfaces internal description + part number (Sub 2 Task 23)`
|
||
|
||
---
|
||
|
||
## Phase C — Required-Field Flip + Cleanup
|
||
|
||
Point of no return. Only execute once Phase B is verified end-to-end.
|
||
|
||
### Task 24 — Flip required flags on fp.part.catalog
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_configurator/models/fp_part_catalog.py`
|
||
|
||
- [ ] **Step 1 — Update the fields**
|
||
|
||
```python
|
||
name = fields.Char(string='Part Name', tracking=True) # was required=True
|
||
part_number = fields.Char(string='Part Number', required=True, tracking=True) # was optional
|
||
revision = fields.Char(string='Revision', required=True, default='A',
|
||
help='Revision letter or number (e.g. Rev: 1B).')
|
||
```
|
||
|
||
- [ ] **Step 2 — Upgrade + verify**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
|
||
```
|
||
Expected: no errors. Existing parts (all backfilled from Task 7) pass validation.
|
||
|
||
- [ ] **Step 3 — Try to save a new part with empty part_number**
|
||
|
||
Manually in browser → New Part → leave Part Number blank → Save. Expected: Odoo rejects with a validation error.
|
||
|
||
- [ ] **Step 4 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py
|
||
git commit -m "feat(configurator): flip part_number + revision to required, name optional (Sub 2 Task 24)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 25 — Flip required flags on description template
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_configurator/models/fp_sale_description_template.py`
|
||
|
||
- [ ] **Step 1 — Update the two new fields to required**
|
||
|
||
```python
|
||
internal_description = fields.Text(
|
||
string='Internal Description',
|
||
required=True,
|
||
help='What the shop floor sees on the WO / traveler. Never on customer documents.',
|
||
)
|
||
customer_facing_description = fields.Text(
|
||
string='Customer-Facing Description',
|
||
required=True,
|
||
help='Prints on the SO, invoice, packing slip, and BoL.',
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 2 — Upgrade + verify**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
|
||
```
|
||
|
||
- [ ] **Step 3 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/models/fp_sale_description_template.py
|
||
git commit -m "feat(configurator): flip dual-descriptions to required on template (Sub 2 Task 25)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 26 — Flip SO-line internal description to required
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_configurator/models/sale_order_line.py`
|
||
|
||
- [ ] **Step 1 — Update the field**
|
||
|
||
```python
|
||
x_fc_internal_description = fields.Text(
|
||
string='Internal Description',
|
||
required=True,
|
||
help='Shop-floor instructions. Prints on WO / traveler. Never on customer docs.',
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 2 — Upgrade + verify historical rows still valid**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
|
||
```
|
||
If any historical SO line has NULL `x_fc_internal_description` (shouldn't, after Task 7), upgrade fails. Re-run migration with `-u fusion_plating_configurator` and verify.
|
||
|
||
- [ ] **Step 3 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/models/sale_order_line.py
|
||
git commit -m "feat(configurator): flip sale.order.line.x_fc_internal_description to required (Sub 2 Task 26)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 27 — Drop the legacy `description` column from the template
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_configurator/models/fp_sale_description_template.py`
|
||
- Modify: `fusion_plating_configurator/migrations/19.0.9.0.0/post-migration.py` (add cleanup step)
|
||
- Modify: views that still reference the old `description` field (should be zero after Task 14, but verify)
|
||
|
||
- [ ] **Step 1 — Remove the `description` field from the Python model**
|
||
|
||
Delete the line:
|
||
```python
|
||
description = fields.Text(...)
|
||
```
|
||
from `fp_sale_description_template.py`.
|
||
|
||
- [ ] **Step 2 — Add column drop to migration**
|
||
|
||
Add to the end of the `migrate()` function in `post-migration.py`:
|
||
|
||
```python
|
||
# Step 6: Drop legacy description column (all reads migrated to new fields)
|
||
cr.execute("""
|
||
ALTER TABLE fp_sale_description_template
|
||
DROP COLUMN IF EXISTS description
|
||
""")
|
||
_logger.info("Sub 2: dropped legacy description column")
|
||
```
|
||
|
||
- [ ] **Step 3 — Verify no views reference the old `description` column**
|
||
|
||
```bash
|
||
grep -rn 'name="description"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/ | grep -i template
|
||
```
|
||
Expected: no matches (Task 14 already replaced with the two new fields). If any remain, remove them.
|
||
|
||
- [ ] **Step 4 — Upgrade + verify column gone**
|
||
|
||
```bash
|
||
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -10
|
||
docker exec odoo-dev-db psql -U odoo -d fusion-dev -c "\d fp_sale_description_template" | grep -i description
|
||
```
|
||
Expected: only `internal_description` and `customer_facing_description` appear; no bare `description` column.
|
||
|
||
- [ ] **Step 5 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_configurator/
|
||
git commit -m "feat(configurator): drop legacy description column; model cleanup (Sub 2 Task 27)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 28 — Bump bridge_mrp version
|
||
|
||
**Files:**
|
||
- Modify: `fusion_plating_bridge_mrp/__manifest__.py`
|
||
|
||
- [ ] **Step 1 — Bump version**
|
||
|
||
```python
|
||
'version': '19.0.9.0.0', # was 19.0.8.0.0
|
||
```
|
||
|
||
- [ ] **Step 2 — Commit**
|
||
|
||
```bash
|
||
git add fusion_plating/fusion_plating_bridge_mrp/__manifest__.py
|
||
git commit -m "chore(bridge_mrp): bump to 19.0.9.0.0 after cert-resolver refactor (Sub 2 Task 28)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 29 — Final end-to-end smoke test
|
||
|
||
**Files:**
|
||
- Create: `docs/superpowers/tests/2026-04-21-sub2-smoke.py`
|
||
|
||
- [ ] **Step 1 — Write the smoke test**
|
||
|
||
Create the file:
|
||
|
||
```python
|
||
"""Sub 2 — end-to-end smoke. Full lifecycle with Sub 2 features.
|
||
|
||
Confirms:
|
||
- new parts reject without part_number / revision
|
||
- description template row requires both fields
|
||
- SO line requires both descriptions
|
||
- customer-facing reports render part_number, not default_code
|
||
- cert cascade via resolver, part-level wins over partner
|
||
"""
|
||
import sys
|
||
env = self.env
|
||
|
||
def ok(msg): print(f" [OK] {msg}")
|
||
def fail(msg): print(f" [FAIL] {msg}"); sys.exit(1)
|
||
def hdr(t): print(f"\n=== {t} ===")
|
||
|
||
# ---- 1. Required fields on fp.part.catalog ----
|
||
hdr("1. Required fields")
|
||
try:
|
||
env['fp.part.catalog'].create({
|
||
'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id,
|
||
})
|
||
fail("accepted part with no part_number / revision")
|
||
except Exception as e:
|
||
ok(f"rejected blank part: {str(e)[:80]}")
|
||
|
||
# ---- 2. Description template two-field requirement ----
|
||
hdr("2. Description template required fields")
|
||
part = env['fp.part.catalog'].create({
|
||
'name': 'Sub2 Smoke Part',
|
||
'part_number': 'SUB2-SMOKE-001',
|
||
'revision': 'A',
|
||
'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id,
|
||
})
|
||
try:
|
||
env['fp.sale.description.template'].create({
|
||
'name': 'Broken template',
|
||
'part_catalog_id': part.id,
|
||
'internal_description': 'ops only',
|
||
# customer_facing_description missing
|
||
})
|
||
fail("accepted template without customer_facing_description")
|
||
except Exception as e:
|
||
ok(f"rejected template: {str(e)[:80]}")
|
||
|
||
# ---- 3. SO line dual descriptions ----
|
||
hdr("3. SO line required internal description")
|
||
tpl = env['fp.sale.description.template'].create({
|
||
'name': 'Standard',
|
||
'part_catalog_id': part.id,
|
||
'internal_description': 'Racking pattern A; mask threaded holes',
|
||
'customer_facing_description': 'Electroless nickel plate per customer spec',
|
||
})
|
||
product = env['product.product'].search([('active', '=', True)], limit=1)
|
||
so_vals = {
|
||
'partner_id': part.partner_id.id,
|
||
'order_line': [(0, 0, {
|
||
'product_id': product.id,
|
||
'product_uom_qty': 1,
|
||
'x_fc_part_catalog_id': part.id,
|
||
'x_fc_description_template_id': tpl.id,
|
||
})],
|
||
}
|
||
if 'x_fc_po_number' in env['sale.order']._fields:
|
||
so_vals['x_fc_po_number'] = 'SUB2-SMOKE-PO'
|
||
so = env['sale.order'].create(so_vals)
|
||
# Onchange should have copied from template
|
||
line = so.order_line[0]
|
||
if line.name != tpl.customer_facing_description:
|
||
fail(f"line.name not set from template: got {line.name!r}")
|
||
if line.x_fc_internal_description != tpl.internal_description:
|
||
fail(f"line.x_fc_internal_description not set: got {line.x_fc_internal_description!r}")
|
||
ok("SO line populated from template onchange")
|
||
|
||
# ---- 4. Cert resolver end-to-end ----
|
||
hdr("4. Cert resolver with part override")
|
||
part.certificate_requirement = 'coc_thickness'
|
||
part.partner_id.x_fc_send_coc = False
|
||
part.partner_id.x_fc_send_thickness_report = False
|
||
so.action_confirm()
|
||
mo = env['mrp.production'].create({
|
||
'product_id': product.id, 'product_qty': 1, 'origin': so.name,
|
||
})
|
||
mo.action_confirm()
|
||
want_coc, want_thickness = mo._fp_resolve_cert_requirement()
|
||
if (want_coc, want_thickness) != (True, True):
|
||
fail(f"resolver returned ({want_coc}, {want_thickness}); expected (True, True)")
|
||
ok("part-level coc_thickness override working end-to-end")
|
||
|
||
# ---- 5. display_name compute ----
|
||
hdr("5. display_name includes part_number + revision")
|
||
part.invalidate_recordset()
|
||
if 'SUB2-SMOKE-001' not in part.display_name:
|
||
fail(f"display_name missing part_number: {part.display_name!r}")
|
||
if 'Rev A' not in part.display_name:
|
||
fail(f"display_name missing revision: {part.display_name!r}")
|
||
ok(f"display_name: {part.display_name}")
|
||
|
||
hdr("SUB 2 SMOKE COMPLETE")
|
||
```
|
||
|
||
- [ ] **Step 2 — Run it**
|
||
|
||
```bash
|
||
docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http --stop-after-init \
|
||
< /Users/gurpreet/Github/Odoo-Modules/docs/superpowers/tests/2026-04-21-sub2-smoke.py \
|
||
2>&1 | grep -E "\[OK\]|\[FAIL\]|===|SUB 2"
|
||
```
|
||
Expected: all 5 sections `[OK]`, ends with `SUB 2 SMOKE COMPLETE`.
|
||
|
||
- [ ] **Step 3 — Regression: QC suite still green**
|
||
|
||
Run the Phase 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.
|