Feature
StockRoom
What arrived, what sold, what's actually on the shelf — and the gap between the last two.
Movements are the truth
There is no stored quantity-on-hand. Every arrival, sale and adjustment is a row in inventory.stock_movements, and on-hand is a view over them.
Why not just keep a number
Because a stored count has to survive every write forever, and drifts the first time one fails halfway. Deriving it means the log is the truth, on-hand can never disagree with it, and a physical count becomes a comparison rather than a correction.
The pages
| Page | Does |
|---|---|
| Products | The catalogue, and each product's terms — franchise price, units per pack, stock unit. |
| Receive | Stock arriving, by supplier order. Multi-row, so a delivery is one entry rather than ten. |
| Stock out | What sold. The sales menu is listed in counting order — the order the shelf is physically walked — so entering a day matches how it's actually counted. |
| Adjust | A day's wastage, supplies used, corrections and returns — one row each, with a reason. |
| Audit | A physical count. Enter what's really there; the variance view does the comparing. |
Prices change; history must not
inventory.product_terms versions a product’s terms with effective dates, so repricing in April doesn’t rewrite what March cost. Reports resolve the terms that were in force on the date being reported, not the ones in force now.
Reports
| Report | Shows |
|---|---|
| Orders | Stock received per day, from the daily view. |
| Stock out | Stock sold per day, in units and in value. |
| Adjustments | Everything that moved stock without being a delivery or a sale, grouped by day and by reason. |
| Low stock | What's under its reorder point and needs ordering. |
| Reconcile | Stock sold against cash recorded — the two features checking each other. |
Reconciliation is the interesting one
StockRoom knows what left the shelf and what it should have been sold for. DailyCash knows what actually reached the drawer. Neither can audit itself; together they can. That’s the number the inventory.sales_reconciliation view produces, and it’s the same shortfall that turns up as Loss against a staff member’s commission.
Adjustments are valued, not recorded
Orders and Stock out sum a line_value stored at the time, so historical prices stay as they were. An adjustment has no such figure — nothing was bought or sold — so its money column is derived: the quantity at the franchise price in force on that date, through the same product_terms lookup a back-dated entry uses. It answers “what did this month’s wastage cost”, and is labelled Estimated cost so it isn’t read as money that crossed the counter.
Correcting a day
Each day on the Orders, Stock out and Adjustments reports carries a menu beside its total. Edit this day opens the page that records that kind of movement — Orders to Receive, Stock out to Stock out, Adjustments to Adjust — on that date, via a ?date= parameter. The page then loads the day and saving replaces it, so correcting from a report is the same flow as picking the date by hand rather than a second way to edit. Delete this day removes every ledger row the report shows for it.
Every day is editable
Receive, Stock out and Adjust all work the same way: pick a date, whatever is recorded loads into the form, and saving replaces that day in a single statement rather than adding a second copy. Stock out has worked like this since migration 0020; the other two got their own replace functions in 0023.
What a replace must not touch
Each function deletes only its own reasons. replace_adjustments deliberately leaves count_correction rows alone: those are written by Audit and are how a physical count realigns on-hand, so editing an unrelated wastage entry must not silently undo an audit performed the same day.
All three refuse to save when the lookup for that date fails. Saving is a replace, so a page that could not read the day cannot safely overwrite it — it would delete rows it never showed anyone.
Entering stock
Receive, Stock out and Adjust all show what they’re about to write before writing it, with the date as the largest thing on the dialog. They used to save the instant the button was pressed, and since the date field defaults to today, a sheet filled in for yesterday would save under today without ever showing which date it used — a mistake that leaves no trace to notice afterwards. A date that isn’t today is flagged rather than blocked, because back-dating is normal.
The product pickers on all three follow the Stock out menu’s order — the order the shelf is physically walked at close — rather than the catalogue’s sort_order. Someone who has just counted in that order shouldn’t have to hunt for each line.
Re-recording a day
Migration 0020_replace_sales_for_date exists so a day entered wrongly can be entered again without duplicating it — the replacement is atomic rather than a delete followed by a hopeful insert. Editing a day that already has sales is a supported path, not a repair job.