import logging from odoo import fields _logger = logging.getLogger(__name__) def get_adp_receivable_aging(env, params): accounts = env['account.account'].search([ ('code', '=like', '1101%'), ('company_ids', 'in', env.company.id), ]) today = fields.Date.today() amls = env['account.move.line'].search([ ('account_id', 'in', accounts.ids), ('reconciled', '=', False), ('parent_state', '=', 'posted'), ]) buckets = {'current': 0, '1_30': 0, '31_60': 0, '61_90': 0, '90_plus': 0} for aml in amls: amt = abs(aml.amount_residual) if not aml.date_maturity or aml.date_maturity >= today: buckets['current'] += amt else: days = (today - aml.date_maturity).days if days <= 30: buckets['1_30'] += amt elif days <= 60: buckets['31_60'] += amt elif days <= 90: buckets['61_90'] += amt else: buckets['90_plus'] += amt return {'total': sum(buckets.values()), 'buckets': buckets} def match_adp_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).exists() if len(amls) < 2: return {'error': 'Need at least 2 existing journal items to reconcile'} amls.reconcile() return {'status': 'matched', 'move_line_ids': amls.ids} def verify_adp_split(env, params): invoice_id = int(params['invoice_id']) invoice = env['account.move'].browse(invoice_id) if not invoice.exists(): return {'error': 'Invoice not found'} lines = invoice.invoice_line_ids total = invoice.amount_untaxed return { 'invoice': invoice.name, 'total_untaxed': total, 'total_with_tax': invoice.amount_total, 'lines': [{'name': l.name, 'subtotal': l.price_subtotal, 'total': l.price_total} for l in lines], 'balanced': abs(sum(l.price_subtotal for l in lines) - total) < 0.01, } def find_adp_without_payment(env, params): adp_partner = env['res.partner'].search([('name', 'ilike', 'ADP')], limit=1) if not adp_partner: return {'status': 'info', 'message': 'No ADP partner found in the system.'} invoices = env['account.move'].search([ ('partner_id', '=', adp_partner.id), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('payment_state', 'in', ('not_paid', 'partial')), ]) return { 'count': len(invoices), 'invoices': [{ 'id': inv.id, 'name': inv.name, 'amount': inv.amount_residual, 'date': str(inv.date), } for inv in invoices[:20]], } def get_adp_summary(env, params): date_from = params.get('date_from') date_to = params.get('date_to') accounts = env['account.account'].search([ ('code', '=like', '1101%'), ('company_ids', 'in', env.company.id), ]) domain = [ ('account_id', 'in', accounts.ids), ('parent_state', '=', 'posted'), ] if date_from: domain.append(('date', '>=', date_from)) if date_to: domain.append(('date', '<=', date_to)) lines = env['account.move.line'].search(domain) total_debit = sum(l.debit for l in lines) total_credit = sum(l.credit for l in lines) return { 'period': f'{date_from or "all"} to {date_to or "now"}', 'billed': total_debit, 'collected': total_credit, 'outstanding': total_debit - total_credit, } def register_adp_batch_payment(env, params): """Register payments for a batch of ADP invoices from a remittance advice. Takes a list of invoice numbers with payment amounts and a payment date. Registers a payment for each invoice via Odoo's payment wizard, which creates outstanding receipt entries (PBNK2) on account 1050. After calling this, use suggest_bank_line_matches on the bank deposit line to match the outstanding receipts against the bank line. """ invoices_data = params.get('invoices', []) payment_date = params.get('payment_date') journal_id = int(params.get('journal_id', 50)) # Default Scotia Current if not invoices_data: return {'error': 'No invoices provided'} if not payment_date: return {'error': 'payment_date is required (YYYY-MM-DD)'} ADP_PARTNER_ID = 3421 # ADP (Assistive Device Program) results = [] total_paid = 0.0 errors = [] for inv_data in invoices_data: inv_number = str(inv_data.get('invoice_number', '')).strip() amount = float(inv_data.get('amount', 0)) if not inv_number or not amount: errors.append(f"Skipped: missing invoice_number or amount in {inv_data}") continue # Find the invoice by name/number invoice = env['account.move'].search([ ('name', 'ilike', inv_number), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('company_id', '=', env.company.id), ], limit=1) if not invoice: # Try without leading zeros or with different format invoice = env['account.move'].search([ ('name', '=like', f'%{inv_number}'), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ], limit=1) if not invoice: errors.append(f"Invoice {inv_number} not found") continue if invoice.payment_state == 'paid': results.append({ 'invoice': inv_number, 'status': 'already_paid', 'move_id': invoice.id, }) continue # Check if amount matches residual (allow partial) if amount > invoice.amount_residual + 0.01: errors.append( f"Invoice {inv_number}: payment ${amount:.2f} exceeds " f"residual ${invoice.amount_residual:.2f}" ) continue # Register payment via the payment wizard try: payment_vals = { 'payment_type': 'inbound', 'partner_type': 'customer', 'partner_id': invoice.partner_id.id or ADP_PARTNER_ID, 'amount': amount, 'date': payment_date, 'journal_id': journal_id, 'ref': f'ADP Remittance - {inv_number}', } # Use the payment register wizard ctx = { 'active_model': 'account.move', 'active_ids': [invoice.id], } wizard = env['account.payment.register'].with_context(**ctx).create({ 'payment_date': payment_date, 'amount': amount, 'journal_id': journal_id, 'payment_method_line_id': env['account.payment.method.line'].search([ ('journal_id', '=', journal_id), ('payment_type', '=', 'inbound'), ], limit=1).id, }) wizard.action_create_payments() results.append({ 'invoice': inv_number, 'status': 'paid', 'amount': amount, 'move_id': invoice.id, 'move_name': invoice.name, }) total_paid += amount except Exception as e: _logger.warning("ADP payment failed for %s: %s", inv_number, e) errors.append(f"Invoice {inv_number}: payment failed — {e}") env.cr.commit() return { 'status': 'completed', 'paid_count': len([r for r in results if r.get('status') == 'paid']), 'already_paid_count': len([r for r in results if r.get('status') == 'already_paid']), 'total_paid': total_paid, 'results': results, 'errors': errors, 'message': ( f"Registered payments for {len([r for r in results if r.get('status') == 'paid'])} invoices " f"totalling ${total_paid:,.2f}. " + (f"{len(errors)} errors." if errors else "No errors.") + " Now use suggest_bank_line_matches to match the bank deposit." ), } TOOLS = { 'get_adp_receivable_aging': get_adp_receivable_aging, 'match_adp_payment_to_invoice': match_adp_payment_to_invoice, 'verify_adp_split': verify_adp_split, 'find_adp_without_payment': find_adp_without_payment, 'get_adp_summary': get_adp_summary, 'register_adp_batch_payment': register_adp_batch_payment, }