test(fusion_accounting_bank_rec): test data factories for bank-rec testing
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
This commit is contained in:
@@ -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
|
||||
|
||||
185
fusion_accounting_bank_rec/tests/_factories.py
Normal file
185
fusion_accounting_bank_rec/tests/_factories.py
Normal file
@@ -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)
|
||||
74
fusion_accounting_bank_rec/tests/test_factories.py
Normal file
74
fusion_accounting_bank_rec/tests/test_factories.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user