docs(plating): spec + plan for Shop Floor live-step + library cleanup

Spec documents the 4 code defects + structural vocabulary mismatch
between fp.step.kind taxonomy and the legacy _STEP_KIND_TO_AREA dict,
plus the 30 library templates missing metadata. Plan breaks the work
into 15 bite-sized tasks across 2 phases.

Implementation shipped in:
- c75d2bde (Odoo 19 session.authenticate signature fix — separate)
- 7b90f210 (Phase 1: fusion_plating)
- b06d28e7 (Phase 2: jobs + shopfloor)
- 6afc9e3c (follow-up tracking + pattern anchor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-24 17:13:58 -04:00
parent 6afc9e3c0d
commit 2285c9def1
2 changed files with 1760 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,635 @@
# Shop Floor — Live Step + Kind/Library Cleanup
**Date:** 2026-05-24
**Modules:** `fusion_plating`, `fusion_plating_jobs`, `fusion_plating_shopfloor`
**Status:** Revised after step-library audit. Awaiting implementation plan.
---
## Problem
All 7 jobs on entech are stuck in the **Receiving** column of the Shop Floor
plant kanban, each tagged with a purple "📋 QA-005 review" chip, even though
every step on every one of them is `done`. The board doesn't reflect shop
state.
Investigation surfaced **four code defects**, a **structural vocabulary
mismatch** between the user-extensible step kind taxonomy and the hardcoded
`area_kind` mapping, **gaps in the kind taxonomy** (no `blast` kind, three
relevant kinds inactive), and **30 step-library templates missing codes,
descriptions, and meaningful icons**.
### Defect 1 — `_compute_card_state` edge case mislabels done jobs
[`fusion_plating_jobs/models/fp_job.py:261-267`](../../../fusion_plating_jobs/models/fp_job.py)
A job whose `active_step_id` is False (all steps done OR no steps at all)
defaults to `'contract_review'` regardless of `job.state`. Done jobs get a
QA-005 chip they don't deserve.
### Defect 2 — `_compute_active_step_id` is too narrow
[`fusion_plating_jobs/models/fp_job.py:386-391`](../../../fusion_plating_jobs/models/fp_job.py)
Only matches `state == 'in_progress'`. Between-step / paused / ready jobs
have `active_step_id = False`. Combined with Defect 3, these teleport to
Receiving.
### Defect 3 — column-resolve fallback is `'receiving'`
[`fusion_plating_shopfloor/controllers/plant_kanban.py:161-170`](../../../fusion_plating_shopfloor/controllers/plant_kanban.py)
When `active_step_id` is False this fallback fires for every non-running
job. Receiving becomes a parking lot.
### Defect 4 — done jobs aren't filtered off the board
Done + cancelled jobs stay visible forever. The 7 stuck cards on entech are
all `state='done'` jobs that shipped weeks ago.
### Defect 5 (structural) — kind→area_kind vocabulary mismatch
`fp.step.kind` is a user-extensible taxonomy (28 records, 12 active in the
dropdown post 2026-05-24 dedup). `kind_id` is `required=True` on both
`fp.step.template` and `fusion.plating.process.node`, defaulting to
`code='other'`.
`fp.job.step._compute_area_kind` reads `recipe_node.default_kind` (the kind
code) through the hardcoded `_STEP_KIND_TO_AREA` dict in
[`fp_job_step.py:25-73`](../../../fusion_plating_jobs/models/fp_job_step.py).
The two vocabularies overlap on **7 of 28 codes**. Adoption on entech:
| `kind.code` | Nodes | Mapping exists? | Falls to |
|---|---|---|---|
| `other` | 240 | ❌ | `'plating'` |
| `racking` | 122 | ✅ | `'racking'` ✓ |
| `wet_process` | 105 | ❌ | `'plating'` (lucky — wet line IS plating) |
| `bake` | 103 | ✅ | `'baking'` ✓ |
| `mask` | 92 | ❌ (dict has `'masking'`) | `'plating'` (wrong) |
| `inspect` | 52 | ❌ (dict has `'inspection'`) | `'plating'` (wrong) |
| `plate` | 35 | ❌ (dict has `'e_nickel_plate'`) | `'plating'` (lucky) |
| `final_inspect` | 31 | ❌ (dict has `'final_inspection'`) | `'plating'` (wrong) |
| `contract_review` | 17 | ✅ | `'receiving'` ✓ |
| `receiving` | 16 | ✅ | `'receiving'` ✓ |
| `ship` | 3 | ❌ (dict has `'shipping'`) | `'plating'` (wrong) |
The structural fix: make `area_kind` a required field on `fp.step.kind`
itself so each kind self-declares its column.
### Defect 6 (taxonomy) — kinds that should exist but don't / are inactive
| Kind | Currently | Needed because |
|---|---|---|
| `blast` | Does not exist | 11 recipe nodes named "Blasting" can't be classified correctly. There's no kind that maps to the Blasting column. |
| `derack` | Exists but `active=False` | 23+ recipe nodes named "De-racking" / "DeRacking" need their own kind for tablet routing clarity (`area_kind='de_racking'`). |
| `demask` | Exists but `active=False` | 33 recipe nodes named "De-Masking" are misclassified as `mask` → land in Masking column. Per spec §D4 De-Masking folds into De-Racking. |
| `gating` | Exists but `active=False` | 50+ "Ready For X" recipe nodes are unclassified gates. Without `gating` they fall back to `other` → catch-all. |
### Defect 7 (library) — 30 step-library templates missing metadata
Step Library audit (38 active templates):
| Field | Has it | Missing |
|---|---|---|
| `code` | 8 | 30 |
| `description` | 8 | 30 |
| Meaningful icon (not `fa-cog`) | 13 | 25 |
| `material_callout` | 0 | 38 |
| `process_type_id` | 0 | 38 |
The 8 well-formed templates (`RECV_STD`, `ELEC_CLEAN_STD`, `STRIKE_STD`, etc.)
came from the XML data file. The remaining 30 came from
`_seed_step_library_if_empty()` (programmatic seed from ENP-ALUM-BASIC recipe)
without their library-management metadata.
Several library templates are also classified to the wrong kind. Examples:
| Template | Currently `kind` | Should be `kind` |
|---|---|---|
| Blasting | `other` | `blast` (kind we're creating) |
| De-Masking | `mask` | `demask` (per spec §D4) |
| Ready for Plating / Ready for processing | `plate` / `other` | `gating` |
| Pre-Measurements / Check Sulfamate Nickel Area | `other` | `inspect` |
| Nickel Strip / Nickel Strip - Steel Line | `plate` | `wet_process` (it's a strip, not plating) |
### Defect 8 (recipe nodes) — in-the-wild misclassifications
Once kinds are fixed and library is corrected, the EXISTING ~880 recipe
nodes still point at the wrong kind in well-defined patterns:
| Pattern | Affected nodes | Re-point to |
|---|---|---|
| `name = 'Blasting'` AND `kind = other` | 11 | `kind = blast` |
| `name ILIKE 'Ready %'` AND `kind != gating` | ~50+ | `kind = gating` |
| `name ILIKE '%De-Masking%' OR '%DeMasking%'` AND `kind = mask` | 33 | `kind = demask` |
| `name = 'Scheduling'` AND `kind = other` | 5 | `kind = gating` |
| `name ILIKE '%Nickel Strip%'` AND `kind = plate` | ~10 | `kind = wet_process` |
| `name ILIKE '%Pre-Measurement%' OR '%Check Sulfamate%'` AND `kind = other` | ~10 | `kind = inspect` |
These are auto-migratable because the patterns are unambiguous. The harder
calls (e.g. "Post Plate Inspection" — `inspect` or `final_inspect`?) stay
manual.
---
## Approved fix
### Change 1 — `_compute_active_step_id` priority chain
Replace the single-state filter with a priority lookup over `step_ids`
sorted by sequence. First match wins:
```
in_progress > paused > ready > first pending
```
If every step is `done` (or no steps exist), returns False — handled by
Change 2.
**Why this order:**
- `in_progress` is the most informative.
- `paused` means someone was working and stopped; the card belongs at that station so the next operator can pick it up.
- `ready` is the next-up step waiting on an operator.
- The first `pending` after a `done` is the "next gate" — where the card visually waits.
**File:** [`fusion_plating_jobs/models/fp_job.py`](../../../fusion_plating_jobs/models/fp_job.py)
### Change 2 — `_compute_card_state` edge case
Replace the buggy "no active step → contract_review" fallback with:
```python
if not job.active_step_id:
if job.state == 'done':
job.card_state = 'done'
elif job._fp_inbound_not_received():
job.card_state = 'no_parts'
else:
job.card_state = 'ready' # no steps yet — recipe not assigned
continue
```
**File:** [`fusion_plating_jobs/models/fp_job.py`](../../../fusion_plating_jobs/models/fp_job.py)
### Change 3 — Board state filter
Add `('state', 'in', ('confirmed', 'in_progress'))` to the `fp.job` search
domain in `/fp/landing/plant_kanban`. Done + cancelled jobs disappear from
the board; they remain reachable elsewhere.
**File:** [`fusion_plating_shopfloor/controllers/plant_kanban.py`](../../../fusion_plating_shopfloor/controllers/plant_kanban.py)
### Change 4 — Column-resolve fallback (comment only)
`_resolve_card_area`'s `'receiving'` fallback stays but updates inline
comment to explain the new semantics (truly orphaned cards only).
**File:** [`fusion_plating_shopfloor/controllers/plant_kanban.py`](../../../fusion_plating_shopfloor/controllers/plant_kanban.py)
### Change 5 — `fp.step.kind.area_kind` field (structural)
Add a required Selection field to `fp.step.kind`. Each kind self-declares
which plant-view column its steps belong in.
```python
area_kind = fields.Selection(
[
('receiving', 'Receiving'),
('masking', 'Masking'),
('blasting', 'Blasting'),
('racking', 'Racking'),
('plating', 'Plating'),
('baking', 'Baking'),
('de_racking', 'De-Racking'),
('inspection', 'Final Inspection'),
('shipping', 'Shipping'),
],
string='Shop Floor Column',
required=True,
index=True,
tracking=True,
help='Determines which column on the Shop Floor plant kanban shows '
'cards whose active step uses this kind.',
)
```
**File:** [`fusion_plating/models/fp_step_kind.py`](../../../fusion_plating/models/fp_step_kind.py)
### Change 6 — `_compute_area_kind` priority chain
Simplify `fp.job.step._compute_area_kind`:
```python
@api.depends(
'work_centre_id.area_kind',
'recipe_node_id.kind_id.area_kind',
)
def _compute_area_kind(self):
for step in self:
# 1. work_centre.area_kind (explicit operator setup)
if step.work_centre_id and step.work_centre_id.area_kind:
step.area_kind = step.work_centre_id.area_kind
continue
# 2. recipe_node.kind_id.area_kind (kind taxonomy is authoritative)
node = step.recipe_node_id
if node and node.kind_id and node.kind_id.area_kind:
step.area_kind = node.kind_id.area_kind
continue
# 3. Catch-all — data integrity issue if we land here
step.area_kind = 'plating'
```
The legacy `_STEP_KIND_TO_AREA` dict is deleted.
**File:** [`fusion_plating_jobs/models/fp_job_step.py`](../../../fusion_plating_jobs/models/fp_job_step.py)
### Change 7 — Step Kind UI surfaces `area_kind`
- **Form view** ([`fp_step_kind_views.xml`](../../../fusion_plating/views/fp_step_kind_views.xml)) — add `area_kind` as a prominent picker next to `code` + `name`, with a help-text inline ("Cards whose active step uses this kind appear in this column on the Shop Floor board").
- **List view** — add `area_kind` as a chip column.
- **Simple Editor kind picker** ([`simple_recipe_editor.xml:506-522`](../../../fusion_plating/static/src/xml/simple_recipe_editor.xml)) — option label becomes "Masking — Masking column" so authors see the routing at pick time. Requires updating `kindOptions` payload in [`simple_recipe_controller.py`](../../../fusion_plating/controllers/simple_recipe_controller.py) to include `area_kind` + a human-readable column label per kind.
### Change 8 — Step Kind taxonomy expansion (Cat A)
XML data file additions / updates in
[`fusion_plating/data/fp_step_kind_data.xml`](../../../fusion_plating/data/fp_step_kind_data.xml):
```xml
<!-- NEW: Blasting kind -->
<record id="step_kind_blast" model="fp.step.kind">
<field name="code">blast</field>
<field name="name">Blasting / Media Blast</field>
<field name="sequence">35</field>
<field name="icon">fa-bullseye</field>
<field name="area_kind">blasting</field>
</record>
<!-- Activate existing kinds + set area_kind. The records already exist
from 19.0.20.6.0 with active=False; here we flip + classify.
noupdate=1 protects user edits, so use a one-shot migration to
do the flip on existing installs (Change 10). -->
```
Migration (Change 10) handles the flip on existing installs since the data
file has `noupdate="1"`:
```python
# Activate kinds that were dropped in 19.0.20.6.0 but are needed
# for the area_kind taxonomy to be complete.
for code, area in (
('derack', 'de_racking'),
('demask', 'de_racking'),
('gating', 'receiving'),
):
cr.execute("""
UPDATE fp_step_kind
SET active = TRUE, area_kind = %s
WHERE code = %s AND active = FALSE
""", (area, code))
```
### Change 9 — Step Template metadata backfill + additions (Cat B)
Migration backfills metadata on the 30 templates seeded without it.
Idempotent — only fills NULL/empty fields, doesn't overwrite human edits.
```python
TEMPLATE_BACKFILL = {
# name : (code, icon, kind_code, description_snippet)
'Acid Dip': ('ACID_DIP_STD', 'fa-flask', 'wet_process', 'Short acid immersion to activate the substrate before plating.'),
'Air Dry': ('AIR_DRY_STD', 'fa-sun-o', 'wet_process', 'Air drying step between wet-line operations.'),
'Bake': ('BAKE_STD', 'fa-fire', 'bake', 'Post-plate bake for hydrogen embrittlement relief.'),
'Blasting': ('BLAST_STD', 'fa-bullseye', 'blast', 'Media or bead blasting to prepare the substrate.'),
'Check Sulfamate Nickel Area': ('CHECK_SN_AREA', 'fa-search', 'inspect', 'Quick visual area check on the sulfamate nickel line.'),
'Contract Review': ('CR_STD', 'fa-file-text-o', 'contract_review', 'QA-005 contract review gate. Required when the customer flag is on.'),
'De-Masking': ('DEMASK_STD', 'fa-eraser', 'demask', 'Remove masking material after plating. Folds into De-Racking column.'),
'DeRacking': ('DERACK_STD', 'fa-th', 'derack', 'Remove parts from racks for inspection / packaging.'),
'Desmut': ('DESMUT_STD', 'fa-flask', 'wet_process', 'Remove smut from aluminium surfaces after etching.'),
'Drying': ('DRYING_STD', 'fa-sun-o', 'wet_process', 'Drying step (oven or air) at the end of the wet line.'),
'E-Nickel Plating': ('ENP_STD', 'fa-diamond', 'plate', 'Electroless nickel plate operation. Time and temp per recipe.'),
'Electroclean': ('ECLEAN_STD', 'fa-bolt', 'wet_process', 'Anodic / cathodic electrocleaning step on the cleaning line.'),
'Etch': ('ETCH_STD', 'fa-flask', 'wet_process', 'Chemical etching to prepare the substrate.'),
'Final Inspection': ('FINAL_INSP_STD','fa-check-circle','final_inspect','Final visual + dimensional QA before packing.'),
'HCl Activation': ('HCL_ACT_STD', 'fa-flask', 'wet_process', 'HCl activation dip prior to strike or plate.'),
'Inspection': ('INSP_STD', 'fa-search', 'inspect', 'In-process inspection step.'),
'Masking': ('MASK_STD', 'fa-paint-brush', 'mask', 'Apply masking to areas that should not be plated.'),
'Nickel Strip (S-1)': ('NI_STRIP_S1', 'fa-undo', 'wet_process', 'Chemical strip of prior nickel deposit (rework path).'),
'Nickel Strip - Steel Line': ('NI_STRIP_SL','fa-undo', 'wet_process', 'Chemical strip on the steel line (rework path).'),
'Post-plate Inspection': ('POST_INSP_STD', 'fa-check-circle','inspect', 'Post-plate inspection — thickness sample + visual.'),
'Pre-Measurements': ('PRE_MEAS_STD', 'fa-tachometer', 'inspect', 'Pre-process dimensional measurements (FAIR start point).'),
'Racking': ('RACK_STD', 'fa-th', 'racking', 'Load parts onto racks for plating.'),
'Ready for Plating': ('GATE_PLATE', 'fa-flag', 'gating', 'Gating step — parts staged ready for the plating line.'),
'Ready for processing': ('GATE_PROC', 'fa-flag', 'gating', 'Generic gating step — parts staged ready for the next operation.'),
'Rinse': ('RINSE_STD', 'fa-tint', 'wet_process', 'Rinse step between wet-line operations.'),
'Shipping': ('SHIP_STD', 'fa-paper-plane', 'ship', 'Final shipping / hand-off to logistics.'),
'Soak Clean': ('SOAK_CLEAN_STD','fa-bathtub', 'wet_process', 'Soak cleaning step at the start of the wet line.'),
'Surface Activation': ('SURF_ACT_STD', 'fa-flask', 'wet_process', 'Surface activation dip prior to plate.'),
'Water Break Test': ('WBF_TEST_STD', 'fa-tint', 'wet_process', 'Water-break test for surface cleanliness.'),
'Zincate': ('ZINCATE_STD', 'fa-flask', 'wet_process', 'Zincate immersion on aluminium prior to plate.'),
}
```
New templates (XML data file additions, `noupdate="1"`):
| Name | Code | Kind | Why add |
|---|---|---|---|
| `Hot Water Porosity Test (A-15)` | `HWP_A15` | `inspect` | 7 recipe nodes use it — should be in the library |
| `Final Inspection / Packaging` | `FINAL_PKG_STD` | `final_inspect` | 3 recipe nodes use it; library has separate inspection + packaging but not the combined one |
**Files:**
- [`fusion_plating/data/fp_step_template_data.xml`](../../../fusion_plating/data/fp_step_template_data.xml) — 2 new template records
- Migration (Change 10) — TEMPLATE_BACKFILL loop, idempotent
### Change 10 — Unified migration
New file: [`fusion_plating/migrations/19.0.21.2.0/pre-migrate.py`](../../../fusion_plating/migrations/19.0.21.2.0/pre-migrate.py)
Pre-migrate runs BEFORE the `area_kind NOT NULL` constraint hits the
schema, so it fills values first.
```python
import logging
_logger = logging.getLogger(__name__)
KIND_TO_AREA = {
'other': 'plating', # catch-all default
'wet_process': 'plating',
'receiving': 'receiving',
'contract_review':'receiving',
'gating': 'receiving',
'racking': 'racking',
'derack': 'de_racking',
'mask': 'masking',
'demask': 'de_racking', # spec §D4
'cleaning': 'plating',
'electroclean': 'plating',
'etch': 'plating',
'rinse': 'plating',
'strike': 'plating',
'plate': 'plating',
'replenishment': 'plating',
'wbf_test': 'plating',
'dry': 'plating',
'bake': 'baking',
'inspect': 'inspection',
'final_inspect': 'inspection',
'hardness_test': 'inspection',
'adhesion_test': 'inspection',
'salt_spray': 'inspection',
'packaging': 'shipping',
'ship': 'shipping',
'blast': 'blasting',
'bead_blast': 'blasting',
'media_blast': 'blasting',
}
def migrate(cr, version):
# Phase 1 — seed area_kind on existing kinds BEFORE NOT NULL hits.
for code, area in KIND_TO_AREA.items():
cr.execute("""
UPDATE fp_step_kind SET area_kind = %s
WHERE code = %s AND (area_kind IS NULL OR area_kind = '')
""", (area, code))
# Anything still NULL: default to 'plating' to clear the constraint.
cr.execute("""
UPDATE fp_step_kind SET area_kind = 'plating'
WHERE area_kind IS NULL OR area_kind = ''
""")
_logger.info('[live-step-fix] kind.area_kind seeded')
# Phase 2 — activate the three inactive kinds we need (Cat A).
for code in ('derack', 'demask', 'gating'):
cr.execute("""
UPDATE fp_step_kind SET active = TRUE
WHERE code = %s AND active = FALSE
""", (code,))
_logger.info('[live-step-fix] derack/demask/gating activated')
```
New file: [`fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py`](../../../fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py)
Post-migrate runs AFTER schema sync, so all fields exist with values.
```python
import logging
_logger = logging.getLogger(__name__)
# Library template metadata backfill — copied from spec Change 9.
TEMPLATE_BACKFILL = { ... } # full dict per Change 9
# Recipe node patterns to repoint (Cat C).
NODE_REPOINTING = [
# (name_filter_sql, current_kind_code, new_kind_code, description)
("name = 'Blasting'", 'other', 'blast', 'Blasting → blast'),
("name ILIKE 'Ready %%'", None, 'gating', 'Ready For X → gating'),
("name ILIKE '%%De-Masking%%' OR name ILIKE '%%DeMasking%%'", 'mask', 'demask', 'De-Masking → demask'),
("name = 'Scheduling'", 'other', 'gating', 'Scheduling → gating'),
("name ILIKE '%%Nickel Strip%%'", 'plate', 'wet_process', 'Nickel Strip → wet_process'),
("name ILIKE '%%Pre-Measurement%%' OR name ILIKE '%%Check Sulfamate%%'", 'other', 'inspect', 'Pre-Meas/Check Sulfamate → inspect'),
]
def migrate(cr, version):
from odoo.api import Environment, SUPERUSER_ID
env = Environment(cr, SUPERUSER_ID, {})
# Phase 1 — template metadata backfill (Cat B). Idempotent.
Tpl = env['fp.step.template']
Kind = env['fp.step.kind']
fixed = 0
for name, (code, icon, kind_code, desc) in TEMPLATE_BACKFILL.items():
tpl = Tpl.search([('name', '=', name)], limit=1)
if not tpl:
continue
vals = {}
if not tpl.code:
vals['code'] = code
if not tpl.description or tpl.description in ('', '<p><br></p>'):
vals['description'] = f'<p>{desc}</p>'
if tpl.icon == 'fa-cog':
vals['icon'] = icon
kind = Kind.search([('code', '=', kind_code)], limit=1)
if kind and tpl.kind_id.code != kind_code:
vals['kind_id'] = kind.id
if vals:
tpl.write(vals)
fixed += 1
_logger.info('[live-step-fix] template backfill: %s templates updated', fixed)
# Phase 2 — recipe node repointing (Cat C). Pattern-driven SQL.
for filter_sql, cur_code, new_code, desc in NODE_REPOINTING:
params = []
sql = f"""
UPDATE fusion_plating_process_node n
SET kind_id = (SELECT id FROM fp_step_kind WHERE code = %s LIMIT 1)
FROM fp_step_kind k
WHERE n.kind_id = k.id
AND ({filter_sql})
"""
params.append(new_code)
if cur_code is not None:
sql += " AND k.code = %s"
params.append(cur_code)
sql += " AND k.code != %s"
params.append(new_code)
cr.execute(sql, params)
_logger.info('[live-step-fix] repointed %s nodes: %s',
cr.rowcount, desc)
# Phase 3 — recompute area_kind on all fp.job.step rows.
steps = env['fp.job.step'].search([])
steps._compute_area_kind()
steps.flush_recordset(['area_kind'])
_logger.info('[live-step-fix] recomputed area_kind on %s steps', len(steps))
# Phase 4 — recompute active_step_id + card_state on in-flight jobs.
jobs = env['fp.job'].search([('state', 'in', ('confirmed', 'in_progress'))])
jobs._compute_active_step_id()
jobs._compute_card_state()
jobs.flush_recordset(['active_step_id', 'card_state'])
_logger.info('[live-step-fix] recomputed jobs: %s', len(jobs))
```
Idempotent across the board: phase 1 only fills NULLs / fa-cog defaults;
phase 2 includes `AND k.code != %s` so re-running won't re-do already
correct rows; phases 3-4 are pure recomputes.
### Change 11 — Version bumps
| Module | From | To |
|---|---|---|
| `fusion_plating` | `19.0.21.1.3` | `19.0.21.2.0` (schema change on fp.step.kind + data file additions) |
| `fusion_plating_jobs` | `19.0.10.23.0` | `19.0.10.24.0` (compute change + migration) |
| `fusion_plating_shopfloor` | `19.0.33.1.2` | `19.0.33.1.3` (controller filter + comment) |
---
## What this approach replaces
| Dropped from the original (pre-restructure) spec | Why |
|---|---|
| `_RESOLVER_KIND_TO_AREA` translation dict | Kind self-declares its column — no translation needed |
| `_resolve_area_kind_from_name` helper | Kind taxonomy is authoritative; name resolution is unnecessary |
| `_STARTER_KIND_BY_NAME` extensions for column routing | The starter resolver is for `default_kind` seeding (Sub 12a library), not column routing — stays as-is for that purpose |
| Parenthetical stripping regex | Not needed when we read the kind directly |
| Backfill of `default_kind` on existing recipe nodes via name resolver | Recipe nodes already have `kind_id` populated by 19.0.20.6.0 pre-migrate |
---
## Test plan
### Manual smoke (on entech after deploy)
1. Open Shop Floor tablet/desktop — confirm the 7 done jobs are GONE from the board.
2. Plating → Configuration → Recipes & Steps → **Step Kind catalog** — confirm:
- `blast` exists, active, area_kind=`blasting`
- `derack`, `demask`, `gating` are now `active=True`, area_kinds correct
- Every kind has area_kind set
3. Plating → Configuration → Recipes & Steps → **Step Library** — confirm:
- All 38 templates now have a code, description, meaningful icon
- `Hot Water Porosity Test (A-15)` and `Final Inspection / Packaging` are listed
- "Blasting" is `kind=blast`, "De-Masking" is `kind=demask`, "Ready for ..." are `kind=gating`
4. Open the Simple Recipe Editor; click "+ Add new kind" — confirm area_kind picker is visible/required in the inline-create flow.
5. Create a fresh test job from any recipe (e.g. ENP-ALUM-BASIC):
a. Confirm it lands in Receiving column with `card_state='ready'`.
b. Walk through all steps — confirm column transitions follow area_kind sequence.
c. Mark job done → confirm card drops off the board.
6. Verify a step with `state='paused'` keeps the card at its column.
### Spot-check existing data
```sql
-- Every node should have a kind with area_kind set.
SELECT n.id, n.name, k.code, k.area_kind
FROM fusion_plating_process_node n
JOIN fp_step_kind k ON k.id = n.kind_id
WHERE k.area_kind IS NULL OR k.area_kind = '';
-- expected: 0 rows
-- Blasting nodes should now use blast kind.
SELECT k.code, COUNT(*) FROM fusion_plating_process_node n
JOIN fp_step_kind k ON k.id = n.kind_id
WHERE n.name = 'Blasting' GROUP BY k.code;
-- expected: all rows have k.code = 'blast'
-- Ready For X gating nodes.
SELECT k.code, COUNT(*) FROM fusion_plating_process_node n
JOIN fp_step_kind k ON k.id = n.kind_id
WHERE n.name ILIKE 'Ready %' GROUP BY k.code;
-- expected: all rows have k.code = 'gating'
-- De-Masking nodes use demask.
SELECT k.code, COUNT(*) FROM fusion_plating_process_node n
JOIN fp_step_kind k ON k.id = n.kind_id
WHERE n.name ILIKE '%De-Masking%' OR n.name ILIKE '%DeMasking%'
GROUP BY k.code;
-- expected: all rows have k.code = 'demask'
-- Template code coverage.
SELECT COUNT(*) FROM fp_step_template
WHERE active = TRUE AND (code IS NULL OR code = '');
-- expected: 0
```
### Automated battle test
New script: `fusion_plating_quality/scripts/bt_s24_between_steps.py` covering
the live-step priority chain end-to-end (see prior version of the spec for
full pseudocode — unchanged).
### Existing tests
Existing tests in `fusion_plating_shopfloor/tests/` and
`fusion_plating_jobs/tests/` may need updates for:
- The new `state` filter in `/fp/landing/plant_kanban`.
- The new `active_step_id` priority chain.
Re-run all `bt_s*.py` scripts to confirm no regressions in S1-S23.
---
## Roll-out
1. Implement Changes 1-11 in a single branch.
2. Local dev test (`docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating,fusion_plating_jobs,fusion_plating_shopfloor --stop-after-init`).
3. Deploy to entech using the standard `pct exec 111` flow. Pre-migrate seeds run automatically.
4. Verify on entech with manual smoke + SQL spot-checks.
5. Commit + push to GitHub.
---
## Non-goals (explicit)
- **Re-assigning historical steps to `work_centre_id`.** The 85+ steps with NULL `work_centre_id` stay that way. The kind→area_kind lookup gives them correct `area_kind` without needing a work_centre.
- **Recipe authoring UX changes** beyond the kind picker hint. Required-field enforcement on `kind_id` already exists.
- **Removing the "Other" kind.** Stays as a catch-all default mapped to `'plating'`.
- **Card_state precedence rework.** Rules 1-13 stay; only the edge-case fallback changes.
- **Mini-timeline rendering.** Separate compute (`mini_timeline_json`), out of scope.
- **Hidden-but-recent done jobs.** No "recent shipments" filter.
- **Subjective node re-classification.** "Post Plate Inspection" stays whatever the recipe author picked (`inspect` vs `final_inspect`). Only the unambiguous patterns in Change 10 phase 2 are auto-migrated.
- **process_type_id / material_callout backfill on templates.** Out of scope for this spec — those need recipe-author input per template.
---
## Files touched (summary)
| File | Change |
|---|---|
| `fusion_plating/models/fp_step_kind.py` | New `area_kind` Selection field (Change 5) |
| `fusion_plating/views/fp_step_kind_views.xml` | Add area_kind to form + list (Change 7) |
| `fusion_plating/controllers/simple_recipe_controller.py` | Include area_kind + label in kindOptions (Change 7) |
| `fusion_plating/static/src/xml/simple_recipe_editor.xml` | Kind picker shows "→ Column" suffix (Change 7) |
| `fusion_plating/data/fp_step_kind_data.xml` | New `step_kind_blast` record (Change 8) |
| `fusion_plating/data/fp_step_template_data.xml` | New `Hot Water Porosity Test` + `Final Inspection / Packaging` templates (Change 9) |
| `fusion_plating/migrations/19.0.21.2.0/pre-migrate.py` | NEW — seed area_kind, activate kinds (Change 10 phase 1-2) |
| `fusion_plating/__manifest__.py` | Version bump (Change 11) |
| `fusion_plating_jobs/models/fp_job.py` | Rewrite `_compute_active_step_id` (Change 1) + `_compute_card_state` edge case (Change 2) |
| `fusion_plating_jobs/models/fp_job_step.py` | Simplify `_compute_area_kind` (Change 6); drop `_STEP_KIND_TO_AREA` dict |
| `fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py` | NEW — template backfill + node repointing + recomputes (Change 10 phase 1-4) |
| `fusion_plating_jobs/__manifest__.py` | Version bump (Change 11) |
| `fusion_plating_shopfloor/controllers/plant_kanban.py` | Add state filter (Change 3) + comment (Change 4) |
| `fusion_plating_shopfloor/__manifest__.py` | Version bump (Change 11) |
| `fusion_plating_quality/scripts/bt_s24_between_steps.py` | NEW — battle test |
Estimated diff: ~400 lines added (most in the migration data tables), ~30 modified, ~50 deleted (the `_STEP_KIND_TO_AREA` dict goes away).