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

Budgeting & Cost Center Allocation

A design reference for spend-limit enforcement and percentage-based cost redistribution inside a single-ledger ERP system

1. Requirements

1.1 Functional requirements

  • Let a Legal Entity define a Budget Envelope: a ceiling on one Ledger Account, scoped against exactly one dimension value — a Cost Center or a Project, never both — over a date span expressed as a from/to Fiscal Year pair.
  • Enforce the ceiling against three independent, togglable categories: open Purchase Orders (ordered but unbilled), open Material Requests (requested but not ordered), and booked actual expense (real Ledger Postings). A fourth mode sums all three into one cumulative ceiling.
  • For each category, separately for an annual figure and an accumulated month-to-date figure, let the operator pick Stop (block), Warn (proceed with a message), or Ignore.
  • Prorate the annual ceiling across sub-annual periods (Monthly/Quarterly/Half-Yearly/Yearly cadence) so the month-to-date ceiling reflects only the periods that have started by the transaction date, not the full annual number.
  • Allow periods to be distributed evenly or hand-edited, keeping totals reconciled to the envelope’s overall amount, and let a submitted envelope be revised — cancel and clone into a fresh draft, preserving its distribution shape.
  • Block duplicate Budget Envelopes for the same (Legal Entity, dimension value, Ledger Account) whose fiscal-year spans overlap.
  • Let a Legal Entity redirect postings recorded against one “main” Cost Center into several “sub” Cost Centers by a fixed percentage split, transparently, when a Ledger Posting is written — the originating voucher never has to know the split exists.
  • Support hierarchical dimensions: a Budget Envelope set against a group-level Cost Center aggregates actual expense from every descendant leaf underneath it.
  • Downgrade any Stop into a Warn when the acting user holds a Legal-Entity-configured “exception approver” role.

1.2 Non-functional requirements

  • Balance preservation: a redistribution split must still produce postings whose total debit equals total credit after rounding, even under aggressive rounding modes.
  • Non-blocking for routine closing: period-close vouchers are always exempt, and cancellation does not re-trigger checks except in one narrow case (§ 3.4).
  • Self-explanatory failures: an exceeded-budget message must name the account, dimension, actual figure, and ceiling breached.
  • Configurable strictness without a global switch: the exception-approver role is a scoped escape hatch, not a company-wide flag.
  • Backward compatibility: an older, simpler validation engine must remain selectable system-wide, with no migration needed to switch between it and the current one.

1.3 Constraints

  • A Budget Envelope’s dimension is exactly one of Cost Center or Project; choosing one clears the other field.
  • The targeted Ledger Account must be a single leaf node whose report classification is Profit and Loss — group nodes and balance-sheet accounts are rejected outright.
  • Total distributed amount/percent across a Budget Envelope’s periods must reconcile to its own total within a small tolerance before saving.
  • A Cost Redistribution Rule’s effective date cannot precede the latest Ledger Posting already recorded against its main Cost Center, and a Cost Center already a “main” in one rule cannot appear as a redirect target elsewhere (or vice versa).

2. High-Level Design

2.1 Component diagram

2.2 How the pieces cooperate

The posting funnel described in the general-ledger design is where enforcement actually lives — this module adds the configuration and the redistribution step that funnel consults, in this order:

  1. A whole-batch check runs against every proposed line before anything is written, using whichever engine the global toggle selects. Skipped on cancellation, for period-close vouchers, and — as a whole step — when the legacy toggle is on (the legacy engine only runs the per-line check below).
  2. If a line’s Cost Center is the “main” Cost Center of an active Cost Redistribution Rule, the line’s full, pre-split amount is checked against any Budget Envelope on that main Cost Center, and only then is the line fanned out into sub-Cost-Center Ledger Postings.
  3. A per-line check runs immediately after each Ledger Posting is written, using the legacy checking function regardless of which engine handled step 1 — the same backstop described in the general-ledger design, and why a Journal Entry’s own cancellation re-validates while other voucher types do not.

3. Deep Dive

3.1 Data model

Budget Envelope — the central record: budget_against (Cost Center or Project, mutually exclusive), company (Legal Entity), account (a single leaf, Profit-and-Loss Ledger Account), from_fiscal_year/to_fiscal_year (resolved into hidden start/end dates), budget_amount, distribution_frequency, distribute_equally (defaults on), a child table of Distribution Period rows, and a read-only distributed-total mirror. A revision_of field (plain text, not a strict link) marks a record produced by revision. Four applicability checkboxes — Material Request, Purchase Order, actual expense, cumulative expense — each pair with annual and accumulated-monthly action selects. Six of these eight action fields stay editable after submission, letting an operator retune an active envelope without a full revision; the cumulative-expense pair is not, an inconsistency worth normalizing (§ 6).

