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

Deferred Revenue/Expense Recognition

A design reference for spreading an invoice line's income or expense across a service period instead of booking it all on the invoice date

1. Requirements

1.1 Functional requirements

  • Any invoice line can be flagged for scheduled recognition instead of immediate income/expense booking, with an explicit service window (start and end date) describing the period it covers.
  • On submission, the line’s amount must land in a dedicated balance-sheet holding account, not the profit-and-loss account, since it is not yet earned or consumed.
  • A scheduled batch process, at least monthly, must move the earned/consumed portion of each line out of the holding account and into the profit-and-loss account, in step with elapsed time.
  • Two recognition bases must be supported, chosen once company-wide: strict daily proration, and equal fixed monthly installments (with proration for partially-covered months).
  • A line’s service window can be cut short after the fact via an early “stop” date, distinct from its originally booked end date.
  • Recognition must be resumable and re-runnable: an administrator can trigger an out-of-cycle run scoped to one company or one holding account, alongside the automatic monthly run.
  • Multi-currency lines must be recognized correctly in both the legal entity’s base currency and the line’s transaction currency when they differ.
  • However many periods a line splits into, the sum recognized must equal its net amount exactly — rounding must never leave a residual uncollected or double-counted.
  • Cancelling (or amending, a cancel followed by a fresh draft) the source invoice must undo every recognition posted against it, not merely the initial entry.

1.2 Non-functional requirements

  • Idempotent progress tracking: each run determines “what’s left to recognize” from the ledger itself, not a run-to-run counter, so a delayed or re-triggered run cannot double-book a period.
  • Auditability: every recognition posting must be traceable to the specific batch run that produced it, distinct from the line and invoice it originated from.
  • Configurable posting style: raw ledger postings vs. a standalone journal voucher (auto-submitted or draft), without altering the recognition math.
  • Failure isolation: an error recognizing one invoice must not corrupt or halt recognition for others in the same run, but must be surfaced to whoever administers the books.
  • Respect for closed accounting periods: a posting must never land inside a period the company has already frozen for reporting.

1.3 Constraints

  • Recognition runs as a background batch process on a fixed monthly schedule per legal entity, split by direction (income vs. expense) — not per invoice or per line.
  • A recognition posting is tagged with the same voucher identity as the invoice it came from; only the batch run that produced it is a separate cross-reference — a simplification with a real consequence explored in the Deep Dive.
  • The holding account a line points to is constrained by account type at selection: liability-type on the revenue side, asset-type on the expense side. Nothing downstream re-validates this — it’s enforced only by filtering the account picker.
  • Invoices carrying any scheduled-recognition line are excluded outright from the ledger repost/self-healing tooling described elsewhere in this module.

2. High-Level Design

2.1 Component diagram

2.2 Data flow — the scheduled recognition pass


3. Deep Dive

3.1 Data model

Deferral Line — an invoice line with scheduled recognition switched on. Carries a service start and end date (the window the amount covers), an optional service stop date (an early cutoff, settable even after submission but — once set — never changeable again), and a link to the Deferral Account holding the not-yet-recognized amount. The net amount, in both the legal entity’s base currency and the line’s transaction currency, is the total eventually recognized.

Deferral Account — an ordinary leaf Ledger Account, selected explicitly per line rather than auto-provisioned: liability-type on the revenue side (unearned income), asset-type on the expense side (a prepaid cost). Enforced only at selection time by filtering the account picker; nothing downstream re-validates it.

Deferral Policy — company-wide switches governing every Recognition Run, none per-line: recognition basis (Days or Months), raw ledger rows vs. a standalone journal voucher, and — if wrapped — auto-submit vs. draft. A further switch gates whether the monthly Recognition Run fires automatically; while on, manual out-of-cycle runs are blocked, and while off, only manual runs create batches.

Recognition Run — a submittable batch record scoping one pass: a company, a direction (Income or Expense), a date range of invoices to consider, and an optional single Deferral Account to narrow the run instead of the whole company (harmless in practice, since a Ledger Account belongs to one legal entity). Submitting it triggers recognition for every Deferral Line whose window overlaps the range; cancelling it reverses only the postings it produced.

Income / Expense Ledger Account — the ordinary profit-and-loss account the amount ultimately belongs in: what would have been credited (revenue) or debited (expense) immediately, had scheduled recognition not been enabled.

3.2 Algorithm — finding each line’s next window

