Skip to content
All guides

Inventory · Updated August 31, 2026

Variance

The variance report puts what your recipes say should have left each shelf next to what actually did, item by item and location by location, and prices the gap in shillings.


The question it answers

You bought twelve bottles of Gilbey's for the main bar, started the week with three, and counted four at the end. Eleven left the shelf. The till says you sold 264 tots, which through a 25 ml recipe is 6,600 ml, or 8.8 bottles. That leaves 2.2 bottles nobody sold, wrote off or transferred. At the bar's average cost, that's the number on this report.

The screen is under Inventory as Variance, and needs view_reports or view_inventory. Every figure on it is the server's; the browser never multiplies a quantity by a cost.

The equation

For every item at every location, over the period:

actual usage = opening + purchases + free issues + transfers in
               - transfers out - waste - transit loss - closing
variance     = theoretical usage - actual usage
at cost      = variance x weighted average cost at period end

Negative is a loss. The screen never shows a bare signed number; a shortfall reads "2.20 short" and a surplus "0.50 over", because on the invoice screens positive means the supplier overcharged you, and a reader who carried that convention across would read a loss as a gain.

Where each term comes from:

  • Opening is the ledger balance at the start of the period. That's the same thing as the previous period's counted closing, because approving a stocktake posts the adjustment that brings the ledger to the counted figure. Where no count was approved, the ledger balance is honestly what remains. Periods chain counted-to-counted, so a bad count corrupts one period rather than every period after it.
  • Purchases are receipts from a goods receipt or a cash purchase. An opening-balance seed is not a purchase; counting it would double the opening.
  • Free issues are receipts a supplier gave rather than sold. They're kept apart from purchases because a manager told they bought 24 bottles when 12 were a gift is being told something false about spend.
  • Transfers in and out are movements between locations.
  • Waste is every approved waste note. Transit loss is stock lost between outlets and flagged at a branch-transfer receipt; a negative transit loss means more arrived than was sent.
  • Closing is the counted quantity from the latest approved stocktake in the period at that location. Not the ledger balance, ever. More on that below.
  • Theoretical usage is the sum of the stored recipe explosions from every active sales import whose business date falls in the period, at the location each sale depleted.

Quantities are held to four decimal places and money to two, rounded once at the end of the fold.

What is deliberately not a term

The count's own adjustment. When a stocktake is approved, the ledger receives a count_variance movement per disagreeing line. That movement is the count, and the closing figure already carries it; adding it as a term would count the count twice. On the hand-computed test week this engine is gated on, doing so turns a 4-bottle variance into 138.

Direct issues and historical manual adjustments aren't terms either. They change what's physically on the shelf without being a purchase, a transfer or a write-off, so they surface where they belong: inside actual usage. New free-form adjustments are no longer accepted.

A reversal is attributed to the term it undoes, never to a bucket of its own. Reject a cash purchase and its reversal nets purchases down, rather than staying counted as a purchase forever with a mystery outflow somewhere else.

Why sales never post

Sales aren't ledger movements. If they were, the ledger would agree with the recipes by construction and every cell would read zero. The explosion is stored at import time, recipe snapshot and all, so last month's figures don't move when someone edits a recipe today. See Recipes for how one sale becomes leaf-item quantities.

This is also why an uncounted location has no variance rather than a variance of zero. Substitute the ledger balance for the missing count and actual usage comes out as exactly zero, so the report announces the whole theoretical usage as a surplus: a windfall for the one room nobody checked. The engine refuses. A cell with no approved count in the period shows a dash and a "not counted" tag, and any total that contains a null is itself null.

Explained and unexplained

Waste, transit loss and transfers are explained movement: there's a document and a second person behind each one. Sales are explained consumption, through recipes. What's left after all of that is the unexplained part, and that's the variance. The practical consequence is that the way to shrink a variance honestly is to document what happened: a spillage as a waste note, staff meals as zero-value sales lines, a move to the other bar as a transfer. The dishonest way is to inflate a recipe, which is why recipe and mapping edits sit behind manage_recipes.

