Files
Odoo-Modules/docs/superpowers/specs/2026-04-22-sub3-default-process-composer.md

19 KiB
Raw Blame History

Sub 3 — Default Process + Composer per Part

Date: 2026-04-22 Module scope: fusion_plating (core), fusion_plating_configurator, fusion_plating_bridge_mrp Status: Design approved section-by-section; ready for implementation Predecessors: Sub 1 (shipped 733236f), Sub 2 (shipped afd8bae) Roadmap entry: fusion_plating/CLAUDE.md → "Fine-Tuning Initiative" → Sub 3


1. Scope

  • 2e — every part has a "Default Process" field + Compose button
  • 2f — reuse the existing recipe tree OWL editor inside a part-scoped wrapper
  • Scope additions from the brainstorm session:
    • Rename opt_in_out labels to Always Included / Included by Default / Excluded by Default (keys stay, only Selection strings change)
    • Tree node colour scheme: Green=completed, Blue=active, Red=error-only
    • New treatment_uom field on fusion.plating.process.node (Lbs / Sq in)

Out of scope

  • Contract Review workflow (node present; sign-off flow is Sub 4)
  • Per-order revision picker (Sub 5)
  • Per-contact notification routing (Sub 6)

2. Data Model Changes

fp.part.catalog — one new field

default_process_id : Many2one('fusion.plating.process.node',
                              domain=[('part_catalog_id', '=', id),
                                      ('node_type', '=', 'recipe')])
                     # Root node of this part's cloned tree.
                     # NULL until the user first composes a process.

x_fc_default_coating_config_id + x_fc_default_treatment_ids remain (Q4 — orthogonal concerns).

fusion.plating.process.node — three new fields + label rename

part_catalog_id : Many2one('fp.part.catalog', ondelete='cascade', index=True)
                  # NULL on shared templates. Populated on every node
                  # of a part's cloned tree. All nodes in a cloned
                  # subtree share the same value.

cloned_from_id  : Many2one('fusion.plating.process.node', ondelete='set null')
                  # Optional. On cloned nodes, points back at the source
                  # template node. Enables future "template has drifted"
                  # indicators. Not load-bearing.

treatment_uom   : Selection([('lbs',   'Lbs (weight-based)'),
                             ('sq_in', 'Sq in (area-based)')])
                  # Per-node UoM for pricing / cost tracking.

# Label rename — keys preserved, only display strings change:
opt_in_out : Selection([
    ('disabled', 'Always Included'),       # was "Disabled"
    ('opt_out',  'Included by Default'),   # was "Opt-Out"
    ('opt_in',   'Excluded by Default'),   # was "Opt-In"
])

fusion.plating.job.node.override — unchanged

