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

Approval Authorization & Transaction Limit Control

How a value-based rule blocks submission of a transaction until the right role or user signs off

1. Requirements

1.1 Functional requirements

  • Gate the ability to submit (finalize) certain transaction types once their value crosses a configured threshold, restricting who may cross that gate to a specific role or a specific user.
  • Thresholds must be definable at several levels of specificity: an overall document total, an average discount percentage, a discount scoped to one customer, one item, or one item group.
  • A threshold can be scoped to one legal entity, or left entity-agnostic so it applies wherever a more specific, entity-scoped rule doesn’t already cover the same transaction.
  • Rule applicability is itself scoped independently of the threshold: to one user, to one role, or left unscoped so it applies to anyone.
  • Coverage spans seven named transaction types across both the sales and purchase document families.
  • A rule must be re-evaluated whenever a submitted document’s value is edited after the fact, not only at the moment of original submission.

1.2 Non-functional requirements

  • Cheap when unconfigured: with no rules defined anywhere in the system, evaluation must be a near-zero-cost no-op rather than a chain of empty queries.
  • Synchronous and blocking: a rule violation must hard-stop the submission with a named list of who is authorized, not merely warn.
  • Configurability over throughput: rules are an editable master list an administrator maintains directly, not a compiled policy engine — favoring operator control at the cost of re-querying rule tables on every submission.

1.3 Constraints

  • Rule matching only ever answers “does the current session hold role X” or “is the current session user X” by asking the platform’s own role/permission framework — this subsystem does not implement its own identity or role storage, and that framework is explicitly out of scope for this document.
  • Amounts are compared using each document’s own base (company-currency) grand total; there is no separate multi-currency threshold definition.

2. High-Level Design

2.1 Component diagram

2.2 Data flow — evaluating a submission against configured rules


3. Deep Dive

3.1 Data model

Authorization Rule — the configuration record. Fields:

  • transaction — one of seven named types: Sales Order, Purchase Order, Quotation, Delivery Note, Sales Invoice, Purchase Invoice, Purchase Receipt.
  • based_on — the basis for the threshold: Grand Total, Average Discount, Customerwise Discount, Itemwise Discount, Item Group wise Discount, or Not Applicable.
  • customer_or_item + master_name — a dynamic link that scopes an item-wise, item-group-wise, or customer-wise rule to one specific master record; left unset, the rule applies across all customers/items/item groups for that basis.
  • company — optional. A rule with a company set only binds transactions for that legal entity; a rule with no company acts as a fallback wherever no company-scoped rule for the same transaction/basis/applicability exists.
  • value — the threshold, in the document’s base currency (or a discount percentage, depending on the basis).
  • Applicability — either system_user or system_role (who the rule applies to), and independently to_emp/to_designation (an unused applicability path — see § 3.2).
  • approving_role / approving_user — who is authorized to submit once the threshold is crossed.

A save-time check rejects an exact duplicate (same transaction, basis, applicability, and approver combination) and enforces basic sanity: at least one of approving role/user must be set; the approver cannot be the same role or user the rule applies to; discount-based bases are disallowed for the three purchase-side transaction types (plus one non-transactable type that no longer appears in the field’s own option list — a vestigial exclusion); an Average Discount threshold cannot exceed 100; a Customerwise Discount rule requires a master_name.

Authorization Control — not a data record in any ordinary sense: it is a singleton with no fields of its own, existing purely to host the enforcement methods below behind a cached, shared instance. Every caller resolves the same cached singleton rather than loading a record.

3.2 Algorithm — how a rule is matched and what makes it bind

The sole enforcement entry point is validate_approving_authority(transaction_type, company, total, document?).

  1. Cheap early exit. If zero Authorization Rule records exist anywhere in the system, return immediately — no queries beyond the one count.
  2. Average-discount input. If a document object was passed in, compute an average discount percentage across its lines (list price vs. actual rate, net of any header discount). This figure feeds the Average Discount and Customerwise Discount bases; without a document object, this stays at zero for the rest of the call (see § 3.3 for why that matters).
  3. User-scoped rules first. Look up which bases (Grand Total, Average Discount, etc.) have a rule naming the current session user specifically, for this transaction type and this company (or a company-agnostic fallback). Evaluate each such basis immediately, then remove it from further consideration — except Itemwise and Item-Group-wise, which stay eligible for re-checking because a per-item rule and a per-item-group rule can coexist.
  4. Role-scoped rules next, restricted to bases not already resolved in step 3, matched against every role the session holds.
  5. Unscoped (global) rules last, for whatever bases neither a user-specific nor a role-specific rule already covered.
  6. Per basis, matching and binding. For a given basis, a candidate rule qualifies only if its threshold value is at or below the actual amount — i.e., the transaction has crossed that ceiling. Among all qualifying rules, the highest crossed threshold governs (the strictest ceiling the transaction has actually passed). The approving role(s)/user(s) named on the rule(s) at that value are collected, and — in the same query — approving identities from any rule whose value exceeds the transaction’s own total are pulled in as well, which can widen the eligible-approver set beyond the rule that actually matched (see § Trade-off analysis).
  7. The bind check. If the current session’s roles intersect the collected approving roles, or the session user is among the collected approving users, evaluation for that basis passes silently. Otherwise it hard-stops with an explicit message naming every eligible role/user.
  8. Itemwise and Item-Group-wise bases run this whole matching process once per line of the document, first checking for a rule scoped to that specific item/item group, and only falling back to a rule with no master set if no item-specific rule matches.

