Files
Odoo-Modules/fusion_accounting_bank_rec/tests/_factories.py
gsinghpal 068a654c2b
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled
fix(fusion_accounting_bank_rec): test factory adapts to V19 Community semantics
After Enterprise's account_accountant is uninstalled,
account.bank.statement.journal_id reverts to its V19 Community definition
\u2014 a read-only computed field derived from line_ids.journal_id. Direct
writes are silently dropped (which is what was happening: 55 tests
errored with 'null value in column journal_id' because the test's
statement had no journal, and the line factory was reading
statement.journal_id (False) and passing that to the line create).

Fix:
- make_bank_statement now bootstraps the statement with one zero-amount
  line carrying journal_id, so the computed journal_id resolves correctly.
- make_bank_line no longer routes journal through the statement \u2014
  journal_id is set directly on the line (which is V19 Community's
  intended path; lines can exist standalone without a statement).

This is a test-only change; runtime behaviour is unchanged. Real users
creating bank lines via the UI already use the correct path.

Made-with: Cursor
2026-04-20 00:52:02 -04:00

206 lines
7.5 KiB
Python

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