Only the opt_in_out Selection label strings align with the new vocabulary (no per-model change — the Selection list is inherited through standard Odoo field references, but since the field is defined per-model, the override model's opt_in_out field gets the same relabel).

Derived classifications

  • Shared template: part_catalog_id IS NULL AND node_type='recipe'. Appears in the "Load Existing Process" dropdown; admin-managed; admin-editable via the existing recipe editor.
  • Part-owned tree: part_catalog_id = X. Hidden from template pickers; accessible only via the part's Compose UI.

Explicitly NOT changing

  • _parent_store / parent_id / _parent_name plumbing on process nodes
  • _generate_workorders_from_recipe() core walker logic (only the root-resolution layer changes)
  • The per-MO fusion.plating.job.node.override model itself

3. Migration Strategy

Runs via fusion_plating/migrations/19.0.X.X.X/post-migration.py on the core module version bump.

Step 1 — Explicit part_catalog_id = NULL on existing nodes

All existing nodes are shared templates. Makes the invariant explicit.

UPDATE fusion_plating_process_node
SET    part_catalog_id = NULL
WHERE  part_catalog_id IS NULL;

Step 2 — Label rename is view-only

No data migration. opt_in_out keys (disabled / opt_in / opt_out) stay exactly as in the DB. Selection strings change at the model definition.

Step 3 — Seed "General Processing" template

Data file: fusion_plating/data/fp_process_general_processing.xml with noupdate="1".

Fresh installs get it automatically. Existing installs (entech) don't get stomped on if admin already renamed / edited.

Structure:

General Processing (recipe)
├── Contract Review (sub_process)
├── Incoming Inspection (operation)
│   ├── Ready for Incoming Inspection (step)
│   └── Incoming Inspection (step)
├── Scheduling (operation)
├── Final Inspection / Packaging (operation)
│   ├── Ready For Final Inspection (step)
│   └── Final Inspection / Packaging (step)
└── Shipping (sub_process)
    ├── Ready For Shipping (step)
    ├── Packing Slip Created (step)
    └── Shipped (step)

Each seeded node's opt_in_out = 'disabled' (Always Included) by default. Admin can relax individual ones later.

Step 4 — _generate_workorders_from_recipe() rewire

New resolution order inside the existing method (new helper on mrp.production):

def _resolve_mo_process_tree(self):
    # Preferred: part's cloned tree (Sub 3)
    if (self.x_fc_part_catalog_id
            and self.x_fc_part_catalog_id.default_process_id):
        return self.x_fc_part_catalog_id.default_process_id
    # Fallback: legacy path — product lookup / coating config
    return self.x_fc_recipe_id

Walker body unchanged; it walks whichever root is returned. fusion.plating.job.node.override lookup unchanged (keyed by production_id + node_id; node_id now points at the part's cloned node, which is unique per part, so no collision).

Step 5 — Index part_catalog_id on process node

Via index=True on the field. Odoo creates the index on model install.

Step 6 — No backfill of default_process_id on existing parts

Existing parts stay NULL; new / composed parts get the new path. Existing MOs continue using x_fc_recipe_id via the fallback branch in Step 4.

No surprise upgrades for in-flight jobs.


4. UI Changes

Part form (fp.part.catalog) — new "Process" tab

┌─────────────────────────────────────────────────────────────────┐
│ Default Process:  [General Processing ▾]  [ COMPOSE ]           │
│                   From: Part Number                             │
├─────────────────────────────────────────────────────────────────┤
│ Configure Opt-In / Opt-Out                                      │
│   ☐ Contract Review     [Included by Default ▾]                 │
│   ☐ Masking             [Included by Default ▾]                 │
│   (mini-list of nodes where opt_in_out != 'disabled')           │
├─────────────────────────────────────────────────────────────────┤
│ Default Treatment Selections                                    │
│   General Processing:  [Lbs / Sq in ▾]                          │
│   Masking:             [Lbs / Sq in ▾]                          │
│   (mini-list of operation / sub_process nodes in this tree)     │
└─────────────────────────────────────────────────────────────────┘
  • default_process_id — Many2one, readonly (set by the Composer)
  • Compose button → action_open_part_composer returns {'type': 'ir.actions.client', 'tag': 'fp_part_process_composer', 'params': {'part_id': self.id}}
  • Opt-in/out: inline editable list of the cloned tree's nodes where opt_in_out != 'disabled'
  • Treatment UoM: inline editable list of operation / sub_process nodes with the treatment_uom field

New OWL client action — fp_part_process_composer

Wrapper around the existing recipe tree OWL editor.

┌─────────────────────────────────────────────────────────────────┐
│  ◄ Back    Process Composer — SC-CLAMP-V2 (Rev A)              │
│            Stairlift Clamp V2                                   │
├─────────────────────────────────────────────────────────────────┤
│  Load Existing Process: [General Processing ▾]  [ LOAD ]        │
│                                                                  │
│  ┌───────────────────────┐  ┌─────────────────────────────┐    │
│  │                       │  │  Tags / Process Nodes       │    │
│  │  (tree editor)        │  │  (existing sidebar)         │    │
│  │                       │  │                             │    │
│  │  part's cloned tree   │  │  Specs / Process Nodes      │    │
│  │  or empty if none     │  │                             │    │
│  └───────────────────────┘  └─────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
  • Header: part number + revision + name (compact breadcrumb)
  • "Load Existing Process" — populated from shared templates
  • Load button: if tree already populated → confirm dialog ("Replace current tree with {template}?"). Clones the full template subtree into new fusion.plating.process.node records, all with part_catalog_id = X; updates default_process_id to the new root; copies each node's opt_in_out and treatment_uom from source.
  • Tree editor: reuses existing OWL internals (drag-drop, node palette, tags sidebar, inline editing) but scoped to this part's tree and hides admin-only actions.

Tree node colour scheme — SCSS update

File: fusion_plating/static/src/scss/recipe_tree_editor.scss

.o_fp_node_card {
    background-color: $fp-card;
    border: 1px solid $fp-border;  // default (pending)
}
.o_fp_node_card--completed {
    background-color: color-mix(in srgb, $fp-ok 10%, $fp-card);
    border: 2px solid $fp-ok;   // Green — WO done
}
.o_fp_node_card--active {
    background-color: color-mix(in srgb, $fp-accent 10%, $fp-card);
    border: 2px solid $fp-accent;   // Blue — WO progress
}
.o_fp_node_card--failed,
.o_fp_node_card--blocked {
    background-color: color-mix(in srgb, $fp-bad 10%, $fp-card);
    border: 2px solid $fp-bad;   // Red — WO cancel / hold active
}

State derivation from linked WO when viewing MO-context tree:

  • Completed → WO state done
  • Active → WO state progress
  • Failed/Blocked → WO state cancel OR linked quality hold active
  • Pending → no linked WO or WO state pending/ready

opt_in_out relabel

Updates propagate automatically through every view rendering the field (node form, tree editor sidebar, part-form opt-in/out list, MO override wizard). No per-view change needed.


5. Clone Semantics

When the user hits "Load Existing Process" with a template selected:

  1. Read the source subtree: all descendants of the chosen template root (recursive).
  2. For each source node, create a new node record with:
    • Same name, code, node_type, opt_in_out, treatment_uom, icon, customer_visible, etc.
    • part_catalog_id = X (the part)
    • cloned_from_id = source_node.id
    • parent_id = the newly-cloned equivalent of the source's parent (mapped via a source→new dict built during traversal)
    • sequence copied as-is
  3. Atomically set part.default_process_id to the new root node.
  4. Clean up if replacing an existing tree: unlink() the prior cloned tree (cascading via parent_id's ondelete='set null' + explicit recursion — or simply delete in parent-first order since part_catalog_id is cascade-delete).
  5. Wrap in savepoint so partial clone failures roll back cleanly.

Operator inputs (fusion.plating.process.node.input) are copied along with each operation / step node.


6. Testing Strategy

Unit tests (odoo-shell scripts following the QC suite pattern)

  • Clone correctness — load "General Processing" into a fresh part. Verify:
    • Every source node has a corresponding cloned node (cloned_from_id back-reference)
    • Every cloned node has part_catalog_id = the part
    • Tree structure matches source (parent chain, sequence, node_type)
    • opt_in_out + treatment_uom copied
  • Reload replaces cleanly — compose once, then load a different template. Verify prior tree is deleted (zero orphaned nodes with part_catalog_id = X).
  • Walker resolution — MO with linked part+default_process_id uses the part's tree. MO without linked part falls back to x_fc_recipe_id.
  • Three-layer precedence — node with opt_out at base, no part override, no job override → included. Override to excluded at part level → excluded. Override to included at job level (via fusion.plating.job.node.override) → included again (job beats part).
  • Label rename — existing records with opt_in_out='disabled' still render as "Always Included" in the UI; opt_in='opt_in' renders as "Excluded by Default".
  • Shared template filter — "Load Existing Process" dropdown only shows templates (part_catalog_id IS NULL AND node_type='recipe'), not part-owned trees.

Regression

  • Phase 13 QC suite (smoke + E2E) still green.
  • Sub 2 smoke still green.
  • Sub 1 behaviour (SO stays draft) unchanged.

7. Defensive Measures

  1. Single-entry tree resolution_resolve_mo_process_tree is the only place that decides which root the walker uses. Sub 4/5 updates change this one method, not every call site.
  2. Shared-template filter everywhere — every place that lists templates uses domain=[('part_catalog_id', '=', False), ('node_type', '=', 'recipe')]. Consistent.
  3. Composer client action is a thin wrapper — reuses existing tree editor OWL internals so future improvements to the editor benefit both admin and part contexts.
  4. Clone is transactional — wrap in a savepoint; if any step fails the prior tree stays intact.
  5. cloned_from_id is optional — enables future "template has drifted" indicators without creating a hard dependency now.

8. Files Touched (Anticipated)

Models

  • fusion_plating/models/fp_process_node.py — add part_catalog_id, cloned_from_id, treatment_uom; relabel opt_in_out Selection strings
  • fusion_plating_configurator/models/fp_part_catalog.py — add default_process_id, add action_open_part_composer method
  • fusion_plating_bridge_mrp/models/mrp_production.py — add _resolve_mo_process_tree; rewire _generate_workorders_from_recipe entry point

Data

  • fusion_plating/data/fp_process_general_processing.xml — NEW. Seeds "General Processing" template with noupdate="1".

Migration

  • fusion_plating/migrations/19.0.X.X.X/post-migration.py — NEW. Steps 1, 2 (relabel is model-level but script ensures integrity), 6 (no-op, documented).

Views

  • fusion_plating_configurator/views/fp_part_catalog_views.xml — new "Process" tab
  • fusion_plating_configurator/wizard/fp_part_process_composer_views.xml (path TBD) — NEW. Client action view / layout

Client Action (OWL)

  • fusion_plating_configurator/static/src/js/fp_part_process_composer.js — NEW. Wrapper client action
  • fusion_plating_configurator/static/src/xml/fp_part_process_composer.xml — NEW. QWeb template
  • fusion_plating/static/src/js/recipe_tree_editor.js — component exports (refactor for reuse if not already)
  • fusion_plating/static/src/scss/recipe_tree_editor.scss — add .o_fp_node_card--{completed,active,failed,blocked} classes

Controllers

  • fusion_plating_configurator/controllers/fp_part_process_composer.py — NEW. RPC endpoints for the wrapper (load template → clone, delete prior tree, update part)

Security

  • fusion_plating_configurator/security/ir.model.access.csv — ensure operators can read process nodes scoped to their parts

Manifest bumps

  • fusion_plating/__manifest__.py — version bump + new data/migration/assets entries
  • fusion_plating_configurator/__manifest__.py — version bump + new view/JS/XML assets
  • fusion_plating_bridge_mrp/__manifest__.py — version bump

9. Rollout

  1. Deploy fusion_plating (new field + migration + seed + SCSS).
  2. Deploy fusion_plating_configurator (part form + composer client action + views + JS/XML).
  3. Deploy fusion_plating_bridge_mrp (walker rewire).
  4. Upgrade all three on entech in sequence (-u fusion_plating,fusion_plating_configurator,fusion_plating_bridge_mrp).
  5. Verify migration ran via SQL checks (part_catalog_id column exists; seeded "General Processing" record present).
  6. Run unit + regression suites.
  7. Smoke-test on entech: pick a part, click Compose, load General Processing, add a Nickel Plating node, save, trigger MO creation, verify WO generation uses the part's tree.
  8. Commit + push.

10. Success Criteria

  • Every part form shows a Process tab with a Compose button.
  • Clicking Compose opens the part-scoped composer with the OWL tree editor.
  • "Load Existing Process" → "General Processing" populates the tree with seeded nodes; part_catalog_id set on every cloned node; default_process_id set on the part.
  • Adding a Nickel Plating operation, saving, then creating an MO for that part → WO generation includes the Nickel Plating operation as a work order.
  • An MO's _fp_resolve_cert_requirement (Sub 2) + the new _resolve_mo_process_tree (Sub 3) coexist without interference.
  • Phase 13 QC regression suite stays green.
  • Tree colours render Green / Blue / neutral / Red per the agreed palette on the MO-context tree view.
  • Old opt_in_out values ("Disabled" / "Opt-In" / "Opt-Out") render as the new labels everywhere without any DB migration.

11. Open Questions (None Blocking)

All brainstorm clarifying questions (Q1Q5) + two scope additions (opt_in_out rename, tree colours) are answered. No blockers.

Implementation-time discovery items (flagged here, not requiring spec changes):

  • The existing OWL tree editor component may need light refactoring to accept a "part-scoped root" prop cleanly (wasn't originally designed for external embedding). Expect a small export-refactor step.
  • The fusion.plating.process.node.input operator-input records need to travel with the clone. If the existing editor uses a separate RPC to fetch inputs, the wrapper's clone logic must mirror that.
  • Seed data for ENP-ALUM-BASIC already exists in the codebase; "General Processing" is an addition, not a replacement. Verify no name collision.