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

Order-to-Cash Lifecycle

How a price quote becomes a committed order, a physical dispatch, a bill, and a settlement — and how completion status is kept honest across all four

1. Requirements

1.1 Functional requirements

  • Represent the sell-side pipeline as a chain of distinct, independently submittable/cancellable documents: a non-binding Quotation, a committed Sales Order, a physical goods dispatch (the fulfillment document described in the delivery-and-shipment design), a Sales Invoice, and a Settlement Entry (the accounting posting described in the general-ledger design) — each one linked back to its immediate predecessor by a stored reference, never inferred.
  • Convert any stage into the next through a field-mapping operation that copies header defaults (customer, currency, sales team, territory) and, line by line, proposes only the remaining unfulfilled quantity or amount — order quantity minus what a quotation line already produced, dispatch quantity minus what has already been delivered, invoice amount minus what has already been billed — so converting the same source document twice never re-proposes a line that is already fully consumed.
  • Track, on the Sales Order itself, two independently derived completion percentages: how much of the order’s stock quantity has physically left the warehouse, and how much of its value has been invoiced. Each is a live recomputation across every submitted downstream document that currently references the order, never an incrementally patched counter.
  • Derive a small, closed set of coarse lifecycle statuses for the order (not yet fulfilled, partially or fully awaiting delivery/billing, fully complete, awaiting an advance payment, on hold, closed, cancelled) purely as a function of those two percentages, a manual hold/close flag, and the document’s own submit/cancel state — never a status a user sets directly.
  • Support partial fulfillment: a buyer may nominate only some order lines for a given dispatch or a given invoice run, and the completion math must account correctly for the lines left behind.
  • Support returns at the dispatch and invoice stages (a negative-quantity dispatch, a negative-quantity or credit invoice) that roll back the same completion percentages they originally advanced.
  • Gate a Sales Order’s submission — and re-check it on every later edit — against the customer’s rolling credit exposure: already-posted receivable balance plus the not-yet-billed value of every other open order for that customer, each weighted by how much of it is still unbilled.
  • Let a request for an advance payment against the order move it into a distinct “awaiting payment” condition, tracked independently of delivery/billing completion, and reflect the payment’s eventual settlement back onto that same condition.
  • Allow a Quotation to be explicitly marked lost — with reasons and competitor detail — only while none of its lines have yet been converted into a committed order, and propagate that event back to the loosely linked opportunity or lead it originated from, if any.
  • Support an alternate fulfillment path where a supplier ships directly to the buyer against one order line (drop-shipping), without requiring a dispatch document for that line at all.

1.2 Non-functional requirements

  • Idempotent conversion: re-running any stage-to-stage conversion after a partial fulfillment must never re-propose a fully consumed line, without relying on a run-once flag.
  • Auditability of percentage math: a completion percentage exposed on an upstream document is always a live recomputation across everything currently pointing at it, never a value trusted to remain correct after an unrelated edit.
  • One shared derivation engine, many callers: the same low-level percentage-and-status routine backs every stage of this pipeline (and the equivalent buy-side pipeline), so a correction to that routine benefits every caller at once — at the cost of every caller needing to hand it an equivalently shaped configuration rather than writing its own arithmetic.
  • No silent parallel status: a document’s visible status must always be re-derivable from currently stored fields; nothing about status may be hand-edited independently of the fields that determine it.

1.3 Constraints

  • The pipeline is document-oriented, not workflow-oriented: nothing enforces that one Sales Order produces exactly one dispatch and one invoice. An order can be partially dispatched and partially invoiced by any number of documents, in either order, subject only to a configurable over-allowance.
  • A billing document can be raised directly off the order line’s own reference, or off a dispatch line’s reference — both are legitimate, and the system distributes billed value across sibling dispatches (oldest first) when a bill only names the order line.
  • The Sales Invoice’s own visible status is not derived through the shared percentage mechanism described here at all; it is a separate override keyed off the invoice’s live outstanding balance (the general-ledger design’s derived balance figure), described only briefly in §3.1 since ledger posting itself is out of scope here.

2. High-Level Design

2.1 Component diagram

2.2 The stage-to-stage conversion contract

