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

Subcontracting Order-to-Receipt Flow with Supplied Materials

A design reference for outsourcing a manufacturing step while keeping the raw materials on your own books

1. Requirements

1.1 Functional requirements

  • Outward direction: issue raw materials the company already owns to a third party, let that party perform a manufacturing step it does not itself supply materials for, and receive back a finished item. The system must know, at all times, how much of each raw material is still at the third party, how much has been consumed into a finished item, and how much has come back unused.
  • Inward direction: accept raw materials a customer owns, perform a manufacturing step on the company’s own floor against them, and deliver a produced item back to that customer — the same three questions, mirrored: received, consumed into production, returned unconsumed.
  • The quantity of each raw material a finished item requires is never entered by hand on the outsourcing order; it is derived from a bill of materials (BOM) tied to the finished item (cross-reference the BOM Explosion & Multi-Level Costing document for how that explosion itself works — this document treats it as an input, not something to re-derive).
  • A single mapping must let an order line carry a billable “service” (the labor/processing being paid for) while a second, linked line carries the actual stock-moving finished item and its BOM — the finished item is not itself directly orderable as a normal purchased or sold line once subcontracting is involved.
  • Both directions must produce ordinary, ledger-visible stock movements. Materials handed to a third party are not an off-books memo; they remain full stock ledger entries, posted against a warehouse that represents the third party’s premises.
  • One shared finished-item / service-item / BOM mapping must serve both directions, so a subcontracted item’s default production recipe is not re-declared per order.
  • Pre-existing outsourcing orders created before outsourcing had its own document family must keep functioning under the mechanism they were originally built with, without being force-migrated.

1.2 Non-functional requirements

  • Reconciliation integrity: the amount consumed into a finished item can never exceed the amount actually moved to (or received from) the counterparty, regardless of how many partial transfers or partial receipts occur, and regardless of the order in which they are submitted or cancelled.
  • Idempotent re-derivation: quantities that summarize a history of movements (how much has been sent, how much has come back) are recomputed by re-scanning the movement history, not by incrementing a counter — so cancelling and resubmitting a movement out of order cannot leave stale totals behind.
  • Traceability across the custody boundary: batch and serial identity must survive the trip to and from the counterparty’s warehouse, so a receipt can be rejected if it tries to consume a batch or serial that was never actually transferred.
  • Valuation continuity: a raw material handed to a third party never leaves the company’s valued stock ledger. Its value is only relieved from the books at the moment it is actually consumed into a finished item, not at the moment it physically leaves the company’s own site.

1.3 Constraints

  • Every warehouse used to represent a counterparty’s custody of materials still belongs to exactly one company/legal entity in the chart of accounts — a third party’s physical possession of stock is never modeled as if it were that third party’s own accounting entity.
  • A raw material’s reconciliation row is scoped to one specific (raw material, finished item, order) combination; it cannot span multiple orders or multiple finished items.
  • The inward direction’s actual production step is not a bespoke consumption engine — it is executed through the ordinary manufacturing machinery (a production order and its shop-floor job cards), reusing that engine’s own material-consumption and costing logic rather than re-implementing it.

2. High-Level Design

2.1 Component diagram

2.2 Why one engine serves both a legacy and a current flow

Before outsourcing had its own document family, a subcontracted purchase was modeled entirely inside the purchase document itself: the purchased item was the finished item, a BOM was attached directly to that purchase line, and raw-material tracking lived in child tables bolted onto the purchase order and purchase receipt. That mechanism still exists, gated end-to-end by a single per-record flag — not a configuration switch an administrator can turn on for new work, but a historical marker set once, by a schema-migration patch run during an earlier upgrade, for the specific purchase orders that were already subcontracted at that time. No code path in current use ever sets it true for a new record; every purchase order created since defaults to the current flow permanently, with no toggle back.

