fix: consumption predictions false positives after restocking
Root cause: baseline was 'restockQty' (only the new items added) but actualQty = pre-existing stock + new items → always looked like 'more than expected'. New approach: baseline = current_qty + consumed_since_restock. This correctly reflects the true starting point regardless of pre-existing stock, eliminating all false positives after shopping trips.
This commit is contained in:
+12
-34
@@ -2144,45 +2144,23 @@ function getConsumptionPredictions(PDO $db): void {
|
|||||||
|
|
||||||
$restockDate = strtotime($restock['created_at']);
|
$restockDate = strtotime($restock['created_at']);
|
||||||
|
|
||||||
// Sum ALL 'in' transactions within 24h of the last restock (= one shopping session).
|
// Baseline = current inventory + what was consumed since the last restock.
|
||||||
// Using only the last single transaction as restockQty causes false positives when
|
// This avoids false positives when pre-existing stock + new restock exceeds
|
||||||
// the user scans multiple items separately (e.g. 3 mozzarelle one by one).
|
// what the model expected from the restock alone.
|
||||||
$sessionIn = $db->prepare("
|
$consumedSinceRestock = $db->prepare("
|
||||||
SELECT SUM(quantity) as total
|
SELECT COALESCE(SUM(quantity), 0)
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE product_id = ? AND location = ? AND type = 'in' AND undone = 0
|
WHERE product_id = ? AND location = ? AND type = 'out' AND undone = 0
|
||||||
AND created_at >= datetime(?, '-24 hours')
|
AND created_at >= datetime(?, 'unixepoch')
|
||||||
");
|
");
|
||||||
$sessionIn->execute([$pid, $loc, $restock['created_at']]);
|
$consumedSinceRestock->execute([$pid, $loc, $restockDate]);
|
||||||
$restockQty = floatval($sessionIn->fetchColumn() ?: $restock['quantity']);
|
$usedSinceRestock = floatval($consumedSinceRestock->fetchColumn() ?: 0);
|
||||||
|
|
||||||
// If inventory was manually edited (updated_at > last restock), use the
|
|
||||||
// manual update as baseline instead — otherwise the prediction is comparing
|
|
||||||
// against a stale restock quantity that no longer reflects reality.
|
|
||||||
$lastManualUpdate = strtotime($item['updated_at']);
|
|
||||||
if ($lastManualUpdate > $restockDate) {
|
|
||||||
// Inventory was manually corrected after last restock → use current qty
|
|
||||||
// as a fresh baseline from that point; only consider OUT transactions
|
|
||||||
// that happened AFTER the manual update.
|
|
||||||
$txnsSinceUpdate = $db->prepare("
|
|
||||||
SELECT SUM(quantity) as total
|
|
||||||
FROM transactions
|
|
||||||
WHERE product_id = ? AND location = ? AND type = 'out'
|
|
||||||
AND created_at > ?
|
|
||||||
");
|
|
||||||
$txnsSinceUpdate->execute([$pid, $loc, $item['updated_at']]);
|
|
||||||
$usedSinceUpdate = floatval($txnsSinceUpdate->fetchColumn() ?: 0);
|
|
||||||
$daysSinceBaseline = max(1, (time() - $lastManualUpdate) / 86400);
|
|
||||||
// The effective "restock" qty is what inventory had at manual edit time
|
|
||||||
// which is current qty + what was consumed since then
|
|
||||||
$restockQty = floatval($item['quantity']) + $usedSinceUpdate;
|
|
||||||
$restockDate = $lastManualUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
$baselineQty = floatval($item['quantity']) + $usedSinceRestock;
|
||||||
$daysSinceRestock = max(1, (time() - $restockDate) / 86400);
|
$daysSinceRestock = max(1, (time() - $restockDate) / 86400);
|
||||||
|
|
||||||
// Predicted remaining qty = restock qty - (daily rate * days since restock)
|
// Predicted remaining qty = baseline - (daily rate * days since restock)
|
||||||
$expectedQty = max(0, $restockQty - ($dailyRate * $daysSinceRestock));
|
$expectedQty = max(0, $baselineQty - ($dailyRate * $daysSinceRestock));
|
||||||
$actualQty = floatval($item['quantity']);
|
$actualQty = floatval($item['quantity']);
|
||||||
|
|
||||||
// Flag if deviation > 30% and absolute diff > meaningful threshold
|
// Flag if deviation > 30% and absolute diff > meaningful threshold
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@
|
|||||||
<title>EverShelf</title>
|
<title>EverShelf</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||||||
<link rel="stylesheet" href="assets/css/style.css?v=20260511g">
|
<link rel="stylesheet" href="assets/css/style.css?v=20260511h">
|
||||||
<!-- QuaggaJS for barcode scanning -->
|
<!-- QuaggaJS for barcode scanning -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||||||
@@ -1469,6 +1469,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/app.js?v=20260511g"></script>
|
<script src="assets/js/app.js?v=20260511h"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user