Every conversion in this pipeline is a mapping operation, not a fresh document built from scratch, and every one follows the same shape:

  1. Header defaults are copied from the source document (customer, currency, sales team, territory, a referring sales partner’s commission rate) — see the sales-partner-and-commission design for how those defaults were set in the first place.
  2. Each source line is offered only if it still has something left to convert. A Quotation line is offered if its still-open quantity (quantity minus whatever quantity other Sales Orders have already claimed against it) is positive. A Sales Order line is offered to a dispatch if its quantity minus its own running delivered_qty is positive, and to an invoice if its amount minus its own running billed_amt is positive (or, when a line carries a non-standard zero-quantity “unit price” flag, the line is offered as-is, since its true quantity is deliberately left uncertain until fulfillment).
  3. The proposed quantity/amount on the new line is the remainder, not the original. A dispatch line’s proposed quantity is source qty − delivered qty so far; an invoice line raised directly off the order is source amount − billed amount so far, further reduced by whatever an intervening dispatch has already billed if the invoice is instead raised off that dispatch.
  4. A standing-commitment (blanket order) reference, if the source line carries one, rides along unchanged onto the new line, so a quantity/price freeze established far upstream (see the blanket-order design) is never lost across a conversion.
  5. A caller may narrow the conversion to a subset of lines (selected rows, a delivery-date range, or a specific batch of source documents processed together); this only changes which lines are offered, not the remainder arithmetic in step 3.

This is why converting an order that has already been half-delivered a second time is safe: every already-delivered unit’s line either drops out of the offer entirely or is reduced to whatever remains, never re-proposed at its original size.

2.3 Data flow — how completion percentages actually get written

The general-ledger design already establishes that a small number of shared primitives back many document types; this pipeline is the clearest illustration of it, because the same completion percentages get written through four structurally different call patterns rather than one:

These four patterns produce the same handful of fields, but only the first two ever touch the generic percentage routine. The other two bypass it entirely, for different reasons: the drop-ship path (Pattern 3) has no dispatch to sum from, so it recomputes per_delivered by hand from the supplier’s own receipt quantity; the zero-total path (Pattern 4) exists because the generic ratio is undefined when a referenced document’s own total is zero, so it recomputes per_billed by hand too — and, while it is in there, computes that ratio from quantity sums instead of the amount sums every other pattern uses, and derives its own Not/Partly/Fully label rather than calling the shared label step. Both bypasses still explicitly trigger the same overall-status re-evaluation the config-driven patterns trigger implicitly as part of writing their fields. A reader who assumes percent-delivered and percent-billed are always produced by the generic routine will miss both of these bypasses; a reader who assumes every non-generic case is at least consistent with the shared arithmetic will miss that Pattern 4 quietly changes what is being divided by what.


3. Deep Dive

3.1 Data model

Quotation — the non-binding entry point. Carries its own conversion state (Open, Partially Ordered, Ordered, plus Lost/Expired/Cancelled), computed live by comparing each line’s quantity against however much of it has been claimed by submitted Sales Order lines — a pull-style check, not a value pushed onto it from the order side (the order side only pushes the raw claimed-quantity figure the pull then reads). A quotation whose validity date has passed and that was never converted is swept to Expired by a periodic check; one that is manually declared lost records reasons and competitor detail and notifies the loosely linked opportunity/lead.

Sales Order — the committed document and the anchor of this design. Beyond the two completion percentages already described, it carries an independent advance-payment condition (Not Requested / Requested / Partially Paid / Fully Paid), refreshed whenever the ledger-derived advance amount against it changes, and a status derivation that folds all of the above together (§3.3). It also carries a standing-commitment drawdown reference per line and a sales-team/commission-rate block per document — both detailed in their own designs (blanket-order; sales-partner-and-commission) rather than repeated here.

Dispatch Record — the physical fulfillment document. This design only concerns itself with the two fields it feeds back upstream (delivered_qty per line, rolling up into the order’s per_delivered) and the fact that a Sales Invoice can reference either the order line or the dispatch line directly; everything else about it — valuation, serial/batch handling, shipment/route scheduling — belongs to the delivery-and-shipment design.

Sales Invoice — the billing document. It feeds billed_amt back onto whichever line it references (order or dispatch) and, through the shared engine, per_billed/billing_status onto the order. Its own visible status, however, is not derived through that engine at all: it is computed independently from its live outstanding balance (a return-adjusted, discount-adjusted, ledger-derived figure — see the general-ledger design’s Party Balance Entry), yielding states such as Unpaid, Partly Paid, Overdue, Paid, Credit Note Issued, or Internal Transfer. This is the one place in the pipeline where a document type overrides the shared status derivation outright rather than supplying it a configuration.