For every Deferral Line touched by a run, the engine answers “how much time have I not yet recognized, and how far can I recognize this pass”:

find_next_window(line, run_window_end):
    last_posted = latest not-cancelled posting against this line's Deferral Account
    start = last_posted.posting_date + 1 day  if last_posted exists  else line.service_start_date
    end = last day of start's calendar month
    is_final = false

    if end >= line.service_end_date:
        end, is_final = line.service_end_date, true
    elif line.service_stop_date and end >= line.service_stop_date:
        end, is_final = line.service_stop_date, true

    if end > run_window_end: end = run_window_end   # never book past this run's cutoff
    if start > end: return nothing to book            # line is already caught up
    return (start, end, is_final)

Recognition advances one calendar month at a time: a run recurses through as many months as fit its own date range, using the previous window’s end as the next start, until the run’s window end is reached or the line’s final window is hit.

3.3 Proration arithmetic

Both bases share one invariant: the last window for a line is never computed by the formula at all — it is simply the line’s net amount minus everything already recognized for it. That single rule absorbs every prior period’s rounding, so the running total can never overshoot or fall short of the net amount.

Days basis — each non-final window’s amount is a straight ratio of days:

amount_for_window = net_amount * (days_in_window / days_in_full_service_period)

Worked example: a line worth 1,000 covering 2026-01-16 through 2026-02-15 (31 days total).

  • First window: 2026-01-16 to 2026-01-31 (16 days). amount = 1000 * 16 / 31 = 516.13 (rounded to currency precision).
  • Second window reaches the service end date, so it is the final window: amount = 1000 - 516.13 = 483.87.
  • Total recognized: 516.13 + 483.87 = 1000.00 — exact, no residual.

Months basis — a fixed number of calendar months is derived from the service window, then divided evenly:

total_months   = (end.year - start.year) * 12 + (end.month - start.month) + 1
prorate_factor = days_in_full_service_period / days_in_the_calendar_months_it_spans
actual_months  = round(total_months * prorate_factor, 1)
amount_per_month = net_amount / actual_months

A partial-month window further scales amount_per_month down by its own day-count ratio. A second safety net runs on every window, not just the final one: if the computed amount plus everything already recognized would exceed the net amount, it is clamped to exactly what’s left.

Worked example, from this behavior’s own test fixture: a line worth 3,000 over three full calendar months (May 1–July 31). actual_months = 3, each month’s share is 1000. A run whose window ends June 30 recognizes May’s 1000 and June’s 1000 in the same pass; July’s 1000 waits for the run that reaches July 31, the final window.

For multi-currency lines, this arithmetic (remainder rule included) runs twice: once against the base-currency net amount, once against the transaction-currency net amount, whenever the two differ.

3.4 Posting direction

The direction of the two legs flips between the initial posting and every later recognition posting:

  • Revenue. At submission, a Deferral Line credits the Deferral Account (a liability) instead of the Income Ledger Account; the customer-facing debit (receivable) posts as normal. Each recognition window debits the Deferral Account and credits the Income Ledger Account, running the liability down as income is earned.
  • Expense. At submission, a Deferral Line debits the Deferral Account (an asset) instead of the Expense Ledger Account; the supplier-facing credit (payable) posts as normal. Each recognition window credits the Deferral Account and debits the Expense Ledger Account, running the prepaid asset down as expense is consumed.

Both directions post as a plain two-line batch handed to the posting funnel, or as a standalone journal voucher carrying the same two legs (plus any accounting dimensions copied from the line), per the Deferral Policy’s posting-style switch.

3.5 Error handling, cancellation, and early termination

  • Closed-period handling. If books are frozen through a date at/after a window’s computed end, that window posts on the first open day after the freeze instead — the window boundaries (and amount) are unaffected, and the engine keeps recursing using the original, un-shifted end date as the next seed.
  • Failure isolation. An exception recognizing one invoice is caught, rolled back, and logged, but the run continues to remaining invoices rather than aborting. If anything failed, one summary notification points at the run to fix and resubmit — it doesn’t name every failing invoice.
  • Invoice cancellation reverses everything. Every recognition posting carries the same voucher type and number as the originating invoice — only the “against” cross-reference distinguishes which run produced it — so cancelling the invoice reverses the initial entry and every recognition ever posted against it, in one pass, regardless of how many periods had run. There is no partial-cancel path. Amending is a cancel plus a fresh draft, so the full unwind happens first; the amended copy starts with no history.
  • Cancelling a Recognition Run itself is narrower: it reverses only that run’s own postings, leaving other runs’ postings for the same invoice untouched.
  • Early termination doesn’t reduce the total. A stop date doesn’t shrink what’s ultimately recognized — the window reaching it is the final window and books the entire remaining balance at once. It accelerates recognition; it is not a write-off.

