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_dict_with_assets(self): # Phase 3 (fusion_accounting_assets) wired list_assets to return # {count, total, assets} — consistent with bank_rec.list_unreconciled etc. adapter = get_adapter(self.env, 'assets') rows = adapter.list_assets() self.assertIsInstance(rows, dict) self.assertIn('assets', rows) self.assertIsInstance(rows['assets'], list)