- fp.step.kind.area_kind: drop tracking=True (model doesn't inherit mail.thread; tracking was a no-op emitting a startup warning). - Migration 19.0.10.25.0: anchor the De-Masking ILIKE so it doesn't wildcard-match "Ready For De-Masking" (which the earlier "Ready %" rule already routes to gating). Also drop the cur_code='mask' filter so the 4 De-Masking nodes still classified as 'other' get picked up on fresh re-runs too. Direct SQL applied the 4-row fix on entech (post-migrate doesn't re-run for already-applied versions); this commit keeps fresh installs and any future re-runs consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
9.4 KiB
Python
156 lines
9.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
"""19.0.10.25.0 — Template metadata backfill + recipe-node repointing.
|
|
|
|
Runs AFTER fusion_plating's pre-migrate 19.0.21.2.0 (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.
|
|
|
|
See docs/superpowers/specs/2026-05-24-shopfloor-live-step-fix-design.md.
|
|
"""
|
|
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'),
|
|
# De-Masking: anchored ILIKE (must start with "De-Masking" or
|
|
# "DeMasking") so we don't match "Ready For De-Masking" which the
|
|
# earlier 'Ready %' rule already moved to gating. cur_code=None
|
|
# catches both 'mask' (the 34 lazy/legacy ones) and 'other' (4
|
|
# older nodes never reclassified after the demask kind was added).
|
|
# The trailing AND k.code != %s safeguard skips already-correct rows.
|
|
("(n.name ILIKE 'De-Masking%%' OR n.name ILIKE 'DeMasking%%')", None, '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 ('', '<p><br></p>', '<p></p>'):
|
|
vals['description'] = '<p>%s</p>' % 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 on re-run.
|
|
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),
|
|
)
|