Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

Dunning & Overdue Payment Escalation

A design reference for detecting overdue receivables and generating templated escalation notices

1. Requirements

1.1 Functional requirements

  • Identify overdue invoice installments for a customer in one legal entity, at installment granularity: an installment qualifies once its due date has passed and it still carries an outstanding balance.
  • Let staff pull overdue installments, across one or more invoices for the same customer, onto a single escalation notice scoped to one customer, legal entity, and currency.
  • Compute per installment the days overdue and a daily interest charge on its outstanding amount, plus one flat fee per notice.
  • Determine an escalation level per installment: how many prior notices already targeted it, so repeat notices are distinguishable from the first.
  • Resolve a language-appropriate letter body and closing paragraph from a reusable, per-legal-entity template set, merged with the notice’s own fields.
  • Track whether a notice is open or satisfied, flipping that automatically as the underlying receivable is paid down, credited, or reopened.

1.2 Non-functional requirements

  • Consistency with the books: outstanding amounts come from the same derived-balance mechanism the rest of receivables relies on (the Party Balance Entry ledger), never a locally cached figure.
  • Currency integrity: a notice never mixes installments in different currencies, since one calculation and one converted total apply to the whole notice.
  • Non-destructive calculation: interest, fee, and letter text can be recomputed any time before submission without altering the underlying invoices.
  • Separation from ledger-writing: submitting the notice must not itself create any accounting posting — that is a separate settlement step.

1.3 Constraints

  • Only overdue, still-open installments on submitted invoices for the chosen customer/legal entity are selectable via a standing filter, not a background job — pulling installments onto a notice is manual and staff-triggered.
  • Exactly one escalation profile may be default per legal entity; a new notice adopts it automatically but any profile can be substituted.
  • Each profile carries at most one letter-text variant per language, and at most one flagged default language, enforced at save time.
  • A notice cannot be submitted once its accounting period is closed, the same safeguard applied to other financial documents.

2. High-Level Design

2.1 Component diagram

Fetching maps qualifying payment-schedule rows into new Overdue Payment Lines and copies the legal entity’s default escalation profile onto the notice; overdue days and totals are then computed and the notice is saved as a draft — nothing has posted to the ledger yet.


3. Deep Dive

3.1 Data model

Dunning Notice — the submittable document: customer, legal entity, currency/conversion rate, assigned escalation profile (rate and fee overridable per notice), print language and generated letter text, and rolled-up total_outstanding / total_interest / dunning_amount / base_dunning_amount / grand_total. Status is Unresolved, Resolved, or Cancelled (§ 3.4). Its line table only accepts rows through the fetch action.

Overdue Payment Line — one row per overdue installment: source invoice, payment-schedule identifier, due date, outstanding amount (drawn from the Party Balance Entry ledger), computed overdue days, interest, and escalation level.

Escalation Profile — a named, per-legal-entity template of default flat fee, yearly interest rate, an income-type Ledger Account and cost center, and a set of Letter Text Variants. One profile per legal entity may be default; its income account and cost center must belong to the same legal entity, be non-group, and (for the account) be of Income type.

Letter Text Variant — a per-language body/closing text pair on one Escalation Profile, written as merge-field templates, with one variant flaggable as the profile’s default language.

3.2 Overdue-days, interest, and escalation-level algorithm

Recomputed on every save, both for live client preview and authoritatively on the server:

daily_interest = rate_of_interest / 100 / 365

for each Overdue Payment Line:
    overdue_days = (notice.posting_date - line.due_date).days   # the notice's own date, not "today"
    interest     = line.outstanding * daily_interest * overdue_days

total_outstanding   = sum(line.outstanding)
total_interest      = sum(line.interest)
dunning_amount      = total_interest + dunning_fee          # one flat fee per notice, not per line
base_dunning_amount = dunning_amount * conversion_rate
grand_total         = total_outstanding + dunning_amount

Escalation level is computed per line, not a running counter on the invoice: it counts how many other, already-submitted notices carry a line against that same payment-schedule identifier, plus one. A cancelled notice drops out of future counts, but a level already saved does not retroactively renumber if an earlier notice is later cancelled.

3.3 Letter text resolution

