19 KiB
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_outlabels 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_uomfield onfusion.plating.process.node(Lbs / Sq in)
- Rename
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 NULLANDnode_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_nameplumbing on process nodes_generate_workorders_from_recipe()core walker logic (only the root-resolution layer changes)- The per-MO
fusion.plating.job.node.overridemodel 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_composerreturns{'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_uomfield
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.noderecords, all withpart_catalog_id = X; updatesdefault_process_idto the new root; copies each node'sopt_in_outandtreatment_uomfrom 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
cancelOR 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:
- Read the source subtree: all descendants of the chosen template root (recursive).
- 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.idparent_id= the newly-cloned equivalent of the source's parent (mapped via a source→new dict built during traversal)sequencecopied as-is
- Same
- Atomically set
part.default_process_idto the new root node. - Clean up if replacing an existing tree:
unlink()the prior cloned tree (cascading viaparent_id'sondelete='set null'+ explicit recursion — or simply delete in parent-first order sincepart_catalog_idis cascade-delete). - 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_idback-reference) - Every cloned node has
part_catalog_id= the part - Tree structure matches source (parent chain, sequence, node_type)
opt_in_out+treatment_uomcopied
- Every source node has a corresponding cloned node (
- 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_outat base, no part override, no job override → included. Override toexcludedat part level → excluded. Override toincludedat job level (viafusion.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 1–3 QC suite (smoke + E2E) still green.
- Sub 2 smoke still green.
- Sub 1 behaviour (SO stays draft) unchanged.
7. Defensive Measures
- Single-entry tree resolution —
_resolve_mo_process_treeis the only place that decides which root the walker uses. Sub 4/5 updates change this one method, not every call site. - Shared-template filter everywhere — every place that lists templates uses
domain=[('part_catalog_id', '=', False), ('node_type', '=', 'recipe')]. Consistent. - 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.
- Clone is transactional — wrap in a savepoint; if any step fails the prior tree stays intact.
cloned_from_idis 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— addpart_catalog_id,cloned_from_id,treatment_uom; relabelopt_in_outSelection stringsfusion_plating_configurator/models/fp_part_catalog.py— adddefault_process_id, addaction_open_part_composermethodfusion_plating_bridge_mrp/models/mrp_production.py— add_resolve_mo_process_tree; rewire_generate_workorders_from_recipeentry point
Data
fusion_plating/data/fp_process_general_processing.xml— NEW. Seeds "General Processing" template withnoupdate="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" tabfusion_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 actionfusion_plating_configurator/static/src/xml/fp_part_process_composer.xml— NEW. QWeb templatefusion_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 entriesfusion_plating_configurator/__manifest__.py— version bump + new view/JS/XML assetsfusion_plating_bridge_mrp/__manifest__.py— version bump
9. Rollout
- Deploy
fusion_plating(new field + migration + seed + SCSS). - Deploy
fusion_plating_configurator(part form + composer client action + views + JS/XML). - Deploy
fusion_plating_bridge_mrp(walker rewire). - Upgrade all three on entech in sequence (
-u fusion_plating,fusion_plating_configurator,fusion_plating_bridge_mrp). - Verify migration ran via SQL checks (part_catalog_id column exists; seeded "General Processing" record present).
- Run unit + regression suites.
- 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.
- 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_idset on every cloned node;default_process_idset 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 1–3 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_outvalues ("Disabled" / "Opt-In" / "Opt-Out") render as the new labels everywhere without any DB migration.
11. Open Questions (None Blocking)
All brainstorm clarifying questions (Q1–Q5) + 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.inputoperator-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-BASICalready exists in the codebase; "General Processing" is an addition, not a replacement. Verify no name collision.