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
This commit is contained in:
@@ -28,7 +28,7 @@ def _bucket_for_days(days):
|
|||||||
|
|
||||||
|
|
||||||
class FollowupAdapter(DataAdapter):
|
class FollowupAdapter(DataAdapter):
|
||||||
FUSION_MODEL = 'fusion.followup.line'
|
FUSION_MODEL = 'fusion.followup.engine'
|
||||||
ENTERPRISE_MODULE = 'account_followup'
|
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):
|
def send_followup(self, partner_id, level_id=None, force=False, options=None):
|
||||||
return self._dispatch('send_followup', partner_id=partner_id, options=options)
|
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):
|
def send_followup_via_fusion(self, partner_id, level_id=None,
|
||||||
return self.send_followup_via_community(partner_id=partner_id, options=options)
|
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)
|
partner = self.env['res.partner'].browse(partner_id)
|
||||||
if not partner.exists():
|
if not partner.exists():
|
||||||
return {'error': 'Partner not found'}
|
return {'error': 'Partner not found'}
|
||||||
@@ -198,7 +212,8 @@ class FollowupAdapter(DataAdapter):
|
|||||||
'result': str(result) if result else 'done',
|
'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 {
|
return {
|
||||||
'error': (
|
'error': (
|
||||||
'Sending follow-ups is only available when account_followup '
|
'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)
|
register_adapter('followup', FollowupAdapter)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting Follow-up',
|
'name': 'Fusion Accounting Follow-up',
|
||||||
'version': '19.0.1.0.15',
|
'version': '19.0.1.0.16',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
|
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ from . import test_account_move_line_inherit
|
|||||||
from . import test_fusion_followup_engine
|
from . import test_fusion_followup_engine
|
||||||
from . import test_engine_integration
|
from . import test_engine_integration
|
||||||
from . import test_followup_controller
|
from . import test_followup_controller
|
||||||
|
from . import test_followup_adapter
|
||||||
|
|||||||
42
fusion_accounting_followup/tests/test_followup_adapter.py
Normal file
42
fusion_accounting_followup/tests/test_followup_adapter.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user