From 993df3a14a6273fa584f7b2f56c23699ffc3b2f0 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 21:02:17 -0400 Subject: [PATCH] feat(fusion_accounting_ai): wire FollowupAdapter fusion paths to engine - Switch FUSION_MODEL to fusion.followup.engine so adapter mode selection matches the new module - Add list_overdue() with fusion/enterprise/community variants - Re-route send_followup_via_fusion to engine.send_followup_email - 4 new TransactionCase tests (73 total) Existing aging / overdue_invoices adapter methods continue to fall back to the community implementation. Made-with: Cursor --- .../services/data_adapters/followup.py | 87 +++++++++++++++++-- fusion_accounting_followup/__manifest__.py | 2 +- fusion_accounting_followup/tests/__init__.py | 1 + .../tests/test_followup_adapter.py | 42 +++++++++ 4 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 fusion_accounting_followup/tests/test_followup_adapter.py diff --git a/fusion_accounting_ai/services/data_adapters/followup.py b/fusion_accounting_ai/services/data_adapters/followup.py index 067011f2..d165669e 100644 --- a/fusion_accounting_ai/services/data_adapters/followup.py +++ b/fusion_accounting_ai/services/data_adapters/followup.py @@ -28,7 +28,7 @@ def _bucket_for_days(days): class FollowupAdapter(DataAdapter): - FUSION_MODEL = 'fusion.followup.line' + FUSION_MODEL = 'fusion.followup.engine' ENTERPRISE_MODULE = 'account_followup' # ------------------------------------------------------------------ @@ -179,15 +179,29 @@ class FollowupAdapter(DataAdapter): } # ------------------------------------------------------------------ - # send_followup — Enterprise-only action + # send_followup — routes to fusion engine when available # ------------------------------------------------------------------ - def send_followup(self, partner_id, options=None): - return self._dispatch('send_followup', partner_id=partner_id, options=options) + def send_followup(self, partner_id, level_id=None, force=False, options=None): + return self._dispatch( + 'send_followup', + partner_id=partner_id, level_id=level_id, + force=force, options=options, + ) - def send_followup_via_fusion(self, partner_id, options=None): - return self.send_followup_via_community(partner_id=partner_id, options=options) + def send_followup_via_fusion(self, partner_id, level_id=None, + force=False, options=None): + if 'fusion.followup.engine' not in self.env.registry: + return {'error': 'fusion_accounting_followup not installed'} + partner = self.env['res.partner'].browse(int(partner_id)) + level = None + if level_id: + level = self.env['fusion.followup.level'].browse(int(level_id)) + return self.env['fusion.followup.engine'].send_followup_email( + partner, level=level, force=bool(force), + ) - def send_followup_via_enterprise(self, partner_id, options=None): + def send_followup_via_enterprise(self, partner_id, level_id=None, + force=False, options=None): partner = self.env['res.partner'].browse(partner_id) if not partner.exists(): return {'error': 'Partner not found'} @@ -198,7 +212,8 @@ class FollowupAdapter(DataAdapter): 'result': str(result) if result else 'done', } - def send_followup_via_community(self, partner_id, options=None): + def send_followup_via_community(self, partner_id, level_id=None, + force=False, options=None): return { 'error': ( 'Sending follow-ups is only available when account_followup ' @@ -206,5 +221,61 @@ class FollowupAdapter(DataAdapter): ), } + # ------------------------------------------------------------------ + # list_overdue — partner-centric overdue rollup (fusion engine) + # ------------------------------------------------------------------ + def list_overdue(self, status=None, limit=50, company_id=None): + return self._dispatch( + 'list_overdue', + status=status, limit=limit, company_id=company_id, + ) + + def list_overdue_via_fusion(self, status=None, limit=50, company_id=None): + if 'fusion.followup.engine' not in self.env.registry: + return {'partners': [], 'count': 0, 'total': 0} + company_id = company_id or self.env.company.id + Line = self.env['account.move.line'].sudo() + partner_ids = Line.search([ + ('parent_state', '=', 'posted'), + ('account_id.account_type', '=', 'asset_receivable'), + ('reconciled', '=', False), + ('amount_residual', '>', 0), + ('date_maturity', '<', date.today()), + ('company_id', '=', company_id), + ]).mapped('partner_id').ids + Partner = self.env['res.partner'].sudo() + domain = [('id', 'in', partner_ids)] + if status: + domain.append(('fusion_followup_status', '=', status)) + partners = Partner.search(domain, limit=int(limit)) + engine = self.env['fusion.followup.engine'] + rows = [] + for p in partners: + try: + overdue = engine.get_overdue_for_partner(p) + rows.append({ + 'partner_id': p.id, + 'partner_name': p.name, + 'overdue_amount': overdue['aging']['total_overdue_amount'], + 'risk_score': overdue['risk']['score'], + 'risk_band': overdue['risk']['band'], + 'status': p.fusion_followup_status, + }) + except Exception: + pass + return {'count': len(rows), 'total': len(partner_ids), 'partners': rows} + + def list_overdue_via_enterprise(self, status=None, limit=50, company_id=None): + return { + 'partners': [], 'count': 0, 'total': 0, + 'error': 'Enterprise account_followup must be used from its UI', + } + + def list_overdue_via_community(self, status=None, limit=50, company_id=None): + return { + 'partners': [], 'count': 0, 'total': 0, + 'error': 'No follow-up engine in pure Community', + } + register_adapter('followup', FollowupAdapter) diff --git a/fusion_accounting_followup/__manifest__.py b/fusion_accounting_followup/__manifest__.py index b3cb24c6..c7f90d9f 100644 --- a/fusion_accounting_followup/__manifest__.py +++ b/fusion_accounting_followup/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Follow-up', - 'version': '19.0.1.0.15', + 'version': '19.0.1.0.16', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.', 'description': """ diff --git a/fusion_accounting_followup/tests/__init__.py b/fusion_accounting_followup/tests/__init__.py index 40bf9f9a..693ef383 100644 --- a/fusion_accounting_followup/tests/__init__.py +++ b/fusion_accounting_followup/tests/__init__.py @@ -11,3 +11,4 @@ from . import test_account_move_line_inherit from . import test_fusion_followup_engine from . import test_engine_integration from . import test_followup_controller +from . import test_followup_adapter diff --git a/fusion_accounting_followup/tests/test_followup_adapter.py b/fusion_accounting_followup/tests/test_followup_adapter.py new file mode 100644 index 00000000..1b8bfaae --- /dev/null +++ b/fusion_accounting_followup/tests/test_followup_adapter.py @@ -0,0 +1,42 @@ +"""FollowupAdapter wiring tests — engine paths.""" + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + +from odoo.addons.fusion_accounting_ai.services.data_adapters.followup import ( + FollowupAdapter, +) + + +@tagged('post_install', '-at_install') +class TestFollowupAdapter(TransactionCase): + + def setUp(self): + super().setUp() + self.adapter = FollowupAdapter(self.env) + + def test_list_overdue_via_fusion_returns_dict(self): + result = self.adapter.list_overdue_via_fusion( + company_id=self.env.company.id, + ) + self.assertIn('partners', result) + self.assertIn('total', result) + self.assertIn('count', result) + + def test_list_overdue_via_community_returns_error(self): + result = self.adapter.list_overdue_via_community() + self.assertIn('error', result) + + def test_send_followup_via_fusion_no_overdue(self): + partner = self.env['res.partner'].create({'name': 'AdapterTest'}) + result = self.adapter.send_followup_via_fusion( + partner_id=partner.id, force=True, + ) + self.assertIn( + result.get('status', ''), + ('no_action', 'no_overdue', 'sent', 'manual_review'), + ) + + def test_send_followup_via_community_returns_error(self): + result = self.adapter.send_followup_via_community(partner_id=1) + self.assertIn('error', result)