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

Landed Cost Allocation

A design reference for distributing freight, customs and other post-purchase charges across received item lines

1. Requirements

1.1 Functional requirements

  • Let an operator raise a single allocation event referencing one or more already-submitted receiving documents in one legal entity: a goods receipt, a stock-impacting purchase invoice, a manufacturing/repack stock movement, or a subcontract receipt, pulling in their item lines (quantity, extended amount, cost center, source back-pointer) automatically.
  • Accept one or more charge lines (freight, customs duty, insurance, handling, etc.), each priced in its own currency with its own expense/clearing account, totaled into the legal entity’s base currency.
  • Distribute the combined charge total across every pulled item line using an operator-chosen basis: by received quantity, by received amount, or fully manual per-line entry, then fold each line’s share back into the valuation of its originating item on submission — so stock value reflects freight/customs cost, not just the vendor’s invoiced price.
  • Treat fixed-asset lines specially: confirm enough linked asset records already exist for the received quantity, and refuse a line whose asset record has already been submitted.
  • Allow a charge to be cross-referenced against a separate, non-stock vendor invoice (e.g. a customs broker’s own invoice), tracking how much is already claimed so a later event can’t claim it again.
  • On cancellation, remove the item’s cost contribution without a bespoke reversal formula, and reset claimed-amount markers on cross-referenced vendor invoices.

1.2 Non-functional requirements

  • Idempotent recompute basis: an item’s cost contribution is always derived by re-summing whichever allocation lines are currently submitted against that receiving line, never by an incremental delta — submit, cancel and amend all reduce to the same recomputation.
  • Deterministic rounding: per-item distributed shares must sum to the charge total at the currency’s own decimal precision; any shortfall or excess is resolved by a fixed rule, not silently dropped.
  • Non-blocking to the receiving document: no human re-opens the original receipt. Its valuation and stock/ledger effects are patched programmatically and the resulting bulk recompute is queued rather than run inline.
  • Multi-currency correctness: a charge line can be entered in a currency different from the legal entity’s base currency, converted using an exchange rate resolved (or entered) at validation time.

1.3 Constraints

  • Every referenced receiving document must already be submitted and belong to the same legal entity as the allocation event; a cross-entity reference is rejected outright.
  • A stock-impacting purchase invoice only qualifies as a receiving document if its own stock-impact flag is enabled — it can still be cross-referenced purely as a charge source otherwise.
  • Each charge line’s expense/clearing account must belong to the same legal entity, enforced only when perpetual inventory accounting is active for that entity.
  • The apportionment step needs a non-zero denominator: if every item line’s basis value is zero, the system refuses to distribute and asks for a different basis.
  • One allocation event has exactly one apportionment basis; it cannot split some charge lines by quantity and others by amount within the same event.

2. High-Level Design

2.1 Component diagram

2.2 Worked example — apportioning one charge by amount

A short worked calculation, not a graph, so it stays in a plain fence. These numbers reproduce a case exercised directly by the module’s own test suite, translated into freight terms:

Basis: Amount (extended receipt cost)

A goods receipt carries three item lines, each with the same
extended amount:
  Line 1: amount 250.00
  Line 2: amount 250.00
  Line 3: amount 250.00
  total_item_cost = 750.00

