From 8eb4b8dc6ca91f2dbbfdd2eab7dc6404ab16aeee Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 21:33:26 -0400 Subject: [PATCH] fix(fusion_accounting_followup): seeded levels + migration idempotency - test_create_minimal/negative_delay used sequence=1, which now collides with the seeded Friendly Reminder level. Use sequences 901/902. - migration backfill: search by name (not raw seq) for idempotency, allocate sequence as max(existing)+1 to avoid both seed clashes and within-batch collisions when Enterprise has duplicate sequence values. Made-with: Cursor --- .../models/fusion_migration_wizard.py | 37 +++++++++++-------- .../tests/test_fusion_followup_level.py | 5 ++- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/fusion_accounting_followup/models/fusion_migration_wizard.py b/fusion_accounting_followup/models/fusion_migration_wizard.py index b2bbea30..f9ceec36 100644 --- a/fusion_accounting_followup/models/fusion_migration_wizard.py +++ b/fusion_accounting_followup/models/fusion_migration_wizard.py @@ -37,31 +37,38 @@ class FusionMigrationWizard(models.TransientModel): result['errors'].append(f"Enterprise search failed: {e}") return result + # Pick a starting offset that doesn't clash with anything already in + # fusion_followup_level (seeded defaults at 1..3 plus any prior + # migration runs). We allocate a unique sequence per Enterprise line + # by max(existing) + 1, ensuring idempotency + within-batch uniqueness. + existing_max = max(FusionLevel.search([]).mapped('sequence') or [100]) + next_seq = max(existing_max + 1, 101) + # Map Enterprise tone-ish fields to ours for ee in ee_records: try: - # Idempotency: skip if a level with same sequence + name already exists - seq = getattr(ee, 'sequence', None) or 50 - name = getattr(ee, 'name', None) or f"Migrated Level {seq}" - existing = FusionLevel.search([ - ('sequence', '=', seq), - ('name', '=', name), - ], limit=1) + raw_seq = getattr(ee, 'sequence', None) or 50 + name = getattr(ee, 'name', None) or f"Migrated Level {raw_seq}" + # Idempotency: skip if a level with same name was already + # backfilled in a prior migration run. + existing = FusionLevel.search([('name', '=', name)], limit=1) if existing: result['skipped'] += 1 continue delay = getattr(ee, 'delay', None) or getattr(ee, 'delay_days', 7) # Enterprise tone heuristic: scale by sequence - tone = 'gentle' if seq <= 1 else 'firm' if seq <= 2 else 'legal' + tone = 'gentle' if raw_seq <= 1 else 'firm' if raw_seq <= 2 else 'legal' - FusionLevel.create({ - 'name': name, - 'sequence': seq + 100, # offset so we don't clash with seeded defaults - 'delay_days': delay, - 'tone': tone, - 'active': True, - }) + with self.env.cr.savepoint(): + FusionLevel.create({ + 'name': name, + 'sequence': next_seq, + 'delay_days': delay, + 'tone': tone, + 'active': True, + }) + next_seq += 1 result['created'] += 1 except Exception as e: result['errors'].append(f"Line {ee.id}: {e}") diff --git a/fusion_accounting_followup/tests/test_fusion_followup_level.py b/fusion_accounting_followup/tests/test_fusion_followup_level.py index 1bb0bcc3..4cde95e3 100644 --- a/fusion_accounting_followup/tests/test_fusion_followup_level.py +++ b/fusion_accounting_followup/tests/test_fusion_followup_level.py @@ -6,8 +6,9 @@ from odoo.tests import tagged class TestFusionFollowupLevel(TransactionCase): def test_create_minimal(self): + # Note: sequences 1-3 are reserved for seeded default levels. level = self.env['fusion.followup.level'].create({ - 'name': 'Reminder', 'sequence': 1, 'delay_days': 7, 'tone': 'gentle', + 'name': 'Reminder', 'sequence': 901, 'delay_days': 7, 'tone': 'gentle', }) self.assertEqual(level.name, 'Reminder') self.assertTrue(level.active) @@ -15,7 +16,7 @@ class TestFusionFollowupLevel(TransactionCase): def test_negative_delay_rejected(self): with self.assertRaises(Exception): self.env['fusion.followup.level'].create({ - 'name': 'Bad', 'sequence': 1, 'delay_days': -5, 'tone': 'gentle', + 'name': 'Bad', 'sequence': 902, 'delay_days': -5, 'tone': 'gentle', }) def test_duplicate_sequence_rejected(self):