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, }