The flag governs, item by item:

  • which fields are even visible on a purchase-order line (the legacy flow shows a BOM field directly on the line; the current flow instead requires a linked finished-item reference and a finished-item quantity, and hides the BOM field);
  • whether the purchase order’s own validate() populates raw-material rows itself (legacy) or defers entirely to a separate outsourcing order (current);
  • whether the purchase receipt/invoice values the received item by pulling supplied-material cost directly into its own valuation rate (legacy) or leaves the entire finished-item valuation to a dedicated receipt document (current);
  • which of two parallel raw-material reconciliation table families gets written — a Purchase Order Item Supplied / Purchase Receipt Item Supplied pair for the legacy flow, or an Outsourcing Order Supplied Item / Outsourcing Receipt Supplied Item pair for the current one. The two families carry an almost identical field set (required, supplied, consumed, returned, net “total supplied”) because both are populated by the same shared engine, just addressed through a different table name.

The inward direction has no legacy equivalent at all — it is exclusively a current-flow concept, introduced once the outward side had already settled on its own document family. There was never an “old inward flow” to preserve.

2.3 Data flow — how one raw material’s numbers reconcile (outward)

The same shape exists on the inward side with different field names and a different write path: required_qty still comes from the BOM, received_qty/returned_qty are re-derived the same way from “receive from customer” and “return raw material” movements, but the consumed_qty leg is written by the ordinary manufacturing engine’s own bookkeeping (once a production order backed by that customer material is submitted), not by a receipt-style backflush. Section 3.2 details why that distinction is real rather than cosmetic.


3. Deep Dive

3.1 Data model

Outsourcing Order (outward). Header carries the supplier, a supplier-side custody warehouse, and totals. Three child tables:

  • Order Item — one row per finished item: quantity, rate (raw-material cost + service cost + any additional cost per unit), the BOM used, a link back to the purchase-order line, and running received/returned quantities.
  • Service Item — one row per billable service line on the originating purchase order; carries the linked finished item and its quantity.
  • Supplied Item — one row per (raw material, finished item) pair: required, supplied, consumed, returned, and net “total supplied” quantities, plus the reserve warehouse the material is drawn from and a reference to the BOM line that generated it.

Outsourcing Receipt (outward). Records what actually came back from the supplier. Its Receipt Item rows carry accepted/rejected/received quantities and the same three cost-per-unit components (raw material, service, additional) that together become the finished item’s valuation rate. Its Receipt Supplied Item rows are the consumption side: required vs. consumed quantity for each raw material backflushed against this receipt, with batch/serial identity and an expense account for the value being relieved from the custody warehouse.

Finished-Item / Service-Item / BOM Map. A small, standalone table: one active row per finished item, pointing to the service item that bills for it, the specific BOM to use (which can differ from that item’s own default BOM), and a derived conversion factor between service-item quantity and finished-item quantity. Both directions consult this same table to turn a bare service line into a real finished-item line with a real BOM attached.

Outsourcing Order (inward). Header carries the customer and a customer-side custody warehouse. Child tables:

  • Item — one row per finished item to be produced: quantity, BOM, produced/delivered/returned/process-loss quantities. Notably, it carries no rate or amount — unlike the outward order’s item row, because the company is not buying a valued finished item from itself; whatever value the finished item accrues comes from the manufacturing engine’s own costing, and the customer is billed through the service line instead.
  • Service Item — mirrors the outward side’s service line: the billable line, linked to the finished item and its quantity.
  • Received Item — the raw-material reconciliation row: required, received, consumed, returned, and a separate “allocated to a production order” quantity, plus a flag marking whether the row is genuinely customer-owned material or an incidental self-procured item consumed alongside it.
  • Secondary Item — byproducts or scrap produced alongside the main finished item, tracked the same way the outward receipt tracks its own secondary/scrap lines.

Custody warehouse. Not a separate entity type — an ordinary warehouse record, still owned by one company, that happens to represent a location physically at a counterparty’s site. A warehouse can optionally be linked to a specific customer, a linkage the warehouse form itself describes as existing only for the inward flow. Because the warehouse is an ordinary chart-of-accounts-scoped location, every movement into or out of it is an ordinary, valued stock ledger entry — this is the mechanism that satisfies the “materials at a third party are still yours” requirement from Section 1: the company’s own inventory account, never one belonging to the counterparty, is debited and credited as material moves in and out of custody.

