Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-29-part-description-history-design.md
gsinghpal 1ae83e187e docs(plating): spec — per-part description history (auto-version on order entry)
Dedicated fp.part.description.version model: latest auto-loads both internal +
customer-facing into a new order line; on SO confirm, a changed description
saves a new version titled "S#### · date". Browsable per-part history;
default_specification_text kept synced. SO surfaces only (not quotes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:38:20 -04:00

11 KiB

Per-Part Description History (auto-version on order entry)

Date: 2026-05-29 Status: Approved design — pending spec review → implementation plan Module: fusion_plating_configurator (description fields, part catalog, SO + wizard live here)

Goal

Give every part a versioned description history. On order entry the part's most-recent internal + customer-facing descriptions auto-load into the line; if the operator changes anything, confirming the order saves a new version titled by Sales Order # + date. The history accumulates per part and is browsable on the part form, so operators always start from "what we did last time" and can see how a part's description evolved.

Background — current state (verified 2026-05-29)

The description system today has two independent mechanisms and no versioning:

  1. Per-part defaultfp.part.catalog.default_specification_text (Text, customer-facing only). Saved back to the part once (first order only — the save-back at fusion_plating_configurator/wizard/fp_direct_order_wizard.py:982 guards on not part.default_specification_text), and auto-fills the line next time via the _DEFAULTS_BY_FIELD map ('line_description' → 'default_specification_text', fp_direct_order_line.py:657). The internal description is never persisted.
  2. Curated template libraryfp.sale.description.template (fusion_plating_configurator/models/fp_sale_description_template.py): each row has internal_description + customer_facing_description + name + tag, scoped by part_catalog_id (→ customer → global fallback). The operator picks one from a dropdown (x_fc_description_template_id + _register_usage). This is a hand-maintained pick-list, not a history.

Description field names differ by surface (important for the design):

Surface Customer-facing field Internal field
Direct-order / express wizard line (fp.direct.order.line) line_description (:329) internal_description (:335)
Real sale order line (sale.order.line) name (standard Odoo) x_fc_internal_description (sale_order_line.py:52)

There is no per-order snapshot, no auto-titling, no is_latest, and no internal-description persistence today.

Note on prior errant code: a misfired exploration agent auto-generated an fp_sale_description_version.py (scoped to the SO line, versioned on template-apply) and edited sale_order_line.py/__init__.py. That work does not match this design (wrong scope + trigger) and was stashed (git stash@{0}), not committed. Implementation here is greenfield; the stash will be dropped.

Locked decisions

# Decision
D1 Storage: a dedicated, separate fp.part.description.version model — kept OUT of the curated fp.sale.description.template picker.
D2 Title: "<Sales Order #> · <date>", e.g. "S00123 · 2026-05-29".
D3 Save trigger: on sale order confirm, and only if the description differs from the part's latest version (dedup). Abandoned drafts never create versions.
D4 Auto-load: the part's latest version auto-loads BOTH internal + customer-facing into a new order line (won't clobber text already typed).
D5 Legacy sync: keep part.default_specification_text updated to the latest customer-facing text (no regressions for anything reading that field). Version history is the new source of truth; default_specification_text is a fallback when a part has no version yet.
D6 Scope: all SO entry surfaces — direct-order wizard, express order entry, SO line form. Quotes excluded (no SO # to title with).

Design

1. Data model — fp.part.description.version

New model in fusion_plating_configurator/models/fp_part_description_version.py:

Field Type Notes
part_catalog_id M2O fp.part.catalog required, ondelete='cascade', indexed
internal_description Text snapshot of the line's internal description
customer_facing_description Text snapshot of the line's customer-facing description
sale_order_id M2O sale.order ondelete='set null' — the order that produced it
sale_order_line_id M2O sale.order.line ondelete='set null' — the exact line (optional)
source_date Date the order's date used in the title
version_no Integer per-part incrementing counter (1, 2, 3 …)
name Char (stored compute) "<sale_order.name> · <source_date>"; collision suffix (2) if a second version shares one order
is_latest Boolean (stored compute) True for the highest version_no per part — drives the cheap auto-load lookup
partner_id M2O res.partner related part_catalog_id.partner_id, stored (grouping)
active Boolean default True

Snapshots are immutable (no edit UI). version_no is assigned at create as max(existing for part) + 1. is_latest recomputes when a new version is added.

Add an inverse one2many on fp.part.catalog: x_fc_description_version_ids (part_catalog_id), used by the history UI (§4).

2. Auto-load at order entry

When a part is set on a line and both description fields are empty, load the part's latest version (is_latest=True): customer_facing_description → the customer-facing field, internal_description → the internal field. Per surface:

  • Wizard line (fp.direct.order.line, covers direct-order and express): on the part_catalog_id change, write line_description + internal_description. This supersedes the current customer-facing-only default_specification_text fill and now loads internal too. Fallback: if the part has no version yet, fall back to default_specification_text (customer-facing) as today, so legacy parts still pre-fill. Preserve the existing first-time-part push_to_defaults behavior (untouched).
  • SO line (sale.order.line): on the part being set (x_fc_part_catalog_id), same load into name + x_fc_internal_description when empty.

"Empty" check prevents clobbering a deliberate edit or a template the operator just picked. Coordinate with the existing fusion_plating_quality default-spec onchange inherit (it owns x_fc_default_customer_spec_id fill) so the two don't fight — the version load runs first and the spec-link fill is unaffected (different field).

3. Save on confirm (deduped)

Hook sale.order.action_confirm (in fusion_plating_configurator/models/sale_order.py — extend the existing override if present, else add one). Run it after the SO name is finalized — this shop renames the SO to its parent number during action_confirm, so reading order.name post-rename gives the title the real confirmed order number (exact placement relative to the receiving/jobs confirm hooks is a planning detail). For each order line carrying a part (x_fc_part_catalog_id):

  1. Read the line's name (customer-facing) + x_fc_internal_description (internal), stripped/normalized for comparison.
  2. Look up the part's latest version. If none, or if either description differs from the latest → create a new fp.part.description.version: part, both descriptions, sale_order_id, sale_order_line_id, source_date = order.date_order.date(), version_no = (latest.version_no or 0) + 1. The new row becomes is_latest.
  3. Identical to latest → no new version (dedup).
  4. Sync legacy (D5): set part.default_specification_text = <latest customer-facing>.

All writes that touch the part/version run with the right privileges (sudo where a low-priv estimator can't write the part catalog — verify the estimator's ACL on fp.part.catalog and the new model during planning; mirror the existing save-back's privilege handling).

4. History UI

On the part form's Descriptions tab (alongside the curated-template list), add a read-only "Description History" sub-list bound to x_fc_description_version_ids, ordered version_no desc. Columns: name (S#·date), a customer_facing_description preview, create_uid, create_date, sale_order_id. No create/edit/delete from here (snapshots are immutable; managers can archive via active if needed). Add an ACL row for the new model (read for estimator+, create via the confirm hook which may sudo; write/unlink manager-only).

5. Edge cases

  • Same part on multiple lines of one order: each distinct description creates its own version (deduped against the running latest); identical lines collapse. Title collision → (2) suffix.
  • Order cancelled after confirm: the version stays (it reflected a real confirmed order); no cleanup (YAGNI).
  • Editing a description on an already-confirmed order: no new version unless re-confirmed — accepted limitation for the MVP.
  • Legacy part (has default_specification_text, no version): first order auto-loads from the legacy field; confirming creates version #1 and starts the history.

6. Testing

  • Model: version_no increments per part; is_latest flips to the newest; name format = "S#### · YYYY-MM-DD".
  • Save-on-confirm: first confirm creates v1; re-order with identical description → no new version; re-order with a changed description → v2; default_specification_text synced to latest customer-facing each time.
  • Auto-load: latest version fills both fields on an empty wizard line / SO line; does NOT overwrite text already entered; legacy-fallback fills customer-facing when no version.
  • Multi-line same part: dedup + title-collision suffix.

7. Out of scope / deferred

  • Quotes / RFQ (fp.quote.configurator) versioning — no SO #; later.
  • A "load a specific older version" picker on the line (auto-load-latest is the MVP; history is browse-only for now).
  • Backfilling history from existing historical SO lines.

Files to touch

  • New: fusion_plating_configurator/models/fp_part_description_version.py
  • fusion_plating_configurator/models/__init__.py (import the new model)
  • fusion_plating_configurator/models/fp_part_catalog.py (x_fc_description_version_ids o2m)
  • fusion_plating_configurator/models/sale_order.py (confirm hook → create versions + sync)
  • fusion_plating_configurator/models/sale_order_line.py (auto-load on SO line; read fields on confirm)
  • fusion_plating_configurator/wizard/fp_direct_order_line.py (auto-load on part onchange, with legacy fallback)
  • fusion_plating_configurator/security/ir.model.access.csv (new model ACL)
  • fusion_plating_configurator/views/fp_part_catalog_views.xml (Description History sub-list) + a view for the new model
  • fusion_plating_configurator/__manifest__.py (version bump; register new model views/security)