Settlement Entry — the eventual accounting posting once an invoice (or an advance against the order) is paid; fully described in the general-ledger design and not re-derived here.

3.2 Algorithm — the shared completion-tracking engine, precisely

A base behavior shared by every document type in this pipeline (and by the buy-side and inventory documents that mirror it) offers three things, invoked in different combinations by different callers:

  1. A per-line sum-and-write step. Given a declared source field, a declared target line and field, and a declared join key, it sums the source field across every currently-submitted row that shares the join key and writes the sum directly onto the target line — this is what fills delivered_qty and billed_amt on an order or dispatch line.
  2. A percentage-and-label step. Given the same declared configuration plus a reference field, it computes sum(min(|actual|, |reference|)) / sum(|reference|) × 100 across every line sharing a parent, rounds it, and derives a Not / Partly / Fully <keyword> label from it — this is what fills per_delivered/delivery_status and per_billed/billing_status on the order.
  3. An overall-status re-evaluation. A closed, ordered list of guard conditions per document type is walked in reverse declared order — meaning the last-declared condition is actually checked first — and the first one whose guard is true wins. This is what turns the raw percentages into the single status label a user sees.

A submitting or cancelling Dispatch Record or Sales Invoice triggers steps 1 and 2 automatically, driven entirely by a small configuration block each document type declares once (which source field, which target, which join key, which percent field to populate, which status label to derive). One situation bypasses that automatic trigger but still calls step 2 directly with a hand-built configuration: a dispatch’s own billed-percentage figure, which must be computed net of whatever has already come back as a return before the standard ratio is applied. Two further situations bypass step 2 entirely, for different reasons. A referenced document whose own total is exactly zero — where the generic amount-based ratio would be undefined — is instead handled by a dedicated method that computes per_billed from quantity sums instead of amount (min(reference qty, billed qty) ÷ reference qty), derives its own Not/Partly/Fully label with a hand-written if/elif/else rather than calling the shared label logic, and writes both directly. A drop-shipped order line has no dispatch to sum from at all, so the order recomputes its own per_delivered by hand from the supplier’s receipt quantity and writes that directly too. Both of these bypass paths still explicitly call step 3 afterward, so the visible status stays consistent with every other line even though the percentage feeding it took a different route to get there.

3.3 Sales Order status lifecycle

Two precise, easy-to-miss points about this diagram:

  • The guard list’s priority order is the reverse of how it reads. The last-declared guard (On Hold) is actually checked first, then Closed, then Cancelled, then Completed, then To Pay, then To Deliver, then To Bill, then To Deliver and Bill, with plain Draft checked last as the fallback. The practical consequence: Completed outranks To Pay. An order sitting in To Pay because an advance payment is still outstanding jumps straight to Completed the instant delivery and billing both reach 100%, without the advance payment’s own state ever being consulted again for status purposes — To Pay only shows while the order is not yet fully delivered and billed.
  • A manual, document-level “skip delivery” flag — not a per-line one — substitutes for 100% delivered wherever delivery completeness is checked. It defaults off and is surfaced only on maintenance-type orders; nothing in the pipeline sets it automatically. Wherever the guards check it, an order flagged this way can reach To Bill and then Completed without a Dispatch Record ever being produced — per_delivered on such an order simply never moves, and nothing in the guard logic requires it to.

3.4 Error handling

  • Over-delivery / over-billing: converting past a line’s remaining quantity or amount is rejected unless a configured allowance (global, or a per-item override) covers the excess, or the acting user holds a specific override role — in which case the excess is logged as a warning rather than blocked. The same allowance mechanism gates every consumer of the percentage engine, not just this pipeline.
  • Credit exposure: submitting (or materially editing) a Sales Order recomputes the customer’s exposure as posted receivable balance plus, for every other open, non-closed order, its not-yet-billed value (grand total × (100 − per_billed) ÷ 100) — directly consuming the same per_billed figure this design tracks. Exceeding the configured limit blocks the action unless the acting user holds a designated override role, in which case the system instead prompts to notify a configured credit controller by email; the check can also be bypassed per customer or per order explicitly.
  • Quotation conversion validity: converting an expired Quotation into an order is blocked unless a setting explicitly permits it.
  • Lost-quotation guard: a Quotation cannot be marked lost once any of its lines has already been converted into a Sales Order.
  • Drop-ship consistency: a drop-shipped line requires a named supplier; once a Purchase Order exists against it, that line’s supplier cannot be changed.
  • Closed-order guard: a closed Sales Order cannot be cancelled directly — it must be reopened first — and cannot be closed at all once both completion percentages already read 100%.
  • Standing-commitment overrun: a line drawing down a blanket order is rejected if the requested quantity exceeds that line’s remaining quantity plus a configured overrun allowance (see the blanket-order design).

