Acumatica · Reports

Acumatica Aged Receivables Report — Tuning Guide

Acumatica Aged Receivables Report — Tuning Guide is one of those reports every business needs and almost no business has a clean implementation of.

John Kihiu12 min read

Aged Receivables (AR631000, and the printed version most collections teams live in) shares the bucket-math problem with Aged Payables, but it has its own specific bottleneck on top: unapplied cash and credit memos. Every open AR document has to be netted against its applications before you know the real "amount past due" per bucket, and that netting is where I've watched this report go from a two-second run to a two-minute one on instances with heavy prepayment or partial-application activity.

Gross balance aging versus net-of-unapplied aging

The stock report has a parameter for this — aging by document balance versus netting unapplied payments against the oldest invoice first — and the two modes have very different costs. Document-balance aging is a straight sum of ARInvoice.CuryDocBal per bucket: cheap, one table, one filter. Netted aging (the version most CFOs actually want, because it matches how collections calls get made) has to walk ARAdjust for every open document to figure out which invoices absorbed which payments, then re-derive an effective balance per invoice before bucketing. That's a correlated subquery or a join with a much wider row count, and it's the difference most people mean when they say "the aged AR report is slow" without realizing they asked for the expensive mode.

SCHEMA — PUSH THE OPEN FILTER, THEN JOIN ADJUSTMENTS ONCE
Base:   ARInvoice  WHERE Released = True AND CuryDocBal <> 0
Join:   ARAdjust   ON AdjdDocType/AdjdRefNbr = ARInvoice keys
                    (LEFT JOIN — invoices with no adjustment still age)
Group:  by CustomerID, bucket(DueDate, @AsOfDate)

Filter to open documents in the schema before the adjustment join, not after — joining every historical invoice to its adjustment history and filtering afterward multiplies the expensive part of the query by however many years of closed AR sit in the table.

Statement-per-customer runs: watch the fan-out

Aged Receivables is frequently run "for all customers" as a batch statement pack rather than interactively for one account. That mode is a subreport-per-customer or a grouped report over the full customer base, and the same N+1 risk that applies to any subreport applies here: if the layout invokes a child report per customer to fetch payment history or contact details, you're paying full report-pipeline overhead per customer instead of once. For a batch pack over a few hundred customers, pre-join what you need into the parent schema instead of delegating to a subreport per group.

Currency matters more here than in most reports

Multi-currency AR aging has to decide whether buckets total in document currency, base currency, or both, and whether the as-of-date exchange rate is the rate at invoice date or a revaluation rate. Get this decision in writing before you tune anything — the "slow" report is sometimes actually doing a rate lookup per row because someone asked for as-of-date revaluation, and that's a correctness requirement, not a bug to optimize away.

Wrapping up

Confirm which aging mode is actually required — gross document balance is cheap, netted-against-unapplied-cash is a real join. Filter to open documents before joining adjustment history, and if the report runs as a full customer-base statement pack, flatten any per-customer subreport into the parent query. Most "slow aged AR" tickets are one of these three things, not a missing index.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.