Distribution Period — one child row per cadence period: start_date, end_date (read-only, computer-derived), amount, percent — together the envelope’s distribution curve.

Cost Redistribution Rule — a submittable record naming one main_cost_center, its Legal Entity, a valid_from date, and a child table of Redistribution Splits (cost_center target + percentage, must total exactly 100). Several rules can coexist for the same main Cost Center at different dates; the most recent one whose date has passed wins.

Cost Center — a hierarchical master per Legal Entity with is_group/disabled flags and a nested left/right index for descendant lookups. A leaf becomes a group only if it has no Ledger Postings; a group becomes a leaf only if it has no children and is neither a main Cost Center nor a split target in any submitted rule.

Seasonal Distribution Template — a separate, standalone 12-row percentage curve (named, optionally tied to a Fiscal Year) that must sum to 100%, distinct from the envelope’s own periods. A helper seeds even 100/12 defaults; a lookup aggregates consecutive months into quarterly/half-yearly/yearly windows for other periodic-target reporting. It is not consumed by the Budget Envelope here — that link, and a fiscal-year field it once read, were removed when the envelope’s schema was reworked to carry its own inline periods. A couple of the envelope’s client-side field handlers still reference those removed fields; the references are now inert.

3.2 Distribution curve generation (proration algorithm)

A save-time procedure on the Budget Envelope, not a query-time computation:

1. Regenerate the Distribution Period rows when: the envelope is new or
   has no rows yet; from/to Fiscal Year, amount, or cadence changed; or
   "distribute equally" is checked. Otherwise, if only the amount changed
   and the envelope is NOT distributed equally, rescale each existing
   row's amount by its own recorded percent instead of rebuilding periods
   (preserves a hand-edited, uneven curve across an amount bump).

2. To regenerate: slice the budget's start/end dates into periods sized
   by cadence (Monthly=1 month, Quarterly=3, Half-Yearly=6, Yearly=12).
   Each period starts on the first day of its start month; its end is
   the cadence window's last day, capped at the overall end date so a
   trailing partial period is shortened, not overrun.

3. Assign every period an equal percentage (100 / period count) and a
   proportional amount, rounded to 3 decimals.

4. On every save: sum the rows' amounts and percentages. If either total
   drifts from the envelope's own amount / 100% by more than 0.10, block
   the save.

A one-year Monthly envelope always produces twelve equal-percent rows unless hand-edited; a Quarterly envelope over a partial remaining year still produces a correctly shortened final period.

3.3 Accumulated ceiling and the two “actual expense” windows

The accumulated (month-to-date) ceiling for a posting date is the sum of every Distribution Period row whose start date has already begun by then — a Monthly envelope checked on 3 March already has January, February and March available, while one checked on the last day of February has only two.

The two enforcement implementations genuinely diverge in how they compute actual expense against that ceiling. The legacy path sums (debit − credit) across confirmed, non-cancelled Ledger Postings for the same account, Legal Entity, and dimension (expanding to descendant leaves for a group dimension), restricted to postings within the envelope’s entire configured span — which can cross more than one Fiscal Year. The current path builds a lookup keyed by (dimension type, dimension value, account) across active envelopes, expanding group dimensions up front, but restricts actual expense to only the Fiscal Year of the transaction’s own posting date — never the envelope’s full multi-year span.

For a single-Fiscal-Year envelope the two agree; for one spanning several years they do not — a real divergence between two files doing the same nominal job, not an invented detail.

3.4 The enforcement decision flow

The annual and accumulated-monthly comparisons are evaluated independently, each triggering its own action — a category can Warn annually but Stop monthly, or vice versa. When cumulative-expense mode is enabled, the same annual/monthly pair runs once more against all three categories summed together, with its own actions. “Ignore” is not special-cased beyond simply not matching “Stop” or “Warn” — an unset action behaves identically, both letting the transaction proceed silently.

3.5 Cost Center redistribution — the percentage fan-out

The redistribution intercept sits inside the general-ledger design’s line-processing step (skipped only for period-close vouchers): for every proposed line whose Cost Center matches an active rule’s main Cost Center, the engine looks up the most recently effective rule and fans that line into several:

Proposed line: credit 100, cost_center = "Main Cost Center 1"
Active rule found (valid_from <= posting date):
  Sub Cost Center 1 -> 60%
  Sub Cost Center 2 -> 40%

Budget check: any Budget Envelope keyed on ("Main Cost Center 1", this
  account) is evaluated against the full, pre-split credit of 100 —
  before the fan-out happens.

Ledger Postings actually written:
  credit 60 -> cost_center = "Sub Cost Center 1"
  credit 40 -> cost_center = "Sub Cost Center 2"