A second method, get_value_based_rule, resolves a rule by looking up an employee’s designation rather than a session role — but nothing in the source tree calls it; it is a defined-but-unused code path, not a live enforcement mechanism, and is not part of the contract below.

3.3 The enforcement contract — eight callers, one shared shape

All eight call sites share the identical call shape: validate_approving_authority(document_type, document.company, document.base_grand_total[, document]). Seven live inside the on_submit hook of the Quotation, Sales Order, Sales Invoice, Delivery Note, Purchase Order, Purchase Invoice, and Purchase Receipt controllers; the eighth lives inside the shared utility that lets an operator edit quantities or rates on an already-submitted Sales Order or Purchase Order, re-checking the recalculated total after the edit rather than only at original submission.

The contract each caller satisfies is narrow: pass the document’s own type name, its company, and its base-currency grand total; optionally pass the document object itself. That last argument is where the callers genuinely diverge, and the divergence has a real consequence: Quotation, Sales Order, Sales Invoice, and Delivery Note all pass the document object, so every basis in § 3.2 is fully live for them. Purchase Order, Purchase Invoice, Purchase Receipt, and the shared post-submission item-update path all omit it — which means, for those four callers, the average-discount figure never leaves zero, and the Itemwise/Item-Group-wise loops (which only run when a document object is present) never execute at all. In effect, those four callers only meaningfully enforce Grand-Total-based rules; a Customerwise, Itemwise, or Item-Group-wise Discount rule configured against a purchase-side transaction type is essentially inert in production, since no caller ever supplies the line data that basis needs to bind.

3.4 Error handling

  • No rules configured: a guaranteed no-op, not a special case handled per basis.
  • Threshold crossed, no eligible approver in session: a hard-stop exception naming every role/user collected for the matched basis; the submission is blocked outright, not merely flagged.
  • Malformed rule definitions: rejected at the rule’s own save time (duplicate combination, missing approver, discount basis on a disallowed transaction type, out-of-range discount value, missing master for a customer-wise rule) — enforcement never has to handle a structurally invalid rule at submission time.
  • Company scoping fallback: every lookup tries the company-scoped rule first and only falls back to a company-agnostic rule if none exists for that company — a company-scoped rule for the same transaction/basis always takes precedence.

4. Scale and Reliability

  • Load pattern: evaluated once per submission of a covered transaction type, plus once per post-submission line-item edit on a Sales Order or Purchase Order — proportional to transaction volume, not a background or scheduled process.
  • The empty-configuration case dominates in practice. Organizations that never define an Authorization Rule pay a single existence check per submission and nothing more; the more expensive per-basis matching only runs once rules actually exist.
  • No caching of rule resolution itself. Each submission re-runs the full user-then-role-then-global cascade against the rule table; this is acceptable because rule tables are small, administrator-maintained, and rarely change, but it does mean rule-table size (not transaction volume) is the main lever on per-submission cost.
  • No concurrency concern specific to this subsystem — it only reads rules and the session’s own identity; it writes nothing, so two simultaneous submissions evaluating the same rule produce no contention.

5. Trade-off Analysis

Decision Trade-off
A single shared enforcement method called from eight sites with one call shape Consistent behavior everywhere it’s wired in, but any future transaction type that needs the same gate must remember to add the call itself — there is no automatic hook.
The document-object argument is optional Lets simple callers skip building extra context, but silently disables discount-basis enforcement for every caller that omits it — a caller-side choice with no error or warning if a discount-basis rule exists but can never bind for that caller.
Highest-crossed-threshold governs, but the approver query also pulls in rules above the transaction’s own total Simplifies the SQL (one query per basis instead of two), but can widen the eligible-approver list beyond the rule that actually matched, which is a looser-than-intended binding rather than a stricter one.
Rule applicability (user/role) is resolved in strict precedence order — user, then role, then unscoped Predictable and cheap to reason about, but a basis “claimed” by a user-specific rule is never re-checked against a role-specific or global rule for the same basis, even if the user-specific rule’s threshold happens to be more permissive.
An unused employee/designation-based rule lookup exists alongside the live session-based one Two lookup shapes coexist in the class with no shared contract between them — a maintainer reading the class must determine which method is actually reachable before trusting either.

6. What to Revisit as the System Grows

  • Give discount-basis rules real coverage on the purchase side. Purchase Order, Purchase Invoice, and Purchase Receipt currently only enforce Grand Total thresholds in practice; if discount-based purchase approval is genuinely needed, those callers need to start passing their document object through.
  • Tighten the approver-resolution query so it only ever returns approving roles/users from the rule that actually matched the crossed threshold, not also from any rule with a higher, uncrossed threshold.
  • Retire or wire up the employee/designation-based rule lookup — a second, parallel matching mechanism with no caller is a maintenance trap for whoever next touches this class.
  • Consider caching rule resolution per (transaction type, company) if rule tables grow large enough that the per-submission cascade becomes measurable, since nothing here currently short-circuits beyond the all-rules-empty case.
  • Make the applicability precedence explicit in the rule’s own editor, since “a user-specific rule pre-empts a role-specific or global rule for the same basis regardless of threshold” is a real behavior an administrator configuring overlapping rules would not otherwise discover.

Was this page helpful?