Reading the screen

Pick an Outlet, a From and a To date, or use the presets. The default is the last seven days ending yesterday. Both dates are business days in the outlet's own timezone (Nairobi unless the outlet says otherwise), and the server counts the To day in, so 1 to 7 August is seven days. The export's header prints the window as a start and an exclusive end, so it will show the day after your To date.

The table has one row per item per location: Actual used, Recipes say, Variance and At cost. Rows sort losses first, largest first, then the unknowns, then the rest, so reading top to bottom starts with the thing worth investigating. The footer is Total for this outlet, and it is a dash whenever any location in scope wasn't counted.

Two banners sit above the table when they apply:

  1. Coverage. "This period has no outlet total, because 1 of 3 locations were not counted", with the stock value and share of the outlet sitting behind the gap. A dash is only honest if you can see how much of the estate is behind it.
  2. Sales coverage. "12 sales lines have no recipe behind them, so this variance is overstated", followed by the unmapped POS names, most sold first, up to twenty. An unmapped line explodes to nothing, which lowers theoretical usage and makes every variance bigger, in the direction that looks like theft. It's the normal state of a tenant's first week and the fix is a mapping job, not an incident.

The Export button (needs export_reports or view_reports) gives you the same report as CSV or Excel, with the full equation as columns: opening, purchases, free issues, transfers in and out, waste, transit loss, closing, actual usage, theoretical usage, variance quantity, weighted average cost and variance in KES. Uncounted cells are genuinely empty, never zero, so a spreadsheet sum can't quietly include them. The footer repeats the total, the counted and uncounted location counts, and the stock value behind the uncounted ones.

An empty period says "Nothing to compare yet": variance needs an approved count to close the period and a day's sales to explode.

The Reports page

Reports, in the same section, opens with a league table of every outlet you can see, ranked losses first: each row shows the period's variance as "KES 41,200 loss" or "KES 3,150 surplus", the waste written off, and the share of stock value that was counted. A dash in the variance column means the outlet had an uncounted location. The Full variance report link brings you back here with the same range.

The large variance alert

Under Settings, the Inventory tab has a Variance card with an Alert threshold (KES), defaulting to 10,000 and editable with manage_inventory_settings. When a stocktake is approved and the value of the count adjustments it posted reaches the threshold, everyone holding view_reports for that outlet gets a notification titled "Large variance on count" followed by the count's code, linking to this report. It's measured on the absolute value, so a large overage alarms as loudly as a shortage; both are worth a look. Set to zero, it fires on every posted count, including the ones that balanced.

Two refinements matter in practice. The first approved count at a location establishes a balance rather than disputing one, so it never alerts. And when part of a later count's discrepancy couldn't be priced, because the item had never been valued at that location, the alert fires regardless of the threshold and says so: a value test can't judge a figure it couldn't value.

FAQ

The outlet total is a dash. Where's my number? One or more locations weren't counted in the period, and a total that skipped them would be a number you'd act on believing it covered the whole outlet. The coverage banner names how many and how much value sits behind them. Count those locations and the dash becomes a figure.

Why doesn't a sale show in Movements? Because it never posts. Sales are compared against the ledger, not written to it. You'll see them as "Recipes say" on this report and as exploded lines on the import.

Variance went down after I mapped a menu item last week. Is history being rewritten? Only forward. Mapping settles lines that were still waiting; they contributed nothing before and now contribute their explosion. Lines that had already exploded keep what they had. The report moved toward the truth, not away from it.

The count was approved but this period still shows "not counted". The count's counted-at time has to fall inside the period, and the count has to be approved, not just submitted. Check the count's date, and check the period's To date covers it.

Why can't I see a total quantity across items? Because bottles and kilograms don't add. Quantities total only within one item across its locations; anything across items is totalled in KES.