From e1fedf72311a57369abc1617e5167b0e4ca99093 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 18:08:35 -0400 Subject: [PATCH] fix(fusion_plating): wet_process passthrough + per-clone unlink safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- fusion_plating/fusion_plating/__init__.py | 6 ++ .../migrations/19.0.10.26.0/post-migrate.py | 57 +++++++++++++------ 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/fusion_plating/fusion_plating/__init__.py b/fusion_plating/fusion_plating/__init__.py index 274fe4f8..db9673ad 100644 --- a/fusion_plating/fusion_plating/__init__.py +++ b/fusion_plating/fusion_plating/__init__.py @@ -362,6 +362,12 @@ RESOLVER_KIND_TO_ACTIVE_KIND = { 'strike': 'wet_process', 'dry': '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) 'contract_review': 'contract_review', 'mask': 'mask', diff --git a/fusion_plating/fusion_plating_jobs/migrations/19.0.10.26.0/post-migrate.py b/fusion_plating/fusion_plating_jobs/migrations/19.0.10.26.0/post-migrate.py index 622821a1..2a2e8340 100644 --- a/fusion_plating/fusion_plating_jobs/migrations/19.0.10.26.0/post-migrate.py +++ b/fusion_plating/fusion_plating_jobs/migrations/19.0.10.26.0/post-migrate.py @@ -153,30 +153,55 @@ def migrate(cr, version): # Identify by name pattern. The configurator names clones # "BASE_NAME - PART_NUMBER Rev X" with an em-dash separator # (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([ ('node_type', '=', 'recipe'), ('name', 'ilike', '% — %'), ]) - if 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: + if not clone_recipes: _logger.info( '[recipe-cleanup] Phase 3: no clone recipes found ' '(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