Inventory · Consumable supply

Knowing you'll be out of stock before you are

A minimum stock level on a spreadsheet only helps if somebody opens the spreadsheet. By the time an item is visibly low, the order that would have prevented the shortage needed placing two weeks ago. The number that matters was never the count on the shelf — it is the count divided by how fast it is leaving, measured against how long resupply takes.

3 alert severity tiers
4 hr background check cycle
19 server operations behind one screen
Confidential client & operation

Built for the stockroom of a contract-logistics site, covering the packaging and floor consumables the operation spends to do its work.

The problem

Consumables were tracked in a shared workbook with a minimum, a maximum, an on-hand column and a lead time. We know the shape of it precisely, because the importers we wrote to seed the new system read exactly those columns — one to create the item catalogue, one to back-fill lead times onto items that already existed.

A sheet like that fails in two quiet ways. The on-hand figure drifts, because it is only correct if every person who takes a box remembers to open the file and change it. And the minimum level is a number nobody reads until an item has already reached it — by which point the resupply lead time has been spent rather than planned for.

  • Stock counts were maintained by hand, and only when someone remembered
  • Nothing connected a minimum level to anybody's attention
  • Lead time was recorded somewhere, but nothing computed against it
  • The first dependable signal of a shortage was an empty shelf

What we built

A stockroom application built around scanning rather than typing. Every unit received takes a licence-plate identifier, and every issue is a scan of that identifier; separately, each item in the catalogue has its own printed barcode label, so pulling an item up on screen is a scan rather than a search. Every event — creation, receipt, issue, move, edit, deletion, count correction, alert — writes a row to one ledger whose columns are the item, the identifier, the quantity change, the user, the time and a note. Eight transaction types share those rows, so the history screen, the forecast and the audit trail all read the same table.

Locations are a controlled list, not a text box. Receiving into or moving to a location that isn't registered is refused, with the instruction to scan a valid one or create it first. That is the difference between a location field and a folk taxonomy a year later.

  • Receipts can auto-generate one identifier per unit, each with a random suffix and a database uniqueness constraint behind it
  • Issuing stock requires a scanned identifier — the convenient 'just decrement the count' path exists in the code and is deliberately refused
  • A scan against an unknown or already-consumed identifier fails and emails an administrator rather than failing quietly
  • Physical counts write back through the same ledger: shortfalls consume the oldest stock first, overages open a new adjustment identifier at the default location
  • Deleting an item is a soft delete, and the function re- checks the caller's leadership or admin flag in the database rather than trusting page-level access
  • Item labels are generated with every user-entered field hex-escaped into the printer language, and the bar module width recalculated from encoded length so the code stays scannable

Forecasting instead of thresholds

A minimum level answers 'are we low', which is the wrong question. The one worth answering is 'will we run out before a replacement order can arrive', and that takes three inputs: what is on hand, how fast it is leaving, and how long resupply takes.

Consumption is a recency-weighted blend of the 30-, 90- and 365-day rates, weighted toward the recent and smoothed by the history. Each rate is divided by the shorter of its own window and the item's days since first stock movement, so an item that sat in the catalogue for months before its first receipt doesn't read as slow-moving. Stock written off at a count is treated as consumption too — material that left the building without being scanned is still demand, and excluding it would flatter every forecast.

The result is a projected stockout date, compared against lead time plus a safety buffer of twenty per cent of lead time or three days, whichever is larger. Below minimum, or projected to run out inside the lead time, is critical: an order placed today would arrive too late. Inside the safety buffer is a warning. Inside a further two-week visibility horizon is a watch — which appears on the dashboard and is never emailed.

  • Items with no minimum are excluded entirely; 'order as needed' is a legitimate policy and forecasting it produces noise
  • Every row carries a confidence grade derived from how many usage events the item has recorded in ninety days, so a projection built on one event is labelled as one
  • Trend compares the last thirty days against the preceding sixty and is flagged at plus or minus twenty- five per cent
  • Lead times entered in days, weeks or months are normalised to days before anything is compared against them
  • The suggested order quantity fills to the configured maximum where that sits above what's on hand, and otherwise covers lead time plus thirty days of projected usage

Sending the alert exactly once

The background service wakes every four hours. The same check also runs inline immediately after any issue or completed count, so an alert can follow the scan that caused it by seconds instead of waiting for the next cycle.

Which makes not spamming people the real engineering problem. Three things guard it. The alert is edge- triggered — it fires for items that have newly entered a critical or warning state, compared against the set outstanding at the last check. It is capped at one email a day, and that cap is a row in the shared ledger rather than a variable in memory, so restarting the application doesn't re-send. And the local file holding the outstanding set is written best-effort, swallowing its own errors; if it is lost, the ledger row is still the backstop. Standing problems belong on the dashboard, not the inbox: an item critical for a week does not email again until something changes, which is the difference between an alert people act on and an alert people filter.

The last guard is about which process is doing the sending. Nothing stops the application being started as more than one process — a second instance, or the standalone worker script that owns some of the same queues — so every background loop coordinates through an exclusive, session-scoped database application lock, and a process that cannot take it logs why and doesn't start the loop. This monitor needs one extra turn of that pattern. Its start function spawns a daemon thread and returns immediately, and the helper releases the lock as soon as the launched call returns — which would leave a second process free to take it and start a second monitor moments later. So the monitor is one of only two workers launched with the flag that holds the lock for the life of the process instead, with a keepalive query every minute so the database session never quietly drops it. Without that, two processes would run the same forecast on the same schedule, and duplicate alerts would be prevented only by the daily ledger row — a backstop doing a design's job.

The result

The stockroom is a scanned, ledgered system rather than a maintained document. On-hand follows from what was received and issued, the age of each unit is a recorded fact rather than a guess, and a year of consumption per item is reportable without anyone assembling it.

The number the system can answer is the one the spreadsheet never held. Not how much is on the shelf — how many days of it are left, and whether that is longer or shorter than the time it takes to get more.

Built with

  • Python
  • Flask
  • SQL Server
  • pandas
  • Chart.js
  • Zebra ZPL
  • Gmail API (OAuth2)

On the details we left out

Client, location, system names and volumes are withheld deliberately. We're happy to go deeper on architecture and approach on a call.

Services this draws on

Legacy Modernization & Migrations

Bring decades-old systems into the modern era without a risky rip-and-replace.

See the service →

Workload Optimization

Squeeze more throughput out of the hardware and licenses you already pay for.

See the service →

More projects

Platform · Multi-year build

One application that replaced a decade of spreadsheets

A warehouse ran its floor on shared spreadsheets, emailed reports, and tribal knowledge. We replaced all of it with a single internal platform — and kept it running while people worked.

Read the story →
Casework · Damage & exceptions

Exceptions that open their own case file

A damage or count discrepancy is a claim, and a claim is worth whatever evidence is attached to it. We built the casework — photo capture at the point of discovery, a case that assigns and escalates itself, and a PDF report that goes out with the pictures already in it.

Read the story →
Workflow · Intake & approvals

A request queue where the approvals set the status

Internal software requests arrived by email and were approved in conversation. We built an intake form, a board and a three-role approval gate where the votes move the card — and gave requesters a private link so they can follow and edit their own request without an account.

Read the story →

Got a system like this?

A free assessment gives you a clear picture and a prioritized plan — no commitment.

Book a free assessment