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:
File diff suppressed because it is too large
Load Diff
@@ -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).
|
||||
Reference in New Issue
Block a user