sql.blacktorgroup.com/Reorder Point

Flagging parts before they actually run out.

Branches reordered parts on a gut feeling, glancing at the shelf rather than at how fast a part was actually being used, which meant some parts ran out mid-repair and others sat as excess stock for months. One query replaces the glance with a number.

A translucent black cube etched with a moss-green map of the world's continents, glowing softly against black.
BlackTor Group Ltd Dartmoor, UK
A note on this page

A reference, not a client write-up.

Illustrative, per the note on the main SQL page: not real client work. It sits alongside the Bill of Materials and Bay Booking pages as a deeper dive on one query pattern that comes up often enough to be worth its own write-up.

The issue

A fixed reorder quantity goes stale as demand shifts.

A part's reorder point, the stock level that should trigger a new order, only means anything relative to two things: how long the supplier takes to deliver, and how fast the part is actually being used. A part used twice as fast this quarter needs reordering at roughly twice the stock level it did last quarter, and a fixed number set once in a spreadsheet never catches up with that on its own.

StockMovements logs every change to a part's quantity, positive for a delivery received, negative for a part used on a job. That's enough to calculate a real, current usage rate directly, rather than guessing at one.

Window function

PartsBelowReorderPoint: usage rate, not a fixed number, sets the threshold

-- PartsBelowReorderPoint: flag any part whose stock on hand won't last -- through its own supplier lead time, based on ACTUAL recent usage rather -- than a fixed reorder quantity that goes stale as demand shifts. WITH DailyUsage AS ( -- One row per part per day it moved at all. Only negative movements -- count as usage, so a delivery received doesn't cancel out real demand. SELECT part_id, movement_date, -SUM(quantity_change) AS units_used FROM StockMovements WHERE quantity_change < 0 AND movement_date >= DATE('now', '-28 days') GROUP BY part_id, movement_date ), AverageUsage AS ( -- Average daily usage per part across the trailing 28-day window. -- AVG() OVER a partition keeps the per-part average and the per-day -- detail in the same result set, rather than needing a second -- GROUP BY pass purely to collapse DailyUsage down to one row per part. SELECT part_id, AVG(units_used) OVER (PARTITION BY part_id) AS avg_daily_usage FROM DailyUsage ) SELECT DISTINCT p.part_id, p.part_name, p.quantity_on_hand, p.lead_time_days, ROUND(au.avg_daily_usage, 2) AS avg_daily_usage, ROUND(au.avg_daily_usage * p.lead_time_days, 0) AS reorder_point FROM Parts AS p INNER JOIN AverageUsage AS au ON au.part_id = p.part_id WHERE p.quantity_on_hand <= au.avg_daily_usage * p.lead_time_days ORDER BY (p.quantity_on_hand - au.avg_daily_usage * p.lead_time_days) ASC;

The reorder point itself is one idea: average daily usage multiplied by lead time days, which is exactly the stock needed to bridge the gap between placing an order and it arriving. A part currently at or below that figure is at real risk of running out before a fresh order can land.

Ordering by how far below the threshold each part sits, rather than alphabetically or by part number, puts the most urgent shortages at the top of the report without anyone having to scan the whole list to find them.

For speed: an index on StockMovements(part_id, movement_date) matters here more than most queries on this site, since StockMovements grows with every single sale and repair, and this report is meant to run daily, not as a one-off.

Result

A daily flagged list, not a monthly guess.

Branches now see exactly which parts need ordering today, based on how fast they're actually moving, rather than finding out a part has run out when the next bike needs it.