"""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)