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

348 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```sql
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`):
```python
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`
```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.