From ffc029a875247d6b2dfcf51a1686d3b492e38d86 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 13:33:29 -0400 Subject: [PATCH] test(fusion_accounting_bank_rec): migration round-trip for bootstrap step Verifies the bank_rec_bootstrap migration step (a) creates precedents from existing partial.reconcile rows, (b) is idempotent on re-run, and (c) refreshes the MV without erroring. Three TransactionCase tests: - test_bootstrap_creates_precedents_from_existing_reconciles seeds two reconciles via the engine, wipes the auto-recorded precedents, then asserts the bootstrap produces source='backfill' precedents. - test_bootstrap_step_idempotent runs the bootstrap twice and asserts the second pass creates zero new precedents. - test_bootstrap_refreshes_mv_without_error runs the bootstrap on a clean partner and asserts no exception is raised and the result dict reports MV + pattern refresh outcomes. Implementation fixes uncovered by these tests: - precedent_backfill.backfill_precedents now pre-filters account.partial.reconcile to rows that touch a bank statement line on either side. Previously it walked every partial in the DB; on the westin-v19 dev DB that's 16k rows and the default limit=10000 missed the newest test fixtures (highest IDs). - backfill skips the periodic env.cr.commit() when running under a TestCursor, since committing inside a test breaks the rollback. Test count: 139 -> 142. Made-with: Cursor --- fusion_accounting_bank_rec/__manifest__.py | 2 +- .../services/precedent_backfill.py | 15 ++- fusion_accounting_bank_rec/tests/__init__.py | 1 + .../tests/test_migration_round_trip.py | 115 ++++++++++++++++++ 4 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 fusion_accounting_bank_rec/tests/test_migration_round_trip.py diff --git a/fusion_accounting_bank_rec/__manifest__.py b/fusion_accounting_bank_rec/__manifest__.py index 581ca267..035caee5 100644 --- a/fusion_accounting_bank_rec/__manifest__.py +++ b/fusion_accounting_bank_rec/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting — Bank Reconciliation', - 'version': '19.0.1.0.22', + 'version': '19.0.1.0.23', 'category': 'Accounting/Accounting', 'sequence': 28, 'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.', diff --git a/fusion_accounting_bank_rec/services/precedent_backfill.py b/fusion_accounting_bank_rec/services/precedent_backfill.py index 32b50c33..21a4864c 100644 --- a/fusion_accounting_bank_rec/services/precedent_backfill.py +++ b/fusion_accounting_bank_rec/services/precedent_backfill.py @@ -43,7 +43,17 @@ def backfill_precedents(env, *, company_id=None, batch_size=500, limit=10000): Partial = env['account.partial.reconcile'].sudo() Line = env['account.bank.statement.line'].sudo() - domain = [] + in_test_mode = env.cr.__class__.__name__ == 'TestCursor' + + # Pre-filter to partials that touch a bank statement line on either side. + # In a real DB we typically have 10x more invoice<->payment partials than + # bank-rec partials; filtering here keeps the loop bounded and makes the + # default limit reflect "real" candidates rather than every partial ever. + domain = [ + '|', + ('debit_move_id.move_id.statement_line_id', '!=', False), + ('credit_move_id.move_id.statement_line_id', '!=', False), + ] if company_id: domain.append(('company_id', '=', company_id)) partials = Partial.search(domain, limit=limit, order='id asc') @@ -91,7 +101,8 @@ def backfill_precedents(env, *, company_id=None, batch_size=500, limit=10000): }) created += 1 if created % batch_size == 0: - env.cr.commit() + if not in_test_mode: + env.cr.commit() _logger.info( "Backfill progress: %d created, %d skipped", created, skipped) diff --git a/fusion_accounting_bank_rec/tests/__init__.py b/fusion_accounting_bank_rec/tests/__init__.py index 3e21e927..b7b1532e 100644 --- a/fusion_accounting_bank_rec/tests/__init__.py +++ b/fusion_accounting_bank_rec/tests/__init__.py @@ -18,3 +18,4 @@ from . import test_cron_methods from . import test_controller from . import test_auto_reconcile_wizard from . import test_bulk_reconcile_wizard +from . import test_migration_round_trip diff --git a/fusion_accounting_bank_rec/tests/test_migration_round_trip.py b/fusion_accounting_bank_rec/tests/test_migration_round_trip.py new file mode 100644 index 00000000..50767cbe --- /dev/null +++ b/fusion_accounting_bank_rec/tests/test_migration_round_trip.py @@ -0,0 +1,115 @@ +"""Migration round-trip: bootstrap step backfills precedents from +existing account.partial.reconcile rows. + +Exercises Task 39's _bank_rec_bootstrap_step end-to-end: +1. Set up a bank-line / invoice reconciliation via the engine. This + creates an account.partial.reconcile row. +2. Wipe the auto-recorded fusion.reconcile.precedent rows so the + backfill has work to do. +3. Run wizard._bank_rec_bootstrap_step(). +4. Assert at least one precedent was created with source='backfill', + the wizard reports successful pattern + MV refresh, and that a + second run is a no-op (idempotent). +""" + +from odoo.tests.common import TransactionCase, tagged + +from . import _factories as f + + +@tagged('post_install', '-at_install') +class TestMigrationRoundTrip(TransactionCase): + + def setUp(self): + super().setUp() + self.partner = self.env['res.partner'].create({ + 'name': 'Migration Round-Trip Partner', + }) + self.journal = f.make_bank_journal( + self.env, name='Migration Bank', code='MIGBK') + self.statement = f.make_bank_statement( + self.env, journal=self.journal, name='Migration Statement') + + def _seed_partial_reconciles(self, amounts): + """Create one reconciled bank-line/invoice pair per amount, reusing + a single bank journal so we don't violate the + account_journal_code_company_uniq constraint. + + Each call here produces one account.partial.reconcile row. + Returns the partial recordset. + """ + Engine = self.env['fusion.reconcile.engine'] + partials = self.env['account.partial.reconcile'] + for amount in amounts: + invoice = f.make_invoice( + self.env, partner=self.partner, amount=amount) + recv_lines = invoice.line_ids.filtered( + lambda l: l.account_id.account_type == 'asset_receivable') + bank_line = f.make_bank_line( + self.env, statement=self.statement, amount=amount, + partner=self.partner) + result = Engine.reconcile_one( + bank_line, against_lines=recv_lines) + partials |= self.env['account.partial.reconcile'].browse( + result['partial_ids']) + return partials + + def _wipe_precedents(self): + self.env['fusion.reconcile.precedent'].search([ + ('partner_id', '=', self.partner.id), + ]).unlink() + + def test_bootstrap_creates_precedents_from_existing_reconciles(self): + partials = self._seed_partial_reconciles([125.00, 275.00]) + self.assertTrue(partials, + "Test setup should produce account.partial.reconcile rows") + + self._wipe_precedents() + before_backfill = self.env['fusion.reconcile.precedent'].search_count([ + ('partner_id', '=', self.partner.id), + ('source', '=', 'backfill'), + ]) + self.assertEqual(before_backfill, 0, + "Precondition: no backfill precedents should exist before bootstrap") + + wizard = self.env['fusion.migration.wizard'].create({}) + result = wizard._bank_rec_bootstrap_step() + + self.assertEqual(result['step'], 'bank_rec_bootstrap') + self.assertGreaterEqual(result['precedents_created'], 1, + "Bootstrap should backfill at least one precedent from the " + "partial.reconcile rows produced in setUp") + self.assertTrue(result['mv_refreshed'], + "Bootstrap should report successful MV refresh") + + after_backfill = self.env['fusion.reconcile.precedent'].search_count([ + ('partner_id', '=', self.partner.id), + ('source', '=', 'backfill'), + ]) + self.assertGreaterEqual(after_backfill, 1, + "At least one source='backfill' precedent should exist post-bootstrap") + + def test_bootstrap_step_idempotent(self): + self._seed_partial_reconciles([411.00]) + self._wipe_precedents() + + wizard = self.env['fusion.migration.wizard'].create({}) + result1 = wizard._bank_rec_bootstrap_step() + created_first_run = result1['precedents_created'] + self.assertGreaterEqual(created_first_run, 1) + + result2 = wizard._bank_rec_bootstrap_step() + self.assertEqual(result2['precedents_created'], 0, + "Second bootstrap should create zero precedents (idempotent)") + self.assertGreaterEqual(result2['precedents_skipped'], created_first_run, + "Second bootstrap should skip at least what the first one created") + + def test_bootstrap_refreshes_mv_without_error(self): + """The bootstrap call must not raise even when there's nothing to do.""" + wizard = self.env['fusion.migration.wizard'].create({}) + try: + result = wizard._bank_rec_bootstrap_step() + except Exception as e: # noqa: BLE001 + self.fail(f"Bootstrap raised: {e}") + self.assertIn('mv_refreshed', result) + self.assertIn('patterns_refreshed', result)