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.
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -282,6 +283,38 @@ _STARTER_KIND_BY_NAME = {
|
||||
'ready for post-plate inspection': 'gating',
|
||||
'ready for final inspection': '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
|
||||
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.
|
||||
"""
|
||||
if not name:
|
||||
@@ -297,6 +333,12 @@ def fp_resolve_step_kind(name):
|
||||
key = name.strip().lower()
|
||||
if key in _STARTER_KIND_BY_NAME:
|
||||
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
|
||||
# is a gating node regardless of the destination step name.
|
||||
if key.startswith('ready for ') or key.startswith('ready '):
|
||||
@@ -304,6 +346,38 @@ def fp_resolve_step_kind(name):
|
||||
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):
|
||||
"""Sub 12a — seed fp.step.template starter library.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.21.2.0',
|
||||
'version': '19.0.21.3.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -784,6 +784,62 @@ class FpProcessNode(models.Model):
|
||||
return self.action_open_simple_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) -----------------------------------------------
|
||||
|
||||
def copy(self, default=None):
|
||||
|
||||
Reference in New Issue
Block a user