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
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
import logging
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_ar_aging(env, params):
|
|
"""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):
|
|
"""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(rows),
|
|
'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],
|
|
}
|
|
|
|
|
|
def get_partner_balance(env, params):
|
|
"""Get AR and AP balance for a partner. Accepts partner_id or partner_name."""
|
|
partner = None
|
|
if params.get('partner_id'):
|
|
partner = env['res.partner'].browse(int(params['partner_id']))
|
|
elif params.get('partner_name'):
|
|
partner = env['res.partner'].search([
|
|
('name', 'ilike', params['partner_name']),
|
|
], limit=1)
|
|
if not partner or not partner.exists():
|
|
return {'error': f"Partner not found: {params.get('partner_name', params.get('partner_id', '?'))}"}
|
|
|
|
# AR balance (receivable)
|
|
ar_amls = env['account.move.line'].search([
|
|
('partner_id', '=', partner.id),
|
|
('account_id.account_type', '=', 'asset_receivable'),
|
|
('parent_state', '=', 'posted'),
|
|
('reconciled', '=', False),
|
|
('company_id', '=', env.company.id),
|
|
])
|
|
ar_balance = sum(aml.amount_residual for aml in ar_amls)
|
|
|
|
# AP balance (payable)
|
|
ap_amls = env['account.move.line'].search([
|
|
('partner_id', '=', partner.id),
|
|
('account_id.account_type', '=', 'liability_payable'),
|
|
('parent_state', '=', 'posted'),
|
|
('reconciled', '=', False),
|
|
('company_id', '=', env.company.id),
|
|
])
|
|
ap_balance = sum(aml.amount_residual for aml in ap_amls)
|
|
|
|
open_items = [{
|
|
'id': aml.id,
|
|
'move_name': aml.move_id.name,
|
|
'ref': aml.ref or '',
|
|
'date': str(aml.date),
|
|
'amount_residual': aml.amount_residual,
|
|
'type': 'receivable' if aml.account_id.account_type == 'asset_receivable' else 'payable',
|
|
'date_maturity': str(aml.date_maturity) if aml.date_maturity else '',
|
|
} for aml in (ar_amls | ap_amls)[:30]]
|
|
|
|
return {
|
|
'partner': partner.name,
|
|
'partner_id': partner.id,
|
|
'ar_balance': ar_balance,
|
|
'ap_balance': ap_balance,
|
|
'net_balance': ar_balance + ap_balance,
|
|
'they_owe_us': ar_balance if ar_balance > 0 else 0,
|
|
'we_owe_them': abs(ap_balance) if ap_balance < 0 else 0,
|
|
'open_items': open_items,
|
|
}
|
|
|
|
|
|
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'])
|
|
options = {
|
|
'partner_id': partner_id,
|
|
'email': params.get('send_email', False),
|
|
'print': params.get('print_letter', False),
|
|
'sms': False,
|
|
}
|
|
if params.get('email_subject'):
|
|
options['email_subject'] = params['email_subject']
|
|
if params.get('body'):
|
|
options['body'] = params['body']
|
|
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'])
|
|
adapter = get_adapter(env, 'followup')
|
|
return adapter.followup_report_html(partner_id=partner_id)
|
|
|
|
|
|
def reconcile_payment_to_invoice(env, params):
|
|
move_line_ids = [int(x) for x in params['move_line_ids']]
|
|
amls = env['account.move.line'].browse(move_line_ids)
|
|
if len(amls) < 2:
|
|
return {'error': 'Need at least 2 journal items to reconcile'}
|
|
amls.reconcile()
|
|
return {
|
|
'status': 'reconciled',
|
|
'move_line_ids': move_line_ids,
|
|
}
|
|
|
|
|
|
def get_unmatched_payments(env, params):
|
|
domain = [
|
|
('account_id.account_type', '=', 'asset_receivable'),
|
|
('parent_state', '=', 'posted'),
|
|
('reconciled', '=', False),
|
|
('move_id.payment_id', '!=', False),
|
|
('company_id', '=', env.company.id),
|
|
]
|
|
amls = env['account.move.line'].search(domain, order='date desc')
|
|
return {
|
|
'count': len(amls),
|
|
'payments': [{
|
|
'id': aml.id,
|
|
'date': str(aml.date),
|
|
'ref': aml.ref or aml.move_id.name,
|
|
'partner': aml.partner_id.name if aml.partner_id else '',
|
|
'amount': abs(aml.amount_residual),
|
|
} for aml in amls[:50]],
|
|
}
|
|
|
|
|
|
TOOLS = {
|
|
'get_ar_aging': get_ar_aging,
|
|
'get_overdue_invoices': get_overdue_invoices,
|
|
'get_partner_balance': get_partner_balance,
|
|
'send_followup': send_followup,
|
|
'get_followup_report': get_followup_report,
|
|
'reconcile_payment_to_invoice': reconcile_payment_to_invoice,
|
|
'get_unmatched_payments': get_unmatched_payments,
|
|
}
|