encapsulate Barcode Operations [OOP]
This commit is contained in:
@@ -0,0 +1,762 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BarcodeManager
|
||||||
|
*
|
||||||
|
* Centralises all md_barcode operations: creating, printing, enabling/disabling,
|
||||||
|
* and resolving SKU and location barcode labels.
|
||||||
|
*
|
||||||
|
* Method order:
|
||||||
|
* Private helpers → product lookup, lot resolution, label fetch, location validation
|
||||||
|
* SKU label basis → create / list for product (SKU) barcodes
|
||||||
|
* Location label → create / list for warehouse location barcodes
|
||||||
|
* Shared operations → recordPrint / setStatus (both types)
|
||||||
|
* Lookup basis → resolve any barcode string to its type and fields
|
||||||
|
* Dropdown basis → lot and product lists for label creation UI
|
||||||
|
*
|
||||||
|
* Note: operates entirely on $pdo2 (the client database, wms2).
|
||||||
|
* The $advancedLocation flag needed by lookup() must be resolved by the
|
||||||
|
* caller via CompanySettingManager($pdo1) — keeps this class scoped to wms2.
|
||||||
|
*
|
||||||
|
* Security: all SQL uses PDO prepared statements with bound parameters.
|
||||||
|
* No user input is ever interpolated directly into a query string.
|
||||||
|
*/
|
||||||
|
class BarcodeManager {
|
||||||
|
|
||||||
|
private PDO $pdo;
|
||||||
|
private int $company_id;
|
||||||
|
|
||||||
|
public function __construct(PDO $pdo, int $company_id) {
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
$this->company_id = $company_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Private helpers
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function stockTableName(int $warehouse_id): string {
|
||||||
|
return 'td_stock_' . $warehouse_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single active product row by SKU.
|
||||||
|
*/
|
||||||
|
private function findProduct(string $sku): array|false {
|
||||||
|
$sth = $this->pdo->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' => $this->company_id, ':sku' => $sku]);
|
||||||
|
return $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a serial is provided, verify it does not already map to a different lot
|
||||||
|
* across any active warehouse stock table.
|
||||||
|
*
|
||||||
|
* Throws if the serial is claimed by a different lot, otherwise returns silently.
|
||||||
|
*/
|
||||||
|
private function validateLotSerial(string $sku, string $lot, string $serial): void {
|
||||||
|
if ($serial === '') return;
|
||||||
|
|
||||||
|
$warehouses = $this->pdo->prepare(
|
||||||
|
"SELECT id FROM md_warehouse
|
||||||
|
WHERE company_id = :company_id AND status = 1"
|
||||||
|
);
|
||||||
|
$warehouses->execute([':company_id' => $this->company_id]);
|
||||||
|
|
||||||
|
foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) {
|
||||||
|
$table = $this->stockTableName((int)$warehouse_id);
|
||||||
|
$sth = $this->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' => $this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a barcode row from md_barcode by barcode string and type.
|
||||||
|
*
|
||||||
|
* Returns false if the row does not exist.
|
||||||
|
* Throws if the row exists but is disabled (status != 1).
|
||||||
|
*/
|
||||||
|
private function findPreparedLabel(string $barcode, string $type): array|false {
|
||||||
|
$sth = $this->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' => $this->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm a rack row exists for the given warehouse + zone + aisle + rack.
|
||||||
|
* Throws if not found.
|
||||||
|
*/
|
||||||
|
private function validateLocation(int $warehouse_id, string $zone, string $aisle, string $rack): void {
|
||||||
|
$sth = $this->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' => $this->company_id,
|
||||||
|
':warehouse' => $warehouse_id,
|
||||||
|
':zone' => $zone,
|
||||||
|
':aisle' => $aisle,
|
||||||
|
':rack' => $rack,
|
||||||
|
]);
|
||||||
|
if (!$sth->fetchColumn()) throw new Exception('Location not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether any warehouse stock table has a positive balance for a given SKU + lot.
|
||||||
|
* Used as a fallback when a lot row is missing from md_lot.
|
||||||
|
*/
|
||||||
|
private function activeStockLotExists(string $sku, string $lot): bool {
|
||||||
|
$warehouses = $this->pdo->prepare(
|
||||||
|
"SELECT id FROM md_warehouse
|
||||||
|
WHERE company_id = :company_id AND status = 1"
|
||||||
|
);
|
||||||
|
$warehouses->execute([':company_id' => $this->company_id]);
|
||||||
|
|
||||||
|
foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) {
|
||||||
|
$table = $this->stockTableName((int)$warehouse_id);
|
||||||
|
$sth = $this->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' => $this->company_id,
|
||||||
|
':product_sku' => $sku,
|
||||||
|
':lot_number' => $lot,
|
||||||
|
]);
|
||||||
|
if ((float)$sth->fetchColumn() > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a lot by SKU + lot number.
|
||||||
|
*
|
||||||
|
* Tries md_lot first; falls back to synthesising a row from active stock
|
||||||
|
* (handles the case where stock was entered without an md_lot record).
|
||||||
|
* Returns false if neither source yields a result.
|
||||||
|
*/
|
||||||
|
private function findLot(string $sku, string $lot): array|false {
|
||||||
|
$sth = $this->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' => $this->company_id,
|
||||||
|
':product_sku' => $sku,
|
||||||
|
':lot_number' => $lot,
|
||||||
|
]);
|
||||||
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($row) return $row;
|
||||||
|
|
||||||
|
$product = $this->findProduct($sku);
|
||||||
|
if (!$product || !$this->activeStockLotExists($sku, $lot)) return false;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'product_sku' => $product['sku'],
|
||||||
|
'lot_number' => $lot,
|
||||||
|
'expiry_date' => null,
|
||||||
|
'product_name' => $product['product_name'],
|
||||||
|
'uom' => $product['uom'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// SKU LABEL BASIS
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all SKU barcode labels, optionally filtered by SKU and/or lot.
|
||||||
|
*
|
||||||
|
* Joined to md_product for product_name. Ordered newest first.
|
||||||
|
*
|
||||||
|
* @param string $sku Filter by product SKU (empty = all).
|
||||||
|
* @param string $lot Filter by lot number (empty = all).
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function listSkuLabels(string $sku = '', string $lot = ''): array {
|
||||||
|
$params = [':company_id' => $this->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 = $this->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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SKU barcode label (or re-activate a previously disabled one).
|
||||||
|
*
|
||||||
|
* Validates that the lot exists (or active stock exists for it).
|
||||||
|
* When $allowUnlisted is true, skips the lot requirement and only checks
|
||||||
|
* that the product SKU is valid — intended for pre-labelling before goods arrive.
|
||||||
|
*
|
||||||
|
* @param string $sku Product SKU.
|
||||||
|
* @param string $lot Lot number.
|
||||||
|
* @param string $serial Serial number (unique within lot/sku).
|
||||||
|
* @param bool $allowUnlisted Allow creating a label for a lot not yet in md_lot or stock.
|
||||||
|
* @return array Keys: product_sku, product_name, lot_number, serial_number, barcode.
|
||||||
|
* @throws Exception On validation failure.
|
||||||
|
*/
|
||||||
|
public function createSkuLabel(string $sku, string $lot, string $serial, bool $allowUnlisted = false): array {
|
||||||
|
$lot_row = $this->findLot($sku, $lot);
|
||||||
|
|
||||||
|
if (!$lot_row) {
|
||||||
|
if (!$allowUnlisted) throw new Exception('Lot not found.');
|
||||||
|
|
||||||
|
$product = $this->findProduct($sku);
|
||||||
|
if (!$product) throw new Exception('SKU not found.');
|
||||||
|
|
||||||
|
$lot_row = [
|
||||||
|
'product_sku' => $product['sku'],
|
||||||
|
'lot_number' => $lot,
|
||||||
|
'expiry_date' => null,
|
||||||
|
'product_name' => $product['product_name'],
|
||||||
|
'uom' => $product['uom'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$barcode = "SKU|{$sku}|{$lot}|{$serial}";
|
||||||
|
|
||||||
|
$this->pdo->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"
|
||||||
|
)->execute([
|
||||||
|
':company_id' => $this->company_id,
|
||||||
|
':barcode' => $barcode,
|
||||||
|
':product_sku' => $sku,
|
||||||
|
':lot_number' => $lot,
|
||||||
|
':serial_number' => $serial,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'product_sku' => $sku,
|
||||||
|
'product_name' => $lot_row['product_name'],
|
||||||
|
'lot_number' => $lot,
|
||||||
|
'serial_number' => $serial,
|
||||||
|
'barcode' => $barcode,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// LOCATION LABEL BASIS
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all location barcode labels for this company.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function listLocationLabels(): array {
|
||||||
|
$sth = $this->pdo->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' => $this->company_id]);
|
||||||
|
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new location barcode label (or re-activate a previously disabled one).
|
||||||
|
*
|
||||||
|
* Validates that the rack exists in md_rack before inserting.
|
||||||
|
*
|
||||||
|
* @param string $barcode Full barcode string (e.g. "LOC|1|A|1|R1").
|
||||||
|
* @param int $warehouse_id Warehouse ID.
|
||||||
|
* @param string $zone Zone code.
|
||||||
|
* @param string $aisle Aisle code.
|
||||||
|
* @param string $rack Rack code.
|
||||||
|
* @return array Keys: barcode, warehouse_id, zone, aisle, rack.
|
||||||
|
* @throws Exception If the location is not found in md_rack.
|
||||||
|
*/
|
||||||
|
public function createLocationLabel(string $barcode, int $warehouse_id, string $zone, string $aisle, string $rack): array {
|
||||||
|
$this->validateLocation($warehouse_id, $zone, $aisle, $rack);
|
||||||
|
|
||||||
|
$this->pdo->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"
|
||||||
|
)->execute([
|
||||||
|
':company_id' => $this->company_id,
|
||||||
|
':barcode' => $barcode,
|
||||||
|
':warehouse_id' => $warehouse_id,
|
||||||
|
':zone' => $zone,
|
||||||
|
':aisle' => $aisle,
|
||||||
|
':rack' => $rack,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'barcode' => $barcode,
|
||||||
|
'warehouse_id' => $warehouse_id,
|
||||||
|
'zone' => $zone,
|
||||||
|
'aisle' => $aisle,
|
||||||
|
'rack' => $rack,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// SHARED LABEL OPERATIONS
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment print_count and update last_printed_dt for an active barcode.
|
||||||
|
*
|
||||||
|
* @param string $barcode Barcode string.
|
||||||
|
* @param string $type 'sku' or 'loc'.
|
||||||
|
*/
|
||||||
|
public function recordPrint(string $barcode, string $type): void {
|
||||||
|
$this->pdo->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 = :barcode_type
|
||||||
|
AND status = 1"
|
||||||
|
)->execute([
|
||||||
|
':company_id' => $this->company_id,
|
||||||
|
':barcode' => $barcode,
|
||||||
|
':barcode_type' => $type,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the status of a barcode label (0 = disabled, 1 = active).
|
||||||
|
*
|
||||||
|
* @param string $barcode Barcode string.
|
||||||
|
* @param string $type 'sku' or 'loc'.
|
||||||
|
* @param int $status Target status value.
|
||||||
|
*/
|
||||||
|
public function setStatus(string $barcode, string $type, int $status): void {
|
||||||
|
$this->pdo->prepare(
|
||||||
|
"UPDATE md_barcode
|
||||||
|
SET status = :status
|
||||||
|
WHERE company_id = :company_id
|
||||||
|
AND barcode = :barcode
|
||||||
|
AND barcode_type = :barcode_type"
|
||||||
|
)->execute([
|
||||||
|
':company_id' => $this->company_id,
|
||||||
|
':barcode' => $barcode,
|
||||||
|
':barcode_type' => $type,
|
||||||
|
':status' => $status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// LOOKUP / RESOLUTION BASIS
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a barcode string to its type and fields.
|
||||||
|
*
|
||||||
|
* Dispatch order:
|
||||||
|
* 1. SKU| prefix → validated SKU label
|
||||||
|
* 2. LOC| prefix → validated location label
|
||||||
|
* 3. Fallback → raw product barcode or SKU field match in md_product
|
||||||
|
*
|
||||||
|
* Returns an array with 'type' set to 'sku', 'loc', or 'raw' plus
|
||||||
|
* the type-specific fields.
|
||||||
|
*
|
||||||
|
* The $advancedLocation flag controls how LOC barcodes are parsed;
|
||||||
|
* resolve it from the caller via CompanySettingManager::get('advanced_location').
|
||||||
|
*
|
||||||
|
* @param string $barcode Full barcode string to resolve.
|
||||||
|
* @param bool $advancedLocation True = zone/aisle/rack parts; false = rack only.
|
||||||
|
* @return array
|
||||||
|
* @throws Exception On parse or validation failure.
|
||||||
|
*/
|
||||||
|
public function lookup(string $barcode, bool $advancedLocation): array {
|
||||||
|
$parts = explode('|', $barcode);
|
||||||
|
$label_type = strtoupper($parts[0] ?? '');
|
||||||
|
|
||||||
|
if ($label_type === 'SKU') {
|
||||||
|
if (count($parts) !== 4) throw new Exception('Invalid SKU label format.');
|
||||||
|
[, $sku, $lot, $serial] = $parts;
|
||||||
|
$sku = trim($sku);
|
||||||
|
$lot = trim($lot);
|
||||||
|
$serial = trim($serial);
|
||||||
|
if ($sku === '' || $lot === '' || $serial === '') {
|
||||||
|
throw new Exception('SKU, lot, and serial are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $this->findPreparedLabel($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 = $this->findProduct($sku);
|
||||||
|
if (!$product) throw new Exception('SKU not found.');
|
||||||
|
$this->validateLotSerial($sku, $lot, $serial);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'sku',
|
||||||
|
'sku' => $product['sku'],
|
||||||
|
'product_name' => $product['product_name'],
|
||||||
|
'uom' => $product['uom'] ?? 'pcs',
|
||||||
|
'lot_number' => $lot,
|
||||||
|
'serial_number' => $serial,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($label_type === 'LOC') {
|
||||||
|
if ($advancedLocation && count($parts) !== 5) throw new Exception('Invalid advanced location label format.');
|
||||||
|
if (!$advancedLocation && count($parts) !== 3) throw new Exception('Invalid location label format.');
|
||||||
|
|
||||||
|
$warehouse_id = (int)($parts[1] ?? 0);
|
||||||
|
if ($advancedLocation) {
|
||||||
|
$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 = $this->findPreparedLabel($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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validateLocation($warehouse_id, $zone, $aisle, $rack);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'loc',
|
||||||
|
'warehouse_id' => $warehouse_id,
|
||||||
|
'zone' => $zone,
|
||||||
|
'aisle' => $aisle,
|
||||||
|
'rack' => $rack,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: match against md_product's barcode or sku fields
|
||||||
|
$sth = $this->pdo->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' => $this->company_id,
|
||||||
|
':barcode' => $barcode,
|
||||||
|
':sku' => $barcode,
|
||||||
|
':barcode_order' => $barcode,
|
||||||
|
]);
|
||||||
|
$product = $sth->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$product) throw new Exception('Barcode not found.');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'raw',
|
||||||
|
'sku' => $product['sku'],
|
||||||
|
'product_name' => $product['product_name'],
|
||||||
|
'uom' => $product['uom'] ?? 'pcs',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// DROPDOWN BASIS
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a merged list of lots for the SKU label creation UI.
|
||||||
|
*
|
||||||
|
* Combines three sources in priority order, deduplicating by SKU+lot:
|
||||||
|
* 1. md_lot rows (canonical lot master)
|
||||||
|
* 2. Active stock rows whose lot is not in md_lot
|
||||||
|
* 3. Existing barcode rows whose lot is not covered by 1 or 2
|
||||||
|
*
|
||||||
|
* Each row is augmented with barcode_active_count and barcode_disabled_count.
|
||||||
|
* Sorted by product_sku → lot_number.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getLabelLots(): array {
|
||||||
|
$sth = $this->pdo->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' => $this->company_id]);
|
||||||
|
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$seen = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$seen[$row['product_sku'] . '|' . $row['lot_number']] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supplement from active stock tables
|
||||||
|
$warehouses = $this->pdo->prepare(
|
||||||
|
"SELECT id FROM md_warehouse
|
||||||
|
WHERE company_id = :company_id AND status = 1"
|
||||||
|
);
|
||||||
|
$warehouses->execute([':company_id' => $this->company_id]);
|
||||||
|
|
||||||
|
foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) {
|
||||||
|
$table = $this->stockTableName((int)$warehouse_id);
|
||||||
|
$stock = $this->pdo->prepare(
|
||||||
|
"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 = :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"
|
||||||
|
);
|
||||||
|
$stock->execute([':company_id' => $this->company_id]);
|
||||||
|
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supplement from existing barcode rows
|
||||||
|
$barcodes = $this->pdo->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' => $this->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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach per-lot label counts
|
||||||
|
$status_rows = $this->pdo->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' => $this->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']]);
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all active products for the label creation product dropdown.
|
||||||
|
*
|
||||||
|
* @return array Each row: sku, product_name, uom.
|
||||||
|
*/
|
||||||
|
public function getLabelProducts(): array {
|
||||||
|
$sth = $this->pdo->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' => $this->company_id]);
|
||||||
|
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require '../../../assets/utils/db_auth.php';
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
require '../../../assets/utils/classes/CompanySettingManager.php';
|
||||||
|
require '../../../assets/utils/classes/BarcodeManager.php';
|
||||||
|
|
||||||
$barcode = trim($data['barcode'] ?? '');
|
$barcode = trim($data['barcode'] ?? '');
|
||||||
|
|
||||||
@@ -10,196 +12,19 @@
|
|||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
|
||||||
function findProduct(PDO $pdo, int $company_id, string $sku): array|false {
|
$csm = new CompanySettingManager($pdo1, $company_id);
|
||||||
$sth = $pdo->prepare(
|
$advanced_location = (bool)$csm->get('advanced_location');
|
||||||
"SELECT sku, product_name, uom
|
$bm = new BarcodeManager($pdo2, $company_id);
|
||||||
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 {
|
try {
|
||||||
if ($label_type === 'SKU') {
|
$result = $bm->lookup($barcode, $advanced_location);
|
||||||
if (count($parts) !== 4) throw new Exception('Invalid SKU label format.');
|
$answer['success'] = 1;
|
||||||
$sku = trim($parts[1]);
|
$answer = array_merge($answer, $result);
|
||||||
$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) {
|
} catch (Exception $e) {
|
||||||
$answer['message'] = $e->getMessage();
|
$answer['message'] = $e->getMessage();
|
||||||
http_response_code(400);
|
$http_code = ($e->getMessage() === 'Barcode not found.') ? 404 : 400;
|
||||||
exit(json_encode($answer));
|
http_response_code($http_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
$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));
|
exit(json_encode($answer));
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -1,30 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require '../../../assets/utils/db_auth.php';
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
require '../../../assets/utils/classes/BarcodeManager.php';
|
||||||
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()) {
|
|
||||||
$answer = ['success' => 0, 'message' => 'Location not found.'];
|
|
||||||
http_response_code(404);
|
|
||||||
exit(json_encode($answer));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$label_action = $data['label_action'] ?? 'create';
|
$label_action = $data['label_action'] ?? 'create';
|
||||||
$barcode = trim($data['barcode'] ?? '');
|
$barcode = trim($data['barcode'] ?? '');
|
||||||
@@ -37,17 +14,11 @@
|
|||||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($label_action === 'list') {
|
$bm = new BarcodeManager($pdo2, $company_id);
|
||||||
$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]);
|
|
||||||
|
|
||||||
|
if ($label_action === 'list') {
|
||||||
$answer['success'] = 1;
|
$answer['success'] = 1;
|
||||||
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
$answer['output'] = $bm->listLocationLabels();
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,18 +28,7 @@
|
|||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
$bm->recordPrint($barcode, 'loc');
|
||||||
$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;
|
$answer['success'] = 1;
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
@@ -79,17 +39,7 @@
|
|||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
$bm->setStatus($barcode, 'loc', 0);
|
||||||
$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;
|
$answer['success'] = 1;
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
@@ -100,58 +50,26 @@
|
|||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
$bm->setStatus($barcode, 'loc', 1);
|
||||||
$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;
|
$answer['success'] = 1;
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default: create
|
||||||
if ($barcode === '' || !$warehouse_id || $zone === '' || $aisle === '' || $rack === '') {
|
if ($barcode === '' || !$warehouse_id || $zone === '' || $aisle === '' || $rack === '') {
|
||||||
$answer['message'] = 'Location barcode data is incomplete.';
|
$answer['message'] = 'Location barcode data is incomplete.';
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
|
||||||
validateLocation($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack);
|
try {
|
||||||
|
$output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $rack);
|
||||||
|
$answer['success'] = 1;
|
||||||
|
$answer['output'] = $output;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$answer['message'] = $e->getMessage();
|
||||||
|
http_response_code($e->getMessage() === 'Location not found.' ? 404 : 400);
|
||||||
|
}
|
||||||
|
|
||||||
$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));
|
exit(json_encode($answer));
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -1,127 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require '../../../assets/utils/db_auth.php';
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
require '../../../assets/utils/classes/BarcodeManager.php';
|
||||||
function stockTableNameFromWarehouseId(int $warehouse_id): string {
|
|
||||||
return 'td_stock_' . $warehouse_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findProduct(PDO $pdo, int $company_id, string $sku): array|false {
|
|
||||||
$sth = $pdo->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';
|
$label_action = $data['label_action'] ?? 'read';
|
||||||
$product_sku = trim($data['product_sku'] ?? '');
|
$product_sku = trim($data['product_sku'] ?? '');
|
||||||
@@ -131,6 +11,8 @@
|
|||||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$bm = new BarcodeManager($pdo2, $company_id);
|
||||||
|
|
||||||
if ($label_action === 'print') {
|
if ($label_action === 'print') {
|
||||||
$barcode = trim($data['barcode'] ?? '');
|
$barcode = trim($data['barcode'] ?? '');
|
||||||
if ($barcode === '') {
|
if ($barcode === '') {
|
||||||
@@ -138,18 +20,7 @@
|
|||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
$bm->recordPrint($barcode, 'sku');
|
||||||
$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;
|
$answer['success'] = 1;
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
@@ -161,23 +32,13 @@
|
|||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
$bm->setStatus($barcode, 'sku', 0);
|
||||||
$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;
|
$answer['success'] = 1;
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($label_action === 'create') {
|
if ($label_action === 'create') {
|
||||||
$serial_number = trim($data['serial_number'] ?? '');
|
$serial_number = trim($data['serial_number'] ?? '');
|
||||||
$allow_unlisted = !empty($data['allow_unlisted']);
|
$allow_unlisted = !empty($data['allow_unlisted']);
|
||||||
|
|
||||||
if ($product_sku === '' || $lot_number === '' || $serial_number === '') {
|
if ($product_sku === '' || $lot_number === '' || $serial_number === '') {
|
||||||
@@ -186,64 +47,19 @@
|
|||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
|
||||||
$lot = findLot($pdo2, $company_id, $product_sku, $lot_number);
|
try {
|
||||||
if (!$lot) {
|
$output = $bm->createSkuLabel($product_sku, $lot_number, $serial_number, $allow_unlisted);
|
||||||
if (!$allow_unlisted) {
|
$answer['success'] = 1;
|
||||||
$answer['message'] = 'Lot not found.';
|
$answer['output'] = $output;
|
||||||
http_response_code(404);
|
} catch (Exception $e) {
|
||||||
exit(json_encode($answer));
|
$answer['message'] = $e->getMessage();
|
||||||
}
|
http_response_code(($e->getMessage() === 'Lot not found.' || $e->getMessage() === 'SKU not found.') ? 404 : 400);
|
||||||
|
|
||||||
$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));
|
exit(json_encode($answer));
|
||||||
}
|
}
|
||||||
|
|
||||||
$answer['success'] = 1;
|
$answer['success'] = 1;
|
||||||
$answer['output'] = listLabels($pdo2, $company_id, $product_sku, $lot_number);
|
$answer['output'] = $bm->listSkuLabels($product_sku, $lot_number);
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -1,159 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require '../../../assets/utils/db_auth.php';
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
require '../../../assets/utils/classes/BarcodeManager.php';
|
||||||
|
|
||||||
function stockTableNameFromWarehouseId(int $warehouse_id): string {
|
$bm = new BarcodeManager($pdo2, $company_id);
|
||||||
return 'td_stock_' . $warehouse_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sth = $pdo2->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['success'] = 1;
|
||||||
$answer['output'] = $rows;
|
$answer['output'] = $bm->getLabelLots();
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require '../../../assets/utils/db_auth.php';
|
require '../../../assets/utils/db_auth.php';
|
||||||
|
require '../../../assets/utils/classes/BarcodeManager.php';
|
||||||
|
|
||||||
$sth = $pdo2->prepare(
|
$bm = new BarcodeManager($pdo2, $company_id);
|
||||||
"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['success'] = 1;
|
||||||
$answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC);
|
$answer['output'] = $bm->getLabelProducts();
|
||||||
exit(json_encode($answer));
|
exit(json_encode($answer));
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<a href="<?php echo $server_url?>index.php" class="d-inline-flex">
|
<a href="<?php echo $server_url?>index.php" class="d-inline-flex">
|
||||||
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
||||||
<span class="logo-text ms-2">
|
<span class="logo-text ms-2">
|
||||||
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="" />
|
<img src="<?php echo $server_url?>assets/images/logo.png" alt="" height="40"/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<a href="<?php echo $server_url?>index.php" class="d-inline-flex">
|
<a href="<?php echo $server_url?>index.php" class="d-inline-flex">
|
||||||
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
||||||
<span class="logo-text ms-2">
|
<span class="logo-text ms-2">
|
||||||
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="" />
|
<img src="<?php echo $server_url?>assets/images/logo.png" alt="" height="40"/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user