fix(fusion_plating): wet_process passthrough + per-clone unlink safety
Two follow-up fixes caught during the entech deploy of recipe cleanup:
1. RESOLVER_KIND_TO_ACTIVE_KIND was missing a self-pass entry for
'wet_process'. The new aliases added in 19.0.21.3.0 (Chemical
Conversion, Trivalent Chromate Conversion, Strip Process - AL,
Plug The Threaded Holes via mask) directly return 'wet_process'
from the resolver — without the passthrough they didn't translate
to any active kind and stayed as 'other'. Added 'wet_process':
'wet_process' so the migration's Phase 2 backfill catches them.
2. Migration 19.0.10.26.0 Phase 3 was using batch unlink
(clone_recipes.unlink()) which tripped a PostgreSQL FK cascade
ordering bug on entech ("insert or update on parent_id violates
FK ..." during the CASCADE chain). Rewrote Phase 3 to delete one
clone at a time with SAVEPOINT per clone — slower but immune to
the batching bug, and one failed clone doesn't roll back the
whole transaction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -362,6 +362,12 @@ RESOLVER_KIND_TO_ACTIVE_KIND = {
|
|||||||
'strike': 'wet_process',
|
'strike': 'wet_process',
|
||||||
'dry': 'wet_process',
|
'dry': 'wet_process',
|
||||||
'wbf_test': 'wet_process',
|
'wbf_test': 'wet_process',
|
||||||
|
'wet_process': 'wet_process', # the alias added in 19.0.21.3.0
|
||||||
|
# for "Strip Process - AL", "Chemical
|
||||||
|
# Conversion", "Trivalent Chromate
|
||||||
|
# Conversion" maps DIRECTLY to
|
||||||
|
# 'wet_process' — this passthrough
|
||||||
|
# entry lets those land correctly.
|
||||||
# 1:1 mappings (kind exists and is active)
|
# 1:1 mappings (kind exists and is active)
|
||||||
'contract_review': 'contract_review',
|
'contract_review': 'contract_review',
|
||||||
'mask': 'mask',
|
'mask': 'mask',
|
||||||
|
|||||||
@@ -153,30 +153,55 @@ def migrate(cr, version):
|
|||||||
# Identify by name pattern. The configurator names clones
|
# Identify by name pattern. The configurator names clones
|
||||||
# "BASE_NAME - PART_NUMBER Rev X" with an em-dash separator
|
# "BASE_NAME - PART_NUMBER Rev X" with an em-dash separator
|
||||||
# (U+2014). No base recipe uses em-dash in its name.
|
# (U+2014). No base recipe uses em-dash in its name.
|
||||||
|
#
|
||||||
|
# IMPORTANT: delete one-clone-at-a-time with savepoint per clone.
|
||||||
|
# Batch unlink (clone_recipes.unlink()) tripped a PostgreSQL FK
|
||||||
|
# cascade ordering bug on entech (insert-or-update on parent_id
|
||||||
|
# during the cascade chain). Per-clone unlink with intermediate
|
||||||
|
# cleanup avoids that path entirely and lets one bad clone fail
|
||||||
|
# without rolling back the others.
|
||||||
clone_recipes = Node.search([
|
clone_recipes = Node.search([
|
||||||
('node_type', '=', 'recipe'),
|
('node_type', '=', 'recipe'),
|
||||||
('name', 'ilike', '% — %'),
|
('name', 'ilike', '% — %'),
|
||||||
])
|
])
|
||||||
if clone_recipes:
|
if not clone_recipes:
|
||||||
clone_names = [c.name for c in clone_recipes]
|
|
||||||
_logger.info(
|
|
||||||
'[recipe-cleanup] Phase 3: deleting %s clone recipes: %s',
|
|
||||||
len(clone_recipes),
|
|
||||||
', '.join(clone_names[:10])
|
|
||||||
+ (' ...' if len(clone_names) > 10 else ''),
|
|
||||||
)
|
|
||||||
clone_recipes.unlink()
|
|
||||||
_logger.info(
|
|
||||||
'[recipe-cleanup] Phase 3: deleted %s clone recipes '
|
|
||||||
'(CASCADE removed their child nodes; FK SET NULL applied '
|
|
||||||
'to historical fp.job + fp.job.step references)',
|
|
||||||
len(clone_recipes),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'[recipe-cleanup] Phase 3: no clone recipes found '
|
'[recipe-cleanup] Phase 3: no clone recipes found '
|
||||||
'(already deleted on a prior run, or none exist)'
|
'(already deleted on a prior run, or none exist)'
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
_logger.info(
|
||||||
|
'[recipe-cleanup] Phase 3: deleting %s clone recipes one '
|
||||||
|
'at a time (per-clone savepoint)', len(clone_recipes),
|
||||||
|
)
|
||||||
|
deleted = 0
|
||||||
|
failed = []
|
||||||
|
for clone in clone_recipes:
|
||||||
|
cid, cname = clone.id, clone.name
|
||||||
|
cr.execute('SAVEPOINT delete_clone')
|
||||||
|
try:
|
||||||
|
clone.unlink()
|
||||||
|
cr.execute('RELEASE SAVEPOINT delete_clone')
|
||||||
|
deleted += 1
|
||||||
|
except Exception as e:
|
||||||
|
cr.execute('ROLLBACK TO SAVEPOINT delete_clone')
|
||||||
|
failed.append((cid, cname, type(e).__name__, str(e)[:120]))
|
||||||
|
_logger.warning(
|
||||||
|
'[recipe-cleanup] Phase 3: failed to delete '
|
||||||
|
'clone %s ("%s"): %s — continuing',
|
||||||
|
cid, cname, type(e).__name__,
|
||||||
|
)
|
||||||
|
_logger.info(
|
||||||
|
'[recipe-cleanup] Phase 3: deleted %s/%s clones '
|
||||||
|
'(%s failures retained for manual review)',
|
||||||
|
deleted, len(clone_recipes), len(failed),
|
||||||
|
)
|
||||||
|
if failed:
|
||||||
|
for cid, cname, errtype, errmsg in failed:
|
||||||
|
_logger.warning(
|
||||||
|
'[recipe-cleanup] Phase 3 leftover: id=%s name=%r '
|
||||||
|
'err=%s: %s', cid, cname, errtype, errmsg,
|
||||||
|
)
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase 4 - Recompute area_kind on all fp.job.step rows
|
# Phase 4 - Recompute area_kind on all fp.job.step rows
|
||||||
|
|||||||
Reference in New Issue
Block a user