4. Scale and Reliability

  • Load shape: purely batch, off the request path — one scheduled invocation per legal entity per direction per month, not one job per invoice. Work scales with the number of distinct invoices whose Deferral Line windows overlap the prior month, not with total invoice volume.
  • Idempotent resumption is structural. The next window always derives from the latest not-cancelled posting already on the ledger for this exact line, so a re-triggered run — after a crash, or manually for catch-up — simply finds nothing left to book for any line already current; there is no separate progress counter to fall out of sync.
  • No visible concurrency guard. Nothing appears to lock a (legal entity, direction, account) combination while a run is in progress; two overlapping runs could both compute the same “next window” before either posts, producing a duplicate — a real gap given the engine relies entirely on read-then-post idempotency rather than an exclusive lock.
  • Per-invoice document loads, not set-based computation. Each candidate invoice is loaded as a full document and processed line by line; a large number of concurrently active Deferral Lines would make this loop, not the candidate-selection query, the first bottleneck.
  • Closed-period handling doubles as a reliability control: it guarantees a posting can never land inside a period already closed for reporting, even when a run is delayed well past its window.
  • Monitoring: the one summary notification per failed run is coarse — it flags that the run needs attention but not which invoice, so diagnosis still requires opening the run and re-deriving what it touched.

5. Trade-off Analysis

Decision Trade-off
Recognition postings share the source invoice’s voucher identity; only the run is a separate cross-reference Simple to trace an invoice’s full history in one place, but cancelling the invoice is all-or-nothing — every recognized period unwinds together. An independent voucher identity per posting could enable a “cancel going forward only” reversal, at the cost of a more complex history lookup.
Recognition basis (Days vs. Months) is a single company-wide switch One setting to reason about, but changing it mid-flight only affects future runs — a company that switches basis partway through a long-lived line sees genuinely different math applied to earlier vs. later periods.
Deferral Account is chosen explicitly per line, with no auto-provisioning Misconfiguration surfaces immediately as a validation failure rather than silently defaulting to a guessed account — safer, but it puts the setup burden entirely on whoever creates the line.
Posting style (raw rows vs. wrapped journal voucher, auto-submitted or draft) is a company-wide switch Lets an organization choose “fully automatic” vs. “always reviewed,” but as one lever for the whole company — no way to require review for some accounts and automation for others.
Failure handling isolates per invoice and reports once per run Keeps one bad invoice from blocking a month’s recognition for everyone else, but a run with several unrelated failures produces one generic notice rather than an itemized list.
Early termination forces full recognition rather than a write-off Conservative — no unrecognized balance is ever silently stranded — but there is no distinct mechanism to genuinely write off a remaining balance instead of recognizing it.

6. What to Revisit as the System Grows

  • Concurrency control around “find the next window.” Add an explicit lock (or a uniqueness constraint at the posting level) scoped to a legal entity, direction, and account, so two overlapping runs can no longer race to compute — and both post — the same window. Today’s safety net is read-then-post idempotency alone, which does not hold up under genuine concurrent execution.
  • Reversal granularity. As service windows lengthen (multi-year contracts, long subscriptions), all-or-nothing invoice cancellation becomes a heavier operation to reason about. A voucher-identity scheme that let a run’s postings reverse independently of the source invoice’s own cancellation would enable a “close out remaining periods without touching history already recognized” workflow that doesn’t exist today.
  • A genuine write-off path for early termination. A stop date always forces full recognition of whatever balance remains; if the business wants to instead leave or write off an unrecognized remainder when a service ends early, that needs a second, distinct mechanism.
  • Bulk computation instead of per-invoice document loads. Once simultaneously active Deferral Lines per legal entity grow large, replacing the per-invoice load-and-loop with set-oriented computation (batch-fetching last-posted dates, batch-computing windows) is the natural next optimization before a run’s wall-clock time becomes a problem.
  • Finer-grained failure reporting. An itemized list of which invoices failed and why, attached to the run or its notification, would remove the “reopen the run and guess” step required after a partially failed pass.

Was this page helpful?