# 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 blast Blasting / Media Blast 35 fa-bullseye blasting ``` 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 ('', '


'): vals['description'] = f'

{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).