238 lines
8.4 KiB
Python
238 lines
8.4 KiB
Python
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,
|
|
}
|