Procure-to-Pay Lifecycle
How an internal shortage becomes a sourced quote, a committed order, a physical receipt, a bill, and a settlement — and where this pipeline quietly diverges from its sell-side mirror
1. Requirements
1.1 Functional requirements
- Represent the buy-side pipeline as a chain of distinct, submittable/cancellable documents: an internal Material Request (a demand signal, not yet a commitment), an optional Request for Quotation broadcast to several suppliers, one Supplier Quotation per responding supplier, a committed Purchase Order, a Goods Receipt, a Purchase Invoice, and a Settlement Entry (the accounting posting described in the general-ledger design) — each linked back to its predecessor by a stored reference, never inferred.
- Convert any stage into the next through a field-mapping operation that copies header defaults and, line by line, proposes only the remaining unfulfilled quantity or amount — net of whatever has already been ordered, received, or billed — so converting the same source document twice never re-proposes an already-consumed line.
- Broadcast a Request for Quotation to named suppliers, each individually emailable (with a portal path to submit rates directly), tracking each response independently.
- Let a Supplier Quotation be compared against its siblings before one is chosen, and lapse automatically past its validity date.
- Track, on the Purchase Order itself, two independently derived completion percentages: how much of the order’s quantity has been physically received, and how much of its value has been invoiced — each a live recomputation across every submitted downstream document referencing the order, never an incrementally patched counter.
- Derive a small, closed set of coarse lifecycle statuses 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 across any number of receipts and invoices, in either order — including a bill raised directly off the order line rather than a specific receipt, which the system distributes across sibling receipts (oldest first).
- Support returns at the receipt and invoice stages that roll back the same completion percentages they originally advanced.
- Gate order and quotation-request submission against the supplier’s own standing: an outright block or a soft warning driven by a computed vendor-performance grade (a companion design), plus an independent hold/disable/freeze mechanism on the supplier record itself.
- Let a request for an advance payment move the order into a distinct “awaiting payment” condition, refreshed by the same shared mechanism the sell side uses, with its own vocabulary for the states involved.
- Let an invoice itself perform the physical intake, skipping a separate Goods Receipt, when flagged to update stock directly — mutually exclusive with a line that already references one.
- Allow drop-shipment (a supplier ships directly to the buyer’s own customer against one order line) without a Goods Receipt for that line, feeding the buyer’s own received quantity back to the originating sales order via a separate cross-document method.
- One order branch sources finished goods built externally by a supplier from buyer-supplied materials (subcontracting); this design does not describe that flow — it is covered separately.
1.2 Non-functional requirements
- Idempotent conversion: re-running a stage-to-stage conversion after a partial fulfillment must never re-propose a fully consumed line, without a run-once flag.
- Auditability of percentage math: a completion percentage is always a live recomputation, never a value trusted to remain correct after an unrelated edit.
- One shared derivation engine, many callers, per-side configuration: the same percentage-and-status routine backs both pipelines, differently configured per side — and, as this design catalogs, several steps here bypass that routine in ways the sell side’s own bypasses do not mirror.
- No silent parallel status: a visible status must always be re-derivable from stored fields — except the Purchase Invoice, which (like its sell-side counterpart) derives its own status from its outstanding balance, not through this pipeline’s shared engine.
1.3 Constraints
- The pipeline is document-oriented, not workflow-oriented: an order can be partially received and partially billed by any number of documents, in either order, subject only to a configurable over-allowance (billing and receiving allowances configured independently, with an override role for either).
- A billing document can be raised directly off the order line, or off a Goods Receipt line — both legitimate, and, exactly as on the sell side, distributed across sibling receipts (oldest first) when a bill only names the order line.
- The Purchase Invoice’s own visible status is a separate override keyed off its live outstanding balance, described only briefly here since ledger posting is out of scope.
- Rate consistency across the chain is optional, configurable one of two mutually exclusive ways: a tolerance check against the order line’s rate, or letting the invoice’s own rate retroactively correct the receipt’s valuation. The suppression is one-directional: enabling the tolerance check clears the retroactive-correction flag; nothing runs the reverse.
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, following largely the same shape the sell side uses:
- Header defaults are copied from the source document (supplier, currency, cost center, warehouse).
- Each source line is offered only if it still has something left to convert. A Material Request line is offered if its quantity minus whatever has already been ordered against it is positive; a Purchase Order line is offered to a Goods Receipt if its quantity minus received-so-far is positive, and to an invoice if its amount minus billed-so-far is positive — with a document-level flag that offers lines priced but not yet quantified (“unit price” rows) as-is rather than computing a remainder.
- The proposed quantity/amount is the remainder, not the original — an invoice line raised directly off the order is further reduced by whatever an intervening receipt has already billed, if raised off that receipt’s line instead.
- A caller may narrow the conversion to a subset of lines, which only changes which lines are offered, not the remainder arithmetic in step 3.
- A Request for Quotation, uniquely here, carries no remainder-based conversion of its own. It is a one-shot broadcast: every line goes to every listed supplier, and what comes back is a separate document per supplier. Its own status only ever reaches Draft, Submitted, or Cancelled; per-supplier progress lives on its supplier rows, updated by each incoming Supplier Quotation’s own submission.
This is why re-converting a half-received Purchase Order a second time is safe: every already-received line drops out of the offer or is reduced to whatever remains. One sourcing branch, for a service item built by a supplier from buyer-supplied materials, produces a distinct downstream document instead of a Goods Receipt — a branch point only, out of scope here.
2.3 Data flow — how the two completion percentages actually get written
The sell-side design already establishes that a small number of shared primitives back many document types, and that the same completion percentages reach the order through structurally different call patterns rather than one. This side meets the same shared engine, but the patterns line up differently — and one of them stops resembling the sell side’s version of it entirely:
Two of these four patterns are recognizable mirrors of the sell side; two are not, for different reasons. Pattern 1 adds one buy-side-only wrinkle — a second source table so an invoice acting as its own intake document counts toward received quantity too. Pattern 4 is literally the same shared function serving both sides. Pattern 2 breaks the mirror one way: the sell side’s equivalent still calls into the shared percentage step, while this side’s is a wholly separate, hand-written formula. Pattern 3 breaks it a different way, and is not even drop-ship-specific — §3.2 works through both breaks and what actually is shared between the two sides here.
3. Deep Dive
3.1 Data model
Material Request — the internal, non-commercial entry point: typed as a Purchase, a Material Transfer, a Material Issue, a Manufacture requirement, a Subcontracting requirement, or stock a customer has supplied. Its completion percentage is written two different ways by type: for Purchase, entirely by the Purchase Order’s own conversion configuration; for every other type, the request hand-computes its own consumed quantity from the relevant stock movement or work order, then invokes the shared percentage step itself. A manual “Stop” action — user-triggered, not derived — freezes a request out of its own recomputation and out of every downstream picker looking for open requests.
Request for Quotation — a genuine submittable, cancellable document, not a communication log: submission triggers real outbound email to each listed supplier plus a supplier-facing portal path to key in rates directly. Gated at validation by the same supplier-standing check the order itself uses.
Supplier Quotation — one per responding supplier, comparable side by side via a dedicated comparison view, and swept to an expired state by a scheduled daily job past its validity date. It converts to a Purchase Order via the same remainder-based mapping every other stage uses, but carries no percentage-complete figure of its own — “how much has been ordered” is answerable only by a helper query, not a stored field.
Purchase Order — the anchor of this design. Carries the two completion percentages already described, plus an independent advance-payment condition refreshed by the same shared routine the sell side uses, which simply branches on receivable-vs-payable to choose which pair of state-words to write. It also defines a status value meaning the order’s drop-shipped lines have all been delivered; nothing in production code ever assigns it.
Goods Receipt — the physical intake document. This design concerns itself only with what it feeds back upstream (received quantity, rolling into the order’s percentage) and its own, separately-tracked billing percentage computed by the bespoke formula in §2.3 — not valuation or serial/batch handling, which belong to their own designs. An invoice can also perform the intake itself, folding its received-quantity contribution into the same aggregate.
Purchase Invoice — the billing document. It feeds a running billed amount onto whichever line it references and, through the shared engine, the order’s own billing percentage. Its own visible status, like its sell-side counterpart, is computed independently from its live outstanding balance, never through the shared engine. A configurable pair of guards can require a Purchase Order and, separately, a Goods Receipt to exist first, each with a per-supplier override.
Settlement Entry — the eventual posting once an invoice or advance is paid; fully described in the general-ledger design. The same reference mechanism used on the sell side applies here, with one buy-side-only guard: no settlement against an invoice currently on hold.
Supplier — the buy-side counterpart of the customer record, with two independent gates: a hold/disable/freeze trio enforced generically wherever a party is referenced, and system-written, non-editable flags (block or warn on new orders or quotation requests) driven entirely by the vendor-performance standing in the companion design.
3.2 Algorithm — the shared engine, and where this side stops using it
The shared completion-tracking engine — a per-line sum-and-write step, a percentage-and-label step, and a reverse-order status guard walk — is exactly the one the sell-side design already describes; nothing about its internals differs here. What differs is which callers use it, and how faithfully.
The genuinely different behavior is the Goods Receipt’s own billed-percentage computation, which answers the same question the sell side’s dispatch-net-of-returns figure answers — “how much of this fulfillment event has been billed, net of returns” — but as an independent, hand-written routine that never touches the shared step. Two consequences follow. First, its handling of an over-billed line inverts the shared step’s arithmetic: where the shared step caps the numerator at the line’s reference amount, this routine inflates the denominator to match the over-billed amount instead — a different path to a similarly bounded result, not safe to assume identical under a rounding edge case. Second, when a configuration toggle is enabled, the same routine also revalues the receipt’s incoming rate from what the invoice actually charged and forces a downstream valuation repost — a side effect no percentage computation should produce, existing only because that toggle and the “keep the rate consistent with the order” toggle are mutually exclusive. Nothing on the sell side’s percentage-writing paths resembles this.
Pattern 3’s local recomputation runs unconditionally on any items update, not only drop-ship ones, and performs no cross-document fetch itself. A separate method, triggered alongside it for drop-ship lines specifically, is what reaches the originating sales order: it loops those lines and calls the sales order’s own delivery-status method, which queries this order’s received quantity directly and writes the sales order’s own delivered percentage. That queried method is the sell side’s cross-document bridge, consuming what this order’s receiving process produced — Pattern 3 itself has no such counterpart. The zero-total bypass, unlike Pattern 3, is not merely similar to the sell side’s version; it is the same shared function, used identically by an invoice referencing a zero-total order or receipt.
3.3 Purchase Order status lifecycle
Three precise, easy-to-miss points about this diagram, each one a genuine divergence from the sell-side lifecycle rather than a restatement of it:
- The guard list’s priority order is reversed relative to the sell side, and the reversal flips a specific pair. Both sides declare guards in one order and check them in reverse. The sell side’s declaration ends Cancelled, Closed, On Hold — On Hold outranks Closed there. This side’s ends Delivered, Cancelled, On Hold, Closed — Closed outranks On Hold here, the opposite priority for the same pair of manual states, and Delivered (below) outranks Completed. Sharing an engine does not mean sharing a priority convention.
- The billed-percentage comparison is written as strict equality here, not “at least.” The sell side checks “billed reaches 100% or more”; this side checks “billed equals exactly 100%,” for both Completed and To Receive. No observable difference today, since every current writer caps the ratio at exactly 100 by construction (§3.2) — but the guard carries no cushion the way “or more” does. A future writer that didn’t preserve that cap, or a rounding artifact a hair above 100, would satisfy neither branch and fall through to the unconditional Draft branch despite being fully submitted.
- A status value meaning “the order’s drop-shipped lines have all been delivered” exists in the schema and guard list, but nothing in production code ever assigns it. It is checked the same self-referential way the manual hold and close states are, but unlike them has no button, method, or job that ever sets it — a dead guard, omitted from the diagram above.
3.4 Error handling
- Over-receipt / over-billing: converting past a line’s remainder is rejected unless a configured allowance (global or per-item, separate for quantity and amount) covers the excess, or the user holds an override role, in which case it is only logged as a warning.
- Supplier standing and hold: submitting an order or quotation request against a supplier flagged to prevent one or the other is blocked outright, citing its vendor-performance standing; a warn-flagged supplier only produces a caution message. Independently, a supplier on hold (all transactions, or scoped to invoices/payments, optionally time-boxed) blocks entirely; a disabled or frozen supplier is blocked by a separate, generic party-level check shared with every transaction type.
- Rate consistency: an optional check compares a receipt/invoice rate against its order line, hard-blocking or soft-warning beyond a small tolerance (override role for the hard-block case) — mutually exclusive with the retroactive rate-correction mechanism from §3.2.
- Reference-document consistency: a receipt or invoice line’s project, item, and unit of measure must match its order line’s, or validation blocks naming the mismatch.
- Zero-quantity (“unit price”) lines: an order line without a quantity, if permitted, is offered to conversion as-is rather than through remainder arithmetic, since its true quantity is deliberately uncertain until fulfillment.
- Closed/on-hold guard propagation: a Purchase Invoice cannot submit or cancel while its Purchase Order is closed or on hold (exempted when the line references a Goods Receipt instead); the same guard runs Purchase Order against Material Request, and Goods Receipt against Purchase Order.
- The invoice-as-receipt exclusivity guard: an invoice cannot perform its own intake if any line already references a Goods Receipt line — exclusive per line, not just per document.
4. Scale and Reliability
- Recomputation cost scales with referencing-document count, not order size — identical in shape to the sell side. Every Goods Receipt or Purchase Invoice submit/cancel re-sums every sibling line, and every percentage write triggers an overall-status re-evaluation.
- The bespoke Goods Receipt billing-percentage routine is the most expensive single write here, since its optional rate-revaluation side effect is unbounded the way a plain percentage write is not — enabling that toggle turns an ordinary invoice submission into the same bulk repost trigger a backdated stock movement would cause.
- The Request for Quotation’s outbound email step is the one genuinely external, latency-bearing operation here — a per-supplier loop with no batching or queuing described.
- No cross-order locking: two conversions racing the same order line are guarded only by a fresh read-then-write, the same soft guarantee already described for the sell side.
- Vendor-standing gates are point-in-time reads, not cached: every submission re-reads the supplier’s current standing directly.
5. Trade-off Analysis
| Decision | Trade-off |
|---|---|
| One shared percentage-and-status engine, reused across both sides but configured independently | A fix benefits every caller at once, at the cost of configurations drifting independently — the Goods Receipt’s own billing step abandoned the engine altogether rather than reconfiguring it. |
| Receiving percentage’s configuration names a second source table for invoice-as-receipt | One aggregate correctly counts both real receipts and self-intaking invoices, at the cost of a shape more complex than the sell side’s equivalent, which has no analogous case. |
| Goods Receipt’s own billed percentage as a bespoke formula (§3.2) rather than a call into the shared step | Lets it carry a rate-correction side effect the generic step can’t express, at the cost of a second arithmetic treatment of over-billing living beside the shared one. |
| Purchase Order’s completion guards use strict equality for billed percentage rather than “at least” | No observable difference today, since every writer caps the ratio at exactly 100 — but no cushion against a future writer, or rounding artifact, that doesn’t preserve that cap. |
| Vendor-performance gating enforced as a point-in-time read, not a cached flag | Always reflects the latest standing, at the cost of an extra read on every submission. |
| Rate-consistency checking and retroactive rate correction as mutually exclusive toggles rather than combinable | Avoids two mechanisms fighting over one field, at the cost of no way to configure both at once. |
| Request for Quotation modeled as a one-shot broadcast with no remainder-based re-conversion of its own lines | Simple mental model — one request, many independent responses — at the cost of no built-in way to “top up” an already-sent request without a new one. |
6. What to Revisit as the System Grows
- Fold the Goods Receipt’s bespoke billing-percentage formula back into the shared engine, separating its legitimate side effect (rate correction) from the percentage arithmetic — today the two are fused into one routine.
- Give the Purchase Order’s billed-percentage guard the same “at least” cushion the sell side uses, rather than depending on every writer preserving an implicit cap.
- Remove or wire up the “Delivered” status value, defined in the schema and guard list but with no assignment path in production code.
- Batch the outbound supplier email step for a large Request for Quotation, once supplier lists grow large enough for a slow mail transport to matter.
- Reconcile the receiving-percentage configuration’s invoice-as-receipt case with the sell side’s own bypass list, so one place explains every “who counts as fulfilled” special case instead of assuming the lists are symmetric.