docs(plating): Sub 3 design spec \u2014 Default Process + Composer per Part
This commit is contained in:
@@ -0,0 +1,347 @@
|
|||||||
|
# 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 1–3 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 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_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 (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.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.
|
||||||
Reference in New Issue
Block a user