diff --git a/index.php b/index.php index 6a5e7fa..3aa6895 100644 --- a/index.php +++ b/index.php @@ -501,20 +501,49 @@ if (isset($_GET['action']) || isset($_POST['action'])) { if ($action === 'pos_get_item_by_sku') { header('Content-Type: application/json'); - $sku = $_GET['sku'] ?? ''; - if (!$sku) { echo json_encode(null); exit; } - + $sku = trim((string)($_GET['sku'] ?? '')); + if ($sku === '') { echo json_encode(null); exit; } + + $weightBarcode = parseWeightBarcode($sku); + $lookupSku = $weightBarcode['item_code'] ?? $sku; + $oid = current_outlet_id(); $stmt = db()->prepare("SELECT * FROM stock_items WHERE sku = ? AND outlet_id = ? LIMIT 1"); - $stmt->execute([$sku, $oid]); + $stmt->execute([$lookupSku, $oid]); $p = $stmt->fetch(PDO::FETCH_ASSOC); - + if ($p) { $p['original_price'] = (float)$p['sale_price']; $p['sale_price'] = getPromotionalPrice($p); + $p['price'] = (float)$p['sale_price']; $p['nameEn'] = $p['name_en']; $p['nameAr'] = $p['name_ar']; $p['vatRate'] = $p['vat_rate']; + + if ($weightBarcode) { + $qty = 0.0; + if ($weightBarcode['mode'] === 'price') { + if ((float)$p['sale_price'] <= 0) { + echo json_encode(['error' => 'This item cannot use price-based scale barcodes because its sale price is zero.']); + exit; + } + $qty = round(((float)$weightBarcode['value']) / (float)$p['sale_price'], 3); + } else { + $qty = round((float)$weightBarcode['value'], 3); + } + + if ($qty <= 0) { + echo json_encode(['error' => 'The weighing scale barcode value is invalid.']); + exit; + } + + $p['qty'] = $qty; + $p['is_scale_barcode'] = true; + $p['scale_barcode_mode'] = $weightBarcode['mode']; + $p['scale_barcode_value'] = (float)$weightBarcode['value']; + $p['scanned_barcode'] = $sku; + } + echo json_encode($p); } else { echo json_encode(null); @@ -826,6 +855,71 @@ if (!isset($_SESSION['user_id'])) { $message = $_SESSION['message'] ?? ''; unset($_SESSION['message']); +function getSettingValue(string $key, ?string $default = null): ?string { + static $cache = []; + if (array_key_exists($key, $cache)) return $cache[$key]; + try { + $stmt = db()->prepare("SELECT value FROM settings WHERE `key` = ? LIMIT 1"); + $stmt->execute([$key]); + $value = $stmt->fetchColumn(); + } catch (Throwable $e) { + $value = false; + } + if ($value === false || $value === null || $value === '') $value = $default; + $cache[$key] = $value; + return $value; +} + +function getWeightBarcodeConfig(): array { + $prefixStart = (int)(getSettingValue('weight_barcode_prefix_start', '20') ?? '20'); + $prefixEnd = (int)(getSettingValue('weight_barcode_prefix_end', '29') ?? '29'); + if ($prefixStart < 20 || $prefixStart > 29) $prefixStart = 20; + if ($prefixEnd < 20 || $prefixEnd > 29) $prefixEnd = 29; + if ($prefixStart > $prefixEnd) { + [$prefixStart, $prefixEnd] = [$prefixEnd, $prefixStart]; + } + $mode = strtolower((string)(getSettingValue('weight_barcode_mode', 'weight') ?? 'weight')); + if (!in_array($mode, ['weight', 'price'], true)) $mode = 'weight'; + return [ + 'prefix_start' => $prefixStart, + 'prefix_end' => $prefixEnd, + 'mode' => $mode, + 'value_divisor' => 1000, + ]; +} + +function isWeightBarcode(string $barcode): bool { + $barcode = trim($barcode); + if (!preg_match('/^\d{13}$/', $barcode)) return false; + $config = getWeightBarcodeConfig(); + $prefix = (int)substr($barcode, 0, 2); + return $prefix >= $config['prefix_start'] && $prefix <= $config['prefix_end']; +} + +function parseWeightBarcode(string $barcode): ?array { + $barcode = trim($barcode); + if (!isWeightBarcode($barcode)) return null; + $config = getWeightBarcodeConfig(); + $rawValue = (int)substr($barcode, 7, 5); + return [ + 'full_barcode' => $barcode, + 'prefix' => substr($barcode, 0, 2), + 'item_code' => substr($barcode, 2, 5), + 'raw_value' => $rawValue, + 'value' => $rawValue / (float)$config['value_divisor'], + 'mode' => $config['mode'], + 'check_digit' => substr($barcode, 12, 1), + ]; +} + +function validateItemSkuBarcode(string $sku): ?string { + $sku = trim($sku); + if ($sku === '') return null; + if (!isWeightBarcode($sku)) return null; + $config = getWeightBarcodeConfig(); + return "This barcode is reserved for weighing scale scans. 13-digit barcodes starting with {$config['prefix_start']}-{$config['prefix_end']} cannot be saved as item barcodes; please save the 5-digit scale item code instead."; +} + function redirectWithMessage($msg, $url = null) { if (!$url) { $url = $_SERVER['REQUEST_URI']; @@ -892,7 +986,10 @@ function getPromotionalPrice($item) { $category_id = (int)$_POST['category_id'] ?: null; $unit_id = (int)$_POST['unit_id'] ?: null; $supplier_id = (int)$_POST['supplier_id'] ?: null; - $sku = $_POST['sku'] ?? ''; + $sku = trim((string)($_POST['sku'] ?? '')); + if ($sku_error = validateItemSkuBarcode($sku)) { + redirectWithMessage($sku_error, 'index.php?page=items'); + } $sale_price = (float)($_POST['sale_price'] ?? 0); $purchase_price = (float)($_POST['purchase_price'] ?? 0); $stock_quantity = (float)($_POST['stock_quantity'] ?? 0); @@ -925,7 +1022,10 @@ function getPromotionalPrice($item) { $category_id = (int)$_POST['category_id'] ?: null; $unit_id = (int)$_POST['unit_id'] ?: null; $supplier_id = (int)$_POST['supplier_id'] ?: null; - $sku = $_POST['sku'] ?? ''; + $sku = trim((string)($_POST['sku'] ?? '')); + if ($sku_error = validateItemSkuBarcode($sku)) { + redirectWithMessage($sku_error, 'index.php?page=items'); + } $sale_price = (float)($_POST['sale_price'] ?? 0); $purchase_price = (float)($_POST['purchase_price'] ?? 0); $stock_quantity = (float)($_POST['stock_quantity'] ?? 0); @@ -1449,6 +1549,8 @@ function getPromotionalPrice($item) { # --- Unified Import Logic (Excel & CSV) --- if (isset($_POST['import_items'])) { error_log("Import items triggered."); + $count = 0; + $skipped = 0; if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { $tmpPath = $_FILES['excel_file']['tmp_name']; $rows = []; @@ -1468,6 +1570,10 @@ function getPromotionalPrice($item) { foreach ($rows as $row) { if (empty($row[0])) continue; $sku = trim((string)$row[0]); + if ($sku_error = validateItemSkuBarcode($sku)) { + $skipped++; + continue; + } $name_en = trim((string)($row[1] ?? '')); $name_ar = trim((string)($row[2] ?? '')); $sale_price = (float)($row[3] ?? 0); @@ -1491,7 +1597,12 @@ function getPromotionalPrice($item) { } $count++; } - redirectWithMessage("Import items completed! $count processed.", "index.php?page=items"); + $weightConfig = getWeightBarcodeConfig(); + $summary = "Import items completed! $count processed."; + if ($skipped > 0) { + $summary .= " $skipped skipped because 13-digit barcodes starting with {$weightConfig['prefix_start']}-{$weightConfig['prefix_end']} are reserved for weighing scale barcodes."; + } + redirectWithMessage($summary, "index.php?page=items"); } } @@ -2437,7 +2548,19 @@ if (isset($_POST['add_hr_department'])) { if (can('settings_view')) { $db = db(); if (isset($_POST['settings']) && is_array($_POST['settings'])) { - foreach ($_POST['settings'] as $key => $value) { + $settings = $_POST['settings']; + $settings['weight_barcode_mode'] = in_array(($settings['weight_barcode_mode'] ?? 'weight'), ['weight', 'price'], true) ? $settings['weight_barcode_mode'] : 'weight'; + $prefixStart = (int)($settings['weight_barcode_prefix_start'] ?? 20); + $prefixEnd = (int)($settings['weight_barcode_prefix_end'] ?? 29); + if ($prefixStart < 20 || $prefixStart > 29) $prefixStart = 20; + if ($prefixEnd < 20 || $prefixEnd > 29) $prefixEnd = 29; + if ($prefixStart > $prefixEnd) { + [$prefixStart, $prefixEnd] = [$prefixEnd, $prefixStart]; + } + $settings['weight_barcode_prefix_start'] = (string)$prefixStart; + $settings['weight_barcode_prefix_end'] = (string)$prefixEnd; + + foreach ($settings as $key => $value) { $stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?"); $stmt->execute([$key, $value, $value]); } @@ -5132,7 +5255,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';