Skip to main content
Software Inventory Stock

Inventory when two branches sell the last item

Two branches can sell the last unit in the same minute. The stock figure has to move with the sale, and a transfer between branches has to be a document, not a phone call that both sides record differently.

LesterTechLabs Engineering Estimated 3 min read
Warehouse shelves and a clipboard on a packing table during a stock count

Two branches can sell the last unit in the same minute. The stock figure has to move with the sale, and a transfer between branches has to be a document, not a phone call that both sides record differently.

Stock is a ledger, not a number on a product

Store a movement: received, sold, transferred out, transferred in, counted, written off. The quantity on hand is the sum of movements for that sku at that branch. A single integer on the product will be edited by whoever is in a hurry, and you will never know which edit was the sale.

The sale and the stock movement commit together. If the receipt prints and the movement rolls back, the drawer and the shelf disagree. If the movement commits and the sale rolls back, you have stock that left without a customer. One transaction, then the receipt.

The last unit needs a lock

Read the on-hand quantity and decrement it under a row lock for that sku and branch, or insert a movement and let a check constraint refuse a negative balance. A check in application code, followed by a save with no lock, is how both branches sell the same generator.

Oversell is a business decision, and it should be a flag on the sku, defaulting off. If the flag is off, the sale stops with a sentence the cashier can say. If the flag is on, the movement may go negative and a purchase order should be able to see that debt.

A transfer is two movements and a status

Branch A records transferred out when the goods leave. Branch B records transferred in when the goods are counted on arrival, not when the truck is dispatched. Between those events the stock is in transit, which is its own balance. Collapsing that into an instant edit makes shrinkage invisible.

A stock take replaces doubt with a count. Record the counted quantity, the expected quantity, and the difference as a movement with a reason. Do not edit history. The next argument about a missing carton should be settleable from the movements, not from memory.

Refuse the sale that the shelf cannot cover

The lock is on the balance row for one sku at one branch. Two cashiers can sell different skus at the same time. They cannot both take the last unit of the same sku. The movement is the audit. The balance is the cache of that audit, updated in the same transaction.

Python · inventory/sell.py

from django.db import transaction

class InsufficientStock(Exception):
    pass

def sell(branch_id: int, sku_id: int, qty: int, sale_id: int):
    with transaction.atomic():
        balance = Balance.objects.select_for_update().get(branch_id=branch_id, sku_id=sku_id)
        if balance.on_hand < qty and not balance.sku.allow_negative:
            raise InsufficientStock(sku_id)
        Movement.objects.create(
            branch_id=branch_id, sku_id=sku_id, qty=-qty, kind="sold", sale_id=sale_id
        )
        balance.on_hand -= qty
        balance.save(update_fields=["on_hand"])

A count you can defend

Every argument about stock becomes a walk through movements: received, sold, transferred, counted, written off. If the only record is a number someone typed, the argument ends in a shrug and a new number. The movement is slower to write and faster to trust. Start with the sku that causes the fights, not with a catalogue of ten thousand quiet items.

Transfers are where shrinkage hides. Goods leave one branch on a phone call and arrive, or do not, with no document. The in-transit balance is the document. It stays until the receiving branch counts. A transfer that edits both numbers in one instant cannot show a carton that never arrived.

Negative stock is a decision you write on the sku. Default it off so the till stops and the cashier can say the item is gone. Turn it on only where you truly take backorders, and make the purchase list show the debt. An accidental negative on every sku means the lock is missing.

Run two sales of the last unit in a test, on two branches if the stock is shared, or on one branch if it is local. The second sale fails or the movement is flagged. Shipping without that test is how you discover the rule in front of a customer.

On the next stock argument

  1. Record every change as a movement against a branch.
  2. Refuse a sale that would make on-hand negative unless that sku allows it.
  3. Keep in-transit stock visible until the receiving branch counts it.

On the site

Software

Have a system to build?

We design and ship custom software with a scope you can track, from the first request to handover.