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
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting — Bank Reconciliation',
|
'name': 'Fusion Accounting — Bank Reconciliation',
|
||||||
'version': '19.0.1.0.22',
|
'version': '19.0.1.0.23',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'sequence': 28,
|
'sequence': 28,
|
||||||
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
|
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
|
||||||
|
|||||||
@@ -43,7 +43,17 @@ def backfill_precedents(env, *, company_id=None, batch_size=500, limit=10000):
|
|||||||
Partial = env['account.partial.reconcile'].sudo()
|
Partial = env['account.partial.reconcile'].sudo()
|
||||||
Line = env['account.bank.statement.line'].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:
|
if company_id:
|
||||||
domain.append(('company_id', '=', company_id))
|
domain.append(('company_id', '=', company_id))
|
||||||
partials = Partial.search(domain, limit=limit, order='id asc')
|
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
|
created += 1
|
||||||
if created % batch_size == 0:
|
if created % batch_size == 0:
|
||||||
env.cr.commit()
|
if not in_test_mode:
|
||||||
|
env.cr.commit()
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"Backfill progress: %d created, %d skipped",
|
"Backfill progress: %d created, %d skipped",
|
||||||
created, skipped)
|
created, skipped)
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ from . import test_cron_methods
|
|||||||
from . import test_controller
|
from . import test_controller
|
||||||
from . import test_auto_reconcile_wizard
|
from . import test_auto_reconcile_wizard
|
||||||
from . import test_bulk_reconcile_wizard
|
from . import test_bulk_reconcile_wizard
|
||||||
|
from . import test_migration_round_trip
|
||||||
|
|||||||
115
fusion_accounting_bank_rec/tests/test_migration_round_trip.py
Normal file
115
fusion_accounting_bank_rec/tests/test_migration_round_trip.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user