# Sub 2 — Part Data Model Overhaul **Date:** 2026-04-21 **Module scope:** `fusion_plating_configurator`, `fusion_plating_reports`, `fusion_plating_bridge_mrp` (cert-resolution wiring) **Status:** Design approved; ready for implementation plan **Predecessor context:** Fine-Tuning Initiative, entry in `fusion_plating/CLAUDE.md` --- ## 1. Scope Four gaps from the Fine-Tuning running plan: | Gap | Summary | |---|---| | **2b** | Part Number + Revision become required; Part Name becomes optional | | **2c** | Dual descriptions (internal + customer-facing) on every description-template row, flowing to SO line | | **2d** | Per-part `certificate_requirement` with `inherit` fallback to the partner's existing flags | | **4** | "SKU" relabel to "Part Number" in UI; customer-facing reports print `part_number`, not `default_code` | ### Out of scope (moved to later sub-projects) - Part's default process / Process Composer → **Sub 3** - Contract Review workflow → **Sub 4** - Order-line additions (serial, job#, thickness dropdown, revision picker) → **Sub 5** - Contact Profiles and per-location notifications → **Sub 6** --- ## 2. Data Model Changes ### 2.1 `fp.part.catalog` (customer parts library) ``` part_number : Char, required=True # was optional revision : Char, required=True, default='A' # was optional name : Char, required=False # was required + certificate_requirement : Selection( ('inherit', 'Inherit from Customer'), ('none', 'No Certificate'), ('coc', 'CoC Only'), ('coc_thickness', 'CoC + Thickness Report'), ), default='inherit', tracking=True ``` ### 2.2 `fp.sale.description.template` (Descriptions tab repeater) ``` description : REMOVED (migrated out and dropped) + internal_description : Text, required=True + customer_facing_description : Text, required=True ``` ### 2.3 `sale.order.line` (Odoo standard, extended) ``` name : (Odoo native, already required. Clarified semantically as THE customer-facing description — no repurpose, just formal naming.) + x_fc_internal_description : Text, required=True + x_fc_description_template_id : Many2one('fp.sale.description.template') (which template row the estimator picked) ``` ### 2.4 Explicitly NOT changing - `product.product` / `product.template.default_code` — untouched. The generic service products (EN Plating, Chrome, etc.) keep their SKUs. - Revision chain (`parent_part_id`, `is_latest_revision`, `revision_ids`) — kept as-is (Sub 5 consumes it for the revision picker). - `fp.part.catalog.notes` Html — kept as freeform drawer notes, not part of the description system. - Partner-level cert flags (`res.partner.x_fc_send_coc`, `x_fc_send_thickness_report`) — kept as the fallback layer for `certificate_requirement = 'inherit'`. --- ## 3. Migration Strategy Runs once via `post_init_hook` on the module upgrade. All steps idempotent (NULL/empty guards). Safe to re-run. ### Step 1 — Backfill `fp.part.catalog.part_number` ```sql UPDATE fp_part_catalog SET part_number = name WHERE part_number IS NULL OR part_number = ''; ``` Defensible: before this change, `name` was the part identifier. Copy over so `required=True` doesn't reject existing records on upgrade. ### Step 2 — Backfill `fp.part.catalog.revision` ```sql UPDATE fp_part_catalog SET revision = 'A' WHERE revision IS NULL OR revision = ''; ``` ### Step 3 — Split `fp.sale.description.template.description` For every template row with non-empty `description`: ``` internal_description := (original description value) customer_facing_description := (original description value) ``` Both fields start with the same text. Estimators separate internal workflow text from customer text over time. The old `description` column is dropped in the same migration. ### Step 4 — Backfill `sale.order.line.x_fc_internal_description` For every SO line in any state: ``` x_fc_internal_description := name (starter, copy from the existing line description) ``` The `required=True` constraint applies to **new lines only** — migration flag skips enforcement on historical records so upgrades don't fail on old orders. ### Step 5 — Default cert requirement Every existing `fp.part.catalog` record gets `certificate_requirement = 'inherit'`. Zero behaviour change for in-flight jobs: the cert cascade still reads partner toggles until a shop admin explicitly sets a part to something other than `inherit`. --- ## 4. UI Changes ### 4.1 Part form (`fp.part.catalog`) Top identity block: ``` Part Number* [required] │ Revision* [required, default 'A'] Part Name [optional] │ Customer [required] ``` New "Quality & Delivery" group: ``` Certificate Requirement: [Inherit from Customer ▾] options: Inherit / None / CoC / CoC + Thickness Help tooltip: "Inherit reads the customer's default (Partner → Plating Documents tab)." ``` Descriptions tab — the existing repeater gains two columns: | Template Name | Category | Internal Description | Customer-Facing Description | Used | Active | |---|---|---|---|---|---| Both description columns required to save a row. Hint above the table: > "Internal = what the shop floor sees on the WO / traveler. Customer-Facing = what prints on SO, invoice, packing slip." ### 4.2 Direct-Order Wizard + SO Line form Per line: - Part picker (unchanged) - **Description Template** dropdown — lists active templates where `part_catalog_id == the chosen part`. Partner-wide and coating-wide templates (which the data model allows) are **not** shown at order entry; the dropdown is narrowed to per-part templates to match the shop's mental model ("canned descriptions for this part"). - **Customer-Facing Description** text box — prefilled from template, editable - **Internal Description** text box — prefilled from template, editable If the part has zero templates, both boxes start blank and the estimator types them. Both required before save. ### 4.3 Universal SKU → Part Number relabel - Fusion Plating form / list / search views: `string="SKU"` → `string="Part Number"` on fields referencing `fp.part.catalog.part_number`. - Customer-facing reports: line rendering flows through the new QWeb macro (§6.1) — prints `part_number + revision + name` (customer-facing desc), no `[default_code]`, no product display name. - Internal reports: show everything — part number, revision, customer-facing description, internal description, service product name, service SKU. - Odoo-native screens (inventory, warehouse, product selector): untouched. Keep showing `default_code` as "SKU". Customer never sees these. --- ## 5. Certificate-Requirement Resolution (Runtime) ### 5.1 Single-source resolver (Defensive Measure 1) All cert-related decisions route through one method on `mrp.production`: ``` def _fp_resolve_cert_requirement(self): """Returns (want_coc: bool, want_thickness: bool) for this MO. Walks SO lines to collect each part's certificate_requirement. Part-level wins; 'inherit' falls back to partner toggles. Multi-line MO: strictest wins. """ ``` ### 5.2 Resolution order ``` 1. Walk MO → origin (SO name) → sale.order → sale.order.lines → x_fc_part_catalog_id. 2. For each line's part: if part.certificate_requirement != 'inherit': want_coc_line = part.certificate_requirement in ('coc', 'coc_thickness') want_thickness_line = part.certificate_requirement == 'coc_thickness' else: # Fallback to partner-level flags want_coc_line = partner.x_fc_send_coc want_thickness_line = partner.x_fc_send_thickness_report 3. Multi-line MO — strictest wins: want_coc = any(want_coc_line across lines) want_thickness = any(want_thickness_line across lines) ``` ### 5.3 Callers (must use the resolver) - `_fp_generate_cert_pdf` in `fusion_plating_bridge_mrp/models/mrp_production.py` (cert cascade on MO done) - QC gate's thickness-required check (if/when it reads partner flags today — audit during implementation) - Notification routing for auto-email (future Sub 6 will hook here) ### 5.4 Multi-line and fallback cases - **Multi-line MO** — strictest wins so a customer never gets less paperwork than promised for any part in the batch. - **MO with no SO link** (manual MO) — falls back to `mo.partner_id` if set, else `want_coc=True, want_thickness=False` as safe default. ### 5.5 Backward compatibility Every existing part has `certificate_requirement = 'inherit'` after migration. The resolver falls through to partner toggles exactly as today. No behaviour change for any shipping job until a shop admin explicitly changes a part's cert requirement. --- ## 6. Report Impact ### 6.1 Shared QWeb line-header macro (Defensive Measure 2) New template in `fusion_plating_reports`: ```xml ``` All four customer-facing report line renderers call ``. When Sub 5 adds the revision picker (and with it the revision snapshot on the SO line), the macro is updated once and all reports follow. ### 6.2 Per-report changes | Report | Audience | Change | |---|---|---| | `report_fp_sale.xml` | Customer | Line header via macro: `part_number + revision + name`. Skip `[default_code]`, skip product display name. | | `report_fp_invoice.xml` | Customer | Same (via macro) | | `report_fp_packing_slip.xml` | Customer | Same (via macro) | | `report_fp_bol.xml` | Customer | Same (via macro) | | `report_fp_work_order.xml` | Internal (operator) | Keep product/service name + `default_code`. Add `part_number`, `revision`, `name` (customer-facing desc), `x_fc_internal_description` (ops workflow). | | `report_fp_job_traveller.xml` | Internal | Same as work order. | | `report_coc.xml` | Customer | Audit — reads `doc.part_number` on `fp.certificate`. Spot-check that the cert model populates `part_number` from `sale_order_line.x_fc_part_catalog_id.part_number`. No change expected. | | `report_fp_receipt.xml` | Customer | Customer macro treatment. | ### 6.3 Fallback for non-part lines The macro's fallback branch renders the generic product name for lines without `x_fc_part_catalog_id` (rush fees, freight, expedite). Those lines were never meant to show a part number. --- ## 7. Testing Strategy ### 7.1 Migration tests (one-shot, runs on upgrade) - `fp.part.catalog` with empty `part_number` → `part_number = name` after migration - `fp.part.catalog` with empty `revision` → `revision = 'A'` after migration - `fp.sale.description.template` rows with text in `description` → both `internal_description` AND `customer_facing_description` hold that same text; old column gone - Confirmed/done SO lines get `x_fc_internal_description = name` backfilled - Every existing part ends with `certificate_requirement = 'inherit'` - Running the migration a second time is a no-op (idempotency) ### 7.2 Unit tests - New part without `part_number` → save rejected (UserError / validation) - New part without `revision` → save rejected - New part without `name` → saves fine (optional) - Description template row: both descriptions must be non-empty - SO line: both `name` AND `x_fc_internal_description` must be non-empty - Cert resolution: part `coc_thickness` + partner `send_coc=False` → result `(True, True)` — part wins - Cert resolution: part `inherit` + partner `send_coc=True, send_thickness=False` → result `(True, False)` — falls through - Cert resolution: multi-line MO with parts `none` + `coc_thickness` → result `(True, True)` — strictest wins - Cert resolution: MO with no SO link → safe fallback ### 7.3 End-to-end smoke (odoo-shell scripts, pattern from QC suite) - Direct order → SO confirm → MO confirm → MO done → CoC generated with customer part number on page 1, no `[default_code]` - Same flow with `certificate_requirement = 'none'` on the part → no CoC generated even if partner has `send_coc=True` - Order entry: pick a description template row → both fields populate on the SO line → save → reopen → both fields persist → customer SO PDF shows only the customer-facing description - Two customers both add a part numbered `WIDGET-001` at different revisions → no collision (different fp.part.catalog records; default_code on the service product is unchanged) ### 7.4 Regression on Phase 1–3 (QC work) After the Sub 2 migration, re-run: - `fp_qc_smoke.py` (9-step smoke) - `fp_qc_e2e.py` (8-case edge suite) - `fp_full_workflow.py` (full lifecycle) All must stay green. Sub 2 only wires cert resolution through a new helper — QC checklist logic is untouched. --- ## 8. Defensive Measures (Prevent Rework When Later Subs Land) 1. **Single-source cert resolution** (§5.1) — `_fp_resolve_cert_requirement` is the only place partner-level fallback is read. When Sub 6 restructures partner flags into per-location or per-contact permissions, one function updates — no call-site hunt. 2. **Shared QWeb macro** (§6.1) — all four customer-facing reports render the line header through one template. Sub 5's revision picker updates the macro, all reports follow. 3. **Idempotent migration** (§3) — safe to re-run; doesn't fight future sub-project migrations. 4. **Additive SO line fields** — `x_fc_internal_description`, `x_fc_description_template_id` sit alongside future Sub 5 fields (`x_fc_serial_number`, `x_fc_job_number`, `x_fc_thickness`, `x_fc_revision_snapshot`) with zero touchpoints. 5. **Clean removal of old `description` column** — migrated then dropped in the same migration. No deprecated-field confusion later. --- ## 9. Files Touched (Anticipated) ### Models - `fusion_plating_configurator/models/fp_part_catalog.py` — field changes (part_number / revision required, name optional, + certificate_requirement) - `fusion_plating_configurator/models/fp_sale_description_template.py` — field split - `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` to call it ### Views - `fusion_plating_configurator/views/fp_part_catalog_views.xml` — required markers, cert requirement field, relabels - `fusion_plating_configurator/views/fp_part_catalog_views.xml` (Descriptions tab) — two-column repeater - `fusion_plating_configurator/views/sale_order_views.xml` — SO line internal-description field - `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` — description-template picker + dual-description inputs - Wherever "SKU" appears in views → relabel to "Part Number" ### Reports - `fusion_plating_reports/report/report_fp_sale.xml` — use macro - `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` — add internal description, keep product/default_code - `fusion_plating_reports/report/report_fp_job_traveller.xml` — same as WO - `fusion_plating_reports/report/customer_line_header.xml` — NEW macro ### Migration - `fusion_plating_configurator/migrations/19.0.X.X.X/post-migration.py` — all five migration steps - `fusion_plating_configurator/hooks.py` — register `post_init_hook` if not already present ### Security - No new models, so no new ACL rows needed. Existing `fp.part.catalog` and `fp.sale.description.template` ACLs already cover the new fields. ### Manifest - `fusion_plating_configurator/__manifest__.py` — bump version; register new view / wizard changes - `fusion_plating_reports/__manifest__.py` — register new macro file - `fusion_plating_bridge_mrp/__manifest__.py` — bump version --- ## 10. Rollout 1. Bump version on all three affected modules. 2. Push to entech via standard deploy: `systemctl stop odoo` → `odoo -d admin -u fusion_plating_configurator,fusion_plating_reports,fusion_plating_bridge_mrp --stop-after-init` → clear asset cache → `systemctl start odoo`. 3. Verify migrations ran: count backfilled records against expected. 4. Run the smoke + E2E + regression suites. 5. Commit + push when green. --- ## 11. Success Criteria - Every part on the system has a non-empty `part_number` and `revision`. - The Descriptions tab on every part shows the two-column repeater; old single-column layout is gone. - Creating a sale order without filling both descriptions on each line is rejected. - Customer-facing CoC / SO / invoice / packing slip / BoL never print Odoo's internal SKU. - Setting a part's `certificate_requirement` to `none` suppresses CoC generation on MO done, even if the partner has `x_fc_send_coc=True`. - Phase 1–3 QC regression suite stays fully green. --- ## 12. Open Questions (None Blocking) All clarifying questions from the brainstorm (Q1–Q6) are answered. No blocking open questions. Possible implementation-time discoveries (flagged for the plan, not for the spec): - Exact list of QWeb reports that currently print `default_code` as a line-prefix (grep during implementation; the macro swap might touch one or two more than listed in §6.2). - Whether any third-party portal template also renders line headers (unlikely; portal jobs show job-level data not line-level, but worth a sanity grep). These are discovery items, not design decisions.