Data AnalyticsDomain analytics and business cases

Accounts receivable ageing with partial payments

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 20 September 2026 · 3 min read
Technically reviewed by Ishaan Sharma
In this article (5 sections)

Age the outstanding invoice balance at a defined cutoff, not the original invoice amount. Partial payments and applied credits reduce the balance, while transactions posted after the cutoff must not change a historical report.

Calculate the balance first, then classify it by days past due. Keep negative balances or overapplications visible rather than silently setting them to zero and losing reconciliation.

Define the historical snapshot

The original synthetic receivables invoices, payment applications and credits use a January 31, 2026 reporting date. Include activity before February 1, exclusive.

This is an analytical teaching ledger. It does not prescribe an accounting policy or represent actual NeuraPath receivables. The payment table contains amounts applied to particular invoices, not every cash receipt in the business.

For F1, the original 100,000 paise invoice has two applied payments totaling 50,000 and a 10,000 credit. Its remaining balance is 40,000, sixteen days past due.

Aggregate each child ledger before joining

sql
WITH paid AS (
 SELECT invoice_id,SUM(amount_paise) AS amount
 FROM ar_payments WHERE applied_at<'2026-02-01' GROUP BY invoice_id
), credited AS (
 SELECT invoice_id,SUM(amount_paise) AS amount
 FROM ar_credits WHERE posted_at<'2026-02-01' GROUP BY invoice_id
)
SELECT i.invoice_id,i.due_at,
 i.amount_paise-COALESCE(p.amount,0)-COALESCE(c.amount,0) AS balance_paise,
 CAST(julianday('2026-01-31')-julianday(i.due_at) AS INTEGER) AS days_past_due
FROM ar_invoices i
LEFT JOIN paid p USING(invoice_id) LEFT JOIN credited c USING(invoice_id)
WHERE i.issued_at<'2026-02-01' ORDER BY i.invoice_id;

Joining raw payments and raw credits directly can multiply rows when an invoice has several of each. Preaggregation keeps one balance row per invoice.

Expected balances are F1 at 40,000, F2 at 50,000, F3 at 5,000 and F4 at negative 5,000 paise. F2's February payment is excluded even though it exists in the extract.

Keep ageing totals and credit positions reconcilable

python
from datetime import date
from build_and_verify import database

db = database()
balances = {}
buckets = {'current':0,'1-30':0,'31-60':0,'61+':0}
credit_positions = 0
for invoice,customer,issued,due,amount in db.execute("SELECT * FROM ar_invoices WHERE issued_at<'2026-02-01'"):
    paid = db.execute("SELECT COALESCE(SUM(amount_paise),0) FROM ar_payments WHERE invoice_id=? AND applied_at<'2026-02-01'",(invoice,)).fetchone()[0]
    credit = db.execute("SELECT COALESCE(SUM(amount_paise),0) FROM ar_credits WHERE invoice_id=? AND posted_at<'2026-02-01'",(invoice,)).fetchone()[0]
    balance = amount-paid-credit
    balances[invoice] = balance
    if balance < 0:
        credit_positions += -balance
    elif balance > 0:
        age = (date(2026,1,31)-date.fromisoformat(due)).days
        band = 'current' if age<=0 else '1-30' if age<=30 else '31-60' if age<=60 else '61+'
        buckets[band] += balance
db.close()
assert balances == {'F1':40000,'F2':50000,'F3':5000,'F4':-5000}
assert buckets == {'current':50000,'1-30':40000,'31-60':5000,'61+':0}
assert credit_positions == 5000
assert sum(buckets.values())-credit_positions == sum(balances.values()) == 90000
print({'positive_balance_buckets':buckets,'credit_positions':credit_positions,'net_balance':90000})

The positive receivable total is 95,000 paise. The separate credit position is 5,000, leaving a net ledger balance of 90,000. F4's negative balance requires review; the example does not automatically reallocate it to another invoice.

Match the report's credit treatment

Oracle's receivables reporting guide documents options for displaying and ageing credit items. Different report settings can legitimately produce different bucket presentations. Reconcile their scope before assuming that one total is wrong.

Decide whether a due-today invoice is current or overdue under the chosen convention. Here, age zero is current. Separate disputed invoices, unapplied cash and write-offs when those states affect the business question; they are not modeled in this compact fixture.

Historical payment application can also differ from payment receipt time. A cash receipt may arrive before it is allocated to an invoice. Store both when needed, rather than assigning one date to two different events.

Exercise: add a second credit to F1 and verify that the preaggregated query still returns one invoice row. Then move a payment across the cutoff and explain which historical balance changes.

NeuraPath's Data Analytics with Generative AI course connects SQL reconciliation with finance operations. A useful ageing report preserves the balance bridge and makes cutoff and credit rules explicit.

Continue learning

This article is part of the Domain analytics and business cases sequence. Use the neighbouring tasks when you need the prerequisite or the next application.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.

This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.

Explore Data Analytics with Generative AI
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.