Key Takeaway
Consignment inventory breaks the assumption built into most ERP stock modules, that whoever holds the goods also owns them. The consignee stores and sells the stock while legal title stays with the consignor, so the ERP has to separate physical custody from ownership in its valuation.
Consignment inventory breaks the assumption baked into most ERP stock modules: that whoever holds the goods also owns them. A consignor ships stock to a consignee's warehouse, the consignee displays and sells it, but legal title stays with the consignor until an end customer actually buys. If your inventory model only tracks quantity and location, you will post revenue and cost of goods sold at the wrong moment, and your balance sheet will show inventory that legally belongs to someone else.
This post walks through a concrete design for consignment stock in a custom ERP: how to separate ownership from possession at the data model level, how consignor settlements and commission accruals should flow, when revenue recognition actually fires under IFRS 15 and ASC 606 style rules, and the exact ledger entries a sale should generate. The patterns here come from building approval and inventory modules for manufacturing and distribution clients where consignment terms are common with dealers and retail partners.
Ownership vs Possession: The Core Modeling Problem
Every conventional stock ledger entry answers one question: how much of an item is in a given warehouse. Consignment forces a second, independent question: who legally owns that quantity right now. These two facts change on different events and at different times, so they cannot live on the same flag. A naive implementation that adds a single boolean like is_consignment to the item master will break the moment the same item is sold both on consignment terms to one partner and outright to another.
- Possession changes when goods physically move between warehouses, triggered by a goods receipt or goods issue voucher.
- Ownership changes only when a sale to the end customer is confirmed, or when consigned stock is formally returned to the consignor.
- The same item, same warehouse, can have units under different ownership states simultaneously if a consignee also holds owned stock of the identical SKU.
Model ownership as a property of the stock ledger entry itself, not of the item or the warehouse. An owner_party_id column on every ledger row lets a single item exist in both consigned and owned states across different warehouses without any schema branching.
A Two-Ledger Data Model
The cleanest implementation keeps two logically separate ledgers that share the same underlying table. The quantity ledger answers where the stock physically sits and moves on receipt, transfer, and physical return vouchers. The ownership ledger answers who holds legal title and only moves on a consignment sale voucher or a formal ownership transfer voucher. Because both ledgers key off the same item and warehouse, you can always answer the two-part question a warehouse manager and a controller ask differently: how much do we have, and how much of what we have is actually ours.
| Event | Quantity Ledger | Ownership Ledger |
|---|---|---|
| Consignor ships to consignee warehouse | Increases at consignee warehouse | Unchanged, stays with consignor |
| Consignee sells to end customer | Decreases at consignee warehouse | Transfers to end customer at the same moment |
| Consignee returns unsold stock | Decreases at consignee, increases at consignor | No change, ownership was never with consignee |
The Consignment Lifecycle in the ERP
A consignment arrangement in the system moves through four distinct voucher types, each with its own posting rules and its own effect, or deliberate lack of effect, on the general ledger.
- Consignment agreement: a master record defines the consignor, the consignee, the commission or margin split, the settlement frequency, and which items are covered. No stock or ledger movement happens here.
- Consignment receipt: goods physically arrive at the consignee's warehouse. The quantity ledger updates immediately. The ownership ledger does not move, and critically, no financial journal entry is posted, because no sale has occurred and no revenue has been earned.
- Consignment sale report: the consignee reports that units were sold to an end customer, usually via a periodic sales report or a point-of-sale integration. This single event triggers the quantity decrease, the ownership transfer, and the full set of journal entries.
- Settlement: on the agreed cadence, the consignor invoices or nets out the consignee for the value of goods sold, less the agreed commission, and the accrued liability from each sale report is cleared.
// stock_ledger_entry
{
id: uuid,
item_id: uuid,
warehouse_id: uuid, // consignee's physical warehouse
owner_party_id: uuid, // consignor (legal owner), NOT the warehouse holder
quantity: decimal,
valuation_rate: decimal, // consignor's cost, hidden from consignee
ownership_flag: 'consignment' | 'owned',
posting_datetime: timestamp,
voucher_type: 'consignment_receipt' | 'consignment_sale' | 'consignment_return',
voucher_id: uuid
}
// Two ledgers move independently:
// 1. Quantity ledger -> updates on physical receipt/return at consignee warehouse
// 2. Ownership ledger -> stays with consignor until a sale voucher firesNever post revenue or relieve inventory value at the consignment receipt step. It is tempting to treat the receipt voucher like a sales delivery because the physical movement looks identical, but doing so recognizes revenue before a sale to the end customer exists, which violates both IFRS 15 and ASC 606 control-transfer criteria and overstates receivables.
Consignor Settlements and Commission Accrual
Settlement is where most homegrown consignment modules fall apart, because it requires netting many small sale-report events into a single periodic payable or receivable, while keeping an audit trail back to each individual sale. The settlement engine should treat every confirmed sale report as an immutable, timestamped record that accrues a payable to the consignee for commission and a receivable from the consignee for the gross sale value, then batch those accruals into a settlement run.
- Each settlement run should be reproducible from the underlying sale reports alone, so a controller can always trace a settlement total back to the individual transactions that produced it.
- Commission rates should be versioned on the consignment agreement, not hardcoded per sale, since rates commonly change mid-agreement and historical settlements must still reflect the rate in effect at the time of sale.
- Disputed sale reports need a hold state that excludes them from the current settlement run without blocking the rest of the batch, otherwise a single disputed line item stalls an entire partner's payment.
Store the commission rate as a snapshot on each sale report record at the moment it is created, in addition to the reference to the agreement. Agreements get renegotiated, and a settlement run six months later should reflect the rate that applied when the sale actually happened, not whatever the agreement says today.
Revenue Recognition: When the Journal Entries Actually Fire
Under both IFRS 15 and ASC 606, the consignor is almost always the principal in the arrangement, since it retains control of the goods, bears inventory risk, and typically sets or approves the resale price. That means the consignor recognizes revenue and cost of goods sold in gross amounts at the moment control transfers to the end customer, not when goods are shipped to the consignee and not when cash is collected. The consignee's cut is a commission expense, not a reduction of revenue, because the consignor is reporting gross, not net.
// Fired only when the consignee reports an end-customer sale,
// never at physical receipt of the consigned goods.
function postConsignmentSale(saleReport: ConsignmentSaleReport) {
const { itemId, qtySold, sellingPrice, commissionRate, costRate } = saleReport
// 1. Recognize revenue (consignor is principal -> gross reporting)
journal.post([
{ account: 'AccountsReceivable_Consignee', debit: qtySold * sellingPrice },
{ account: 'SalesRevenue', credit: qtySold * sellingPrice },
])
// 2. Relieve consignment inventory, recognize COGS
journal.post([
{ account: 'COGS', debit: qtySold * costRate },
{ account: 'ConsignmentInventory', credit: qtySold * costRate },
])
// 3. Accrue the consignee's commission as an expense, net at settlement
const commission = qtySold * sellingPrice * commissionRate
journal.post([
{ account: 'CommissionExpense', debit: commission },
{ account: 'AccountsPayable_Consignee', credit: commission },
])
}Common Failure Modes to Design Around
Most of the pain in consignment inventory modules comes from a handful of recurring mistakes that are easy to avoid if you design for them up front rather than patching them in after a client complains.
- Treating consignment as a special case of a standard sale instead of a distinct voucher type leads to reports that double-count revenue when a generic sales dashboard sums every delivery, including consignment receipts that were never actually sold.
- Valuing consigned stock at the consignee's cost instead of the consignor's cost corrupts inventory valuation reports the moment a single item exists in both consigned and owned form in the same warehouse.
- Skipping a formal consignment return voucher and instead just deleting the quantity ledger entry destroys the audit trail a controller needs to reconcile unsold stock against what was originally shipped.
Get the two-ledger split right at the schema level and the rest of the consignment workflow, agreements, receipts, sale reports, settlements, and revenue recognition, falls out as a straightforward sequence of voucher postings rather than a tangle of special-case logic.

