Files
Odoo-Modules/docs/superpowers/specs/2026-04-21-sub2-part-data-model-design.md
gsinghpal a15b75e38a docs(plating): fine-tuning initiative roadmap + Sub 2 design spec
Captures the current state of the system-wide fine-tuning initiative so a
fresh Claude Code session can resume without context loss.

CLAUDE.md additions (fusion_plating/CLAUDE.md):
* Sub-project roadmap (Sub 1 through 8 + two deferred items)
* Sub 2 locked decisions (Q1–Q6 answers)
* Sub 2 defensive measures that prevent rework when later subs land
* Sub 6 / 7 / 8 previews from the client transcript
* Client-confirmed operational thresholds (tank polling, active tanks)
* How to resume in a fresh session

Sub 2 design spec (docs/superpowers/specs/):
* Part Data Model Overhaul — covers gaps 2b, 2c, 2d, 4
* 12 sections: scope, data model, migration, UI, cert resolution,
  reports, testing, defensive measures, files touched, rollout,
  success criteria, open questions
* All clarifying questions answered; zero placeholders
* Ready for writing-plans skill to generate implementation plan

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

18 KiB
Raw Blame History

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

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

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:

<template id="customer_line_header">
  <!-- Renders part_number + revision + customer-facing description.
       Falls back to the generic product name for non-part lines
       (rush fees, freight, expedite charges).
       Called from every customer-facing report. -->
</template>

All four customer-facing report line renderers call <t t-call="fusion_plating_reports.customer_line_header"/>. 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_numberpart_number = name after migration
  • fp.part.catalog with empty revisionrevision = '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 13 (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 fieldsx_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 odooodoo -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 13 QC regression suite stays fully green.

12. Open Questions (None Blocking)

All clarifying questions from the brainstorm (Q1Q6) 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.