One charge line is entered: freight = 123.22 (legal entity's base currency)
total_charge = 123.22

Per-line share = line.amount * (total_charge / total_item_cost),
rounded to the currency's decimal precision (2 places here):

  Line 1: 250.00 * (123.22 / 750.00) = 41.0733... -> 41.07
  Line 2: 250.00 * (123.22 / 750.00) = 41.0733... -> 41.07
  Line 3: 250.00 * (123.22 / 750.00) = 41.0733... -> 41.07  (before remainder)

Running sum of rounded shares = 41.07 + 41.07 + 41.07 = 123.21
Shortfall against the charge total = 123.22 - 123.21 = 0.01

The shortfall is added to the LAST line processed, not spread evenly:
  Line 3 becomes 41.07 + 0.01 = 41.08

Final allocation: 41.07 / 41.07 / 41.08 -> sums to 123.22 exactly,
matching the charge total to the last cent.

2.3 Allocation-then-repost flow


3. Deep Dive

3.1 Data model

Cost Allocation Voucher — the top-level, submittable record: one legal entity, one posting date, one apportionment basis for the whole event, a running total of charge lines, and a separate running total of cross-referenced vendor-invoice claims.

Receipt Reference — one row per receiving document included in the event: its type (goods receipt, stock-impacting purchase invoice, manufacturing/repack stock movement, subcontract receipt), the document itself, and read-only context fields (supplier, posting date, grand total).

Allocation Line — one row per received item, populated by the item pull: item code, description, quantity, rate and amount copied from the source line, a mandatory cost center, a fixed-asset flag fetched from the item master, the distributed applicable charge, and a back-pointer to the source line. Editable directly only when the voucher’s basis is manual distribution.

Charge Line — one row per chargeable cost: description, amount in its own account currency, an exchange rate, the resulting base amount in the legal entity’s currency, and an expense/clearing Ledger Account. The same child record shape is reused elsewhere as an operating-cost table on other stock transactions; those fields stay unused here.

Vendor Invoice Claim — an optional row cross-referencing a non-stock vendor invoice and the amount claimed from it. Tracked in parallel to the charge-line total but not fed into apportionment — nothing sums it into the distributed amount. Its only effect is writing a running claimed-amount value back onto the referenced invoice.

3.2 Apportionment algorithm

Three bases are supported, selected once per voucher:

  • Quantity — each line’s basis value is its received quantity.
  • Amount — each line’s basis value is its received extended amount.
  • Manual distribution — the operator enters applicable_charges directly per line; no basis value is read.

For the first two bases:

total_item_cost = sum(line.basis_value for line in Allocation Lines)
line.applicable_charges = round(line.basis_value * (total_charge / total_item_cost), currency_precision)

If total_item_cost is zero (every line’s basis value is zero, e.g. distributing “by amount” against free-of-cost lines), the system refuses to divide by zero and suggests the quantity basis instead.

Each line’s share is rounded independently, so the rounded shares rarely sum to exactly the charge total. The difference is computed once, after every line is rounded, and added to the last line in iteration order — never spread proportionally. This is the mechanism shown in §2.2.

For manual distribution no formula runs during entry; at submission the sum of manually entered shares is compared against the charge total. A difference under two units of currency precision is folded into the last line (the same absorption rule); a larger difference blocks submission.

3.3 Propagation into posted receiving-document valuation

Submitting the voucher reaches back into every referenced receiving document, rewrites its item-level valuation, and regenerates that document’s own stock and ledger effects in place:

  1. Sum the applicable charges from every currently-submitted Allocation Line pointing at each item line, and write that sum onto the item’s own cost field.
  2. Recalculate the item’s valuation rate: most receiving-document types fold the summed landed cost into the formula already combining net receipt value, item-level tax amount and any purchase-invoice rate difference, then divide by received quantity. A subcontract receipt instead recomputes quantities and amounts through its own costing routine.
  3. For serial-tracked, non-fixed-asset items, push the new rate onto the linked serial-number records so a later delivery already reflects it — the module’s own tests confirm a subsequent delivery’s Stock Valuation Delta shifts by exactly the added charge once this runs.
  4. Fixed-asset lines are checked first: enough asset records must exist for the received quantity, and the voucher refuses to proceed if any linked asset is already submitted. Skipped while the voucher is being cancelled.
  5. The document’s existing Stock Movement Entries and Ledger Postings are then reversed in memory (as if cancelled) and regenerated (as if resubmitted) with the new rate — inside the voucher’s own submit transaction, without changing the document’s real submitted status.
  6. The document’s own repost-trigger routine then decides — based on whether later movements of the same item/warehouse exist, or a Cost Layer Queue needs replaying — whether a queued bulk recompute is required downstream.

3.4 Hand-off to the Valuation Repost Job

When step 6 determines a recompute is needed, it creates a Valuation Repost Job carrying a dedicated flag marking it as originated by a landed-cost allocation, and submits it immediately, queuing the recompute rather than running it synchronously. This document does not re-explain how that queue drains, retries, or checkpoints its progress — that is the job’s own mechanism, covered separately. The seam that matters here: the voucher fixes the one receiving document’s own numbers and hands off everything downstream, rather than walking every later movement itself.

3.5 Cancellation and amendment

Cancelling a Cost Allocation Voucher calls the same two routines used on submission — the item-level recompute (§3.3) and the vendor-invoice claim write-back — with no separate reversal formula. This works because step 1 of §3.3 only sums Allocation Lines currently in a submitted state: once the cancelled voucher’s own lines are marked cancelled, that summation naturally excludes them, so the receiving document’s valuation recomputes to whatever it would be with only the other, still-submitted allocation events applied. The reverse-then-redo of stock and ledger effects and the repost hand-off run again exactly as on submit, against a lower (or zero) figure, and Vendor Invoice Claim amounts reset to zero.

Amending a cancelled voucher produces a fresh draft copy that, once submitted, re-runs the entire flow above as a new event, not a patch to the original.

3.6 Error handling

Beyond the entity, stock-impact and expense-account checks already listed in §1.3: a voucher with no referenced receiving documents is rejected before anything else runs; every Allocation Line must reference a document present in the Receipt Reference table and carry a cost center; and the zero-denominator guard plus the submission-time rounding-tolerance check (§3.2) are the two places a distribution is rejected outright rather than silently patched.


4. Scale and Reliability

  • Load pattern: allocation events are low-frequency, back-office work — typically one per shipment or import batch, not a hot path. The expensive part is what they trigger: every later movement of the affected item/warehouse potentially needs its valuation replayed forward, so that part is deferred to a queue.
  • Bounded synchronous work: the reverse-then-redo (§3.3, step 5) touches only the referenced receiving document, synchronously, inside the voucher’s own submit transaction. Everything beyond it goes to the queued Valuation Repost Job, keeping submit time bounded regardless of transaction history.
  • Idempotent recompute as the reliability backbone: an item’s landed-cost contribution is always a fresh sum over currently-submitted Allocation Lines, so a retried submit, an out-of-order cancel, or an amendment cannot double-count.
  • Resumability lives downstream: checkpointing and resuming after an interruption is the Valuation Repost Job’s concern, not the voucher’s — the voucher only marks the job as landed-cost-originated and hands off.
  • Concurrency risk: allocation events can reference the same receiving line over time (freight now, customs later). Each submits and reposts independently; nothing serializes two vouchers racing to recompute the same line.

5. Trade-off Analysis

Decision Trade-off
One apportionment basis per voucher, not per charge line Simple to configure, but weight-based freight and value-based insurance can’t coexist in one event — it forces two vouchers instead.
Rounding remainder dumped onto the last line, not spread proportionally Deterministic and cheap, but the “last” line has no relationship to which line should fairly absorb the correction — can look arbitrary to a reviewer.
Vendor Invoice Claim tracked as a parallel, unenforced total Flexible cross-reference to a non-stock invoice without forcing its figure into the distributed pool, but nothing checks the two totals stay consistent.
In-place reverse-then-redo of the receiving document’s own effects, run synchronously Keeps that document’s numbers correct immediately, at the cost of real stock-ledger and general-ledger work inside the voucher’s submit transaction.
Cancellation reuses the forward recompute instead of a dedicated reversal No second formula to keep in sync, correct by construction as long as the docstatus-filtered summation stays authoritative.
Fixed-asset lines gated behind a submitted-asset check rather than a corrective path Avoids silently rewriting an already-submitted (possibly depreciated) asset’s cost, but offers no remediation beyond removing the line.

6. What to Revisit as the System Grows

  • Per-charge apportionment basis: let different charge lines within one voucher use different bases (freight by weight, duty by value) instead of one basis for the whole event.
  • Cross-validation between the two totals: tie the Vendor Invoice Claim total to the distributed charge total instead of letting them drift independently — the source shows no such check today.
  • Concurrency control on shared receiving lines: add serialization, or at least a conflict check, for two allocation events racing to recompute the same line.
  • A remediation path for locked fixed-asset lines: today the only response to an already-submitted linked asset is removing the item; a corrective mechanism is left entirely manual.

Two of the five source records this document is grounded in are little more than join tables (the receipt reference and the vendor invoice claim); the load-bearing logic — apportionment, propagation, cancellation symmetry — is concentrated in one file. The repost queue’s own internals are left out deliberately, since a separate document already covers them.

Was this page helpful?