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
206 lines
7.5 KiB
Python
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)
|