Self-hosted stock control where the cost of goods can be proven, line by line
Every balance is a queue of lots. Shipments consume it oldest-first, and each one remembers exactly which lots it was made of and what they cost.
Most stock systems answer "how many" and shrug at "how much". They keep one average price per item, and the moment purchase prices drift, that average quietly stops matching reality — the margin on every order is off by a few percent and nobody can say why.
Crate keeps the layers. A receipt creates a lot with its own price; a shipment eats the queue from the oldest end and writes down which lots it took. Open any order and you see the arithmetic: four screws from the June lot at 333,42 ₽, one from late June, eight from July — and what the same line would have cost under a weighted average.
- Real FIFO, not an average — lots are read with
SELECT … FOR UPDATEand consumed in receipt order, so two simultaneous orders can never spend the same stock twice - A cost you can defend — every shipment line stores its layers: lot, date, quantity taken, purchase price. The screen also shows the weighted-average alternative and the difference between them
- An immutable ledger — a posted document is never edited. A mistake is fixed by a reversal, which is itself a document and returns the goods to the very lots they came from
- Reversal has rules — a receipt whose lot has already been shipped cannot be reversed: putting it back would be a lie about the cost of orders already made from it
- Transfers keep age and price — moving stock between warehouses recreates the same layers on the other side instead of resetting the queue
- Stock counts that mean something — the sheet snapshots the expected quantity when it opens, because the warehouse keeps working while somebody walks the aisles with a tablet. Surplus is received at the average cost of the remainder, shortage is written off through the queue: two different documents, not one with a sign
- Integers everywhere — money in kopecks, quantity in thousandths of a unit. No floats in a system whose whole job is that the numbers add up
crate:verify— recomputes every balance from the ledger and compares it with the lots. A mismatch means a bug, and the nightly run finds it before an inventory does- Reorder alerts — an hourly job raises a signal when stock drops below the point and clears it by itself when it recovers
git clone https://github.com/dripips/crate.git
cd crate
composer install
npm install && npm run build
cp .env.example .env
php artisan key:generate
createdb crate # PostgreSQL 16+
php artisan migrate --seed # four months of a hardware shop
php artisan serveOpen http://localhost:8000 and sign in as owner@example.com / crate (owner), sklad@example.com (storekeeper) or buh@example.com (viewer) — same password.
The seeder posts every document through the real ledger: 436 documents, 314 lots and about a thousand consumption rows in seven seconds. It is deterministic, so the numbers in the screenshots are the numbers you get.
php artisan queue:work # reorder alerts
php artisan schedule:work # hourly alerts, nightly ledger verification
php artisan crate:verify # on demand| Question | Answer |
|---|---|
| Which stock leaves first? | The oldest lot by received_at, then by id |
| What did it cost? | Σ (quantity taken from a lot × that lot's price), rounded to the kopeck once per layer, half up |
| What would the average have said? | The weighted average of the remainder at the moment of posting, stored on the line |
| Where did the balance come from? | Σ lots.quantity_left — and crate:verify proves it equals the ledger |
php artisan test40 tests: FIFO order and layer splitting, rounding of fractional quantities, rollback when stock is short, reversal rules in both directions, transfers preserving age and price, stock counts, reorder alerts, roles, and an end-to-end integrity check after every kind of movement.
They run on PostgreSQL rather than SQLite on purpose: row locking, bigint precision and the partial index behind the FIFO queue are exactly what would differ in production.
docker compose up --build
docker compose exec app php artisan migrate --seedLaravel 13 · PHP 8.5 · PostgreSQL 18 · Blade · Tailwind 4 · Vite 8
MIT


