refactor(fusion_accounting_ai): route accounts_receivable tools through FollowupAdapter
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
This commit is contained in:
@@ -1,66 +1,36 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_ar_aging(env, params):
|
||||
today = fields.Date.today()
|
||||
domain = [
|
||||
('account_id.account_type', '=', 'asset_receivable'),
|
||||
('parent_state', '=', 'posted'),
|
||||
('reconciled', '=', False),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
amls = env['account.move.line'].search(domain)
|
||||
|
||||
buckets = {'current': 0, '1_30': 0, '31_60': 0, '61_90': 0, '90_plus': 0}
|
||||
for aml in amls:
|
||||
if not aml.date_maturity or aml.date_maturity >= today:
|
||||
buckets['current'] += aml.amount_residual
|
||||
else:
|
||||
days = (today - aml.date_maturity).days
|
||||
if days <= 30:
|
||||
buckets['1_30'] += aml.amount_residual
|
||||
elif days <= 60:
|
||||
buckets['31_60'] += aml.amount_residual
|
||||
elif days <= 90:
|
||||
buckets['61_90'] += aml.amount_residual
|
||||
else:
|
||||
buckets['90_plus'] += aml.amount_residual
|
||||
|
||||
return {
|
||||
'total': sum(buckets.values()),
|
||||
'buckets': buckets,
|
||||
'line_count': len(amls),
|
||||
}
|
||||
"""Return AR aging buckets. Routed through FollowupAdapter for tri-mode consistency."""
|
||||
from ..data_adapters import get_adapter
|
||||
adapter = get_adapter(env, 'followup')
|
||||
return adapter.aged_receivables(company_id=env.company.id)
|
||||
|
||||
|
||||
def get_overdue_invoices(env, params):
|
||||
today = fields.Date.today()
|
||||
days_overdue = int(params.get('min_days_overdue', 1))
|
||||
from datetime import timedelta
|
||||
cutoff = today - timedelta(days=days_overdue)
|
||||
invoices = env['account.move'].search([
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('payment_state', 'in', ('not_paid', 'partial')),
|
||||
('invoice_date_due', '<', cutoff),
|
||||
('company_id', '=', env.company.id),
|
||||
], order='invoice_date_due asc', limit=int(params.get('limit', 50)))
|
||||
"""Return overdue customer invoices. Routed through FollowupAdapter."""
|
||||
from ..data_adapters import get_adapter
|
||||
adapter = get_adapter(env, 'followup')
|
||||
rows = adapter.overdue_invoices(
|
||||
days_overdue=int(params.get('min_days_overdue', 1)),
|
||||
limit=int(params.get('limit', 50)),
|
||||
)
|
||||
return {
|
||||
'count': len(invoices),
|
||||
'count': len(rows),
|
||||
'invoices': [{
|
||||
'id': inv.id,
|
||||
'name': inv.name,
|
||||
'partner': inv.partner_id.name if inv.partner_id else '',
|
||||
'email': inv.partner_id.email or '' if inv.partner_id else '',
|
||||
'phone': inv.partner_id.phone or '' if inv.partner_id else '',
|
||||
'amount_total': inv.amount_total,
|
||||
'amount_residual': inv.amount_residual,
|
||||
'date_due': str(inv.invoice_date_due),
|
||||
'days_overdue': (today - inv.invoice_date_due).days,
|
||||
} for inv in invoices],
|
||||
'id': r['id'],
|
||||
'name': r['name'],
|
||||
'partner': r['partner_name'] or '',
|
||||
'email': r['partner_email'],
|
||||
'phone': r['partner_phone'],
|
||||
'amount_total': r['amount_total'],
|
||||
'amount_residual': r['amount_residual'],
|
||||
'date_due': str(r['invoice_date_due']) if r['invoice_date_due'] else '',
|
||||
'days_overdue': r['days_overdue'],
|
||||
} for r in rows],
|
||||
}
|
||||
|
||||
|
||||
@@ -119,10 +89,10 @@ def get_partner_balance(env, params):
|
||||
|
||||
|
||||
def send_followup(env, params):
|
||||
"""Send a follow-up to a partner. Routed through FollowupAdapter so the
|
||||
Enterprise-only execute_followup path is isolated behind the adapter."""
|
||||
from ..data_adapters import get_adapter
|
||||
partner_id = int(params['partner_id'])
|
||||
partner = env['res.partner'].browse(partner_id)
|
||||
if not partner.exists():
|
||||
return {'error': 'Partner not found'}
|
||||
options = {
|
||||
'partner_id': partner_id,
|
||||
'email': params.get('send_email', False),
|
||||
@@ -133,21 +103,16 @@ def send_followup(env, params):
|
||||
options['email_subject'] = params['email_subject']
|
||||
if params.get('body'):
|
||||
options['body'] = params['body']
|
||||
result = partner.execute_followup(options)
|
||||
return {'status': 'sent', 'partner': partner.name, 'result': str(result) if result else 'done'}
|
||||
adapter = get_adapter(env, 'followup')
|
||||
return adapter.send_followup(partner_id=partner_id, options=options)
|
||||
|
||||
|
||||
def get_followup_report(env, params):
|
||||
"""Return the follow-up report HTML for a partner. Routed through FollowupAdapter."""
|
||||
from ..data_adapters import get_adapter
|
||||
partner_id = int(params['partner_id'])
|
||||
partner = env['res.partner'].browse(partner_id)
|
||||
if not partner.exists():
|
||||
return {'error': 'Partner not found'}
|
||||
try:
|
||||
report = env['account.followup.report']
|
||||
html = report._get_followup_report_html(partner)
|
||||
return {'partner': partner.name, 'html': html}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
adapter = get_adapter(env, 'followup')
|
||||
return adapter.followup_report_html(partner_id=partner_id)
|
||||
|
||||
|
||||
def reconcile_payment_to_invoice(env, params):
|
||||
|
||||
Reference in New Issue
Block a user