From fcecf9d925bac104789886d640b02632182ebf35 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 11:05:06 -0400 Subject: [PATCH] test(fusion_accounting_bank_rec): test data factories for bank-rec testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides make_bank_journal, make_bank_statement, make_bank_line, make_invoice, make_vendor_bill, make_suggestion, make_pattern, make_precedent, make_reconcileable_pair helpers used across the bank-rec test suite. Replaces the original plan's SQL-fixture capture with programmatic factories — same testing intent, simpler maintenance, no real Westin data baked into the repo. Note: the original plan called for 5 SQL fixtures captured from the local DB (westin_simple_match.sql, westin_partial_chain.sql, etc.). Those are replaced by factory-driven test creation in Task 19 — eliminates fragile hand-curated SQL while testing the same code paths. Made-with: Cursor --- fusion_accounting_bank_rec/tests/__init__.py | 1 + .../tests/_factories.py | 185 ++++++++++++++++++ .../tests/test_factories.py | 74 +++++++ 3 files changed, 260 insertions(+) create mode 100644 fusion_accounting_bank_rec/tests/_factories.py create mode 100644 fusion_accounting_bank_rec/tests/test_factories.py diff --git a/fusion_accounting_bank_rec/tests/__init__.py b/fusion_accounting_bank_rec/tests/__init__.py index 94e7ac87..480ab5d9 100644 --- a/fusion_accounting_bank_rec/tests/__init__.py +++ b/fusion_accounting_bank_rec/tests/__init__.py @@ -7,3 +7,4 @@ from . import test_pattern_extraction from . import test_confidence_scoring from . import test_reconcile_engine_unit from . import test_reconcile_engine_property +from . import test_factories diff --git a/fusion_accounting_bank_rec/tests/_factories.py b/fusion_accounting_bank_rec/tests/_factories.py new file mode 100644 index 00000000..8a2c0961 --- /dev/null +++ b/fusion_accounting_bank_rec/tests/_factories.py @@ -0,0 +1,185 @@ +"""Test data factories for fusion_accounting_bank_rec. + +Provides recordset builders for use across all test files. Sane defaults +let tests be readable: `make_bank_line(env, amount=100, partner=p)` instead +of 30 lines of recordset setup. + +These factories work against the real Odoo registry — they exercise the +same code paths as production. Each factory is idempotent in the sense +that calling it multiple times returns separate records. +""" + +from datetime import date, timedelta + +from odoo import fields + + +# ============================================================ +# Bank journal + statements +# ============================================================ + +def make_bank_journal(env, *, name='Test Bank', code=None): + """Create a bank journal. `code` defaults to first 5 chars of `name`.""" + code = code or name[:5].upper().replace(' ', '') + return env['account.journal'].create({ + 'name': name, + 'type': 'bank', + 'code': code, + }) + + +def make_bank_statement(env, *, journal=None, name='Test Statement', date_=None): + """Create a bank statement. Auto-creates a bank journal if not provided.""" + journal = journal or make_bank_journal(env) + return env['account.bank.statement'].create({ + 'name': name, + 'journal_id': journal.id, + 'date': date_ or date.today(), + }) + + +def make_bank_line(env, *, journal=None, statement=None, amount=100.00, + partner=None, memo='Test line', date_=None): + """Create a bank statement line. Creates statement if not provided. + + Most-common factory in tests. Defaults give a $100 line with no partner.""" + if not statement: + statement = make_bank_statement(env, journal=journal, date_=date_) + return env['account.bank.statement.line'].create({ + 'statement_id': statement.id, + 'journal_id': statement.journal_id.id, + 'date': date_ or date.today(), + 'payment_ref': memo, + 'amount': amount, + 'partner_id': partner.id if partner else False, + }) + + +# ============================================================ +# Invoices + journal items +# ============================================================ + +def _ensure_test_product(env): + """Get or create a service product suitable for invoice lines.""" + product = env['product.product'].search([('type', '=', 'service')], limit=1) + if not product: + product = env['product.product'].create({ + 'name': 'Fusion Test Service', + 'type': 'service', + }) + return product + + +def make_invoice(env, *, partner, amount=100.00, date_=None, currency=None, + product=None, posted=True): + """Create a customer invoice (out_invoice). Posted by default.""" + product = product or _ensure_test_product(env) + vals = { + 'move_type': 'out_invoice', + 'partner_id': partner.id, + 'invoice_date': date_ or date.today(), + 'invoice_line_ids': [(0, 0, { + 'product_id': product.id, + 'name': 'Test invoice line', + 'quantity': 1, + 'price_unit': amount, + })], + } + if currency: + vals['currency_id'] = currency.id + move = env['account.move'].create(vals) + if posted: + move.action_post() + return move + + +def make_vendor_bill(env, *, partner, amount=100.00, date_=None, currency=None, + product=None, posted=True): + """Create a vendor bill (in_invoice). Posted by default.""" + product = product or _ensure_test_product(env) + vals = { + 'move_type': 'in_invoice', + 'partner_id': partner.id, + 'invoice_date': date_ or date.today(), + 'invoice_line_ids': [(0, 0, { + 'product_id': product.id, + 'name': 'Test bill line', + 'quantity': 1, + 'price_unit': amount, + })], + } + if currency: + vals['currency_id'] = currency.id + move = env['account.move'].create(vals) + if posted: + move.action_post() + return move + + +# ============================================================ +# Suggestions + patterns + precedents (fusion-specific) +# ============================================================ + +def make_suggestion(env, *, statement_line, candidate_move_lines=None, + confidence=0.92, rank=1, reasoning='Test suggestion', + state='pending'): + """Create a fusion.reconcile.suggestion against a bank line.""" + candidate_ids = candidate_move_lines.ids if candidate_move_lines else [] + return env['fusion.reconcile.suggestion'].create({ + 'company_id': env.company.id, + 'statement_line_id': statement_line.id, + 'proposed_move_line_ids': [(6, 0, candidate_ids)], + 'confidence': confidence, + 'rank': rank, + 'reasoning': reasoning, + 'state': state, + }) + + +def make_pattern(env, *, partner, reconcile_count=10, pref_strategy='exact_amount', + typical_cadence_days=14.0, common_memo_tokens='RBC,ETF'): + """Create a fusion.reconcile.pattern for a partner.""" + return env['fusion.reconcile.pattern'].create({ + 'company_id': env.company.id, + 'partner_id': partner.id, + 'reconcile_count': reconcile_count, + 'pref_strategy': pref_strategy, + 'typical_cadence_days': typical_cadence_days, + 'common_memo_tokens': common_memo_tokens, + }) + + +def make_precedent(env, *, partner, amount=1847.50, days_ago=14, + memo_tokens='RBC,ETF,REF', count=1, source='manual'): + """Create a fusion.reconcile.precedent.""" + return env['fusion.reconcile.precedent'].create({ + 'company_id': env.company.id, + 'partner_id': partner.id, + 'amount': amount, + 'currency_id': env.company.currency_id.id, + 'date': date.today() - timedelta(days=days_ago), + 'memo_tokens': memo_tokens, + 'matched_move_line_count': count, + 'reconciled_at': fields.Datetime.now(), + 'source': source, + }) + + +# ============================================================ +# Convenience composite — bank line + matching invoice ready to reconcile +# ============================================================ + +def make_reconcileable_pair(env, *, amount=100.00, partner=None, date_=None): + """Create a bank line + a customer invoice with the same partner+amount. + Returns (bank_line, invoice_recv_lines) ready to pass to engine.reconcile_one(). + + Returns: + (bank_line, invoice_receivable_lines) tuple + """ + if not partner: + partner = env['res.partner'].create({'name': 'Reconcile Test Partner'}) + invoice = make_invoice(env, partner=partner, amount=amount, date_=date_) + bank_line = make_bank_line(env, amount=amount, partner=partner, date_=date_) + recv_lines = invoice.line_ids.filtered( + lambda l: l.account_id.account_type == 'asset_receivable') + return (bank_line, recv_lines) diff --git a/fusion_accounting_bank_rec/tests/test_factories.py b/fusion_accounting_bank_rec/tests/test_factories.py new file mode 100644 index 00000000..e044cf83 --- /dev/null +++ b/fusion_accounting_bank_rec/tests/test_factories.py @@ -0,0 +1,74 @@ +"""Smoke tests verifying the factories produce usable records. + +Not testing factory correctness exhaustively — just that each helper +returns a record of the expected type with the expected basic state.""" + +from odoo.tests.common import TransactionCase, tagged + +from . import _factories as f + + +@tagged('post_install', '-at_install') +class TestFactories(TransactionCase): + + def test_make_bank_journal(self): + journal = f.make_bank_journal(self.env) + self.assertEqual(journal._name, 'account.journal') + self.assertEqual(journal.type, 'bank') + + def test_make_bank_statement(self): + statement = f.make_bank_statement(self.env) + self.assertEqual(statement._name, 'account.bank.statement') + self.assertTrue(statement.journal_id) + + def test_make_bank_line(self): + line = f.make_bank_line(self.env, amount=250.00, memo='Smoke memo') + self.assertEqual(line._name, 'account.bank.statement.line') + self.assertEqual(line.amount, 250.00) + self.assertEqual(line.payment_ref, 'Smoke memo') + self.assertFalse(line.is_reconciled) + + def test_make_bank_line_with_partner(self): + partner = self.env['res.partner'].create({'name': 'Factory Partner'}) + line = f.make_bank_line(self.env, partner=partner, amount=500) + self.assertEqual(line.partner_id, partner) + + def test_make_invoice_posted(self): + partner = self.env['res.partner'].create({'name': 'Invoice Partner'}) + invoice = f.make_invoice(self.env, partner=partner, amount=300) + self.assertEqual(invoice._name, 'account.move') + self.assertEqual(invoice.move_type, 'out_invoice') + self.assertEqual(invoice.state, 'posted') + self.assertAlmostEqual(invoice.amount_total, 300, places=2) + + def test_make_vendor_bill_posted(self): + partner = self.env['res.partner'].create({'name': 'Vendor Partner'}) + bill = f.make_vendor_bill(self.env, partner=partner, amount=400) + self.assertEqual(bill.move_type, 'in_invoice') + self.assertEqual(bill.state, 'posted') + + def test_make_suggestion(self): + line = f.make_bank_line(self.env, amount=100) + sug = f.make_suggestion(self.env, statement_line=line, confidence=0.85) + self.assertEqual(sug._name, 'fusion.reconcile.suggestion') + self.assertEqual(sug.confidence, 0.85) + self.assertEqual(sug.state, 'pending') + + def test_make_pattern(self): + partner = self.env['res.partner'].create({'name': 'Pattern Partner'}) + pattern = f.make_pattern(self.env, partner=partner, reconcile_count=20) + self.assertEqual(pattern._name, 'fusion.reconcile.pattern') + self.assertEqual(pattern.reconcile_count, 20) + + def test_make_precedent(self): + partner = self.env['res.partner'].create({'name': 'Precedent Partner'}) + precedent = f.make_precedent(self.env, partner=partner, amount=999.99) + self.assertEqual(precedent._name, 'fusion.reconcile.precedent') + self.assertEqual(precedent.amount, 999.99) + self.assertEqual(precedent.source, 'manual') + + def test_make_reconcileable_pair(self): + bank_line, recv_lines = f.make_reconcileable_pair(self.env, amount=750) + self.assertEqual(bank_line.amount, 750.00) + self.assertGreater(len(recv_lines), 0) + self.assertAlmostEqual(sum(recv_lines.mapped('amount_residual')), 750, places=2)