# Shop Floor — Live Step + Kind/Library Cleanup Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Fix the Shop Floor plant kanban so cards land in the correct column based on each step's `area_kind`, drop done jobs from the board, expand the step-kind taxonomy with `blast`/`derack`/`demask`/`gating`, and backfill missing metadata on 30 library templates. **Architecture:** Make `fp.step.kind` authoritative for column routing by adding a required `area_kind` Selection field. Delete the hardcoded `_STEP_KIND_TO_AREA` dict. Pre-migrate seeds existing kinds; post-migrate backfills template metadata + repoints recipe nodes using unambiguous name patterns. All migrations are idempotent (re-running `-u` is safe). **Spec:** [docs/superpowers/specs/2026-05-24-shopfloor-live-step-fix-design.md](../specs/2026-05-24-shopfloor-live-step-fix-design.md) **Tech Stack:** Odoo 19, Python (api/orm/migrations), PostgreSQL, QWeb XML, OWL components. --- ## File Inventory (what each task touches) | Path | Responsibility | |---|---| | `fusion_plating/models/fp_step_kind.py` | Add `area_kind` Selection field | | `fusion_plating/views/fp_step_kind_views.xml` | Form + list views surface `area_kind` | | `fusion_plating/data/fp_step_kind_data.xml` | Add new `step_kind_blast` record | | `fusion_plating/data/fp_step_template_data.xml` | Add Hot Water Porosity + Final Inspection / Packaging templates | | `fusion_plating/controllers/simple_recipe_controller.py` | Include `area_kind` in `kindOptions` payload | | `fusion_plating/static/src/xml/simple_recipe_editor.xml` | Kind picker shows "→ Column" suffix | | `fusion_plating/migrations/19.0.21.2.0/pre-migrate.py` | NEW — seed `area_kind`, activate `derack`/`demask`/`gating` | | `fusion_plating/__manifest__.py` | Version bump to `19.0.21.2.0` | | `fusion_plating_jobs/models/fp_job.py` | `_compute_active_step_id` priority chain + `_compute_card_state` edge case | | `fusion_plating_jobs/models/fp_job_step.py` | Simplify `_compute_area_kind`; delete `_STEP_KIND_TO_AREA` | | `fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py` | NEW — template backfill + node repointing + recomputes | | `fusion_plating_jobs/__manifest__.py` | Version bump to `19.0.10.24.0` | | `fusion_plating_shopfloor/controllers/plant_kanban.py` | State filter + comment update | | `fusion_plating_shopfloor/__manifest__.py` | Version bump to `19.0.33.1.3` | | `fusion_plating_quality/scripts/bt_s24_between_steps.py` | NEW — battle test | --- ## Task 1: Add `area_kind` field on `fp.step.kind` **Files:** - Modify: `fusion_plating/models/fp_step_kind.py` - [ ] **Step 1: Add the field definition** In [`fusion_plating/models/fp_step_kind.py`](../../fusion_plating/models/fp_step_kind.py), inside the `FpStepKind` class, add the field right after the `description` field (around line 31): ```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. Step kinds drive ' 'routing automatically — picking a kind tells the system both ' 'what gates fire AND where the card lives.', ) ``` - [ ] **Step 2: Load test deferred to Task 7** The `required=True` will fail on existing rows until pre-migrate seeds them. Don't try to load fusion_plating yet — the pre-migrate (Task 7) must land first. --- ## Task 2: Update Step Kind form + list views **Files:** - Modify: `fusion_plating/views/fp_step_kind_views.xml` - [ ] **Step 1: Update form view** In [`fusion_plating/views/fp_step_kind_views.xml`](../../fusion_plating/views/fp_step_kind_views.xml), find the form view's `` and add `area_kind` to the main field group: ```xml ``` - [ ] **Step 2: Update list view** In the same file, find the list view. Add `area_kind` after `name`: ```xml ``` - [ ] **Step 3: No commit yet — grouped with Tasks 1, 3, 4, 5, 7 under one fusion_plating commit.** --- ## Task 3: Add `step_kind_blast` data record + seed area_kind on existing kinds **Files:** - Modify: `fusion_plating/data/fp_step_kind_data.xml` - [ ] **Step 1: Add the new blast kind** In [`fusion_plating/data/fp_step_kind_data.xml`](../../fusion_plating/data/fp_step_kind_data.xml), inside the "ACTIVE KINDS" block. After the `step_kind_mask` record (sequence=40), add: ```xml blast Blasting / Media Blast 35 fa-bullseye blasting ``` - [ ] **Step 2: Add `` to each existing active kind record** For each active kind record in the file, add the area_kind line. The mapping (from spec Change 10 `KIND_TO_AREA`): | code | area_kind | |---|---| | other | plating | | wet_process | plating | | receiving | receiving | | contract_review | receiving | | racking | racking | | mask | masking | | cleaning | plating | | electroclean | plating | | etch | plating | | rinse | plating | | strike | plating | | plate | plating | | replenishment | plating | | wbf_test | plating | | dry | plating | | bake | baking | | demask | de_racking | | derack | de_racking | | inspect | inspection | | hardness_test | inspection | | adhesion_test | inspection | | salt_spray | inspection | | final_inspect | inspection | | packaging | shipping | | ship | shipping | | gating | receiving | Example for `step_kind_other`: ```xml other Other 5 fa-circle-o plating ``` Note: the file is wrapped in ``, so this only affects FRESH installs. Existing installs (like entech) are handled by the pre-migrate in Task 7. - [ ] **Step 3: No commit yet — grouped with Task 7.** --- ## Task 4: Add new templates to data file **Files:** - Modify: `fusion_plating/data/fp_step_template_data.xml` - [ ] **Step 1: Add Hot Water Porosity Test template** At the bottom of [`fusion_plating/data/fp_step_template_data.xml`](../../fusion_plating/data/fp_step_template_data.xml), inside the existing `` block: ```xml Hot Water Porosity Test (A-15) HWP_A15 10 fa-tint <p>Hot-water porosity test for plated samples. Verify continuity of the deposit across the test panel; record any porosity sites.</p> ``` - [ ] **Step 2: Add Final Inspection / Packaging template** Right after, add: ```xml Final Inspection / Packaging FINAL_PKG_STD 10 fa-check-circle <p>Combined final visual + dimensional inspection followed by packaging into the customer&rsquo;s original boxes for shipment.</p> ``` - [ ] **Step 3: No commit yet — grouped with Task 7.** --- ## Task 5: Update Simple Editor kind picker UI **Files:** - Modify: `fusion_plating/controllers/simple_recipe_controller.py` - Modify: `fusion_plating/static/src/xml/simple_recipe_editor.xml` - [ ] **Step 1: Include `area_kind` + label in the kindOptions payload** Open [`fusion_plating/controllers/simple_recipe_controller.py`](../../fusion_plating/controllers/simple_recipe_controller.py). Search for `kindOptions` or the function that builds the kind list. Look for a list comprehension over `env['fp.step.kind'].search([])`. In the per-kind dict, add two new keys: ```python { 'id': k.id, 'code': k.code, 'name': k.name, 'icon': k.icon, 'sequence': k.sequence, # NEW — for the picker UI hint (spec Change 7) 'area_kind': k.area_kind, 'area_kind_label': dict(k._fields['area_kind'].selection).get(k.area_kind, ''), } ``` (The existing payload shape may differ — add the two new keys to whatever the actual dict is.) - [ ] **Step 2: Update the picker option in XML** In [`fusion_plating/static/src/xml/simple_recipe_editor.xml`](../../fusion_plating/static/src/xml/simple_recipe_editor.xml) around line 510, change: ```xml ``` to: ```xml ``` - [ ] **Step 3: No commit yet — grouped with Task 7.** --- ## Task 6: (Removed — covered by Task 2) The form-level Step Kind catalog UI is already handled by Task 2. Skipping. --- ## Task 7: Write `fusion_plating` pre-migrate + version bump + commit Phase 1 **Files:** - Create: `fusion_plating/migrations/19.0.21.2.0/pre-migrate.py` - Modify: `fusion_plating/__manifest__.py` - [ ] **Step 1: Create the migration directory** ```bash mkdir -p fusion_plating/migrations/19.0.21.2.0 ``` - [ ] **Step 2: Write the pre-migrate file** Create [`fusion_plating/migrations/19.0.21.2.0/pre-migrate.py`](../../fusion_plating/migrations/19.0.21.2.0/pre-migrate.py): ```python # -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) """19.0.21.2.0 — Shop Floor live-step + kind taxonomy. Seeds fp.step.kind.area_kind on existing kinds BEFORE the NOT NULL constraint added by Change 5 hits the schema. Also activates the three inactive kinds (derack/demask/gating) needed for the full area_kind taxonomy. Idempotent: only fills NULLs / inactive rows. """ import logging _logger = logging.getLogger(__name__) KIND_TO_AREA = { 'other': 'plating', 'wet_process': 'plating', 'receiving': 'receiving', 'contract_review': 'receiving', 'gating': 'receiving', 'racking': 'racking', 'derack': 'de_racking', 'mask': 'masking', 'demask': 'de_racking', '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 — Pre-create the column (NULL-permitting) so we can # seed it BEFORE Odoo's schema sync tries to enforce NOT NULL. cr.execute(""" ALTER TABLE fp_step_kind ADD COLUMN IF NOT EXISTS area_kind VARCHAR """) # Phase 2 — Seed area_kind on existing kinds where it's NULL. 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), ) _logger.info('[live-step-fix] kind.area_kind seeded for known codes') # Phase 3 — Fallback: user-created kinds not in our seed map → # 'plating'. Clears the NOT NULL constraint for any leftover row. cr.execute( "UPDATE fp_step_kind SET area_kind = 'plating' " "WHERE area_kind IS NULL OR area_kind = ''" ) _logger.info( '[live-step-fix] %s unknown kinds defaulted to plating', cr.rowcount, ) # Phase 4 — Activate 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') ``` - [ ] **Step 3: Bump manifest version** In [`fusion_plating/__manifest__.py`](../../fusion_plating/__manifest__.py), change: ```python 'version': '19.0.21.1.3', ``` to: ```python 'version': '19.0.21.2.0', ``` - [ ] **Step 4: Local dev test — upgrade fusion_plating only** ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating --stop-after-init 2>&1 | tail -40 ``` Expected: clean upgrade with log line `[live-step-fix] kind.area_kind seeded for known codes`, no tracebacks. - [ ] **Step 5: Verify the field landed** ```bash docker exec odoo-dev-db psql -U odoo -d fusion-dev -c \ "SELECT code, area_kind, active FROM fp_step_kind ORDER BY sequence, id;" ``` Expected: every row has `area_kind` set; `derack`, `demask`, `gating` rows show `active=t`. A `blast` row exists with `area_kind='blasting'`. - [ ] **Step 6: Commit Phase 1** ```bash git add fusion_plating/models/fp_step_kind.py \ fusion_plating/views/fp_step_kind_views.xml \ fusion_plating/data/fp_step_kind_data.xml \ fusion_plating/data/fp_step_template_data.xml \ fusion_plating/controllers/simple_recipe_controller.py \ fusion_plating/static/src/xml/simple_recipe_editor.xml \ fusion_plating/migrations/19.0.21.2.0/pre-migrate.py \ fusion_plating/__manifest__.py git commit -m "$(cat <<'EOF' feat(fusion_plating): kind.area_kind drives Shop Floor column routing Add required area_kind Selection to fp.step.kind so each kind self-declares which plant-view column its steps belong in. Replaces the hardcoded _STEP_KIND_TO_AREA dict (removed in fp_job_step.py in a follow-up commit). - New `blast` kind for the Blasting column - Activate `derack` / `demask` / `gating` (were dropped in 19.0.20.6.0) - Step Kind form + list views surface area_kind - Simple Editor kind picker shows "→ Column" suffix - Add Hot Water Porosity Test + Final Inspection/Packaging templates - Pre-migrate seeds area_kind on existing kinds before NOT NULL hits Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 8: Rewrite `_compute_active_step_id` priority chain **Files:** - Modify: `fusion_plating_jobs/models/fp_job.py:385-391` - [ ] **Step 1: Replace the compute** In [`fusion_plating_jobs/models/fp_job.py`](../../fusion_plating_jobs/models/fp_job.py), find `_compute_active_step_id` (around line 386). Replace the entire method with: ```python @api.depends('step_ids.state', 'step_ids.sequence') def _compute_active_step_id(self): """Pick the "live" step — first match by priority then sequence. Priority order: in_progress > paused > ready > first pending in_progress is the most informative (someone is actively working on it). 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 for an operator. The first pending after a done step is the "next gate" — where the card visually waits between steps. Returns False only when every step is `done` (job finished) or when there are no steps at all (recipe not assigned). See spec 2026-05-24-shopfloor-live-step-fix-design.md Change 1. """ PRIORITY_STATES = ('in_progress', 'paused', 'ready', 'pending') for job in self: ordered = job.step_ids.sorted('sequence') live = job.env['fp.job.step'] for state in PRIORITY_STATES: live = ordered.filtered(lambda s: s.state == state) if live: break job.active_step_id = live[:1].id if live else False ``` - [ ] **Step 2: No commit yet — grouped with Task 12.** --- ## Task 9: Fix `_compute_card_state` edge case **Files:** - Modify: `fusion_plating_jobs/models/fp_job.py:261-267` - [ ] **Step 1: Replace the edge-case branch** In the same file, find `_compute_card_state` (around line 257). Replace the first `if not job.active_step_id:` block with: ```python if not job.active_step_id: # Edge: no live step. # - job.state='done' → 'done' (defensive — done jobs are filtered # off the board upstream, but the field still needs a value). # - confirmed + parts not yet received → 'no_parts'. # - else → 'ready' (job awaiting work, no steps yet OR recipe # not assigned). if job.state == 'done': job.card_state = 'done' elif (job.state == 'confirmed' and job._fp_inbound_not_received()): job.card_state = 'no_parts' else: job.card_state = 'ready' continue ``` - [ ] **Step 2: No commit yet — grouped with Task 12.** --- ## Task 10: Simplify `_compute_area_kind`; delete `_STEP_KIND_TO_AREA` **Files:** - Modify: `fusion_plating_jobs/models/fp_job_step.py` - [ ] **Step 1: Delete the `_STEP_KIND_TO_AREA` module-level dict** In [`fusion_plating_jobs/models/fp_job_step.py`](../../fusion_plating_jobs/models/fp_job_step.py), remove the block from line 20 (`# Mapping from fp.process.node.default_kind → fp.work.centre.area_kind.`) through line 73 (the closing `}` of `_STEP_KIND_TO_AREA`). Total ~54 lines deleted. - [ ] **Step 2: Replace the compute** Find `_compute_area_kind` (around line 178). Replace with: ```python @api.depends( 'work_centre_id.area_kind', 'recipe_node_id.kind_id.area_kind', ) def _compute_area_kind(self): """Resolve the plant-view column this step belongs in. Priority chain: 1. work_centre.area_kind (explicit operator setup wins) 2. recipe_node.kind_id.area_kind (kind taxonomy authoritative) 3. catch-all 'plating' (data integrity issue if we land here) The legacy _STEP_KIND_TO_AREA dict was removed — fp.step.kind now self-declares its area_kind, so the kind taxonomy IS the source of truth. See spec 2026-05-24-shopfloor-live-step-fix-design.md. """ for step in self: # 1. Explicit work_centre wins if step.work_centre_id and step.work_centre_id.area_kind: step.area_kind = step.work_centre_id.area_kind continue # 2. Kind taxonomy 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 — only reached for orphaned steps (no work_centre # AND no recipe_node). step.area_kind = 'plating' ``` - [ ] **Step 3: Search for any leftover `_STEP_KIND_TO_AREA` references** ```bash grep -rn "_STEP_KIND_TO_AREA" --include='*.py' . ``` Expected: zero results. If any show up (tests, scripts), update them — most likely they just import the dict and check membership, which can be removed. - [ ] **Step 4: No commit yet — grouped with Task 12.** --- ## Task 11: Add board state filter + comment update **Files:** - Modify: `fusion_plating_shopfloor/controllers/plant_kanban.py` - Modify: `fusion_plating_shopfloor/__manifest__.py` - [ ] **Step 1: Add state filter to the fp.job search** In [`fusion_plating_shopfloor/controllers/plant_kanban.py`](../../fusion_plating_shopfloor/controllers/plant_kanban.py), find where the controller searches `fp.job`. Look for `env['fp.job'].search(` in the main payload-building function (around the `_render_card` / column-building section). Update the search domain so done + cancelled jobs drop off: ```python # Defect 4 fix: done + cancelled jobs drop off the live board. # They stay reachable via smart buttons, history views, and the # backend Plating Jobs list. See spec 2026-05-24-shopfloor-live-step-fix-design.md. jobs = env['fp.job'].search([ ('state', 'in', ('confirmed', 'in_progress')), # ...keep any existing domain conditions... ]) ``` (Adapt the exact patch to the actual function — there may already be a domain you need to extend with the state filter.) - [ ] **Step 2: Update `_resolve_card_area` docstring** In the same file, find `_resolve_card_area` (around line 161). Replace the docstring with: ```python def _resolve_card_area(job): """Pick the column a card lives in. Active-step area_kind wins. With the live-step priority chain (see fp.job._compute_active_step_id), active_step_id is False only when the job has NO steps at all (recipe not assigned) OR every step is done. Done jobs are filtered off the board upstream, so this fallback fires only for truly orphaned cards. """ if job.active_step_id and job.active_step_id.area_kind: return job.active_step_id.area_kind # Orphan fallback — represents a data integrity issue, not a # normal state. Cards here have NO steps assigned at all. return 'receiving' ``` - [ ] **Step 3: Bump fusion_plating_shopfloor manifest version** In [`fusion_plating_shopfloor/__manifest__.py`](../../fusion_plating_shopfloor/__manifest__.py): ```python 'version': '19.0.33.1.2', ``` to: ```python 'version': '19.0.33.1.3', ``` - [ ] **Step 4: No commit yet — grouped with Task 12.** --- ## Task 12: Write post-migrate + battle test + commit Phase 2 **Files:** - Create: `fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py` - Modify: `fusion_plating_jobs/__manifest__.py` - Create: `fusion_plating_quality/scripts/bt_s24_between_steps.py` - [ ] **Step 1: Create the migration directory** ```bash mkdir -p fusion_plating_jobs/migrations/19.0.10.24.0 ``` - [ ] **Step 2: Write the post-migrate** Create [`fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py`](../../fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py): ```python # -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) """19.0.10.24.0 — Template metadata backfill + recipe-node repointing. Runs AFTER fusion_plating's pre-migrate (which seeds kind.area_kind and activates derack/demask/gating). At this point: - All kinds have area_kind set. - blast / derack / demask / gating exist and are active. - XML data files have loaded (new templates exist). This migration: 1. Backfills code / description / icon / kind_id on the 30 library templates seeded without metadata. 2. Repoints existing recipe nodes from wrong kinds to correct ones using unambiguous name patterns. 3. Recomputes area_kind on all fp.job.step rows. 4. Recomputes active_step_id + card_state on in-flight jobs. All phases idempotent — re-running -u is safe. """ import logging from odoo.api import Environment, SUPERUSER_ID _logger = logging.getLogger(__name__) # (name : (code, icon, kind_code, description_snippet)) TEMPLATE_BACKFILL = { '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.'), } # (filter_sql, current_kind_code, new_kind_code, description) # current_kind_code=None means "any kind that isn't the target" NODE_REPOINTING = [ ("n.name = 'Blasting'", 'other', 'blast', 'Blasting -> blast'), ("n.name ILIKE 'Ready %%'", None, 'gating', 'Ready For X -> gating'), ("n.name ILIKE '%%De-Masking%%' OR n.name ILIKE '%%DeMasking%%'", 'mask', 'demask', 'De-Masking -> demask'), ("n.name = 'Scheduling'", 'other', 'gating', 'Scheduling -> gating'), ("n.name ILIKE '%%Nickel Strip%%'", 'plate', 'wet_process', 'Nickel Strip -> wet_process'), ("n.name ILIKE '%%Pre-Measurement%%' OR n.name ILIKE '%%Check Sulfamate%%'", 'other', 'inspect', 'Pre-Meas/Check Sulfamate -> inspect'), ] def migrate(cr, version): env = Environment(cr, SUPERUSER_ID, {}) # Phase 1 — Template metadata backfill. Idempotent: only fills # NULL/empty fields, doesn't overwrite admin edits. Tpl = env['fp.step.template'] Kind = env['fp.step.kind'] fixed_tpl = 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 cur_desc = (tpl.description or '').strip() if cur_desc in ('', '


