"""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. NOTE: in V19 Community, ``account.bank.statement.journal_id`` is a read-only computed field derived from ``line_ids.journal_id`` — direct writes are silently dropped. Enterprise's ``account_accountant`` used to override this to make it writable; without Enterprise we have to derive the journal from a line. We attach a single token line at create time (later removed/replaced by the test) to bootstrap the journal. """ journal = journal or make_bank_journal(env) return env['account.bank.statement'].create({ 'name': name, 'date': date_ or date.today(), 'line_ids': [(0, 0, { 'journal_id': journal.id, 'date': date_ or date.today(), 'payment_ref': 'Statement bootstrap line', 'amount': 0.0, })], }) 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 a journal (and optionally a statement) if not provided. In V19 Community, lines can exist standalone — a statement is not required. We create one only if the test explicitly passes ``statement=``. """ if statement and not journal: journal = statement.journal_id if not journal: journal = make_bank_journal(env) vals = { 'journal_id': journal.id, 'date': date_ or date.today(), 'payment_ref': memo, 'amount': amount, 'partner_id': partner.id if partner else False, } if statement: vals['statement_id'] = statement.id return env['account.bank.statement.line'].create(vals) # ============================================================ # 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)