The SAP Business One alert pack
Eleven ready-made queries that turn SAP Business One's built-in Alerts Management into an early-warning system for inventory: dead stock, over-promised orders, repeated corrections, late postings, stock that was never put away and more. Each one is read-only, free, and tested on SAP's own demo company.
How to set up an alert in SAP Business One
- Open the Query Generator (Tools → Queries → Query Generator), switch to editing the SQL, paste a query from this page and save it to a category in Query Manager.
- Go to Administration → Alerts Management and create a new user-defined alert.
- Choose the saved query, set the frequency (suggestions below), the priority and the users who should receive it, and mark it Active.
B1 runs the query on schedule and alerts the users only when it returns rows — so a quiet day means nothing needs attention. The queries use fixed thresholds (7, 90, 365 days and so on) because alerts can't prompt for values; change the numbers to suit you. As with any query, try it in a test company first.
The queries
- Dead stock
- Promised beyond stock, even after open purchase orders
- Below minimum stock after open orders
- Manual stock corrections this week — gross vs net
- Items corrected again and again
- Movements recorded late
- Goods receipts that always match the purchase order
- Stock not put away
- Negative stock
- Transfer requests left open
- Bin stock that hasn't moved in six months
1. Dead stock
Stock with no movement for a year is cash you can't use — and SAP Business One has no standard report that lists it.
/* Dead stock: items with stock and no movement in the last 365 days */
SELECT TOP 100
W.ItemCode, I.ItemName, W.WhsCode, W.OnHand, W.StockValue,
M.LastMovement, DATEDIFF(DAY, M.LastMovement, GETDATE()) AS DaysSinceMovement
FROM OITW W
JOIN OITM I ON I.ItemCode = W.ItemCode
LEFT JOIN (
SELECT ItemCode, Warehouse, MAX(DocDate) AS LastMovement
FROM OINM WHERE InQty <> 0 OR OutQty <> 0
GROUP BY ItemCode, Warehouse
) M ON M.ItemCode = W.ItemCode AND M.Warehouse = W.WhsCode
WHERE W.OnHand > 0 AND I.InvntItem = 'Y'
AND (M.LastMovement IS NULL OR M.LastMovement < DATEADD(DAY, -365, GETDATE()))
ORDER BY W.StockValue DESC
SAP HANA version
/* Dead stock: items with stock and no movement in the last 365 days */
SELECT
W."ItemCode", I."ItemName", W."WhsCode", W."OnHand", W."StockValue",
M."LastMovement", DAYS_BETWEEN(M."LastMovement", CURRENT_DATE) AS "DaysSinceMovement"
FROM "OITW" W
JOIN "OITM" I ON I."ItemCode" = W."ItemCode"
LEFT JOIN (
SELECT "ItemCode", "Warehouse", MAX("DocDate") AS "LastMovement"
FROM "OINM" WHERE "InQty" <> 0 OR "OutQty" <> 0
GROUP BY "ItemCode", "Warehouse"
) M ON M."ItemCode" = W."ItemCode" AND M."Warehouse" = W."WhsCode"
WHERE W."OnHand" > 0 AND I."InvntItem" = 'Y'
AND (M."LastMovement" IS NULL OR M."LastMovement" < ADD_DAYS(CURRENT_DATE, -365))
ORDER BY W."StockValue" DESC
LIMIT 1002. Promised beyond stock, even after open purchase orders
Customers have been promised more than you have — and what's on order won't cover it. These are tomorrow's late deliveries.
/* Promised to customers beyond stock, even after open purchase orders */
SELECT
W.ItemCode, I.ItemName, W.WhsCode, W.OnHand, W.IsCommited AS Committed, W.OnOrder AS Ordered,
W.OnHand - W.IsCommited + W.OnOrder AS Available
FROM OITW W
JOIN OITM I ON I.ItemCode = W.ItemCode
WHERE I.InvntItem = 'Y' AND W.OnHand - W.IsCommited + W.OnOrder < 0
ORDER BY W.OnHand - W.IsCommited + W.OnOrder
SAP HANA version
/* Promised to customers beyond stock, even after open purchase orders */
SELECT
W."ItemCode", I."ItemName", W."WhsCode", W."OnHand", W."IsCommited" AS "Committed", W."OnOrder" AS "Ordered",
W."OnHand" - W."IsCommited" + W."OnOrder" AS "Available"
FROM "OITW" W
JOIN "OITM" I ON I."ItemCode" = W."ItemCode"
WHERE I."InvntItem" = 'Y' AND W."OnHand" - W."IsCommited" + W."OnOrder" < 0
ORDER BY W."OnHand" - W."IsCommited" + W."OnOrder"3. Below minimum stock after open orders
Checks open purchase orders before flagging a reorder, so you don't double-order. Works whether you set minimums per warehouse or per item.
/* Below minimum stock after open purchase orders (warehouse or item level) */
SELECT W.ItemCode, I.ItemName, W.WhsCode, W.OnHand - W.IsCommited + W.OnOrder AS Available, W.MinStock AS Minimum, 'Warehouse' AS Level
FROM OITW W JOIN OITM I ON I.ItemCode = W.ItemCode
WHERE I.InvntItem = 'Y' AND W.MinStock > 0 AND W.OnHand - W.IsCommited + W.OnOrder < W.MinStock
UNION ALL
SELECT I.ItemCode, I.ItemName, '*', I.OnHand - I.IsCommited + I.OnOrder, I.MinLevel, 'Item'
FROM OITM I
WHERE I.InvntItem = 'Y' AND I.MinLevel > 0 AND I.OnHand - I.IsCommited + I.OnOrder < I.MinLevel
SAP HANA version
/* Below minimum stock after open purchase orders (warehouse or item level) */
SELECT W."ItemCode", I."ItemName", W."WhsCode", W."OnHand" - W."IsCommited" + W."OnOrder" AS "Available", W."MinStock" AS "Minimum", 'Warehouse' AS "Level"
FROM "OITW" W JOIN "OITM" I ON I."ItemCode" = W."ItemCode"
WHERE I."InvntItem" = 'Y' AND W."MinStock" > 0 AND W."OnHand" - W."IsCommited" + W."OnOrder" < W."MinStock"
UNION ALL
SELECT I."ItemCode", I."ItemName", '*', I."OnHand" - I."IsCommited" + I."OnOrder", I."MinLevel", 'Item'
FROM "OITM" I
WHERE I."InvntItem" = 'Y' AND I."MinLevel" > 0 AND I."OnHand" - I."IsCommited" + I."OnOrder" < I."MinLevel"4. Manual stock corrections this week — gross vs net
A net figure lets overages hide shortages. This shows how much was corrected up and down, per item.
/* Manual stock corrections in the last 7 days: gross vs net per item */
SELECT
N.ItemCode, N.Warehouse, COUNT(*) AS Corrections,
SUM(N.InQty) AS QtyUp, SUM(N.OutQty) AS QtyDown,
SUM(ABS(N.TransValue)) AS GrossValue, SUM(N.TransValue) AS NetValue
FROM OINM N
WHERE N.DocDate >= DATEADD(DAY, -7, GETDATE())
AND (N.TransType = 10000071
OR (N.TransType = 59 AND NOT EXISTS (SELECT 1 FROM IGN1 L WHERE L.DocEntry = N.CreatedBy AND L.LineNum = N.DocLineNum AND L.BaseType = 202))
OR (N.TransType = 60 AND NOT EXISTS (SELECT 1 FROM IGE1 L WHERE L.DocEntry = N.CreatedBy AND L.LineNum = N.DocLineNum AND L.BaseType = 202)))
GROUP BY N.ItemCode, N.Warehouse
ORDER BY SUM(ABS(N.TransValue)) DESC
SAP HANA version
/* Manual stock corrections in the last 7 days: gross vs net per item */
SELECT
N."ItemCode", N."Warehouse", COUNT(*) AS "Corrections",
SUM(N."InQty") AS "QtyUp", SUM(N."OutQty") AS "QtyDown",
SUM(ABS(N."TransValue")) AS "GrossValue", SUM(N."TransValue") AS "NetValue"
FROM "OINM" N
WHERE N."DocDate" >= ADD_DAYS(CURRENT_DATE, -7)
AND (N."TransType" = 10000071
OR (N."TransType" = 59 AND NOT EXISTS (SELECT 1 FROM "IGN1" L WHERE L."DocEntry" = N."CreatedBy" AND L."LineNum" = N."DocLineNum" AND L."BaseType" = 202))
OR (N."TransType" = 60 AND NOT EXISTS (SELECT 1 FROM "IGE1" L WHERE L."DocEntry" = N."CreatedBy" AND L."LineNum" = N."DocLineNum" AND L."BaseType" = 202)))
GROUP BY N."ItemCode", N."Warehouse"
ORDER BY SUM(ABS(N."TransValue")) DESC5. Items corrected again and again
An item that needs correcting three times in 90 days usually has a location or process problem — the classic "phantom stock".
/* Items corrected 3 or more times in 90 days: likely phantom stock or a process gap */
SELECT
N.ItemCode, N.Warehouse, COUNT(*) AS Corrections,
SUM(N.InQty) AS QtyUp, SUM(N.OutQty) AS QtyDown, MAX(N.DocDate) AS LastCorrection
FROM OINM N
WHERE N.DocDate >= DATEADD(DAY, -90, GETDATE())
AND (N.TransType = 10000071
OR (N.TransType = 59 AND NOT EXISTS (SELECT 1 FROM IGN1 L WHERE L.DocEntry = N.CreatedBy AND L.LineNum = N.DocLineNum AND L.BaseType = 202))
OR (N.TransType = 60 AND NOT EXISTS (SELECT 1 FROM IGE1 L WHERE L.DocEntry = N.CreatedBy AND L.LineNum = N.DocLineNum AND L.BaseType = 202)))
GROUP BY N.ItemCode, N.Warehouse
HAVING COUNT(*) >= 3
ORDER BY COUNT(*) DESC
SAP HANA version
/* Items corrected 3 or more times in 90 days: likely phantom stock or a process gap */
SELECT
N."ItemCode", N."Warehouse", COUNT(*) AS "Corrections",
SUM(N."InQty") AS "QtyUp", SUM(N."OutQty") AS "QtyDown", MAX(N."DocDate") AS "LastCorrection"
FROM "OINM" N
WHERE N."DocDate" >= ADD_DAYS(CURRENT_DATE, -90)
AND (N."TransType" = 10000071
OR (N."TransType" = 59 AND NOT EXISTS (SELECT 1 FROM "IGN1" L WHERE L."DocEntry" = N."CreatedBy" AND L."LineNum" = N."DocLineNum" AND L."BaseType" = 202))
OR (N."TransType" = 60 AND NOT EXISTS (SELECT 1 FROM "IGE1" L WHERE L."DocEntry" = N."CreatedBy" AND L."LineNum" = N."DocLineNum" AND L."BaseType" = 202)))
GROUP BY N."ItemCode", N."Warehouse"
HAVING COUNT(*) >= 3
ORDER BY COUNT(*) DESC6. Movements recorded late
When receipts or issues are entered days after they happened, the system "learns late" — the main cause of stock that's there on screen but not on the shelf.
/* Stock movements entered more than 3 days after their posting date (last 7 days of entries) */
SELECT TOP 100
N.TransType, N.BASE_REF AS DocNum, N.ItemCode, N.Warehouse, N.InQty - N.OutQty AS Qty,
N.DocDate AS PostingDate, N.CreateDate AS EnteredOn,
DATEDIFF(DAY, N.DocDate, N.CreateDate) AS DaysLate, U.USER_CODE AS EnteredBy
FROM OINM N
LEFT JOIN OUSR U ON U.USERID = N.UserSign
WHERE N.CreateDate >= DATEADD(DAY, -7, GETDATE())
AND (N.InQty <> 0 OR N.OutQty <> 0)
AND DATEDIFF(DAY, N.DocDate, N.CreateDate) > 3
ORDER BY DATEDIFF(DAY, N.DocDate, N.CreateDate) DESC
SAP HANA version
/* Stock movements entered more than 3 days after their posting date (last 7 days of entries) */
SELECT
N."TransType", N."BASE_REF" AS "DocNum", N."ItemCode", N."Warehouse", N."InQty" - N."OutQty" AS "Qty",
N."DocDate" AS "PostingDate", N."CreateDate" AS "EnteredOn",
DAYS_BETWEEN(N."DocDate", N."CreateDate") AS "DaysLate", U."USER_CODE" AS "EnteredBy"
FROM "OINM" N
LEFT JOIN "OUSR" U ON U."USERID" = N."UserSign"
WHERE N."CreateDate" >= ADD_DAYS(CURRENT_DATE, -7)
AND (N."InQty" <> 0 OR N."OutQty" <> 0)
AND DAYS_BETWEEN(N."DocDate", N."CreateDate") > 3
ORDER BY DAYS_BETWEEN(N."DocDate", N."CreateDate") DESC
LIMIT 1007. Goods receipts that always match the purchase order
If every receipt equals the PO quantity exactly, goods are probably being received by copying the order, not by counting.
/* Goods receipts (last 30 days) where every line equals the open PO quantity, by user: counted or copied? */
SELECT
U.USER_CODE AS ReceivedBy, COUNT(*) AS ReceiptLines,
SUM(CASE WHEN G.Quantity = P.Quantity THEN 1 ELSE 0 END) AS ExactlyPOQty,
CAST(100.0 * SUM(CASE WHEN G.Quantity = P.Quantity THEN 1 ELSE 0 END) / COUNT(*) AS DECIMAL(5,1)) AS ExactPct
FROM OPDN H
JOIN PDN1 G ON G.DocEntry = H.DocEntry
JOIN POR1 P ON G.BaseType = 22 AND P.DocEntry = G.BaseEntry AND P.LineNum = G.BaseLine
LEFT JOIN OUSR U ON U.USERID = H.UserSign
WHERE H.DocDate >= DATEADD(DAY, -30, GETDATE())
GROUP BY U.USER_CODE
HAVING COUNT(*) >= 10
ORDER BY ExactPct DESC
SAP HANA version
/* Goods receipts (last 30 days) where every line equals the open PO quantity, by user: counted or copied? */
SELECT
U."USER_CODE" AS "ReceivedBy", COUNT(*) AS "ReceiptLines",
SUM(CASE WHEN G."Quantity" = P."Quantity" THEN 1 ELSE 0 END) AS "ExactlyPOQty",
CAST(100.0 * SUM(CASE WHEN G."Quantity" = P."Quantity" THEN 1 ELSE 0 END) / COUNT(*) AS DECIMAL(5,1)) AS "ExactPct"
FROM "OPDN" H
JOIN "PDN1" G ON G."DocEntry" = H."DocEntry"
JOIN "POR1" P ON G."BaseType" = 22 AND P."DocEntry" = G."BaseEntry" AND P."LineNum" = G."BaseLine"
LEFT JOIN "OUSR" U ON U."USERID" = H."UserSign"
WHERE H."DocDate" >= ADD_DAYS(CURRENT_DATE, -30)
GROUP BY U."USER_CODE"
HAVING COUNT(*) >= 10
ORDER BY 4 DESC8. Stock not put away
Goods left in a receiving or system bin are in stock on screen but nowhere a picker will look.
/* Stock still in receiving or system bins (not put away), with days since it arrived there */
SELECT
Q.WhsCode, B.BinCode, Q.ItemCode, I.ItemName, Q.OnHandQty,
DATEDIFF(DAY, A.LastIn, GETDATE()) AS DaysInBin
FROM OIBQ Q
JOIN OBIN B ON B.AbsEntry = Q.BinAbs
JOIN OITM I ON I.ItemCode = Q.ItemCode
LEFT JOIN (
SELECT T.BinAbs, L.ItemCode, MAX(L.DocDate) AS LastIn
FROM OBTL T JOIN OILM L ON L.MessageID = T.MessageID
WHERE T.Quantity > 0
GROUP BY T.BinAbs, L.ItemCode
) A ON A.BinAbs = Q.BinAbs AND A.ItemCode = Q.ItemCode
WHERE Q.OnHandQty > 0 AND (B.SysBin = 'Y' OR B.ReceiveBin = 'Y')
ORDER BY DaysInBin DESC
SAP HANA version
/* Stock still in receiving or system bins (not put away), with days since it arrived there */
SELECT
Q."WhsCode", B."BinCode", Q."ItemCode", I."ItemName", Q."OnHandQty",
DAYS_BETWEEN(A."LastIn", CURRENT_DATE) AS "DaysInBin"
FROM "OIBQ" Q
JOIN "OBIN" B ON B."AbsEntry" = Q."BinAbs"
JOIN "OITM" I ON I."ItemCode" = Q."ItemCode"
LEFT JOIN (
SELECT T."BinAbs", L."ItemCode", MAX(L."DocDate") AS "LastIn"
FROM "OBTL" T JOIN "OILM" L ON L."MessageID" = T."MessageID"
WHERE T."Quantity" > 0
GROUP BY T."BinAbs", L."ItemCode"
) A ON A."BinAbs" = Q."BinAbs" AND A."ItemCode" = Q."ItemCode"
WHERE Q."OnHandQty" > 0 AND (B."SysBin" = 'Y' OR B."ReceiveBin" = 'Y')
ORDER BY 6 DESC9. Negative stock
Negative stock means something was issued before it was received — usually a posting-order or timing problem that distorts costs.
/* Negative stock right now */
SELECT W.ItemCode, I.ItemName, W.WhsCode, W.OnHand
FROM OITW W JOIN OITM I ON I.ItemCode = W.ItemCode
WHERE W.OnHand < 0
ORDER BY W.OnHand
SAP HANA version
/* Negative stock right now */
SELECT W."ItemCode", I."ItemName", W."WhsCode", W."OnHand"
FROM "OITW" W JOIN "OITM" I ON I."ItemCode" = W."ItemCode"
WHERE W."OnHand" < 0
ORDER BY W."OnHand"10. Transfer requests left open
A transfer that was requested but never completed leaves one warehouse short and the other confused.
/* Inventory transfer requests still open after 14 days */
SELECT H.DocNum, H.DocDate, DATEDIFF(DAY, H.DocDate, GETDATE()) AS DaysOpen, H.Filler AS FromWhs, H.ToWhsCode AS ToWhs,
L.ItemCode, L.OpenQty
FROM OWTQ H JOIN WTQ1 L ON L.DocEntry = H.DocEntry
WHERE H.DocStatus = 'O' AND L.LineStatus = 'O' AND H.DocDate < DATEADD(DAY, -14, GETDATE())
ORDER BY H.DocDate
SAP HANA version
/* Inventory transfer requests still open after 14 days */
SELECT H."DocNum", H."DocDate", DAYS_BETWEEN(H."DocDate", CURRENT_DATE) AS "DaysOpen", H."Filler" AS "FromWhs", H."ToWhsCode" AS "ToWhs",
L."ItemCode", L."OpenQty"
FROM "OWTQ" H JOIN "WTQ1" L ON L."DocEntry" = H."DocEntry"
WHERE H."DocStatus" = 'O' AND L."LineStatus" = 'O' AND H."DocDate" < ADD_DAYS(CURRENT_DATE, -14)
ORDER BY H."DocDate"11. Bin stock that hasn't moved in six months
Stock that sits untouched in a bin is a candidate for a recount, a move to overflow storage, or a dead-stock review.
/* Bin stock that hasn't moved in or out of its bin for 180 days */
SELECT Q.WhsCode, B.BinCode, Q.ItemCode, I.ItemName, Q.OnHandQty, M.LastMove,
DATEDIFF(DAY, M.LastMove, GETDATE()) AS DaysIdle
FROM OIBQ Q
JOIN OBIN B ON B.AbsEntry = Q.BinAbs
JOIN OITM I ON I.ItemCode = Q.ItemCode
LEFT JOIN (
SELECT T.BinAbs, L.ItemCode, MAX(L.DocDate) AS LastMove
FROM OBTL T JOIN OILM L ON L.MessageID = T.MessageID
GROUP BY T.BinAbs, L.ItemCode
) M ON M.BinAbs = Q.BinAbs AND M.ItemCode = Q.ItemCode
WHERE Q.OnHandQty > 0 AND (M.LastMove IS NULL OR M.LastMove < DATEADD(DAY, -180, GETDATE()))
ORDER BY M.LastMove
SAP HANA version
/* Bin stock that hasn't moved in or out of its bin for 180 days */
SELECT Q."WhsCode", B."BinCode", Q."ItemCode", I."ItemName", Q."OnHandQty", M."LastMove",
DAYS_BETWEEN(M."LastMove", CURRENT_DATE) AS "DaysIdle"
FROM "OIBQ" Q
JOIN "OBIN" B ON B."AbsEntry" = Q."BinAbs"
JOIN "OITM" I ON I."ItemCode" = Q."ItemCode"
LEFT JOIN (
SELECT T."BinAbs", L."ItemCode", MAX(L."DocDate") AS "LastMove"
FROM "OBTL" T JOIN "OILM" L ON L."MessageID" = T."MessageID"
GROUP BY T."BinAbs", L."ItemCode"
) M ON M."BinAbs" = Q."BinAbs" AND M."ItemCode" = Q."ItemCode"
WHERE Q."OnHandQty" > 0 AND (M."LastMove" IS NULL OR M."LastMove" < ADD_DAYS(CURRENT_DATE, -180))
ORDER BY M."LastMove"Notes
- All queries only read data. They use standard B1 tables: OITW, OITM, OINM, OPDN/PDN1, POR1, IGN1/IGE1, OWTQ/WTQ1, and for bins OIBQ, OBIN, OBTL and OILM (B1 9.0 and later).
- Manual corrections exclude receipts and issues linked to production orders; numeric transaction types 59 (goods receipt), 60 (goods issue) and 10000071 (inventory posting) are B1's own object codes.
- The SQL Server versions were run against SAP's UK demo company. The HANA versions use the same tables and columns, translated to HANA syntax; they haven't been run on a HANA system.
Prefer not to touch SQL?
Inventory Doctor runs most of these checks from standard B1 exports — dead stock, gross vs net corrections, stockout risk after open orders, posting discipline and bin put-away — in your browser, with nothing uploaded. Click any item to see why its number is what it is.
See all of it in five minutes
Free inventory health check for SAP Business One. Runs in your browser — nothing is uploaded.
Run the free check