feat(fusion_plating): extend resolver + auto-classify hook on process node
Resolver (fp_resolve_step_kind) extensions: - New aliases: blasting/bead blast/media blast variants, adhesion testing, corrosion testing, lab testing, strip process, chemical conversion, trivalent chromate, plug the threaded holes, air dry, desmut, soak clean, cleaner, nickel strike/strip - Parenthetical suffix stripping - "Masking (If Required)" resolves through "masking", "Incoming Inspection (Standard)" through "incoming inspection", "Trivalent Chromate Conversion (A-14 / A)" through "trivalent chromate conversion" - New RESOLVER_KIND_TO_ACTIVE_KIND map translates the resolver's vocabulary (cleaning/electroclean/etch/rinse/strike/dry/wbf_test -> wet_process) so the resolver output lands on active kinds only Auto-classify hook on fusion.plating.process.node: - _fp_autoclassify_kind() upgrades kind_id when current is 'other' AND name resolves via the resolver. Idempotent - never overrides a non-'other' kind. Skip via context flag fp_skip_kind_autoclassify - Wired into create() and write() (only fires when name or kind_id changed on write) - Side-effects: recipe duplication via copy() auto-corrects newly copied nodes; Simple/Tree editor authoring auto-classifies as soon as the name is saved Spec: docs/superpowers/specs/2026-05-24-recipe-cleanup-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from . import controllers
|
from . import controllers
|
||||||
from . import models
|
from . import models
|
||||||
@@ -282,6 +283,38 @@ _STARTER_KIND_BY_NAME = {
|
|||||||
'ready for post-plate inspection': 'gating',
|
'ready for post-plate inspection': 'gating',
|
||||||
'ready for final inspection': 'gating',
|
'ready for final inspection': 'gating',
|
||||||
'ready for shipping': 'gating',
|
'ready for shipping': 'gating',
|
||||||
|
# 2026-05-24 — Recipe cleanup additions (spec
|
||||||
|
# 2026-05-24-recipe-cleanup-design.md). Covers names the existing
|
||||||
|
# resolver didn't know that turned up in the entech recipes audit.
|
||||||
|
# Blasting variants
|
||||||
|
'blasting': 'blast',
|
||||||
|
'bead blast': 'blast',
|
||||||
|
'bead blasting': 'blast',
|
||||||
|
'media blast': 'blast',
|
||||||
|
'media blasting': 'blast',
|
||||||
|
# Inspection variants
|
||||||
|
'adhesion test coupon': 'inspect',
|
||||||
|
'adhesion testing': 'inspect',
|
||||||
|
'corrosion testing': 'inspect',
|
||||||
|
'lab testing': 'inspect',
|
||||||
|
'check sulfamate nickel area': 'inspect',
|
||||||
|
'pre-measurements': 'inspect',
|
||||||
|
'pre measurements': 'inspect',
|
||||||
|
'hot water porosity': 'inspect',
|
||||||
|
# Strip / chemical conversion / plugging (wet line)
|
||||||
|
'strip process': 'wet_process',
|
||||||
|
'strip process - al': 'wet_process',
|
||||||
|
'nickel strip - aluminum line': 'wet_process',
|
||||||
|
'chemical conversion': 'wet_process',
|
||||||
|
'trivalent chromate conversion': 'wet_process',
|
||||||
|
'plug the threaded holes': 'mask',
|
||||||
|
# Misc wet-line variants seen on entech recipes
|
||||||
|
'air dry': 'dry',
|
||||||
|
'desmut': 'etch',
|
||||||
|
'soak clean': 'cleaning',
|
||||||
|
'cleaner': 'cleaning',
|
||||||
|
'nickel strike': 'plate',
|
||||||
|
'nickel strip': 'plate',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -290,6 +323,9 @@ def fp_resolve_step_kind(name):
|
|||||||
case. Used by both the seeder and the migration backfill so we don't
|
case. Used by both the seeder and the migration backfill so we don't
|
||||||
have two slightly-different lookup paths.
|
have two slightly-different lookup paths.
|
||||||
|
|
||||||
|
Handles parenthetical suffixes like "(Standard)", "(If Required)",
|
||||||
|
"(A-14 / A)" by stripping them and re-trying the lookup.
|
||||||
|
|
||||||
Returns the kind str or None when no match.
|
Returns the kind str or None when no match.
|
||||||
"""
|
"""
|
||||||
if not name:
|
if not name:
|
||||||
@@ -297,6 +333,12 @@ def fp_resolve_step_kind(name):
|
|||||||
key = name.strip().lower()
|
key = name.strip().lower()
|
||||||
if key in _STARTER_KIND_BY_NAME:
|
if key in _STARTER_KIND_BY_NAME:
|
||||||
return _STARTER_KIND_BY_NAME[key]
|
return _STARTER_KIND_BY_NAME[key]
|
||||||
|
# Parenthetical strip — "Masking (If Required)" → "masking",
|
||||||
|
# "Incoming Inspection (Standard)" → "incoming inspection",
|
||||||
|
# "Trivalent Chromate Conversion (A-14 / A)" → "trivalent chromate conversion".
|
||||||
|
bare = re.sub(r'\s*\([^)]*\)\s*', ' ', key).strip()
|
||||||
|
if bare and bare != key and bare in _STARTER_KIND_BY_NAME:
|
||||||
|
return _STARTER_KIND_BY_NAME[bare]
|
||||||
# Gating "Ready for / Ready For" prefix — anything starting with that
|
# Gating "Ready for / Ready For" prefix — anything starting with that
|
||||||
# is a gating node regardless of the destination step name.
|
# is a gating node regardless of the destination step name.
|
||||||
if key.startswith('ready for ') or key.startswith('ready '):
|
if key.startswith('ready for ') or key.startswith('ready '):
|
||||||
@@ -304,6 +346,38 @@ def fp_resolve_step_kind(name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Translates resolver kind output to the active fp.step.kind.code values.
|
||||||
|
# The resolver still returns the OLD vocabulary (cleaning, electroclean,
|
||||||
|
# etch, rinse, strike, dry, wbf_test) which were deactivated in
|
||||||
|
# 19.0.20.6.0 — those roll up to the active wet_process kind. Other
|
||||||
|
# codes pass through 1:1. Used by the auto-classify hook on
|
||||||
|
# fusion.plating.process.node + the recipe-cleanup migration
|
||||||
|
# (fusion_plating_jobs 19.0.10.26.0).
|
||||||
|
RESOLVER_KIND_TO_ACTIVE_KIND = {
|
||||||
|
# Wet-line kinds → wet_process (active rollup)
|
||||||
|
'cleaning': 'wet_process',
|
||||||
|
'electroclean': 'wet_process',
|
||||||
|
'etch': 'wet_process',
|
||||||
|
'rinse': 'wet_process',
|
||||||
|
'strike': 'wet_process',
|
||||||
|
'dry': 'wet_process',
|
||||||
|
'wbf_test': 'wet_process',
|
||||||
|
# 1:1 mappings (kind exists and is active)
|
||||||
|
'contract_review': 'contract_review',
|
||||||
|
'mask': 'mask',
|
||||||
|
'racking': 'racking',
|
||||||
|
'plate': 'plate',
|
||||||
|
'bake': 'bake',
|
||||||
|
'derack': 'derack',
|
||||||
|
'demask': 'demask',
|
||||||
|
'inspect': 'inspect',
|
||||||
|
'final_inspect': 'final_inspect',
|
||||||
|
'ship': 'ship',
|
||||||
|
'gating': 'gating',
|
||||||
|
'blast': 'blast',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _seed_step_library_if_empty(env):
|
def _seed_step_library_if_empty(env):
|
||||||
"""Sub 12a — seed fp.step.template starter library.
|
"""Sub 12a — seed fp.step.template starter library.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating',
|
'name': 'Fusion Plating',
|
||||||
'version': '19.0.21.2.0',
|
'version': '19.0.21.3.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -784,6 +784,62 @@ class FpProcessNode(models.Model):
|
|||||||
return self.action_open_simple_editor()
|
return self.action_open_simple_editor()
|
||||||
return self.action_open_tree_editor()
|
return self.action_open_tree_editor()
|
||||||
|
|
||||||
|
# ---- Auto-classify kind from name (2026-05-24, spec
|
||||||
|
# docs/superpowers/specs/2026-05-24-recipe-cleanup-design.md) -------
|
||||||
|
# Safety net: when a node's kind is the catch-all 'other' AND its
|
||||||
|
# name resolves via fp_resolve_step_kind(), upgrade kind_id to the
|
||||||
|
# resolved active kind. Runs on create() and on write() when name
|
||||||
|
# or kind_id changes. Prevents recipe authoring + recipe
|
||||||
|
# duplication from silently leaving nodes as 'other' (which then
|
||||||
|
# routes them to the wrong Shop Floor column).
|
||||||
|
#
|
||||||
|
# Skip with context flag fp_skip_kind_autoclassify=True for admin
|
||||||
|
# workflows that need to keep kind=other despite a known name.
|
||||||
|
|
||||||
|
def _fp_autoclassify_kind(self):
|
||||||
|
"""Upgrade kind_id when current is 'other' and name resolves."""
|
||||||
|
if self.env.context.get('fp_skip_kind_autoclassify'):
|
||||||
|
return
|
||||||
|
from odoo.addons.fusion_plating import (
|
||||||
|
fp_resolve_step_kind,
|
||||||
|
RESOLVER_KIND_TO_ACTIVE_KIND,
|
||||||
|
)
|
||||||
|
Kind = self.env['fp.step.kind']
|
||||||
|
other = Kind.search([('code', '=', 'other')], limit=1)
|
||||||
|
if not other:
|
||||||
|
return
|
||||||
|
# Cache active-kind ids by code so we don't re-search per row.
|
||||||
|
kind_by_code = {}
|
||||||
|
for node in self:
|
||||||
|
if not node.name or node.kind_id != other:
|
||||||
|
continue
|
||||||
|
resolver_code = fp_resolve_step_kind(node.name)
|
||||||
|
if not resolver_code:
|
||||||
|
continue
|
||||||
|
target_code = RESOLVER_KIND_TO_ACTIVE_KIND.get(resolver_code)
|
||||||
|
if not target_code:
|
||||||
|
continue
|
||||||
|
if target_code not in kind_by_code:
|
||||||
|
tgt = Kind.search([('code', '=', target_code)], limit=1)
|
||||||
|
kind_by_code[target_code] = tgt.id if tgt else False
|
||||||
|
target_id = kind_by_code[target_code]
|
||||||
|
if target_id:
|
||||||
|
node.with_context(
|
||||||
|
fp_skip_kind_autoclassify=True,
|
||||||
|
).write({'kind_id': target_id})
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
nodes = super().create(vals_list)
|
||||||
|
nodes._fp_autoclassify_kind()
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if 'name' in vals or 'kind_id' in vals:
|
||||||
|
self._fp_autoclassify_kind()
|
||||||
|
return res
|
||||||
|
|
||||||
# ---- Copy (deep-duplicate) -----------------------------------------------
|
# ---- Copy (deep-duplicate) -----------------------------------------------
|
||||||
|
|
||||||
def copy(self, default=None):
|
def copy(self, default=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user