Task 13 Step 7 of phase-0 plan.
Routes the AR tools through the FollowupAdapter so they work identically on
fusion-native, Enterprise, and pure Community installs:
- get_ar_aging → FollowupAdapter.aged_receivables()
- get_overdue_invoices → FollowupAdapter.overdue_invoices()
- send_followup → FollowupAdapter.send_followup()
- get_followup_report → FollowupAdapter.followup_report_html()
FollowupAdapter extended:
- overdue_invoices() now includes partner_email, partner_phone and
amount_total so the tool wrapper can render its richer response.
- aged_receivables() and aged_payables() new shared-implementation method
_aged_buckets() produces the 5-bucket aging shape the AR/AP tools emit.
- followup_report_html() and send_followup() isolate the Enterprise
account.followup.report / partner.execute_followup calls; Community mode
returns a graceful error dict.
Pure-Community tools in accounts_receivable.py (get_partner_balance,
reconcile_payment_to_invoice, get_unmatched_payments) unchanged — they touch
account.move / account.move.line directly which is tri-mode safe.
3 new data-adapter tests added (total: 9; all passing on westin-v19).
Made-with: Cursor
125 lines
5.1 KiB
Python
125 lines
5.1 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')
|
|
# Compute an empty-filter trial balance for the current company. Should
|
|
# return a list (possibly empty in a fresh test DB) without errors.
|
|
result = adapter.trial_balance()
|
|
self.assertIsInstance(result, list)
|
|
# Each row should have account_id and balance keys
|
|
for row in result:
|
|
self.assertIn('account_id', row)
|
|
self.assertIn('balance', row)
|
|
|
|
|
|
@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)
|