Files
Odoo-Modules/fusion_accounting_ai/tests/test_data_adapters.py
gsinghpal e983a370aa refactor(fusion_accounting_ai): route reporting tools through ReportsAdapter
Task 13 Step 9 of phase-0 plan.

All Enterprise account.report entry points now go through ReportsAdapter:

  - get_profit_loss → ReportsAdapter.run_report(account_reports.profit_and_loss)
  - get_balance_sheet → ReportsAdapter.run_report(account_reports.balance_sheet)
  - get_trial_balance → ReportsAdapter.run_report(...) with Community fallback
    to the existing trial_balance() account.move.line aggregation
  - get_cash_flow → ReportsAdapter.run_report(account_reports.cash_flow_statement)
  - compare_periods → two run_report() calls
  - export_report → ReportsAdapter.export_report() (PDF/XLSX via Enterprise)

ReportsAdapter extended with:

  - run_report(ref_id, date_from, date_to, limit) — generic Enterprise
    account.report wrapper. Enterprise mode returns {report_name, lines};
    Community mode returns a graceful error dict pointing users at the
    raw trial_balance() aggregation tool.
  - export_report(ref_id, fmt, date_from, date_to) — Enterprise-only PDF/XLSX
    export; Community mode returns an error dict.

Pure-Community tools in reporting.py (get_invoicing_summary, get_billing_summary,
get_collections_summary) unchanged — they aggregate account.move /
account.payment directly which is tri-mode safe.

3 new data-adapter tests added for run_report happy/error paths and
export_report shape. Total: 12 tests, all passing on westin-v19.

Made-with: Cursor
2026-04-18 23:33:54 -04:00

147 lines
6.0 KiB
Python

from odoo.tests.common import TransactionCase, tagged
from odoo.addons.fusion_accounting_ai.services.data_adapters.base import (
DataAdapter, AdapterMode,
)
from odoo.addons.fusion_accounting_ai.services.data_adapters import get_adapter
@tagged('post_install', '-at_install')
class TestDataAdapterBase(TransactionCase):
"""Verify the data adapter base class chooses the correct backend."""
def test_adapter_mode_pure_community(self):
"""With no fusion native and no Enterprise, adapter selects COMMUNITY."""
adapter = DataAdapter(self.env)
mode = adapter._select_mode(
fusion_native_model='fusion.bank.rec.widget',
enterprise_module='account_accountant',
)
self.assertIn(mode, (AdapterMode.FUSION, AdapterMode.ENTERPRISE, AdapterMode.COMMUNITY))
def test_adapter_falls_back_when_fusion_model_missing(self):
"""Adapter must not error when the fusion native model isn't loaded."""
adapter = DataAdapter(self.env)
mode = adapter._select_mode(
fusion_native_model='fusion.never.exists',
enterprise_module='also_does_not_exist',
)
self.assertEqual(mode, AdapterMode.COMMUNITY)
@tagged('post_install', '-at_install')
class TestBankRecAdapter(TransactionCase):
"""Verify the bank-rec adapter returns rows in any install profile."""
def setUp(self):
super().setUp()
self.journal = self.env['account.journal'].create({
'name': 'Test Bank',
'type': 'bank',
'code': 'TBNK',
})
self.statement = self.env['account.bank.statement'].create({
'name': 'Test Statement',
'journal_id': self.journal.id,
})
self.line = self.env['account.bank.statement.line'].create({
'statement_id': self.statement.id,
'journal_id': self.journal.id,
'date': '2026-04-18',
'payment_ref': 'Test Payment',
'amount': 100.0,
})
def test_list_unreconciled_returns_our_test_line(self):
"""The adapter should find the unreconciled line we just created."""
adapter = get_adapter(self.env, 'bank_rec')
rows = adapter.list_unreconciled(journal_id=self.journal.id, limit=10)
ids = [r['id'] for r in rows]
self.assertIn(self.line.id, ids,
f"Expected line {self.line.id} in unreconciled list, got: {ids}")
@tagged('post_install', '-at_install')
class TestReportsAdapter(TransactionCase):
"""Verify the reports adapter computes a trial-balance-shaped result."""
def test_trial_balance_returns_rows_in_pure_community(self):
adapter = get_adapter(self.env, 'reports')
result = adapter.trial_balance()
self.assertIsInstance(result, list)
for row in result:
self.assertIn('account_id', row)
self.assertIn('balance', row)
def test_run_report_returns_lines_or_error_dict(self):
"""run_report() must always return either an Enterprise-shaped
{'report_name', 'lines'} dict or an {'error': ...} dict — never raise."""
adapter = get_adapter(self.env, 'reports')
result = adapter.run_report(ref_id='account_reports.profit_and_loss')
self.assertIsInstance(result, dict)
# Either a report_name+lines response or an error — both valid
self.assertTrue(
('lines' in result and 'report_name' in result) or 'error' in result,
f"Unexpected result shape: {result!r}",
)
def test_run_report_with_unknown_ref_returns_error(self):
adapter = get_adapter(self.env, 'reports')
result = adapter.run_report(ref_id='nonexistent.report.xml_id')
self.assertIsInstance(result, dict)
self.assertIn('error', result)
def test_export_report_returns_dict(self):
adapter = get_adapter(self.env, 'reports')
result = adapter.export_report(
ref_id='account_reports.profit_and_loss', fmt='pdf',
)
self.assertIsInstance(result, dict)
@tagged('post_install', '-at_install')
class TestFollowupAdapter(TransactionCase):
def test_overdue_invoices_returns_list(self):
adapter = get_adapter(self.env, 'followup')
rows = adapter.overdue_invoices(days_overdue=30)
self.assertIsInstance(rows, list)
def test_overdue_invoices_row_has_contact_fields(self):
"""The enriched shape must include email, phone, and amount_total so
the accounts_receivable tool wrapper can render them."""
adapter = get_adapter(self.env, 'followup')
rows = adapter.overdue_invoices(days_overdue=30, limit=5)
for row in rows:
for key in (
'id', 'name', 'partner_id', 'partner_name',
'partner_email', 'partner_phone',
'invoice_date_due', 'amount_total', 'amount_residual',
'days_overdue',
):
self.assertIn(key, row, f"Missing key {key!r} in overdue row")
def test_aged_receivables_returns_bucket_shape(self):
adapter = get_adapter(self.env, 'followup')
result = adapter.aged_receivables(company_id=self.env.company.id)
self.assertIn('total', result)
self.assertIn('buckets', result)
self.assertIn('line_count', result)
for bucket in ('current', '1_30', '31_60', '61_90', '90_plus'):
self.assertIn(bucket, result['buckets'])
def test_aged_payables_returns_bucket_shape(self):
adapter = get_adapter(self.env, 'followup')
result = adapter.aged_payables(company_id=self.env.company.id)
self.assertIn('total', result)
self.assertIn('buckets', result)
self.assertIn('line_count', result)
for bucket in ('current', '1_30', '31_60', '61_90', '90_plus'):
self.assertIn(bucket, result['buckets'])
@tagged('post_install', '-at_install')
class TestAssetsAdapter(TransactionCase):
def test_list_assets_returns_list(self):
adapter = get_adapter(self.env, 'assets')
rows = adapter.list_assets()
self.assertIsInstance(rows, list)