Legacy raw-material ledger. Purchase Order Item Supplied and Purchase Receipt Item Supplied — field-for-field near-duplicates of the current flow’s Supplied Item tables, populated by the same shared engine when the legacy flag is set. Purchase Receipt Item Supplied is filed under the purchasing module rather than the stock module, despite tracking the same kind of consumption its current-flow counterpart tracks — a naming/placement artifact, not a functional difference.

Status lifecycle — Outsourcing Order (outward).

The status is fully recomputed from scratch on every relevant event (not incremented), by checking, in order: has anything been received at all, and if not, how much of the required raw material has been transferred. Closed is not part of that computed chain at all — it is pushed onto the order from outside, only when the purchase order it was created from is itself closed.

The inward order’s own lifecycle uses a materially different vocabulary — Ongoing, Produced, Delivered, Returned — because it tracks a production process and a delivery, not a transfer-then-receive cycle, and is driven by six independently maintained percentages (raw material received, raw material returned, produced, process loss, delivered, returned) rather than the outward order’s single received-percentage.

3.2 Algorithm(s)

Deriving what is required. At order creation, for each finished-item line, the engine walks the attached BOM (its direct lines, or its fully-exploded lines under an “include exploded items” flag) and multiplies each raw material’s per-unit BOM quantity by the ordered finished-item quantity. Two details matter beyond the bare multiplication: a phantom BOM line (a sub-assembly that never itself becomes a stockable transaction) is walked through rather than treated as a raw material, and a line explicitly flagged “sourced by the supplier” is skipped entirely — that material is the counterparty’s own responsibility, not something the company owes them. (Full explosion mechanics, including multi-level costing, are covered by the BOM Explosion & Multi-Level Costing document; this document only consumes that output.)

Two backflush modes (outward only). A company-wide setting chooses how a receipt decides what quantity of each raw material to consume:

  • By BOM (the default): the receipt ignores what was physically transferred and simply recomputes the BOM-required quantity for the quantity actually received, splitting across whatever batches/serials are on hand. An over-transfer allowance setting governs how much more than required a transfer may move before being blocked, which only makes sense in this mode.
  • By what was actually transferred: the receipt draws down a running pool of specifically-transferred lots (tracked per raw material, per finished item, per order, including which batches/serials moved), consuming from that pool in the order transfers happened. A second, independently toggled setting can additionally cross-check the resulting consumption against the BOM requirement and hard-block a receipt that falls short.

Either way, once a receipt is submitted, the order’s own Supplied Item rows are updated by summing consumption across every receipt filed against that order and capping it at the row’s own supplied_qty — consumption can reduce the available pool, but a coding or data error elsewhere can never push a row’s recorded consumption above what was actually supplied.

Weighted-average custody valuation (inward). Because a customer can hand over the same raw material in more than one delivery, at different declared costs, the inward receiving step maintains a weighted-average rate on each Received Item row: on every “receive from customer” movement, the engine computes the pre-existing balance (received minus returned minus consumed) and its value, adds the new movement’s quantity and value, and re-derives the rate from the combined total — rather than simply overwriting the rate or averaging naively across movements of different sizes.

Production is delegated, not re-implemented (inward). Once customer-owned material has been received, the inward order can spawn one production order per finished-item line, sized to whatever quantity of customer material is currently available (received, less returned, less what a prior production order already claimed). From that point the ordinary manufacturing engine owns consumption: a “transfer for production” movement pulls customer material from the custody warehouse into work-in-progress, guarded against pulling more than the production order’s own required or reserved quantity, and a “manufacture” movement records the resulting output — both movement types identical to the ones ordinary, non-subcontracted manufacturing uses. The inward flow’s own bookkeeping only listens for them and rolls their quantities back onto the Received Item and Item rows. This is where the two flows diverge in kind, not just in field names: the outward flow’s consumption engine lives inside the outsourcing order/receipt classes themselves, while the inward flow’s consumption bookkeeping is a separate module mixed directly into the generic stock movement document, reacting to movement purposes that exist only for this purpose.