4. Scale and Reliability

  • Recomputation cost scales with referencing-document count, not order size. Every dispatch or invoice submit/cancel triggers a full re-sum of every sibling line sharing the same order line, and every write to a percentage field triggers the order’s own overall-status re-evaluation — cheap per event, but an order fulfilled across many small dispatches or invoices pays that re-sum cost repeatedly rather than once.
  • The overall-status guard list is walked in full on every single field write, not batched across the several writes one conversion can trigger — three percentage updates from one invoice submission mean three status re-evaluations, not one.
  • Credit-exposure checking re-scans every other open order for the customer at each submission, rather than maintaining a running total; cost grows with how many orders a single customer keeps open simultaneously.
  • No cross-order locking: two conversions racing against the same order line (e.g., two invoices raised in quick succession against the same remaining amount) are each guarded only by a fresh read-then-write, not a shared lock — the same soft-guarantee pattern described for stock reservation elsewhere in this document set.
  • The drop-ship path is a narrow, low-volume exception, invoked only when a Purchase Order referencing a drop-shipped line is itself submitted, cancelled, or updated — it does not add load to the common dispatch/invoice path.

5. Trade-off Analysis

Decision Trade-off
One shared percentage-and-status engine, configured differently by each document type A fix or new completion rule benefits every consumer at once, at the cost of every caller needing to hand it an equivalent, correctly-shaped configuration rather than writing its own arithmetic — and of at least two callers (drop-ship, and the zero-total case) finding it easier to bypass the engine entirely, one of them also quietly changing which ratio is being computed.
Percentages always recomputed live from current submitted documents, never incrementally maintained Immune to drift from a missed incremental update; costs a full re-sum on every referencing document’s submit/cancel rather than a cheap increment.
Reverse-priority guard list for overall status, rather than an explicit state machine Compact, reused across many document types with very different guard sets — but priority is implicit in declaration order, so Completed silently outranking To Pay is easy to miss when reading the guards top to bottom instead of bottom to top.
Sales Invoice status derived independently from its own outstanding balance, not from the shared engine Gives the invoice a status that directly reflects money owed rather than a delivered/billed ratio — at the cost of this pipeline having two genuinely different status-derivation mechanisms side by side, one shared and one bespoke.
Conversion proposes only the remaining quantity/amount per line, computed fresh each time Makes repeated partial conversion safe without a run-once flag, at the cost of every conversion re-deriving “what’s left” rather than reading it from a single stored counter.
Credit-exposure check blends posted ledger balance with each open order’s live unbilled percentage Catches exposure from orders that haven’t been invoiced yet, not just posted receivables — at the cost of re-scanning every open order for the customer on every submission rather than maintaining a running figure.

6. What to Revisit as the System Grows

  • Consolidate the two bypass paths (drop-ship and zero-total) onto the generic percentage step instead of each hand-computing its own figure — the zero-total path’s silent switch from an amount-based to a quantity-based ratio is exactly the kind of divergence a shared engine is meant to prevent, and two independent bypasses make that drift more likely to recur, not less.
  • Batch the overall-status re-evaluation so that a single conversion touching several lines triggers one re-derivation instead of one per field write, once order volumes make the repeated guard-list walk measurable.
  • Give the credit-exposure check a maintained running figure per customer rather than a full re-scan of open orders at every submission, once customers routinely carry many simultaneously open orders.
  • Make the reverse-priority guard ordering explicit (an assigned priority number per guard, rather than implicit declaration order) for any document type whose guard list grows past a handful of entries — the current form is easy to get wrong when adding a new status without re-reading the whole list bottom to top.
  • Reconcile the two “what’s left to convert” checks — the pull-style check a Quotation uses to derive its own conversion status, and the push-style figure the Sales Order writes onto it — so a future reader does not have to independently verify they always agree.

Was this page helpful?