Restock Manual, as shipped in the download
Restock — User Manual
Demand forecasting and a reorder board, computed from your own sales history. Version 3.1.4 [src: app/controllers/api.php:95].
About this manual
Every statement here was written by reading Restock's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.
Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.
What Restock is
It forecasts demand from your own sales history and tells you what to reorder [src: app/controllers/mcp.php:60]. Each product carries a lead time, a safety buffer and a current stock level; from those it computes average daily demand, a reorder point, days of stock left and a suggested order quantity.
There is no model involved. The engine is pure, deterministic arithmetic with no AI and no network call [src: app/src/Forecast.php:3], and every function in it is unit-tested against hand-computed fixtures [src: app/src/Forecast.php:16].
The arithmetic, in full
The source states the whole method in five lines [src: app/src/Forecast.php:5], and it is short enough to reproduce here so you can check it against your own spreadsheet:
avg daily demand = units sold in the window / window days (window ends today, inclusive)
safety stock = manual override qty, OR safety-days × avg daily demand
reorder point = (avg daily demand × lead-time days) + safety stock
days of stock = current stock / avg daily demand (null when demand is zero)
on order = units on SENT purchase orders not yet received
status = (current stock + on order) against the reorder point
suggested order = ceil(target-cover days × avg daily demand − current stock − on order), floored at 0
Stock already on order counts (since 3.1.4) [src: app/src/Forecast.php:99]. A product on a sent purchase order is judged by what is on the shelf plus what is coming, so it is not flagged — or suggested — a second time while the delivery is on its way; days of stock still counts only the shelf. A draft order is not on order until it is sent, and a received order is already in stock [src: app/controllers/app.php:272].
A few details the formulas alone do not show:
Days with no sales count as zero-demand days, not as missing data [src: app/src/Forecast.php:34]. The window is the N days ending today, inclusive [src: app/src/Forecast.php:35].
A manual safety-stock override wins outright over the calculated figure [src: app/src/Forecast.php:61]. If you know a product needs a floor of 40 units regardless of what the maths says, that is the number used.
Days of stock is null rather than zero when there is no measurable demand [src: app/src/Forecast.php:73]. A product nobody is buying does not have "0 days left"; it has no meaningful answer, and the product says so rather than inventing an alarming number.
A suggested order is never negative [src: app/src/Forecast.php:80]. Overstocked products suggest nothing rather than a negative quantity.
Status: below, near, healthy
Three states [src: app/src/Forecast.php:87]:
| Status | Meaning |
|---|---|
below | At or under the reorder point — order now |
near | Within 25% above the reorder point — order soon [src: app/src/Forecast.php:23] |
healthy | Above that |
A product with no demand and no safety floor reports healthy [src: app/src/Forecast.php:89], because a reorder point of zero cannot be breached. That is worth knowing: healthy here can mean "genuinely well stocked" or "nothing is happening with this product at all", and the days-of-stock column is what distinguishes them.
Importing sales and products
The importer treats every upload as adversarial input, and the specifics are worth knowing because they determine what happens to a messy file [src: app/src/Importer.php:5]:
Columns are mapped by header name, never by position, with common aliases accepted [src: app/src/Importer.php:6]. Reordering your columns does not break the import.
Every string is clipped to its column width and every integer clamped, so a 500-character SKU or a 15-digit quantity lands truncated rather than producing a database error [src: app/src/Importer.php:7].
Malformed rows are skipped with a per-line reason, and you are shown how many imported, how many were skipped, and why [src: app/src/Importer.php:9].
There are hard caps on both the upload size and the row count, and the file is stream-parsed so a huge one never loads into memory [src: app/src/Importer.php:11].
Importing the same dates twice counts them once (since 3.1.4). A sales file's rows are totalled per SKU per date, and each total replaces what is stored for that SKU on that date, so a weekly export that overlaps last week's no longer inflates demand [src: app/src/Importer.php:149], [src: app/src/Importer.php:209]. The dry run says how many stored SKU-days a file would replace. Rows sent over the REST API or MCP are added, as before, unless the request says "replace": true.
Importing sales does not change stock levels. Stock changes when you import the product file, correct a count, or receive a purchase order.
The dry run is a real run that writes nothing
In dry-run mode every row is parsed, validated and classified exactly as it would be on a real import — including the unknown-SKU lookup and the insert-versus-update decision — but nothing is written [src: app/src/Importer.php:14].
That is the useful kind of preview: it tells you what would happen rather than what might.
Purchase-order exports
The reorder board decides what to buy; a purchase order records that you did. The export presets are the last hop — the file your supplier or bookkeeper will actually accept [src: app/src/ExportPresets.php:5].
Each preset is a fixed, documented column list, and the source is explicit that a supplier's import template breaks the moment a column moves, so the headers are covered by tests and must not be reordered casually [src: app/src/ExportPresets.php:7].
Formula injection is neutralised
Every cell passes through the CSV writer, which is RFC 4180 and quotes any cell beginning =, +, -, @, tab or carriage return unless it is a plain number — so a product named =cmd|' /c calc'!A0 cannot execute in your supplier's spreadsheet [src: app/src/ExportPresets.php:11].
Plain numbers are left alone, so quantity and price columns still import as numbers.
Users and roles
Three roles, and the source names the split [src: app/controllers/app.php:130]: a buyer needs to correct stock, ingest sales and raise purchase orders, while pricing, settings, roles, deleting products and wiping history stay with the owner.
| Permission | viewer | member | admin |
|---|---|---|---|
.view permissions | yes | yes | yes |
audit.view | yes | yes | yes |
stock.set | — | yes | yes |
sales.ingest | — | yes | yes |
po.create / po.advance | — | yes | yes |
product.edit | — | yes | yes |
| everything else | — | — | yes |
⚠️ The audit trail is readable by a viewer
Restock's viewer role is granted *.view — a wildcard rather than a list [src: app/controllers/app.php:132]. Because the audit permission is named audit.view, the wildcard matches it, so a viewer can read the audit trail. This has been raised against the product.
Backups follow the model
Restock's database backup downloads are permission-gated, requiring the settings permission [src: app/controllers/api.php:254]. A viewer cannot download the database.
The API and agent access
Five tools are exposed to an agent, of which two write [src: app/controllers/mcp.php:67]: listing products, product detail and the reorder report on the read side; adjusting a stock level and ingesting sales on the write side.
The instructions state the constraint that matters: you cannot change a forecast directly — it is derived from sales [src: app/controllers/mcp.php:65].
That is the right shape. An agent can correct the inputs, and the outputs follow; it cannot reach in and set a reorder point to a number it prefers.
What Restock does not do
It does not use AI to forecast [src: app/src/Forecast.php:3]. The arithmetic is published above and you can check it by hand.
It does not let anyone edit a forecast [src: app/controllers/mcp.php:65]. Correct the sales history or the stock level; the forecast follows.
It does not order anything. It produces a purchase order and a file your supplier accepts [src: app/src/ExportPresets.php:5]; sending it is yours.
It does not invent a days-of-stock figure for a product with no demand [src: app/src/Forecast.php:73].
It does not know about seasonality, promotions or a supplier's minimum order. The window average is exactly that — an average over the window you chose.