3.3 Adapter/API contract — the movement-purpose seam

Both directions plug into the same generic stock movement document through a fixed, named set of movement purposes rather than through a bespoke API. Each purpose carries an implicit contract for which order-link field it expects and what it updates on submit/cancel:

Send to Subcontractor            -> outward order link; increases supplied_qty
Material Transfer (is_return=1)  -> outward order link; increases returned_qty
Receive from Customer             -> inward order link; increases received_qty
Return Raw Material to Customer   -> inward order link; increases returned_qty
Material Transfer for Manufacture -> production order link; ordinary WIP transfer
Manufacture                       -> production order link; increases consumed_qty
                                     on the matching Received Item row
Subcontracting Delivery           -> inward order link; increases delivered_qty
Subcontracting Return              -> inward order link; increases returned_qty
                                     on the finished-item row

Every row within one of these movements also carries a reference back to the specific reconciliation row it affects (a raw-material ledger row for transfers/receipts, a finished-item row for deliveries/returns), which is what lets the reconciliation in Section 3.1/3.2 stay row-accurate rather than only order-accurate.

3.4 Error handling

  • A dead validation on the legacy path. The legacy flow’s own “select a BOM for this item” guard compares a child-row object against a list of item-master names using plain membership testing — a comparison that can never be true, so the guard silently never fires for a legacy-flow subcontracted line missing a BOM. It would need to compare the item code, not the row object, against that list. Only the legacy flow is affected; the current flow enforces a BOM requirement at the finished-item-mapping stage instead, where it works.
  • Closed/cancelled order protection. Submitting a receipt against an outward order that is already Closed or Cancelled is a hard, explicit failure — the receipt checks the order’s own status before touching its quantities, rather than relying on the order to reject a stale write after the fact.
  • Batch/serial provenance. A receipt that tries to consume a batch or serial number the reconciliation never recorded as transferred is rejected outright, naming the batch/serial and the order — what makes the custody trail auditable rather than just numerically balanced.
  • BOM shortfall. In the “actually transferred” backflush mode, if the optional cross-check setting is on and a receipt’s real consumption falls short of what the BOM demands, the receipt is blocked with a dedicated error naming the shortfall.
  • Inward overconsumption guard. Transferring more customer-owned material into work-in-progress than a production order’s required-or-reserved quantity allows is blocked before the movement is submitted, named per item and per production order.
  • Silent partial failure worth flagging. Spawning a production order per finished-item line (inward) catches an over-production error per line and simply skips it rather than surfacing it — a caller only learns something went wrong if the entire batch produced zero production orders.

4. Scale and Reliability

  • Reconciliation is re-derived by full re-scan, not maintained incrementally. Every time a transfer/return movement against an order is submitted or cancelled, the order’s supplied/returned/net quantities are recomputed by re-summing every movement ever filed against that order, then overwritten wholesale — not adjusted by a delta. This deliberately makes out-of-order cancellation safe (there is no running counter to desynchronize), at the cost of a scan whose size grows with the number of partial transfers an order accumulates. For the volumes a subcontracted order normally sees (a handful of transfers) this is unremarkable; it would not scale gracefully to an order with hundreds of partial movements.
  • Reservation figures are live aggregate queries, not cached counters. The quantity a warehouse balance treats as “reserved for outsourcing” is computed on demand by querying open orders and open outbound transfer movements together, each time it is needed, rather than maintained as a running field. This mirrors a pattern already documented for the inventory balance cache generally: some balance fields are ledger-derived, others are commitment figures queried directly outside the ledger-replay path, and this reservation figure sits in the second category.
  • Weighted-average custody costing is cheap and local. The inward receiving step’s rate recomputation touches only the one Received Item row being updated, so its cost is bounded and does not grow with order history the way the outward reconciliation re-scan does.
  • No queueing between the two directions’ own steps. BOM explosion, movement validation, reconciliation writes and ledger posting all run synchronously inside the submitting document’s own transaction; a caller that reads the order immediately after a receipt submits will already see the update.
  • Cross-direction scaling is asymmetric. The outward flow’s consumption bookkeeping is self-contained inside the outsourcing order/receipt documents, so its write volume tracks the number of receipts filed. The inward flow’s equivalent bookkeeping instead tracks the volume of the underlying manufacturing engine’s own stock movements — normally larger per unit of output, since a production order and its job cards typically generate several movements. A capacity plan sized off outward-flow experience would understate inward-flow load.

