# 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
{desc}
' 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).