feat(fusion_accounting_ai): 5 new customer follow-up AI tools
Adds Task 17 tool layer: - fusion_list_overdue - fusion_get_partner_followup_detail - fusion_generate_followup_text - fusion_send_followup - fusion_get_partner_risk_score Tools register through TOOL_DISPATCH and degrade with a clear error message when fusion_accounting_followup is not installed. 5 TransactionCase tests added (78 total). Made-with: Cursor
This commit is contained in:
@@ -11,12 +11,13 @@ from .reporting import TOOLS as REPORTING_TOOLS
|
|||||||
from .audit import TOOLS as AUDIT_TOOLS
|
from .audit import TOOLS as AUDIT_TOOLS
|
||||||
from .financial_reports import TOOLS as FINANCIAL_REPORTS_TOOLS
|
from .financial_reports import TOOLS as FINANCIAL_REPORTS_TOOLS
|
||||||
from .asset_management import TOOLS as ASSET_MANAGEMENT_TOOLS
|
from .asset_management import TOOLS as ASSET_MANAGEMENT_TOOLS
|
||||||
|
from .customer_followup import TOOLS as CUSTOMER_FOLLOWUP_TOOLS
|
||||||
|
|
||||||
TOOL_DISPATCH = {}
|
TOOL_DISPATCH = {}
|
||||||
for tools_dict in [
|
for tools_dict in [
|
||||||
BANK_RECON_TOOLS, HST_TOOLS, AR_TOOLS, AP_TOOLS, JOURNAL_TOOLS,
|
BANK_RECON_TOOLS, HST_TOOLS, AR_TOOLS, AP_TOOLS, JOURNAL_TOOLS,
|
||||||
MONTH_END_TOOLS, PAYROLL_TOOLS, INVENTORY_TOOLS, ADP_TOOLS,
|
MONTH_END_TOOLS, PAYROLL_TOOLS, INVENTORY_TOOLS, ADP_TOOLS,
|
||||||
REPORTING_TOOLS, AUDIT_TOOLS, FINANCIAL_REPORTS_TOOLS,
|
REPORTING_TOOLS, AUDIT_TOOLS, FINANCIAL_REPORTS_TOOLS,
|
||||||
ASSET_MANAGEMENT_TOOLS,
|
ASSET_MANAGEMENT_TOOLS, CUSTOMER_FOLLOWUP_TOOLS,
|
||||||
]:
|
]:
|
||||||
TOOL_DISPATCH.update(tools_dict)
|
TOOL_DISPATCH.update(tools_dict)
|
||||||
|
|||||||
98
fusion_accounting_ai/services/tools/customer_followup.py
Normal file
98
fusion_accounting_ai/services/tools/customer_followup.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""Fusion-engine-routed AI tools for customer follow-ups.
|
||||||
|
|
||||||
|
These tools are exposed through TOOL_DISPATCH and let the assistant query
|
||||||
|
the customer follow-up engine via natural language. All tools degrade
|
||||||
|
gracefully when fusion_accounting_followup is not installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fusion_list_overdue(env, params):
|
||||||
|
"""List partners with overdue invoices, sorted by risk."""
|
||||||
|
if 'fusion.followup.engine' not in env.registry:
|
||||||
|
return {'error': 'fusion_accounting_followup not installed'}
|
||||||
|
from ..data_adapters import get_adapter
|
||||||
|
adapter = get_adapter(env, 'followup')
|
||||||
|
return adapter.list_overdue(
|
||||||
|
status=params.get('status'),
|
||||||
|
limit=int(params.get('limit', 50)),
|
||||||
|
company_id=int(params['company_id'])
|
||||||
|
if params.get('company_id') else env.company.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fusion_get_partner_followup_detail(env, params):
|
||||||
|
"""Detailed follow-up state for a single partner: aging, risk, history."""
|
||||||
|
if 'fusion.followup.engine' not in env.registry:
|
||||||
|
return {'error': 'fusion_accounting_followup not installed'}
|
||||||
|
Partner = env['res.partner']
|
||||||
|
partner = Partner.browse(int(params['partner_id']))
|
||||||
|
if not partner.exists():
|
||||||
|
return {'error': 'Partner not found'}
|
||||||
|
engine = env['fusion.followup.engine']
|
||||||
|
overdue = engine.get_overdue_for_partner(partner)
|
||||||
|
history = engine.snapshot_followup_history(partner, limit=10)
|
||||||
|
return {
|
||||||
|
'partner_id': partner.id,
|
||||||
|
'partner_name': partner.name,
|
||||||
|
'overdue': overdue,
|
||||||
|
'history': history,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fusion_generate_followup_text(env, params):
|
||||||
|
"""Generate (or fall back to template) follow-up subject + body."""
|
||||||
|
if 'fusion.followup.engine' not in env.registry:
|
||||||
|
return {'error': 'fusion_accounting_followup not installed'}
|
||||||
|
from odoo.addons.fusion_accounting_followup.services.followup_text_generator import (
|
||||||
|
generate_followup_text,
|
||||||
|
)
|
||||||
|
return generate_followup_text(
|
||||||
|
env,
|
||||||
|
partner_name=params.get('partner_name', ''),
|
||||||
|
total_overdue=float(params.get('total_overdue', 0)),
|
||||||
|
currency_code=params.get('currency_code', 'USD'),
|
||||||
|
longest_overdue_days=int(params.get('longest_overdue_days', 0)),
|
||||||
|
tone=params.get('tone', 'gentle'),
|
||||||
|
invoice_count=int(params.get('invoice_count', 0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fusion_send_followup(env, params):
|
||||||
|
"""Send a follow-up email via the engine (creates a fusion.followup.run)."""
|
||||||
|
if 'fusion.followup.engine' not in env.registry:
|
||||||
|
return {'error': 'fusion_accounting_followup not installed'}
|
||||||
|
from ..data_adapters import get_adapter
|
||||||
|
adapter = get_adapter(env, 'followup')
|
||||||
|
return adapter.send_followup(
|
||||||
|
partner_id=int(params['partner_id']),
|
||||||
|
level_id=int(params['level_id']) if params.get('level_id') else None,
|
||||||
|
force=bool(params.get('force', False)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fusion_get_partner_risk_score(env, params):
|
||||||
|
"""Compute and return the payment-risk score + drivers for a partner."""
|
||||||
|
if 'fusion.followup.engine' not in env.registry:
|
||||||
|
return {'error': 'fusion_accounting_followup not installed'}
|
||||||
|
partner = env['res.partner'].browse(int(params['partner_id']))
|
||||||
|
if not partner.exists():
|
||||||
|
return {'error': 'Partner not found'}
|
||||||
|
overdue = env['fusion.followup.engine'].get_overdue_for_partner(partner)
|
||||||
|
return {
|
||||||
|
'partner_id': partner.id,
|
||||||
|
'partner_name': partner.name,
|
||||||
|
'risk': overdue['risk'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS = {
|
||||||
|
'fusion_list_overdue': fusion_list_overdue,
|
||||||
|
'fusion_get_partner_followup_detail': fusion_get_partner_followup_detail,
|
||||||
|
'fusion_generate_followup_text': fusion_generate_followup_text,
|
||||||
|
'fusion_send_followup': fusion_send_followup,
|
||||||
|
'fusion_get_partner_risk_score': fusion_get_partner_risk_score,
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting Follow-up',
|
'name': 'Fusion Accounting Follow-up',
|
||||||
'version': '19.0.1.0.16',
|
'version': '19.0.1.0.17',
|
||||||
'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': """
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ 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
|
from . import test_followup_adapter
|
||||||
|
from . import test_followup_tools
|
||||||
|
|||||||
61
fusion_accounting_followup/tests/test_followup_tools.py
Normal file
61
fusion_accounting_followup/tests/test_followup_tools.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""AI tool dispatch tests for fusion follow-up tools."""
|
||||||
|
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
from odoo.addons.fusion_accounting_ai.services.tools import customer_followup as tools
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestFusionFollowupTools(TransactionCase):
|
||||||
|
|
||||||
|
def test_fusion_list_overdue(self):
|
||||||
|
result = tools.fusion_list_overdue(
|
||||||
|
self.env, {'company_id': self.env.company.id},
|
||||||
|
)
|
||||||
|
self.assertIn('partners', result)
|
||||||
|
|
||||||
|
def test_fusion_get_partner_detail(self):
|
||||||
|
partner = self.env['res.partner'].create({
|
||||||
|
'name': 'Tool Partner', 'email': 't@t.local',
|
||||||
|
})
|
||||||
|
result = tools.fusion_get_partner_followup_detail(
|
||||||
|
self.env, {'partner_id': partner.id},
|
||||||
|
)
|
||||||
|
self.assertEqual(result['partner_id'], partner.id)
|
||||||
|
|
||||||
|
def test_fusion_generate_text_uses_fallback(self):
|
||||||
|
self.env['ir.config_parameter'].sudo().search([
|
||||||
|
('key', 'in', [
|
||||||
|
'fusion_accounting.provider.followup_text',
|
||||||
|
'fusion_accounting.provider.default',
|
||||||
|
]),
|
||||||
|
]).unlink()
|
||||||
|
result = tools.fusion_generate_followup_text(self.env, {
|
||||||
|
'partner_name': 'Acme', 'total_overdue': 1000,
|
||||||
|
'currency_code': 'USD', 'longest_overdue_days': 15,
|
||||||
|
'tone': 'gentle',
|
||||||
|
})
|
||||||
|
self.assertIn('subject', result)
|
||||||
|
self.assertIn('body', result)
|
||||||
|
|
||||||
|
def test_fusion_get_risk_score(self):
|
||||||
|
partner = self.env['res.partner'].create({'name': 'Risk Test'})
|
||||||
|
result = tools.fusion_get_partner_risk_score(
|
||||||
|
self.env, {'partner_id': partner.id},
|
||||||
|
)
|
||||||
|
self.assertIn('risk', result)
|
||||||
|
|
||||||
|
def test_tools_registered_in_dispatch(self):
|
||||||
|
from odoo.addons.fusion_accounting_ai.services.tools import TOOL_DISPATCH
|
||||||
|
for tool_name in [
|
||||||
|
'fusion_list_overdue',
|
||||||
|
'fusion_get_partner_followup_detail',
|
||||||
|
'fusion_generate_followup_text',
|
||||||
|
'fusion_send_followup',
|
||||||
|
'fusion_get_partner_risk_score',
|
||||||
|
]:
|
||||||
|
self.assertIn(
|
||||||
|
tool_name, TOOL_DISPATCH,
|
||||||
|
f"{tool_name} not registered in TOOL_DISPATCH",
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user