lookup Letter Text Variant where parent = notice.escalation_profile
  and (language = notice.print_language) or (is_default_language = true)

if found:  render body_text/closing_text against the notice's own fields; adopt variant.language
else:      show a dismissible "letter text not found" alert; leave body/closing text blank

Templates use sandboxed merge-field rendering and are syntax-checked when the escalation profile is saved. Generated text lands in ordinary, editable fields, so staff can hand-adjust wording before printing — nothing here sends or emails a letter; that is a separate, out-of-scope action.

3.4 Notice status lifecycle

The Resolved/Unresolved flip fires as a side effect whenever the Party Balance Entry ledger recomputes an outstanding amount for one of the notice’s linked invoices — not a scheduled check. A notice can also be force-closed with a manual “Resolve” action, independent of whether anything actually settled it.

3.5 Interaction contract (illustrative)

CALL get_dunning_letter_text(notice) → re-resolves body/closing text + print_language

CALL create_settlement_for(reference_type="Dunning Notice", reference_name=notice.id)
  → one reference line per invoice (allocated_amount = line.outstanding), plus
    one posting line for -1 * notice.dunning_amount against the profile's
    income account/cost center — the posting funnel described in the
    general-ledger design then debits cash/bank in full, credits receivables,
    and credits income for the rest

3.6 Error handling

  • Mixed currency: an installment whose invoice currency differs from the notice’s currency is rejected at validation, naming the offending invoice.
  • Missing letter text: a profile/language with no matching variant leaves the letter fields blank with a non-blocking alert, rather than failing the save.
  • Misconfigured profile accounts: an income account/cost center from a different legal entity, a group node, disabled, or not of Income type is rejected when the profile is saved; duplicate letter-text languages or more than one flagged default are rejected the same way.
  • Cancellation: cancelling a notice does not unwind downstream ledger or reconciliation records a settlement built on top of it may have created; those follow their own cancellation paths.

4. Scale and Reliability

  • Volume is bounded by receivables aging, not transaction throughput — notices are created in small, staff-driven batches per collections cycle.
  • Escalation-level lookup costs one count query per line, per save; it scales with how many times an installment has previously been dunned, which stays small in practice.
  • Auto-resolution rides on the outstanding-amount recompute every other receivables event already triggers, so status stays correct with no dedicated scheduled job. There is no external-facing surface in this flow (no outbound call, webhook, or queue) — actually emailing a generated letter sits outside what this module does.
  • The interest/overdue-days formula exists independently on the client and server and must be kept in sync by hand; an unsubmitted draft also keeps whatever figures were last calculated until someone re-saves it.

5. Trade-off Analysis

Decision Trade-off
Escalation level counts prior submitted notices per installment, not a stored counter Self-corrects if an earlier notice is cancelled — but a level already saved does not retroactively renumber.
One flat fee per notice rather than per installment Simple “one letter, one fee” model — but a bundled notice can’t charge a different fee per installment.
Interest computed from the notice’s own posting date, not wall-clock “today” Consistent dating for a given accounting day — but a draft left open for days keeps stale figures until re-saved.
Ledger posting kept out of the notice’s submission, delegated to a follow-up settlement Pure calculation/paperwork layer — but a submitted, unresolved notice can sit with zero accounting impact until someone settles it.
Letter text resolved by per-language lookup with sandboxed templating Flexible per-locale wording without code changes — but a missing template degrades silently to blank print text.
Status auto-flip wired off the shared outstanding-amount recompute, not a dunning-specific event Tracks accounting truth with no scheduled job — but couples dunning to a code path whose main purpose is unrelated.

6. What to Revisit as the System Grows

  • Escalation-level renumbering: if the level is ever used to auto-select a harsher profile, it needs recomputing on read rather than trusted as a stored value.
  • Fee tiering: a single flat fee per profile cannot express “first notice free, later notices costlier” without manually switching profiles.
  • No proactive detection job: nothing in the read source auto-generates a notice or nudges staff at a new overdue-days threshold — fetch, compute, and resolve are all manually triggered.
  • Duplicated interest formula: the same day-count calculation lives independently on the client and server, a maintenance seam worth consolidating.

Was this page helpful?