A round-off account is a special case: instead of splitting proportionally it is redirected in full to the rule’s first sub Cost Center, avoiding a fractional leftover spread across accounts. Every other field is rounded per split row at the Ledger Account’s configured precision, which is how debit and credit still balance even when percentages (33/33/34, say) don’t divide evenly under every rounding mode. Because written Ledger Postings only ever carry a sub Cost Center, a Budget Envelope set directly on a “main” Cost Center shows little accumulated actual expense from ordinary history once a rule is active — its real enforcement moment is the pre-split check above, against each transaction’s full amount as it happens.

3.6 Guardrails on Cost Redistribution Rules

Beyond the 100%-split total already noted, a rule’s valid_from cannot predate the latest Ledger Posting already recorded against its main Cost Center, protecting posted history from retroactive reinterpretation. If a newer rule already exists for the same main Cost Center with a later valid_from, saving an older one produces a non-blocking warning that it only applies up to the day before the newer rule starts — both persist, and the most recent applicable one wins at query time. A main Cost Center cannot appear among its own splits; a Cost Center used as a main elsewhere cannot be a split target, and vice versa — the two roles are mutually exclusive Legal-Entity-wide.

3.7 Error handling

  • Already-broken budgets: creating (not revising) an envelope whose window already shows actual expense above the proposed amount is hard-blocked at save, unconditionally — not configurable via Stop/Warn/Ignore.
  • Revision: cancels the current submitted envelope and inserts a clone as a fresh draft linked back informally via revision_of, preserving the distribution shape.

4. Scale and Reliability

  • Both enforcement paths run synchronously inline with voucher submission and every Ledger Posting write, so a slow actual-expense aggregation directly adds latency to any transaction touching a budgeted account — indexed account/dimension/date lookups on the ledger matter more here than in most reporting paths.
  • The current implementation batches cross-referencing: one lookup of keys per document or Posting Batch, answered once per distinct key rather than once per line — scaling with distinct budgeted combinations, not line count, which matters for high-line-count Purchase Orders.
  • Group-dimension budgets expand to every descendant leaf through a single indexed range lookup rather than a per-child loop; Cost Redistribution Rule lookups are cached per (Legal Entity, posting date, Cost Center) within a request, so a batch with many lines against one main Cost Center pays that lookup cost once.
  • The dual-engine design is a reliability lever independent of performance: if the current implementation regresses, an operator falls back to the legacy engine with a single flag flip and no migration, at the cost of batched cross-checking and cumulative-expense granularity.
  • The backdating guard on Cost Redistribution Rules avoids expensive retroactive ledger reposts that would otherwise be needed to reconcile a rule change against already-written history.

5. Trade-off Analysis

Decision Trade-off
Two coexisting enforcement engines, toggled globally Escape hatch if the newer engine regresses, at the cost of two implementations and, per § 3.3, different “actual expense” windows for multi-Fiscal-Year envelopes — a toggle flipped for unrelated reasons can silently change enforcement semantics.
Redistribution validated against the pre-split amount, not the fragments Correct control-wise — the main Cost Center’s budget reflects the whole transaction — but persisted Ledger Postings there stay sparse, so reading history off the ledger shows a different picture than what was enforced.
Accumulated ceiling stepped by whole Distribution Periods, not day-prorated Simple and predictable, but a step function, not a ramp — day one of a new period gets the full allocation; the last day of the prior period does not.
“Ignore” and an unset action behave identically One fewer state to special-case, but an unconfigured field is indistinguishable from a deliberate “don’t enforce” choice.
Six of eight action fields editable after submission; cumulative-expense pair is not Lets most categories be retuned without a formal revision, but the exception is unexplained in the schema.
Cost Center main/sub roles mutually exclusive, enforced at save Keeps redistribution a simple one-level fan-out, at the cost of not supporting genuinely tiered allocation.
Seasonal Distribution Template kept separate, no longer linked to the Budget Envelope Each consumer owns its distribution logic independently, but seasonality assumptions must be maintained twice if both should agree.

6. What to Revisit as the System Grows

  • Converge the two enforcement engines, or at minimum make the actual-expense window (whole span vs. current-Fiscal-Year-only) an explicit, documented choice rather than a side effect of the toggle — the § 3.3 divergence is easy to miss until a multi-year budget behaves unexpectedly.
  • Give “Ignore” a distinct, visible state from an unset action, so a never-fully-configured category doesn’t silently behave as deliberately unenforced.
  • Reconsider the one-level-only redistribution model if tiered allocation (a sub Cost Center itself further redistributed) is ever needed — the mutual-exclusivity guard would need to become hierarchy-aware.
  • Re-link or formally retire the Seasonal Distribution Template relative to the envelope’s own periods — the two percentage-curve mechanisms are independent today, and the envelope’s client-side script still references fields removed in its schema rework, a maintenance trap for anyone expecting that script to still work.

Was this page helpful?