Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-24-shopfloor-live-step-fix-design.md
gsinghpal 2285c9def1 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>
2026-05-24 17:13:58 -04:00

30 KiB

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

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

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

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.

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

Change 2 — _compute_card_state edge case

Replace the buggy "no active step → contract_review" fallback with:

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

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

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

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.

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

Change 6 — _compute_area_kind priority chain

Simplify fp.job.step._compute_area_kind:

@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

Change 7 — Step Kind UI surfaces area_kind

  • Form view (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) — option label becomes "Masking — Masking column" so authors see the routing at pick time. Requires updating kindOptions payload in 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:

<!-- 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":

# 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.

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:

Change 10 — Unified migration

New file: 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.

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

Post-migrate runs AFTER schema sync, so all fields exist with values.

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

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