fix: prevent scale double-deduction (duplicate inventory_use)
Root cause: after scale auto-confirm fires submitUse(), the old code called
_cancelScaleAutoConfirm(false) which reset _scaleLastConfirmedGrams to null.
This allowed the scale (still showing the same reading) to start a new 10-second
stability+confirm cycle and trigger a second identical deduction.
JS fix:
- submitUse() now calls _cancelScaleTimersOnly() instead of
_cancelScaleAutoConfirm(false), preserving _scaleLastConfirmedGrams so the
same weight is rejected until the product is removed from the plate.
- _scaleStabilityVal reset to null so a genuinely new weight starts fresh.
- Duplicate API response (result.duplicate) silently ignored in the UI.
PHP fix (server-side safety net):
- useFromInventory() rejects a second 'out' transaction for the same product
within 12 seconds with { success: false, duplicate: true }.
This catches any client-side edge cases regardless of scale timing.
This commit is contained in:
@@ -909,6 +909,30 @@ function useFromInventory(PDO $db): void {
|
||||
echo json_encode(['error' => 'Product ID required']);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Server-side deduplication ─────────────────────────────────────────
|
||||
// Reject if the same product already has an 'out' transaction in the last
|
||||
// 12 seconds. This guards against scale double-triggers (the scale can fire
|
||||
// a second stable reading ~10 s after the first auto-confirm, while the
|
||||
// product is still on the plate), regardless of the client-side guard.
|
||||
if (!$useAll) {
|
||||
$dedup = $db->prepare(
|
||||
"SELECT id FROM transactions
|
||||
WHERE product_id = ? AND type IN ('out','waste') AND undone = 0
|
||||
AND created_at >= datetime('now', '-12 seconds')
|
||||
LIMIT 1"
|
||||
);
|
||||
$dedup->execute([$productId]);
|
||||
if ($dedup->fetch()) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Operazione già registrata di recente — attendi qualche secondo.',
|
||||
'duplicate' => true,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Handle "throw all from all locations"
|
||||
if ($useAll && $location === '__all__') {
|
||||
|
||||
+8
-1
@@ -6176,7 +6176,12 @@ async function submitUse(e) {
|
||||
e.preventDefault();
|
||||
if (_useSubmitting) return; // prevent double-submit from scale auto-confirm
|
||||
_useSubmitting = true;
|
||||
_cancelScaleAutoConfirm(false); // stop any running auto-confirm
|
||||
// Stop timers but KEEP _scaleLastConfirmedGrams: this prevents the scale from
|
||||
// re-triggering another auto-submit while the product is still on the plate.
|
||||
// (Calling _cancelScaleAutoConfirm(false) would reset the sentinel to null,
|
||||
// allowing the same weight to start a new 10-second cycle immediately.)
|
||||
_cancelScaleTimersOnly();
|
||||
_scaleStabilityVal = null; // reset sentinel so a new DIFFERENT weight restarts correctly
|
||||
showLoading(true);
|
||||
try {
|
||||
let qty = parseFloat(document.getElementById('use-quantity').value) || 1;
|
||||
@@ -6212,6 +6217,8 @@ async function submitUse(e) {
|
||||
: () => showPage('dashboard');
|
||||
// Check low stock → Bring! prompt
|
||||
showLowStockBringPrompt(result, moveCallback);
|
||||
} else if (result.duplicate) {
|
||||
// Silently ignore: this was a scale double-trigger, not a real error
|
||||
} else {
|
||||
showToast(result.error || 'Errore', 'error');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user