From d331dc5fa6ddb98629d98246f71dfaa109e3533e Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 18 Apr 2026 23:08:53 -0400 Subject: [PATCH] feat(fusion_accounting_ai): add BankRecAdapter for tri-mode bank-rec lookups Made-with: Cursor --- .../services/data_adapters/__init__.py | 3 ++ .../services/data_adapters/bank_rec.py | 53 +++++++++++++++++++ .../tests/test_data_adapters.py | 33 ++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 fusion_accounting_ai/services/data_adapters/bank_rec.py diff --git a/fusion_accounting_ai/services/data_adapters/__init__.py b/fusion_accounting_ai/services/data_adapters/__init__.py index 8926891d..b6cdbfcb 100644 --- a/fusion_accounting_ai/services/data_adapters/__init__.py +++ b/fusion_accounting_ai/services/data_adapters/__init__.py @@ -1,4 +1,7 @@ from .base import DataAdapter, AdapterMode from ._registry import get_adapter, register_adapter +# Side-effect imports: each adapter module calls register_adapter at module load. +from . import bank_rec # noqa: F401 + __all__ = ['DataAdapter', 'AdapterMode', 'get_adapter', 'register_adapter'] diff --git a/fusion_accounting_ai/services/data_adapters/bank_rec.py b/fusion_accounting_ai/services/data_adapters/bank_rec.py new file mode 100644 index 00000000..727e9c3e --- /dev/null +++ b/fusion_accounting_ai/services/data_adapters/bank_rec.py @@ -0,0 +1,53 @@ +"""Bank reconciliation data adapter. + +Routes bank-rec data lookups across: +- FUSION: fusion.bank.rec.widget (added by fusion_accounting_bank_rec, Phase 1) +- ENTERPRISE: account_accountant's bank_rec_widget JS service +- COMMUNITY: pure search on account.bank.statement.line +""" + +from .base import DataAdapter +from ._registry import register_adapter + + +class BankRecAdapter(DataAdapter): + FUSION_MODEL = 'fusion.bank.rec.widget' + ENTERPRISE_MODULE = 'account_accountant' + + def list_unreconciled(self, journal_id, limit=100): + """Return unreconciled bank statement lines for a journal.""" + return self._dispatch('list_unreconciled', journal_id=journal_id, limit=limit) + + def list_unreconciled_via_fusion(self, journal_id, limit=100): + # Phase 1 will add fusion.bank.rec.widget; this method becomes the primary path. + # For now: even when the model exists, delegate to community read shape. + return self.list_unreconciled_via_community(journal_id=journal_id, limit=limit) + + def list_unreconciled_via_enterprise(self, journal_id, limit=100): + # Enterprise's bank rec uses a JS-side service; from Python the cleanest + # backend access is the same Community search (the data lives in + # account.bank.statement.line either way). This adapter's purpose is + # to expose a stable shape to AI tools regardless of which UI the user has. + return self.list_unreconciled_via_community(journal_id=journal_id, limit=limit) + + def list_unreconciled_via_community(self, journal_id, limit=100): + Line = self.env['account.bank.statement.line'].sudo() + records = Line.search([ + ('journal_id', '=', journal_id), + ('is_reconciled', '=', False), + ], limit=limit, order='date desc, id desc') + return [ + { + 'id': r.id, + 'date': r.date, + 'payment_ref': r.payment_ref, + 'amount': r.amount, + 'partner_id': r.partner_id.id if r.partner_id else None, + 'partner_name': r.partner_id.name if r.partner_id else None, + 'currency_id': r.currency_id.id if r.currency_id else None, + } + for r in records + ] + + +register_adapter('bank_rec', BankRecAdapter) diff --git a/fusion_accounting_ai/tests/test_data_adapters.py b/fusion_accounting_ai/tests/test_data_adapters.py index 34689058..a22be0db 100644 --- a/fusion_accounting_ai/tests/test_data_adapters.py +++ b/fusion_accounting_ai/tests/test_data_adapters.py @@ -2,6 +2,7 @@ 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') @@ -25,3 +26,35 @@ class TestDataAdapterBase(TransactionCase): 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}")