Every inventory system worth trusting shares one property: its source of truth is a log of what happened, not a running total. Banks do it. Accounting software does it. Most ERPs do it β and then hide it behind an interface that pretends otherwise.
The running-total model is seductive because it's fast. A single row per product, a single integer to update, a single read to answer 'how many do we have?' It's also fragile, because that single integer is a derived value with no provenance. When it's wrong β and it will eventually be wrong β there's no way to know why, when, or by whom. You can only fix it by overwriting it, which destroys the evidence of the error.
Why append-only If every change writes a row and no row is ever edited, the audit trail is trivial. Questions like 'what was stock of SKU AB-001 on March 14th?' are a single SQL sum. Questions like 'who moved this?' are a simple join.
The operational difference is real. In a mutable system, answering a historical question requires trust β trust that nobody edited the history, trust that the backup is accurate, trust that the person who made the change remembered to log it somewhere else. In an append-only system, answering a historical question requires a query. The answer is deterministic, reproducible, and admissible in a dispute with a customer or auditor.
No delete button
Our StockMovement admin explicitly disables add/change/delete permissions β even for superusers. This <a href='/security' class='text-[var(--color-accent)] underline'>security measure</a> means corrections go through a dedicated POST /api/stock/{id}/adjust/ endpoint that writes an ADJUSTMENT row with a required note. The ledger stays honest.
This is the discipline that makes append-only work. If there's a back door β a database admin who can UPDATE stock_movement SET quantity = 5 WHERE id = 1234 β then the ledger is just a log with an asterisk. By disabling all mutating operations at the Django admin level and providing a single, auditable correction path, we guarantee that every number in the ledger has a paper trail. Even the corrections have a paper trail β the adjustment note is required, timestamped, and attributed to the admin who made it.
Cache, not truth
StockItem.on_hand and StockItem.reserved are a cache, updated in the same transaction as the ledger write. If they ever drift, the ledger rebuilds them.
The cache exists for performance β you don't want to SUM six years of stock movements every time you render the inventory page. But the cache is disposable by design. A management command can zero every StockItem and rebuild it from the ledger in a single pass. This design pattern β cache as optimization, ledger as truth β is borrowed from <a href='/docs/product/finance' class='text-[var(--color-accent)] underline'>accounting systems</a> and applied to inventory. It's not novel, but it's correct, and correctness in inventory is worth more than novelty.
