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), } 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 { 'count': len(invoices), '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], } 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): 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), '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'] result = partner.execute_followup(options) return {'status': 'sent', 'partner': partner.name, 'result': str(result) if result else 'done'} def get_followup_report(env, params): 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)} 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, }