From ada701bd53e04a4f8a1d4cf7ff908add50934b41 Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Wed, 6 May 2026 15:18:47 +0700 Subject: [PATCH] Barcode Operations --- app/assets/images/apple-touch-icon.png | Bin 13191 -> 0 bytes app/assets/images/favicon-16x16.png | Bin 802 -> 0 bytes app/assets/images/favicon-32x32.png | Bin 1866 -> 0 bytes app/assets/images/favicon.png | Bin 0 -> 28831 bytes app/assets/js/scanner.js | 173 ++++++ app/assets/utils/classes/ProductManager.php | 9 +- app/ics/api/engine/barcode_lookup.php | 205 +++++++ app/ics/api/engine/location_barcode_label.php | 153 +++++ app/ics/api/engine/sku_barcode_label.php | 245 ++++++++ app/ics/api/engine/sku_label_lots.php | 159 ++++++ app/ics/api/engine/sku_label_products.php | 17 + app/ics/api/engine/validate_scan_location.php | 86 +++ app/ics/location_barcode_label.php | 388 +++++++++++++ app/ics/manage_stock_in.php | 129 ++++- app/ics/manage_stock_out.php | 145 ++++- app/ics/manage_stock_transfer.php | 182 +++++- app/ics/sku_barcode_label.php | 538 ++++++++++++++++++ app/include_header.php | 12 +- app/include_master_sidebar.php | 20 +- app/include_setting_sidebar.php | 6 +- app/include_sidebar.php | 10 +- app/inventory/manage_product.php | 7 +- 22 files changed, 2449 insertions(+), 35 deletions(-) delete mode 100644 app/assets/images/apple-touch-icon.png delete mode 100644 app/assets/images/favicon-16x16.png delete mode 100644 app/assets/images/favicon-32x32.png create mode 100644 app/assets/images/favicon.png create mode 100644 app/assets/js/scanner.js create mode 100644 app/ics/api/engine/barcode_lookup.php create mode 100644 app/ics/api/engine/location_barcode_label.php create mode 100644 app/ics/api/engine/sku_barcode_label.php create mode 100644 app/ics/api/engine/sku_label_lots.php create mode 100644 app/ics/api/engine/sku_label_products.php create mode 100644 app/ics/api/engine/validate_scan_location.php create mode 100644 app/ics/location_barcode_label.php create mode 100644 app/ics/sku_barcode_label.php diff --git a/app/assets/images/apple-touch-icon.png b/app/assets/images/apple-touch-icon.png deleted file mode 100644 index dc37230b4ec85c17f9808ecb3321eb082b56800d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13191 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 500) { + _buffer = ''; + _max_key_gap = 0; + } else if (_buffer.length > 0) { + _max_key_gap = Math.max(_max_key_gap, gap); + } + _last_key_ts = now; + + if (e.key === 'Enter' && _buffer.length >= 3) { + var barcode = _buffer; + var is_scanner_input = _max_key_gap <= _HW_THRESHOLD; + _buffer = ''; + _max_key_gap = 0; + if (is_scanner_input) { + e.preventDefault(); + _remove_typed_barcode(barcode); + _resolve(barcode); + } + return; + } + + if (e.key.length === 1) { + _buffer += e.key; + } + } + + function _remove_typed_barcode(barcode) { + var el = document.activeElement; + if (!el || !('value' in el)) return; + if (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA') return; + + var value = String(el.value || ''); + if (!value.endsWith(barcode)) return; + el.value = value.slice(0, value.length - barcode.length); + $(el).trigger('input'); + } + + function _resolve(barcode) { + if (!barcode || !_callback) return; + + $.ajax({ + url: server_url + 'ics/api/engine/barcode_lookup.php', + type: 'POST', + dataType: 'json', + data: { json: JSON.stringify({ barcode: barcode, action: 'read' }) }, + headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }, + success: function(res) { + if (res && res.success) { + if ((!_allow_raw && res.type === 'raw') || (_allowed_types && _allowed_types.indexOf(res.type) === -1)) { + _show_error('This page only accepts generated barcode labels.'); + return; + } + _callback(res.sku || '', res.product_name || '', res.uom || '', res); + return; + } + _show_error('Barcode not found: ' + barcode); + }, + error: function(xhr) { + var msg = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : 'Scanner error. Please try again.'; + _show_error(msg === 'Barcode not found.' ? 'Barcode not found: ' + barcode : msg); + } + }); + } + + function _build_modal() { + if (_modal) return; + $('body').append(` + `); + _modal = new bootstrap.Modal(document.getElementById('scanner_modal')); + document.getElementById('scanner_modal').addEventListener('hidden.bs.modal', _stop_camera); + } + + function _start_camera() { + if (_camera) return; + if (typeof Html5Qrcode === 'undefined') { + _show_error('Camera scanner library is not available.'); + return; + } + _camera = new Html5Qrcode('scanner_qr_reader'); + _camera.start( + { facingMode: 'environment' }, + { fps: 10, qrbox: { width: 250, height: 120 } }, + function(decodedText) { + _modal.hide(); + _resolve(decodedText); + }, + function() {} + ).catch(function(err) { + _show_error('Camera error: ' + err); + }); + } + + function _stop_camera() { + if (!_camera) return; + _camera.stop().catch(function() {}).finally(function() { + _camera = null; + }); + } + + function _show_error(msg) { + var $err = $('#scanner_error'); + if ($err.length) { + $err.text(msg).removeClass('d-none'); + setTimeout(function() { $err.addClass('d-none'); }, 3000); + return; + } + if (typeof bootbox !== 'undefined') bootbox.alert(msg); + } + + return { + init: function(options) { + options = options || {}; + _callback = options.onScan || null; + _allow_raw = options.allowRaw !== false; + _allowed_types = Array.isArray(options.allowedTypes) ? options.allowedTypes : null; + document.removeEventListener('keydown', _on_keydown); + document.removeEventListener('paste', _on_paste); + document.addEventListener('keydown', _on_keydown); + document.addEventListener('paste', _on_paste); + _build_modal(); + }, + open_camera: function() { + if (!_modal) _build_modal(); + _modal.show(); + setTimeout(_start_camera, 400); + }, + destroy: function() { + document.removeEventListener('keydown', _on_keydown); + document.removeEventListener('paste', _on_paste); + _stop_camera(); + } + }; +})(); diff --git a/app/assets/utils/classes/ProductManager.php b/app/assets/utils/classes/ProductManager.php index a3ba21c..2467eb3 100644 --- a/app/assets/utils/classes/ProductManager.php +++ b/app/assets/utils/classes/ProductManager.php @@ -317,7 +317,8 @@ class ProductManager { $sth = $this->pdo->prepare( "SELECT * FROM md_product WHERE company_id = :company_id - AND sku LIKE :keyword + AND (sku LIKE :keyword OR barcode LIKE :keyword) + AND status > 0 LIMIT 50" ); $sth->execute([ @@ -358,6 +359,7 @@ class ProductManager { ':company_id' => $this->company_id, ':product_name' => $data['product_name'], ':sku' => $data['sku'], + ':barcode' => trim($data['barcode'] ?? '') ?: null, ':uom' => $data['uom'] ?? 'pcs', ':price' => $data['price'], ':cost_price' => (float)($data['cost_price'] ?? 0), @@ -376,6 +378,7 @@ class ProductManager { "UPDATE md_product SET product_name = :product_name, sku = :sku, + barcode = :barcode, uom = :uom, price = :price, cost_price = :cost_price, @@ -391,10 +394,10 @@ class ProductManager { } else { $this->pdo->prepare( "INSERT INTO md_product - (company_id, product_name, sku, uom, price, cost_price, min_stock, reorder_point, + (company_id, product_name, sku, barcode, uom, price, cost_price, min_stock, reorder_point, category, product_image, `description`, `status`, `log`) VALUES - (:company_id, :product_name, :sku, :uom, :price, :cost_price, :min_stock, :reorder_point, + (:company_id, :product_name, :sku, :barcode, :uom, :price, :cost_price, :min_stock, :reorder_point, :category, :product_image, :description, :status, :log)" )->execute($params); } diff --git a/app/ics/api/engine/barcode_lookup.php b/app/ics/api/engine/barcode_lookup.php new file mode 100644 index 0000000..089faae --- /dev/null +++ b/app/ics/api/engine/barcode_lookup.php @@ -0,0 +1,205 @@ +prepare( + "SELECT sku, product_name, uom + FROM md_product + WHERE company_id = :company_id + AND sku = :sku + AND status > 0 + LIMIT 1" + ); + $sth->execute([':company_id' => $company_id, ':sku' => $sku]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + function validateLotSerial(PDO $pdo, int $company_id, string $sku, string $lot, string $serial): void { + if ($serial === '') return; + + $warehouses = $pdo->prepare( + "SELECT id FROM md_warehouse + WHERE company_id = :company_id AND status = 1" + ); + $warehouses->execute([':company_id' => $company_id]); + + foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { + $table = 'td_stock_' . (int)$warehouse_id; + $sth = $pdo->prepare( + "SELECT lot_number + FROM `{$table}` + WHERE company_id = :company_id + AND product_sku = :sku + AND serial_number = :serial + AND status != -1 + LIMIT 1" + ); + $sth->execute([':company_id' => $company_id, ':sku' => $sku, ':serial' => $serial]); + $existing_lot = $sth->fetchColumn(); + if ($existing_lot !== false && (string)$existing_lot !== $lot) { + throw new Exception('Serial number belongs to a different lot.'); + } + } + } + + function findPreparedLabel(PDO $pdo, int $company_id, string $barcode, string $type): array|false { + $sth = $pdo->prepare( + "SELECT * + FROM md_barcode + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = :barcode_type + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':barcode' => $barcode, + ':barcode_type' => $type, + ]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + if ($row && (int)$row['status'] !== 1) { + throw new Exception('Barcode is disabled.'); + } + return $row ?: false; + } + + function advancedLocation(PDO $pdo, int $company_id): bool { + $sth = $pdo->prepare( + "SELECT value FROM company_setting + WHERE company_id = :company_id AND setting_key = 'advanced_location' + LIMIT 1" + ); + $sth->execute([':company_id' => $company_id]); + $value = $sth->fetchColumn(); + return (int)($value === false ? 0 : $value) === 1; + } + + function validateLocation(PDO $pdo, int $company_id, int $warehouse_id, string $zone, string $aisle, string $rack): void { + $sth = $pdo->prepare( + "SELECT 1 FROM md_rack + WHERE company_id = :company_id + AND warehouse = :warehouse + AND zone = :zone + AND aisle = :aisle + AND rack = :rack + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':warehouse' => $warehouse_id, + ':zone' => $zone, + ':aisle' => $aisle, + ':rack' => $rack, + ]); + if (!$sth->fetchColumn()) throw new Exception('Location not found.'); + } + + $parts = explode('|', $barcode); + $label_type = strtoupper($parts[0] ?? ''); + + try { + if ($label_type === 'SKU') { + if (count($parts) !== 4) throw new Exception('Invalid SKU label format.'); + $sku = trim($parts[1]); + $lot = trim($parts[2]); + $serial = trim($parts[3]); + if ($sku === '' || $lot === '' || $serial === '') throw new Exception('SKU, lot, and serial are required.'); + + $label = findPreparedLabel($pdo2, $company_id, $barcode, 'sku'); + if (!$label) throw new Exception('SKU barcode label was not prepared.'); + if ($label['product_sku'] !== $sku || $label['lot_number'] !== $lot || $label['serial_number'] !== $serial) { + throw new Exception('SKU barcode label does not match its registry.'); + } + + $product = findProduct($pdo2, $company_id, $sku); + if (!$product) throw new Exception('SKU not found.'); + validateLotSerial($pdo2, $company_id, $sku, $lot, $serial); + + $answer['success'] = 1; + $answer['type'] = 'sku'; + $answer['sku'] = $product['sku']; + $answer['product_name'] = $product['product_name']; + $answer['uom'] = $product['uom'] ?? 'pcs'; + $answer['lot_number'] = $lot; + $answer['serial_number'] = $serial; + exit(json_encode($answer)); + } + + if ($label_type === 'LOC') { + $advanced = advancedLocation($pdo1, $company_id); + if ($advanced && count($parts) !== 5) throw new Exception('Invalid advanced location label format.'); + if (!$advanced && count($parts) !== 3) throw new Exception('Invalid location label format.'); + + $warehouse_id = (int)($parts[1] ?? 0); + if ($advanced) { + $zone = trim($parts[2]); + $aisle = trim($parts[3]); + $rack = trim($parts[4]); + } else { + $rack = trim($parts[2]); + $zone = $rack; + $aisle = $rack; + } + if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') throw new Exception('Location label is incomplete.'); + + $label = findPreparedLabel($pdo2, $company_id, $barcode, 'loc'); + if (!$label) throw new Exception('Location barcode label was not prepared.'); + if ((int)$label['warehouse_id'] !== $warehouse_id || $label['zone'] !== $zone || $label['aisle'] !== $aisle || $label['rack'] !== $rack) { + throw new Exception('Location barcode label does not match its registry.'); + } + + validateLocation($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack); + + $answer['success'] = 1; + $answer['type'] = 'loc'; + $answer['warehouse_id'] = $warehouse_id; + $answer['zone'] = $zone; + $answer['aisle'] = $aisle; + $answer['rack'] = $rack; + exit(json_encode($answer)); + } + } catch (Exception $e) { + $answer['message'] = $e->getMessage(); + http_response_code(400); + exit(json_encode($answer)); + } + + $sth = $pdo2->prepare( + "SELECT sku, product_name, uom + FROM md_product + WHERE company_id = :company_id + AND (barcode = :barcode OR sku = :sku) + AND status > 0 + ORDER BY (barcode = :barcode_order) DESC + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':barcode' => $barcode, + ':sku' => $barcode, + ':barcode_order' => $barcode, + ]); + $product = $sth->fetch(PDO::FETCH_ASSOC); + + if (!$product) { + $answer['message'] = 'Barcode not found.'; + http_response_code(404); + exit(json_encode($answer)); + } + + $answer['success'] = 1; + $answer['type'] = 'raw'; + $answer['sku'] = $product['sku']; + $answer['product_name'] = $product['product_name']; + $answer['uom'] = $product['uom'] ?? 'pcs'; + exit(json_encode($answer)); +?> diff --git a/app/ics/api/engine/location_barcode_label.php b/app/ics/api/engine/location_barcode_label.php new file mode 100644 index 0000000..9d4c577 --- /dev/null +++ b/app/ics/api/engine/location_barcode_label.php @@ -0,0 +1,153 @@ +prepare( + "SELECT 1 FROM md_rack + WHERE company_id = :company_id + AND warehouse = :warehouse + AND zone = :zone + AND aisle = :aisle + AND rack = :rack + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':warehouse' => $warehouse_id, + ':zone' => $zone, + ':aisle' => $aisle, + ':rack' => $rack, + ]); + if (!$sth->fetchColumn()) { + $answer = ['success' => 0, 'message' => 'Location not found.']; + http_response_code(404); + exit(json_encode($answer)); + } + } + + $label_action = $data['label_action'] ?? 'create'; + $barcode = trim($data['barcode'] ?? ''); + $warehouse_id = (int)($data['warehouse_id'] ?? 0); + $zone = trim($data['zone'] ?? ''); + $aisle = trim($data['aisle'] ?? ''); + $rack = trim($data['rack'] ?? ''); + + if ($label_action === 'list') { + $sth = $pdo2->prepare( + "SELECT barcode, warehouse_id, zone, aisle, rack, status, print_count, last_printed_dt + FROM md_barcode + WHERE company_id = :company_id + AND barcode_type = 'loc'" + ); + $sth->execute([':company_id' => $company_id]); + + $answer['success'] = 1; + $answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC); + exit(json_encode($answer)); + } + + if ($label_action === 'print') { + if ($barcode === '') { + $answer['message'] = 'No barcode provided.'; + http_response_code(400); + exit(json_encode($answer)); + } + + $sth = $pdo2->prepare( + "UPDATE md_barcode + SET print_count = print_count + 1, + last_printed_dt = NOW() + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = 'loc' + AND status = 1" + ); + $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); + + $answer['success'] = 1; + exit(json_encode($answer)); + } + + if ($label_action === 'disable') { + if ($barcode === '') { + $answer['message'] = 'No barcode provided.'; + http_response_code(400); + exit(json_encode($answer)); + } + + $sth = $pdo2->prepare( + "UPDATE md_barcode + SET status = 0 + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = 'loc' + AND status = 1" + ); + $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); + + $answer['success'] = 1; + exit(json_encode($answer)); + } + + if ($label_action === 'enable') { + if ($barcode === '') { + $answer['message'] = 'No barcode provided.'; + http_response_code(400); + exit(json_encode($answer)); + } + + $sth = $pdo2->prepare( + "UPDATE md_barcode + SET status = 1 + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = 'loc' + AND status <> 1" + ); + $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); + + $answer['success'] = 1; + exit(json_encode($answer)); + } + + if ($barcode === '' || !$warehouse_id || $zone === '' || $aisle === '' || $rack === '') { + $answer['message'] = 'Location barcode data is incomplete.'; + http_response_code(400); + exit(json_encode($answer)); + } + + validateLocation($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack); + + $sth = $pdo2->prepare( + "INSERT INTO md_barcode + (company_id, barcode_type, barcode, warehouse_id, zone, aisle, rack) + VALUES + (:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :rack) + ON DUPLICATE KEY UPDATE + barcode_type = VALUES(barcode_type), + warehouse_id = VALUES(warehouse_id), + zone = VALUES(zone), + aisle = VALUES(aisle), + rack = VALUES(rack), + status = 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':barcode' => $barcode, + ':warehouse_id' => $warehouse_id, + ':zone' => $zone, + ':aisle' => $aisle, + ':rack' => $rack, + ]); + + $answer['success'] = 1; + $answer['output'] = [ + 'barcode' => $barcode, + 'warehouse_id' => $warehouse_id, + 'zone' => $zone, + 'aisle' => $aisle, + 'rack' => $rack, + ]; + exit(json_encode($answer)); +?> diff --git a/app/ics/api/engine/sku_barcode_label.php b/app/ics/api/engine/sku_barcode_label.php new file mode 100644 index 0000000..a0bc5d1 --- /dev/null +++ b/app/ics/api/engine/sku_barcode_label.php @@ -0,0 +1,245 @@ +prepare( + "SELECT sku, product_name, uom + FROM md_product + WHERE company_id = :company_id + AND sku = :sku + AND status > 0 + LIMIT 1" + ); + $sth->execute([':company_id' => $company_id, ':sku' => $sku]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + function activeStockLotExists(PDO $pdo, int $company_id, string $sku, string $lot): bool { + $warehouses = $pdo->prepare( + "SELECT id FROM md_warehouse + WHERE company_id = :company_id AND status = 1" + ); + $warehouses->execute([':company_id' => $company_id]); + + foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { + $table = stockTableNameFromWarehouseId((int)$warehouse_id); + $sth = $pdo->prepare( + "SELECT ROUND(SUM(COALESCE(`in`, 0)) - SUM(COALESCE(`out`, 0)), 2) AS balance + FROM `{$table}` + WHERE company_id = :company_id + AND product_sku = :product_sku + AND lot_number = :lot_number + AND status = 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':product_sku' => $sku, + ':lot_number' => $lot, + ]); + if ((float)$sth->fetchColumn() > 0) return true; + } + + return false; + } + + function findLot(PDO $pdo, int $company_id, string $sku, string $lot): array|false { + $sth = $pdo->prepare( + "SELECT + l.product_sku, + l.lot_number, + l.expiry_date, + p.product_name, + p.uom + FROM md_lot l + INNER JOIN md_product p + ON p.company_id = l.company_id + AND p.sku = l.product_sku + WHERE l.company_id = :company_id + AND l.product_sku = :product_sku + AND l.lot_number = :lot_number + AND p.status > 0 + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':product_sku' => $sku, + ':lot_number' => $lot, + ]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + if ($row) return $row; + + $product = findProduct($pdo, $company_id, $sku); + if (!$product || !activeStockLotExists($pdo, $company_id, $sku, $lot)) return false; + + return [ + 'product_sku' => $product['sku'], + 'lot_number' => $lot, + 'expiry_date' => null, + 'product_name' => $product['product_name'], + 'uom' => $product['uom'], + ]; + } + + function listLabels(PDO $pdo, int $company_id, string $sku, string $lot): array { + $params = [':company_id' => $company_id]; + $where = "WHERE b.company_id = :company_id"; + + if ($sku !== '') { + $where .= " AND b.product_sku = :product_sku"; + $params[':product_sku'] = $sku; + } + + if ($lot !== '') { + $where .= " AND b.lot_number = :lot_number"; + $params[':lot_number'] = $lot; + } + + $sth = $pdo->prepare( + "SELECT + b.id, + b.product_sku, + b.lot_number, + b.serial_number, + b.barcode, + b.status, + b.print_count, + b.last_printed_dt, + b.created_dt, + p.product_name + FROM md_barcode b + LEFT JOIN md_product p + ON p.company_id = b.company_id + AND p.sku = b.product_sku + {$where} + AND b.barcode_type = 'sku' + ORDER BY b.created_dt DESC, b.id DESC" + ); + $sth->execute($params); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + $label_action = $data['label_action'] ?? 'read'; + $product_sku = trim($data['product_sku'] ?? ''); + $lot_number = trim($data['lot_number'] ?? ''); + + if ($label_action === 'print') { + $barcode = trim($data['barcode'] ?? ''); + if ($barcode === '') { + $answer['message'] = 'No barcode provided.'; + http_response_code(400); + exit(json_encode($answer)); + } + + $sth = $pdo2->prepare( + "UPDATE md_barcode + SET print_count = print_count + 1, + last_printed_dt = NOW() + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = 'sku' + AND status = 1" + ); + $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); + + $answer['success'] = 1; + exit(json_encode($answer)); + } + + if ($label_action === 'disable') { + $barcode = trim($data['barcode'] ?? ''); + if ($barcode === '') { + $answer['message'] = 'No barcode provided.'; + http_response_code(400); + exit(json_encode($answer)); + } + + $sth = $pdo2->prepare( + "UPDATE md_barcode + SET status = 0 + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = 'sku' + AND status = 1" + ); + $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); + + $answer['success'] = 1; + exit(json_encode($answer)); + } + + if ($label_action === 'create') { + $serial_number = trim($data['serial_number'] ?? ''); + $allow_unlisted = !empty($data['allow_unlisted']); + + if ($product_sku === '' || $lot_number === '' || $serial_number === '') { + $answer['message'] = 'SKU, lot no, and serial no are required.'; + http_response_code(400); + exit(json_encode($answer)); + } + + $lot = findLot($pdo2, $company_id, $product_sku, $lot_number); + if (!$lot) { + if (!$allow_unlisted) { + $answer['message'] = 'Lot not found.'; + http_response_code(404); + exit(json_encode($answer)); + } + + $product = findProduct($pdo2, $company_id, $product_sku); + if (!$product) { + $answer['message'] = 'SKU not found.'; + http_response_code(404); + exit(json_encode($answer)); + } + + $lot = [ + 'product_sku' => $product['sku'], + 'lot_number' => $lot_number, + 'expiry_date' => null, + 'product_name' => $product['product_name'], + 'uom' => $product['uom'], + ]; + } + + $barcode = "SKU|{$product_sku}|{$lot_number}|{$serial_number}"; + + $sth = $pdo2->prepare( + "INSERT INTO md_barcode + (company_id, barcode_type, barcode, product_sku, lot_number, serial_number) + VALUES + (:company_id, 'sku', :barcode, :product_sku, :lot_number, :serial_number) + ON DUPLICATE KEY UPDATE + barcode_type = VALUES(barcode_type), + product_sku = VALUES(product_sku), + lot_number = VALUES(lot_number), + serial_number = VALUES(serial_number), + status = 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':product_sku' => $product_sku, + ':lot_number' => $lot_number, + ':serial_number' => $serial_number, + ':barcode' => $barcode, + ]); + + $answer['success'] = 1; + $answer['output'] = [ + 'product_sku' => $product_sku, + 'product_name' => $lot['product_name'], + 'lot_number' => $lot_number, + 'serial_number' => $serial_number, + 'barcode' => $barcode, + ]; + exit(json_encode($answer)); + } + + $answer['success'] = 1; + $answer['output'] = listLabels($pdo2, $company_id, $product_sku, $lot_number); + exit(json_encode($answer)); +?> diff --git a/app/ics/api/engine/sku_label_lots.php b/app/ics/api/engine/sku_label_lots.php new file mode 100644 index 0000000..ba56836 --- /dev/null +++ b/app/ics/api/engine/sku_label_lots.php @@ -0,0 +1,159 @@ +prepare( + "SELECT + l.id, + l.product_sku, + l.lot_number, + l.expiry_date, + l.description, + p.product_name, + p.uom, + 'md_lot' AS source + FROM md_lot l + INNER JOIN md_product p + ON p.company_id = l.company_id + AND p.sku = l.product_sku + WHERE l.company_id = :company_id + AND p.status > 0 + ORDER BY l.product_sku ASC, l.lot_number ASC" + ); + $sth->execute([':company_id' => $company_id]); + $rows = $sth->fetchAll(PDO::FETCH_ASSOC); + $seen = []; + + foreach ($rows as $row) { + $seen[$row['product_sku'] . '|' . $row['lot_number']] = true; + } + + $warehouses = $pdo2->prepare( + "SELECT id FROM md_warehouse + WHERE company_id = :company_id AND status = 1" + ); + $warehouses->execute([':company_id' => $company_id]); + + foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { + $table = stockTableNameFromWarehouseId((int)$warehouse_id); + $stock = $pdo2->query( + "SELECT + s.product_sku, + s.lot_number, + p.product_name, + p.uom, + ROUND(SUM(COALESCE(s.`in`, 0)) - SUM(COALESCE(s.`out`, 0)), 2) AS balance + FROM `{$table}` s + INNER JOIN md_product p + ON p.company_id = s.company_id + AND p.sku = s.product_sku + WHERE s.company_id = " . (int)$company_id . " + AND s.status = 1 + AND s.lot_number IS NOT NULL + AND s.lot_number <> '' + AND p.status > 0 + GROUP BY s.product_sku, s.lot_number, p.product_name, p.uom + HAVING balance > 0" + ); + + foreach ($stock->fetchAll(PDO::FETCH_ASSOC) as $row) { + $key = $row['product_sku'] . '|' . $row['lot_number']; + if (isset($seen[$key])) continue; + $seen[$key] = true; + $rows[] = [ + 'id' => null, + 'product_sku' => $row['product_sku'], + 'lot_number' => $row['lot_number'], + 'expiry_date' => null, + 'description' => 'Found in active stock; missing md_lot row.', + 'product_name'=> $row['product_name'], + 'uom' => $row['uom'], + 'source' => 'active_stock', + ]; + } + } + + $barcodes = $pdo2->prepare( + "SELECT + b.product_sku, + b.lot_number, + b.serial_number, + b.barcode, + b.status, + b.created_dt, + p.product_name, + p.uom + FROM md_barcode b + INNER JOIN md_product p + ON p.company_id = b.company_id + AND p.sku = b.product_sku + WHERE b.company_id = :company_id + AND b.barcode_type = 'sku' + AND b.product_sku IS NOT NULL + AND b.product_sku <> '' + AND b.lot_number IS NOT NULL + AND b.lot_number <> '' + AND p.status > 0 + ORDER BY b.created_dt DESC, b.id DESC" + ); + $barcodes->execute([':company_id' => $company_id]); + + foreach ($barcodes->fetchAll(PDO::FETCH_ASSOC) as $row) { + $key = $row['product_sku'] . '|' . $row['lot_number']; + if (isset($seen[$key])) continue; + $seen[$key] = true; + $rows[] = [ + 'id' => null, + 'product_sku' => $row['product_sku'], + 'lot_number' => $row['lot_number'], + 'expiry_date' => null, + 'description' => 'Prepared barcode exists; missing md_lot row.', + 'product_name' => $row['product_name'], + 'uom' => $row['uom'], + 'source' => 'md_barcode', + 'serial_number' => $row['serial_number'], + 'barcode' => $row['barcode'], + ]; + } + + $status_rows = $pdo2->prepare( + "SELECT + product_sku, + lot_number, + SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS active_count, + SUM(CASE WHEN status <> 1 THEN 1 ELSE 0 END) AS disabled_count + FROM md_barcode + WHERE company_id = :company_id + AND barcode_type = 'sku' + AND product_sku IS NOT NULL + AND product_sku <> '' + AND lot_number IS NOT NULL + AND lot_number <> '' + GROUP BY product_sku, lot_number" + ); + $status_rows->execute([':company_id' => $company_id]); + $label_status = []; + foreach ($status_rows->fetchAll(PDO::FETCH_ASSOC) as $row) { + $label_status[$row['product_sku'] . '|' . $row['lot_number']] = [ + 'active_count' => (int)$row['active_count'], + 'disabled_count' => (int)$row['disabled_count'], + ]; + } + + foreach ($rows as &$row) { + $status = $label_status[$row['product_sku'] . '|' . $row['lot_number']] ?? null; + $row['barcode_active_count'] = $status['active_count'] ?? 0; + $row['barcode_disabled_count'] = $status['disabled_count'] ?? 0; + } + unset($row); + + usort($rows, fn($a, $b) => [$a['product_sku'], $a['lot_number']] <=> [$b['product_sku'], $b['lot_number']]); + + $answer['success'] = 1; + $answer['output'] = $rows; + exit(json_encode($answer)); +?> diff --git a/app/ics/api/engine/sku_label_products.php b/app/ics/api/engine/sku_label_products.php new file mode 100644 index 0000000..4b0b1a0 --- /dev/null +++ b/app/ics/api/engine/sku_label_products.php @@ -0,0 +1,17 @@ +prepare( + "SELECT sku, product_name, uom + FROM md_product + WHERE company_id = :company_id + AND status > 0 + ORDER BY sku ASC" + ); + $sth->execute([':company_id' => $company_id]); + + $answer['success'] = 1; + $answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC); + exit(json_encode($answer)); +?> diff --git a/app/ics/api/engine/validate_scan_location.php b/app/ics/api/engine/validate_scan_location.php new file mode 100644 index 0000000..0a710dc --- /dev/null +++ b/app/ics/api/engine/validate_scan_location.php @@ -0,0 +1,86 @@ +prepare( + "SELECT id, product_sku, td_stock_id + FROM md_rack + WHERE company_id = :company_id + AND warehouse = :warehouse + AND zone = :zone + AND aisle = :aisle + AND rack = :rack + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $company_id, + ':warehouse' => $warehouse_id, + ':zone' => $zone, + ':aisle' => $aisle, + ':rack' => $rack, + ]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + try { + $rack_row = rackRow($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack); + if (!$rack_row) throw new Exception("Location {$zone}-{$aisle}-{$rack} does not exist."); + + if ($mode === 'in' || $mode === 'to') { + if ($rack_row['product_sku'] !== null) { + throw new Exception("Location {$zone}-{$aisle}-{$rack} is already occupied by {$rack_row['product_sku']}."); + } + + if ($mode === 'in' && $product_sku !== '') { + $whMgmt->validateStockUnique($product_sku, $lot_number ?: null, $serial ?: null); + } + } elseif ($mode === 'out' || $mode === 'from') { + if ($product_sku === '') throw new Exception('Scan the SKU label first.'); + + $stock = $whMgmt->getRackStock($warehouse_id, $zone, $aisle, $rack); + if (!$stock) { + throw new Exception("Location {$zone}-{$aisle}-{$rack} has no approved stock to move out."); + } + + if ($stock['product_sku'] !== $product_sku) { + throw new Exception("Location holds {$stock['product_sku']}, not {$product_sku}."); + } + + if ($lot_number !== '' && (string)$stock['lot_number'] !== $lot_number) { + throw new Exception("Location holds lot '{$stock['lot_number']}', not '{$lot_number}'."); + } + + if ($serial !== '' && (string)$stock['serial_number'] !== $serial) { + throw new Exception("Location holds serial '{$stock['serial_number']}', not '{$serial}'."); + } + } else { + throw new Exception('Invalid scan validation mode.'); + } + + $answer['success'] = 1; + exit(json_encode($answer)); + } catch (Exception $e) { + $answer['message'] = $e->getMessage(); + http_response_code(400); + exit(json_encode($answer)); + } +?> diff --git a/app/ics/location_barcode_label.php b/app/ics/location_barcode_label.php new file mode 100644 index 0000000..0051a0d --- /dev/null +++ b/app/ics/location_barcode_label.php @@ -0,0 +1,388 @@ + + + + + + +
+
+
+
+
+

Location Barcode Labels

+

Print or reprint labels from the rack master list.

+
+
+ + + +
+
+
+ +
+
+
+
+ + + + +
+
+ + + + + + + + + + + + + + + +
WarehouseZoneAisleRackStatusLabel StatusAction
Loading locations...
+
+
+
+
+
+
+ Select a location to preview its label. +
+
+
+
+
+
+ + + + + + + diff --git a/app/ics/manage_stock_in.php b/app/ics/manage_stock_in.php index 854aeb8..d6263ee 100644 --- a/app/ics/manage_stock_in.php +++ b/app/ics/manage_stock_in.php @@ -30,8 +30,13 @@
- +
+ + +
@@ -130,6 +135,8 @@ var label_rack = 'Location'; var label_zone = 'Zone'; var label_aisle = 'Aisle'; + var scan_sku_ready = false; + var scan_location_ready = false; function load_location_config(callback) { return ajax_request({ @@ -166,6 +173,100 @@ } } + function set_select_value(selector, value, label) { + if (!value) return; + var $select = $(selector); + if (!$select.find(`option[value="${value}"]`).length) { + $select.append(``); + } + $select.val(value); + } + + function apply_sku_label_scan(data) { + $('#product_sku').val(data.sku).attr('secondary', data.sku); + $('#product_name').val(data.product_name || '').attr('secondary', data.product_name || ''); + $('#lot_number').val(data.lot_number || ''); + $('#serial_number').val(data.serial_number || ''); + $('#quantity').val($('#quantity').val() || 1); + scan_sku_ready = true; + scan_location_ready = false; + $('#warehouse').val(''); + $('#zone').val(''); + $('#aisle').val(''); + $('#rack').val(''); + $('#product_sku').trigger('blur'); + } + + function apply_location_label_scan(data) { + if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) { + bootbox.alert('Scan the SKU label first, then scan the location label.'); + return; + } + validate_scanned_location('in', data, function() { + set_select_value('#warehouse', data.warehouse_id, data.warehouse_id); + if (advanced) { + set_select_value('#zone', data.zone, data.zone); + set_select_value('#aisle', data.aisle, data.aisle); + } + set_select_value('#rack', data.rack, data.rack); + scan_location_ready = true; + }); + } + + function validate_scanned_location(mode, data, onSuccess) { + ajax_request({ + url: 'ics/api/engine/validate_scan_location.php', + autoPrepare: true, + checkRequired: 0, + action: 'read', + queueLock: false, + noLoading: true, + data: { + mode: mode, + warehouse_id: data.warehouse_id, + zone: data.zone, + aisle: data.aisle, + rack: data.rack, + product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(), + lot_number: $('#lot_number').val(), + serial_number: $('#serial_number').val() + }, + onSuccess: onSuccess + }); + } + + function reset_stock_in_inputs() { + $('form')[0].reset(); + $('#product_sku').removeAttr('secondary').prop('disabled', false); + $('#product_name').removeAttr('secondary').val(''); + $('#lot_number, #serial_number, #expiry_date').prop('disabled', false); + $('#contact').removeAttr('secondary data-id'); + if (expiryPicker) expiryPicker.clear(); + if (!advanced) { + $('#zone').val(''); + $('#aisle').val(''); + } + $('#warehouse').val(''); + $('#zone').val(''); + $('#aisle').val(''); + $('#rack').val(''); + scan_sku_ready = false; + scan_location_ready = false; + $('#product_sku').focus(); + } + + function save_scanned_stock_in() { + if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) { + bootbox.alert('Scan the SKU label first.'); + return; + } + if (!scan_location_ready && !$('#rack').val()) { + bootbox.alert('Scan the location label before saving.'); + return; + } + manage_stock_in(); + } + function manage_stock_in() { var rack_val = $('#rack').val(); @@ -200,7 +301,11 @@ }, action: 'manage', onSuccess: function() { + + reset_stock_in_inputs(); + window.location.href = 'ics/stock_in.php'; + } }); @@ -238,6 +343,8 @@ url: "ics/api/engine/retrieve_warehouse.php", autoPrepare: true, checkRequired: 0, + noLoading: true, + queueLock: false, data: { "type": type }, @@ -262,6 +369,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data : { "type": type }, @@ -289,6 +397,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: { "type": type }, @@ -315,6 +424,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: { "type": type }, @@ -462,6 +572,14 @@ await load_location_config(apply_location_labels); await retrieve_warehouse('to'); await retrieve_for_edit(); + scanner.init({ + allowRaw: false, + allowedTypes: ['sku', 'loc'], + onScan: function(sku, product_name, uom, data) { + if (data.type === 'sku') apply_sku_label_scan(data); + if (data.type === 'loc') apply_location_label_scan(data); + } + }); prefill_from_restock(); @@ -488,6 +606,13 @@ + $(document).on('keydown', function(e) { + if (e.key === 'F2') { + e.preventDefault(); + save_scanned_stock_in(); + } + }); + $("select#warehouse").change(function() { if (advanced) { retrieve_zone('to'); diff --git a/app/ics/manage_stock_out.php b/app/ics/manage_stock_out.php index 4f82482..ca4deb9 100644 --- a/app/ics/manage_stock_out.php +++ b/app/ics/manage_stock_out.php @@ -28,8 +28,13 @@
- +
+ + +
@@ -117,6 +122,97 @@ var label_rack = 'Location'; var label_zone = 'Zone'; var label_aisle = 'Aisle'; + var scan_sku_ready = false; + var scan_location_ready = false; + var pending_scan_lot = ''; + var pending_scan_serial = ''; + + function set_select_value(selector, value, label) { + if (!value) return; + var $select = $(selector); + if (!$select.find(`option[value="${value}"]`).length) { + $select.append(``); + } + $select.val(value).prop('disabled', false); + } + + function apply_sku_label_scan(data) { + $('#product_sku').val(data.sku).attr('secondary', data.sku); + $('#product_name').val(data.product_name || '').attr('secondary', data.product_name || ''); + pending_scan_lot = data.lot_number || ''; + pending_scan_serial = data.serial_number || ''; + set_select_value('#lot_number', data.lot_number, data.lot_number); + set_select_value('#serial_number', data.serial_number, data.serial_number); + scan_sku_ready = true; + scan_location_ready = false; + reset_location(); + retrieve_active_lot(); + } + + function apply_location_label_scan(data) { + if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) { + bootbox.alert('Scan the SKU label first, then scan the location label.'); + return; + } + validate_scanned_location('out', data, function() { + set_select_value('#warehouse', data.warehouse_id, data.warehouse_id); + if (advanced) { + set_select_value('#zone', data.zone, data.zone); + set_select_value('#aisle', data.aisle, data.aisle); + } + set_select_value('#rack', data.rack, data.rack); + scan_location_ready = true; + }); + } + + function validate_scanned_location(mode, data, onSuccess) { + ajax_request({ + url: 'ics/api/engine/validate_scan_location.php', + autoPrepare: true, + checkRequired: 0, + action: 'read', + queueLock: false, + noLoading: true, + data: { + mode: mode, + warehouse_id: data.warehouse_id, + zone: data.zone, + aisle: data.aisle, + rack: data.rack, + product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(), + lot_number: $('#lot_number').val(), + serial_number: $('#serial_number').val() + }, + onSuccess: onSuccess + }); + } + + function reset_stock_out_inputs() { + $('form')[0].reset(); + $('#product_sku').removeAttr('secondary').prop('disabled', false); + $('#product_name').removeAttr('secondary').val(''); + $('#lot_number').html('').prop('disabled', false); + $('#serial_number').html('').prop('disabled', false); + $('#contact').removeAttr('secondary data-id'); + reset_location(); + scan_sku_ready = false; + scan_location_ready = false; + pending_scan_lot = ''; + pending_scan_serial = ''; + $('#product_sku').focus(); + } + + function save_scanned_stock_out() { + if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) { + bootbox.alert('Scan the SKU label first.'); + return; + } + if (!scan_location_ready && !$('#rack').val()) { + bootbox.alert('Scan the location label before saving.'); + return; + } + manage_stock_out(); + } function load_location_config(callback) { return ajax_request({ @@ -171,10 +267,16 @@ 'rack': rack_val, 'product_sku': sku_val, 'warehouse': wh_val, + 'lot_number': $('#lot_number').val(), + 'serial_number': $('#serial_number').val(), }, action: 'manage', onSuccess: function() { + + reset_stock_out_inputs(); + window.location.href = 'ics/stock_out.php'; + } }); } @@ -215,6 +317,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, action: 'read', data: { "product_sku": sku }, onSuccess: function(res) { @@ -226,10 +329,18 @@ option += ``; }); $('#lot_number').html(option).prop('disabled', false); + if (pending_scan_lot) { + set_select_value('#lot_number', pending_scan_lot, pending_scan_lot); + } $('#serial_number').html('').prop('disabled', false); // Reset downstream - reset_location(); + if (!scan_location_ready) reset_location(); + if (pending_scan_lot) { + retrieve_active_serial(); + } else { + retrieve_warehouse(); + } } }); } @@ -248,6 +359,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, action: 'read', data: { "product_sku": sku, "lot_number": lot }, onSuccess: function(res) { @@ -257,9 +369,14 @@ option += ``; }); $('#serial_number').html(option).prop('disabled', false); + if (pending_scan_serial) { + set_select_value('#serial_number', pending_scan_serial, pending_scan_serial); + pending_scan_serial = ''; + } // Reset downstream - reset_location(); + if (!scan_location_ready) reset_location(); + retrieve_warehouse(); } }); } @@ -273,6 +390,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: { "type": 'from', "product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(), @@ -280,11 +398,13 @@ }, action: 'read', onSuccess: function(res) { + var current = $('#warehouse').val(); var option = ''; $.each(res.output, function(key, item) { option += ``; }); $('#warehouse').html(option).prop('disabled', false); + if (current) set_select_value('#warehouse', current, current); } }); } @@ -298,6 +418,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: { "warehouse": $('#warehouse').val(), "type": 'from', @@ -324,6 +445,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: { "warehouse": $('#warehouse').val(), "zone": $('#zone').val(), @@ -351,6 +473,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: { "warehouse": $('#warehouse').val(), "zone": $('#zone').val(), @@ -454,6 +577,14 @@ await load_location_config(apply_location_labels); await retrieve_warehouse(); await retrieve_for_edit(); + scanner.init({ + allowRaw: false, + allowedTypes: ['sku', 'loc'], + onScan: function(sku, product_name, uom, data) { + if (data.type === 'sku') apply_sku_label_scan(data); + if (data.type === 'loc') apply_location_label_scan(data); + } + }); } catch(e) { console.log(e); } @@ -462,6 +593,12 @@ // ── Change handlers (create mode only) ─────────────────────────────── + $(document).on('keydown', function(e) { + if (e.key === 'F2') { + e.preventDefault(); + save_scanned_stock_out(); + } + }); // SKU selected → load lots $('input#product_sku').on('blur', function() { diff --git a/app/ics/manage_stock_transfer.php b/app/ics/manage_stock_transfer.php index f09d664..f4e7c4a 100644 --- a/app/ics/manage_stock_transfer.php +++ b/app/ics/manage_stock_transfer.php @@ -28,8 +28,13 @@
- +
+ + +
@@ -150,6 +155,133 @@ var label_rack = 'Location'; var label_zone = 'Zone'; var label_aisle = 'Aisle'; + var scan_sku_ready = false; + var scan_from_ready = false; + var scan_to_ready = false; + var pending_scan_lot = ''; + var pending_scan_serial = ''; + + function set_select_value(selector, value, label) { + if (!value) return; + var $select = $(selector); + if (!$select.find(`option[value="${value}"]`).length) { + $select.append(``); + } + $select.val(value).prop('disabled', false); + } + + function apply_sku_label_scan(data) { + $('#product_sku').val(data.sku).attr('secondary', data.sku); + $('#product_name').val(data.product_name || '').attr('secondary', data.product_name || ''); + pending_scan_lot = data.lot_number || ''; + pending_scan_serial = data.serial_number || ''; + set_select_value('#lot_number', data.lot_number, data.lot_number); + set_select_value('#serial_number', data.serial_number, data.serial_number); + scan_sku_ready = true; + scan_from_ready = false; + scan_to_ready = false; + reset_from_location(); + reset_to_location(); + retrieve_active_lot(); + } + + function apply_location_label_scan(data) { + if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) { + bootbox.alert('Scan the SKU label first, then scan the from-location label.'); + return; + } + + var role = scan_from_ready ? 'to' : 'from'; + if (role === 'to' && same_location_as_from(data)) { + bootbox.alert('To-location must be different from from-location.'); + return; + } + + validate_scanned_location(role, data, function() { + set_select_value('#warehouse_' + role, data.warehouse_id, data.warehouse_id); + if (advanced) { + set_select_value('#zone_' + role, data.zone, data.zone); + set_select_value('#aisle_' + role, data.aisle, data.aisle); + } + set_select_value('#rack_' + role, data.rack, data.rack); + + if (role === 'from') { + scan_from_ready = true; + } else { + scan_to_ready = true; + } + }); + } + + function same_location_as_from(data) { + return String($('#warehouse_from').val()) === String(data.warehouse_id) + && String($('#zone_from').val()) === String(data.zone) + && String($('#aisle_from').val()) === String(data.aisle) + && String($('#rack_from').val()) === String(data.rack); + } + + function validate_scanned_location(role, data, onSuccess) { + ajax_request({ + url: 'ics/api/engine/validate_scan_location.php', + autoPrepare: true, + checkRequired: 0, + action: 'read', + queueLock: false, + noLoading: true, + data: { + mode: role, + warehouse_id: data.warehouse_id, + zone: data.zone, + aisle: data.aisle, + rack: data.rack, + product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(), + lot_number: $('#lot_number').val(), + serial_number: $('#serial_number').val() + }, + onSuccess: onSuccess + }); + } + + function reset_to_location() { + $('#warehouse_to').val(''); + $('#zone_to').html('').val(''); + $('#aisle_to').html('').val(''); + $('#rack_to').html('').val(''); + } + + function reset_stock_transfer_inputs() { + $('form')[0].reset(); + $('#product_sku').removeAttr('secondary').prop('disabled', false); + $('#product_name').removeAttr('secondary').val(''); + $('#lot_number').html('').prop('disabled', false); + $('#serial_number').html('').prop('disabled', false); + $('#contact').removeAttr('secondary data-id'); + reset_from_location(); + reset_to_location(); + retrieve_warehouse('to'); + scan_sku_ready = false; + scan_from_ready = false; + scan_to_ready = false; + pending_scan_lot = ''; + pending_scan_serial = ''; + $('#product_sku').focus(); + } + + function save_scanned_stock_transfer() { + if (!scan_sku_ready && !($('#product_sku').attr('secondary') || $('#product_sku').val())) { + bootbox.alert('Scan the SKU label first.'); + return; + } + if (!scan_from_ready && !$('#rack_from').val()) { + bootbox.alert('Scan the from-location label before saving.'); + return; + } + if (!scan_to_ready && !$('#rack_to').val()) { + bootbox.alert('Scan the to-location label before saving.'); + return; + } + manage_stock_transfer(); + } function load_location_config(callback) { return ajax_request({ @@ -224,10 +356,16 @@ 'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val, 'rack_to': rack_to_val, 'product_sku': sku_val, + 'lot_number': $('#lot_number').val(), + 'serial_number': $('#serial_number').val(), }, action: 'manage', onSuccess: function() { + + reset_stock_transfer_inputs(); + window.location.href = 'ics/stock_transfer.php'; + } }); } @@ -266,6 +404,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, action: 'read', data: { 'product_sku': sku }, onSuccess: function(res) { @@ -276,8 +415,16 @@ option += ''; }); $('#lot_number').html(option).prop('disabled', false); + if (pending_scan_lot) { + set_select_value('#lot_number', pending_scan_lot, pending_scan_lot); + } $('#serial_number').html('').prop('disabled', false); - reset_from_location(); + if (!scan_from_ready) reset_from_location(); + if (pending_scan_lot) { + retrieve_active_serial(); + } else { + retrieve_warehouse('from'); + } } }); } @@ -292,6 +439,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, action: 'read', data: { 'product_sku': sku, 'lot_number': $('#lot_number').val() }, onSuccess: function(res) { @@ -300,7 +448,12 @@ option += ''; }); $('#serial_number').html(option).prop('disabled', false); - reset_from_location(); + if (pending_scan_serial) { + set_select_value('#serial_number', pending_scan_serial, pending_scan_serial); + pending_scan_serial = ''; + } + if (!scan_from_ready) reset_from_location(); + retrieve_warehouse('from'); } }); } @@ -313,6 +466,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: { 'type': type, 'product_sku': type === 'from' ? ($('#product_sku').attr('secondary') || $('#product_sku').val()) : '', @@ -320,11 +474,13 @@ }, action: 'read', onSuccess: function(res) { + var current = $('#warehouse_' + type).val(); var option = ''; $.each(res.output, function(key, item) { option += ''; }); $('select[name="warehouse"][role="' + type + '"]').html(option).prop('disabled', false); + if (current) set_select_value('#warehouse_' + type, current, current); } }); } @@ -348,6 +504,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: Object.assign({ 'type': type }, extra), action: 'read', onSuccess: function(res) { @@ -380,6 +537,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: Object.assign({ 'type': type }, extra), action: 'read', onSuccess: function(res) { @@ -414,6 +572,7 @@ autoPrepare: true, checkRequired: 0, noLoading: true, + queueLock: false, data: Object.assign({ 'type': type }, extra), action: 'read', onSuccess: function(res) { @@ -535,6 +694,14 @@ await retrieve_warehouse('to'); await retrieve_for_edit(); + scanner.init({ + allowRaw: false, + allowedTypes: ['sku', 'loc'], + onScan: function(sku, product_name, uom, data) { + if (data.type === 'sku') apply_sku_label_scan(data); + if (data.type === 'loc') apply_location_label_scan(data); + } + }); } catch(e) { console.log(e); } @@ -544,6 +711,13 @@ // ── Change handlers (create mode only) ─────────────────────────────────── + $(document).on('keydown', function(e) { + if (e.key === 'F2') { + e.preventDefault(); + save_scanned_stock_transfer(); + } + }); + $('input#product_sku').on('blur', function() { var sku = $(this).attr('secondary'); if (!sku) return; diff --git a/app/ics/sku_barcode_label.php b/app/ics/sku_barcode_label.php new file mode 100644 index 0000000..a3aa7dd --- /dev/null +++ b/app/ics/sku_barcode_label.php @@ -0,0 +1,538 @@ + + + + + + +
+
+
+
+
+

SKU Barcode Labels

+

Print or reprint product identity labels from the lot master list.

+
+
+ + + +
+
+
+ +
+
+
+
+
+ + + + +
+
+ + + + + + + + + + + + + + +
SKUProductLot NoExpirySourceAction
Loading lots...
+
+
+
+
+ +
+
+
+
+ Select a lot to preview its label. +
+
+
+
+
+
+
+ + + + + + + + + + + + + diff --git a/app/include_header.php b/app/include_header.php index d66fb79..3ae00bb 100644 --- a/app/include_header.php +++ b/app/include_header.php @@ -6,9 +6,9 @@ ThreeWMS - - - + + + @@ -58,4 +58,8 @@ - \ No newline at end of file + + + + diff --git a/app/include_master_sidebar.php b/app/include_master_sidebar.php index bd0b24f..2501d12 100644 --- a/app/include_master_sidebar.php +++ b/app/include_master_sidebar.php @@ -6,11 +6,7 @@