Consignment Inventory in ERP: Own the Stock You Do Not Own

Photo by toolstop on flickr
Consignment inventory is stock that a consignor ships to a consignee's warehouse while retaining legal ownership until an end customer buys it. In an ERP, this means the item exists in the consignee's physical stock count but does not belong to the consignee's balance sheet, so the system needs to track quantity and ownership as two separate facts rather than one.
A standard sales order assumes ownership transfers the moment goods are delivered, which triggers revenue recognition and inventory relief immediately. Consignment goods are delivered without a sale occurring, so if you reuse the standard flow you will recognize revenue and reduce inventory value before an end customer has actually purchased anything, overstating both revenue and receivables.
Add an owner_party_id column to the stock ledger entry table itself rather than to the item master or warehouse record. This lets the quantity ledger update on physical receipt and transfer while the ownership ledger only updates on a confirmed sale or a formal return voucher, so the same SKU can carry consigned and owned stock side by side in the same warehouse.
Under IFRS 15 and ASC 606 style rules, revenue is recognized when control of the goods transfers to the end customer, which is when the consignee reports a sale, not when the consignor originally shipped goods to the consignee and not when cash is eventually collected. The consignor is typically the principal in the arrangement and reports revenue and cost of goods sold gross, with the consignee's cut booked as a commission expense.
Treat each confirmed sale report as an immutable record that accrues a commission payable to the consignee and a receivable for the gross sale value, then batch those accruals into a periodic settlement run. Snapshot the commission rate on the sale report itself at the time of sale, since agreements get renegotiated and a settlement months later must still reflect the rate that applied when the sale happened.

Photo by toolstop on flickr
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.
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.
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.
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 |
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.
// 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.
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.
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.
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 },
])
}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.
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.