5. Trade-off Analysis

Decision Trade-off
One shared engine base class for both directions and both the legacy and current flows Large reuse win (one place implements BOM-derived requirements, batch/serial bookkeeping, and reconciliation for six record types) at the cost of a constructor that branches four ways to decide field names for the object’s whole life — hard to read in isolation, and any shared-logic change risks all four branches at once.
Reconciliation by full re-scan on every movement event, rather than incremental counters Immune to drift from out-of-order cancellation or replay, with no reconciliation job ever needed — at the cost of a scan that grows with an order’s own transfer history and cannot be sharded or cached without reintroducing the drift risk it was built to avoid.
Inward production reuses the full manufacturing engine instead of a bespoke consumption model Inherits a mature, independently-tested costing and shop-floor engine for free, and keeps “how production consumes material” defined in one place system-wide — at the cost of the inward flow having no consumption logic of its own to specialize; any manufacturing-engine limitation is inherited unconditionally.
Materials at a third party stay on the company’s own valued stock ledger, in a company-owned custody warehouse Correct accounting (the company never pretends it doesn’t own stock it is only temporarily not holding) and one valuation model for every warehouse — at the cost of every counterparty needing a warehouse record provisioned and mapped before any transfer can happen.
Two configurable backflush modes for outward consumption (by BOM vs. by what was transferred) Lets a company choose speed (trust the BOM) over precision (track the exact lot consumed) or vice versa — at the cost of the choice being global to a company rather than per item, so strict lot-tracking cannot be mixed with fast BOM-trust in one company.
Legacy flow preserved per-record via a hidden flag rather than a hard migration Old subcontracted purchase orders keep working exactly as before, with zero data rewrite — at the cost of a permanent second code path, and a near-duplicate raw-material table pair, that every future change to the shared engine must not silently break even though no new record can enter that path.

6. What to Revisit as the System Grows

  • Fix or remove the dead legacy BOM guard. The comparison bug in Section 3.4 lets a legacy-flow subcontracted purchase line be submitted without a BOM at all, silently, until something downstream that actually needs the BOM fails less clearly. Since the legacy path can no longer accept new records, the cheaper fix may be to remove the guard along with the rest of the dead branch once no in-flight legacy order remains.
  • Surface partial production-order failures explicitly. The inward flow’s per-line over-production swallow should at minimum report which finished-item lines failed to spawn a production order, rather than only detecting total failure.
  • Reconsider the reconciliation re-scan at higher transfer volumes. If outsourcing orders start accumulating transfer counts an order of magnitude beyond today’s typical handful, the full re-scan on every movement event becomes the dominant cost of submitting a transfer; an incremental counter with a periodic reconciling re-scan would trade a small drift window for much cheaper writes.
  • Give the inward flow a consolidating document. The outward flow’s single Outsourcing Receipt ties a batch of consumption to one finished-item intake event — a natural audit unit. The inward flow’s equivalent history is scattered across however many manufacturing-engine movements a production run generated, with no single document saying “this is what this run consumed and produced.” That gap will likely be felt first by anyone auditing a single production run end to end.
  • Check whether the reservation figure inherits the same blind spot found in the broader stock-reservation mechanism. The reservation quantity here is queried live rather than cached; if a newer, dedicated reservation-entry mechanism used elsewhere is not itself reflected in that query, outsourcing’s own figure would silently omit it too — worth a direct check rather than an assumption.

Was this page helpful?