', '

'): vals['description'] = '

%s

' % 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_tpl += 1 _logger.info( '[live-step-fix] template metadata backfilled: %s templates updated', fixed_tpl, ) # Phase 2 — Recipe node repointing. Idempotent: AND k.code != %s # ensures already-correct rows are skipped. for filter_sql, cur_code, new_code, desc in NODE_REPOINTING: params = [new_code] sql = ( "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 + ")" ) 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 every fp.job.step row. steps = env['fp.job.step'].search([]) if steps: 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')), ]) if jobs: jobs._compute_active_step_id() jobs._compute_card_state() jobs.flush_recordset(['active_step_id', 'card_state']) _logger.info( '[live-step-fix] recomputed active_step_id + card_state on %s jobs', len(jobs), ) ``` - [ ] **Step 3: Bump fusion_plating_jobs manifest version** In [`fusion_plating_jobs/__manifest__.py`](../../fusion_plating_jobs/__manifest__.py): ```python 'version': '19.0.10.23.0', ``` to: ```python 'version': '19.0.10.24.0', ``` - [ ] **Step 4: Write the battle test** Create [`fusion_plating_quality/scripts/bt_s24_between_steps.py`](../../fusion_plating_quality/scripts/bt_s24_between_steps.py): ```python # -*- coding: utf-8 -*- """Battle test S24 — Live step priority chain + board state filter. Run end-to-end via odoo shell with stdin redirection: ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \\ \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" \\ < /mnt/extra-addons/custom/fusion_plating_quality/scripts/bt_s24_between_steps.py'" Asserts: 1. Job between steps (one done, next pending) has live active_step_id pointing at the next pending step, NOT False. 2. Card column resolves to that pending step's area_kind, NOT receiving. 3. Paused steps still count as active. 4. state='done' jobs are excluded from the live-board search domain. """ import logging _logger = logging.getLogger(__name__) def _resolve_card_area(job): """Mirror of plant_kanban._resolve_card_area for test purposes.""" if job.active_step_id and job.active_step_id.area_kind: return job.active_step_id.area_kind return 'receiving' def run(): partner = env['res.partner'].search([('customer_rank', '>', 0)], limit=1) if not partner: raise AssertionError('No customer partner found — seed test data first') recipe = env['fusion.plating.process.node'].search([ ('node_type', '=', 'recipe'), ('child_ids', '!=', False), ], limit=1) if not recipe: raise AssertionError('No recipe found — seed test data first') job = env['fp.job'].create({ 'partner_id': partner.id, 'recipe_id': recipe.id, 'qty': 1, }) job._fp_generate_steps_from_recipe() steps = job.step_ids.sorted('sequence') assert len(steps) >= 3, 'Need at least 3 steps for the test' # === Phase A — between-step assertion === s1 = steps[0] s2 = steps[1] s1.button_start() s1.button_finish() job.invalidate_recordset(['active_step_id', 'card_state']) assert job.active_step_id.id == s2.id, ( 'Expected active_step_id = %s (next pending), got %s' % (s2.id, job.active_step_id.id) ) assert _resolve_card_area(job) == s2.area_kind, ( 'Card column should match s2.area_kind=%s, got %s' % ( s2.area_kind, _resolve_card_area(job), ) ) _logger.info('[bt_s24] Phase A OK — between-step routing correct') # === Phase B — paused step assertion === s2.button_start() s2.button_pause('lunch break') job.invalidate_recordset(['active_step_id', 'card_state']) assert job.active_step_id.id == s2.id, ( 'Paused step should remain the live step, got %s' % job.active_step_id.id ) _logger.info('[bt_s24] Phase B OK — paused step stays live') # === Phase C — done job filter === for s in steps: if s.state != 'done': if s.state == 'paused': s.button_resume() if s.state != 'in_progress': s.button_start() s.button_finish() job.with_context( fp_skip_step_gate=True, fp_skip_qty_reconcile=True, fp_skip_bake_gate=True, ).button_mark_done() assert job.state == 'done' jobs_on_board = env['fp.job'].search([ ('state', 'in', ('confirmed', 'in_progress')), ]) assert job.id not in jobs_on_board.ids, ( 'Done job %s should be filtered off board' % job.id ) _logger.info('[bt_s24] Phase C OK — done jobs filtered off board') _logger.info('[bt_s24] ALL ASSERTIONS PASSED') run() ``` - [ ] **Step 5: Commit Phase 2** ```bash git add fusion_plating_jobs/models/fp_job.py \ fusion_plating_jobs/models/fp_job_step.py \ fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py \ fusion_plating_jobs/__manifest__.py \ fusion_plating_shopfloor/controllers/plant_kanban.py \ fusion_plating_shopfloor/__manifest__.py \ fusion_plating_quality/scripts/bt_s24_between_steps.py git commit -m "$(cat <<'EOF' feat(jobs+shopfloor): live-step priority chain + done-job filter Fix the Shop Floor plant kanban so cards land in the right column: - fp.job._compute_active_step_id walks priority chain (in_progress > paused > ready > pending), not just in_progress - fp.job._compute_card_state edge case respects job.state='done' - fp.job.step._compute_area_kind reads kind.area_kind directly; legacy _STEP_KIND_TO_AREA dict removed - /fp/landing/plant_kanban filters out done/cancelled jobs Migration backfills template metadata (codes, descriptions, icons, kind_id) on 30 unfinished library templates and repoints recipe nodes for 6 unambiguous name patterns (Blasting -> blast, Ready For X -> gating, De-Masking -> demask, etc.). Battle test bt_s24_between_steps.py covers between-step routing, paused step lifecycle, and done-job board filter. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Task 13: Local dev test — end-to-end - [ ] **Step 1: Upgrade all 3 modules in dev** ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating,fusion_plating_jobs,fusion_plating_shopfloor --stop-after-init 2>&1 | tail -50 ``` Expected log lines: - `[live-step-fix] kind.area_kind seeded for known codes` - `[live-step-fix] derack/demask/gating activated` - `[live-step-fix] template metadata backfilled: N templates updated` - `[live-step-fix] repointed M nodes: ...` - `[live-step-fix] recomputed area_kind on X steps` - `[live-step-fix] recomputed active_step_id + card_state on Y jobs` No tracebacks. - [ ] **Step 2: SQL spot-checks** ```bash docker exec odoo-dev-db psql -U odoo -d fusion-dev -c \ "SELECT code, area_kind, active FROM fp_step_kind WHERE active = TRUE ORDER BY sequence;" ``` Expected: every active kind has area_kind set; blast/derack/demask/gating all active. ```bash docker exec odoo-dev-db psql -U odoo -d fusion-dev -c \ "SELECT COUNT(*) FROM fp_step_template WHERE active = TRUE AND (code IS NULL OR code = '');" ``` Expected: 0 (every active template has a code). ```bash docker exec odoo-dev-db psql -U odoo -d fusion-dev -c \ "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: rows with `k.code = 'blast'`. - [ ] **Step 3: Run battle test on dev DB** ```bash docker exec -i odoo-dev-app odoo shell -d fusion-dev --no-http < fusion_plating_quality/scripts/bt_s24_between_steps.py 2>&1 | tail -30 ``` Expected: `[bt_s24] ALL ASSERTIONS PASSED` at the end. If any assertion fires, fix the underlying bug (don't loosen the assertion) and re-run. --- ## Task 14: Deploy to entech - [ ] **Step 1: Fetch + check for concurrent Cursor commits** ```bash git fetch origin git status git log origin/main..HEAD --oneline git log HEAD..origin/main --oneline ``` If origin/main has commits we don't have → STOP. Rebase or pull first. (Per concurrent-cursor memory.) - [ ] **Step 2: Copy files to entech** Loop over each touched file and `cat | ssh pve-worker5 ...`: ```bash for f in \ fusion_plating/models/fp_step_kind.py \ fusion_plating/views/fp_step_kind_views.xml \ fusion_plating/data/fp_step_kind_data.xml \ fusion_plating/data/fp_step_template_data.xml \ fusion_plating/controllers/simple_recipe_controller.py \ fusion_plating/static/src/xml/simple_recipe_editor.xml \ fusion_plating/migrations/19.0.21.2.0/pre-migrate.py \ fusion_plating/__manifest__.py \ fusion_plating_jobs/models/fp_job.py \ fusion_plating_jobs/models/fp_job_step.py \ fusion_plating_jobs/migrations/19.0.10.24.0/post-migrate.py \ fusion_plating_jobs/__manifest__.py \ fusion_plating_shopfloor/controllers/plant_kanban.py \ fusion_plating_shopfloor/__manifest__.py \ fusion_plating_quality/scripts/bt_s24_between_steps.py; do cat "$f" | ssh pve-worker5 "pct exec 111 -- bash -c \"mkdir -p \\\$(dirname /mnt/extra-addons/custom/$f) && cat > /mnt/extra-addons/custom/$f\"" done ``` - [ ] **Step 3: Upgrade modules + restart** ```bash ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating,fusion_plating_jobs,fusion_plating_shopfloor --stop-after-init\" 2>&1 | tail -50 && systemctl start odoo && sleep 2 && systemctl is-active odoo'" ``` Expected: same log lines as Task 13 Step 1 + `active` at the end. - [ ] **Step 4: SQL spot-checks on entech** ```bash ssh pve-worker5 "pct exec 111 -- bash -c 'echo \"SELECT code, area_kind, active FROM fp_step_kind WHERE active = TRUE ORDER BY sequence;\" | sudo -u postgres psql -d admin'" ``` Expected: 14+ active kinds, all with area_kind. ```bash ssh pve-worker5 "pct exec 111 -- bash -c 'echo \"SELECT k.code, COUNT(*) FROM fp_job_step s JOIN fusion_plating_process_node n ON n.id = s.recipe_node_id JOIN fp_step_kind k ON k.id = n.kind_id WHERE n.name = '\\''Blasting'\\'' GROUP BY k.code;\" | sudo -u postgres psql -d admin'" ``` Expected: rows with `k.code = 'blast'`. - [ ] **Step 5: Manual smoke on the live UI** Open the Shop Floor in a browser. Verify: 1. The 7 done jobs are GONE from the board (the original symptom). 2. Plating → Configuration → Recipes & Steps → Step Kind catalog — every kind has a "Shop Floor Column" value. 3. Step Library — every active template has a code, description, meaningful icon. 4. Simple Editor → drop into a recipe → kind picker shows "Masking — Masking column" etc. - [ ] **Step 6: Run battle test on entech** ```bash ssh pve-worker5 "pct exec 111 -- bash -c 'su - odoo -s /bin/bash -c \"/usr/bin/odoo shell -c /etc/odoo/odoo.conf -d admin --no-http\" < /mnt/extra-addons/custom/fusion_plating_quality/scripts/bt_s24_between_steps.py'" 2>&1 | tail -30 ``` Expected: `[bt_s24] ALL ASSERTIONS PASSED`. --- ## Task 15: Push commits to remote - [ ] **Step 1: Final fetch before push** ```bash git fetch origin git status ``` If origin/main has moved → rebase first. - [ ] **Step 2: Push** ```bash git push origin main ``` --- ## Rollback If anything goes sideways on entech: ```bash # Restore previous file content from a known-good commit ssh pve-worker5 "pct exec 111 -- bash -c 'cd /mnt/extra-addons/custom && find . -path \"*/fusion_plating*\" -newer /tmp/marker -print'" # Then revert via re-copy of the prior commit's files using the same loop as Task 14 Step 2 but from an earlier git checkout. # Force the modules into to-upgrade state and restart: ssh pve-worker5 "pct exec 111 -- bash -c 'echo \"UPDATE ir_module_module SET state = '\\''to upgrade'\\'' WHERE name IN ('\\''fusion_plating'\\'','\\''fusion_plating_jobs'\\'','\\''fusion_plating_shopfloor'\\'');\" | sudo -u postgres psql -d admin'" ssh pve-worker5 "pct exec 111 -- systemctl restart odoo" ``` (Pre-/post-migrate scripts are idempotent so partial rollback + retry is safe.)