Change 'Rack' to 'Bin'
This commit is contained in:
@@ -543,7 +543,7 @@ function load_location_config(callback) {
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
if (callback) callback();
|
||||
@@ -1156,9 +1156,9 @@ function show_stock_rows(source, source_id, label) {
|
||||
'0':'<span class="badge bg-warning text-dark">Draft</span>',
|
||||
'1':'<span class="badge bg-success">Approved</span>'}[String(r.status)]
|
||||
|| '—';
|
||||
var location = [r.zone, r.aisle, r.rack].filter(function(v) {
|
||||
return v && v !== r.rack;
|
||||
}).concat([r.rack]).filter(Boolean).join(' / ');
|
||||
var location = [r.zone, r.aisle, r.bin].filter(function(v) {
|
||||
return v && v !== r.bin;
|
||||
}).concat([r.bin]).filter(Boolean).join(' / ');
|
||||
html += `<tr>
|
||||
<td class="ps-4">${type_badge}</td>
|
||||
<td>${escape_html(r.product_name || r.product_sku)}<br><small class="text-muted">${escape_html(r.product_sku)}</small></td>
|
||||
|
||||
@@ -121,17 +121,17 @@ class BarcodeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm a rack row exists for the given warehouse + zone + aisle + rack.
|
||||
* Confirm a bin row exists for the given warehouse + zone + aisle + bin.
|
||||
* Throws if not found.
|
||||
*/
|
||||
private function validateLocation(int $warehouse_id, string $zone, string $aisle, string $rack): void {
|
||||
private function validateLocation(int $warehouse_id, string $zone, string $aisle, string $bin): void {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT 1 FROM md_rack
|
||||
"SELECT 1 FROM md_bin
|
||||
WHERE company_id = :company_id
|
||||
AND warehouse = :warehouse
|
||||
AND COALESCE(zone, '') = :zone
|
||||
AND COALESCE(aisle,'') = :aisle
|
||||
AND rack = :rack
|
||||
AND bin = :bin
|
||||
LIMIT 1"
|
||||
);
|
||||
$sth->execute([
|
||||
@@ -139,7 +139,7 @@ class BarcodeManager {
|
||||
':warehouse' => $warehouse_id,
|
||||
':zone' => $zone,
|
||||
':aisle' => $aisle,
|
||||
':rack' => $rack,
|
||||
':bin' => $bin,
|
||||
]);
|
||||
if (!$sth->fetchColumn()) throw new Exception('Location not found.');
|
||||
}
|
||||
@@ -343,7 +343,7 @@ class BarcodeManager {
|
||||
*/
|
||||
public function listLocationLabels(): array {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT barcode, warehouse_id, zone, aisle, rack, status, print_count, last_printed_dt
|
||||
"SELECT barcode, warehouse_id, zone, aisle, bin, status, print_count, last_printed_dt
|
||||
FROM md_barcode
|
||||
WHERE company_id = :company_id
|
||||
AND barcode_type = 'loc'"
|
||||
@@ -355,30 +355,30 @@ class BarcodeManager {
|
||||
/**
|
||||
* Create a new location barcode label (or re-activate a previously disabled one).
|
||||
*
|
||||
* Validates that the rack exists in md_rack before inserting.
|
||||
* Validates that the bin exists in md_bin 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.
|
||||
* @param string $bin Bin code.
|
||||
* @return array Keys: barcode, warehouse_id, zone, aisle, bin.
|
||||
* @throws Exception If the location is not found in md_bin.
|
||||
*/
|
||||
public function createLocationLabel(string $barcode, int $warehouse_id, string $zone, string $aisle, string $rack): array {
|
||||
$this->validateLocation($warehouse_id, $zone, $aisle, $rack);
|
||||
public function createLocationLabel(string $barcode, int $warehouse_id, string $zone, string $aisle, string $bin): array {
|
||||
$this->validateLocation($warehouse_id, $zone, $aisle, $bin);
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO md_barcode
|
||||
(company_id, barcode_type, barcode, warehouse_id, zone, aisle, rack)
|
||||
(company_id, barcode_type, barcode, warehouse_id, zone, aisle, bin)
|
||||
VALUES
|
||||
(:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :rack)
|
||||
(:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :bin)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
barcode_type = VALUES(barcode_type),
|
||||
warehouse_id = VALUES(warehouse_id),
|
||||
zone = VALUES(zone),
|
||||
aisle = VALUES(aisle),
|
||||
rack = VALUES(rack),
|
||||
bin = VALUES(bin),
|
||||
status = 1"
|
||||
)->execute([
|
||||
':company_id' => $this->company_id,
|
||||
@@ -386,7 +386,7 @@ class BarcodeManager {
|
||||
':warehouse_id' => $warehouse_id,
|
||||
':zone' => $zone,
|
||||
':aisle' => $aisle,
|
||||
':rack' => $rack,
|
||||
':bin' => $bin,
|
||||
]);
|
||||
|
||||
return [
|
||||
@@ -394,7 +394,7 @@ class BarcodeManager {
|
||||
'warehouse_id' => $warehouse_id,
|
||||
'zone' => $zone,
|
||||
'aisle' => $aisle,
|
||||
'rack' => $rack,
|
||||
'bin' => $bin,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ class BarcodeManager {
|
||||
* 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.
|
||||
* @param bool $advancedLocation True = zone/aisle/bin parts; false = bin only.
|
||||
* @return array
|
||||
* @throws Exception On parse or validation failure.
|
||||
*/
|
||||
@@ -515,13 +515,13 @@ class BarcodeManager {
|
||||
if ($advancedLocation) {
|
||||
$zone = trim($parts[2]);
|
||||
$aisle = trim($parts[3]);
|
||||
$rack = trim($parts[4]);
|
||||
$bin = trim($parts[4]);
|
||||
} else {
|
||||
$rack = trim($parts[2]);
|
||||
$zone = $rack;
|
||||
$aisle = $rack;
|
||||
$bin = trim($parts[2]);
|
||||
$zone = $bin;
|
||||
$aisle = $bin;
|
||||
}
|
||||
if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') {
|
||||
if (!$warehouse_id || $zone === '' || $aisle === '' || $bin === '') {
|
||||
throw new Exception('Location label is incomplete.');
|
||||
}
|
||||
|
||||
@@ -531,19 +531,19 @@ class BarcodeManager {
|
||||
(int)$label['warehouse_id'] !== $warehouse_id ||
|
||||
$label['zone'] !== $zone ||
|
||||
$label['aisle'] !== $aisle ||
|
||||
$label['rack'] !== $rack
|
||||
$label['bin'] !== $bin
|
||||
) {
|
||||
throw new Exception('Location barcode label does not match its registry.');
|
||||
}
|
||||
|
||||
$this->validateLocation($warehouse_id, $zone, $aisle, $rack);
|
||||
$this->validateLocation($warehouse_id, $zone, $aisle, $bin);
|
||||
|
||||
return [
|
||||
'type' => 'loc',
|
||||
'warehouse_id' => $warehouse_id,
|
||||
'zone' => $zone,
|
||||
'aisle' => $aisle,
|
||||
'rack' => $rack,
|
||||
'bin' => $bin,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* default_stock_status int 0 = draft, 1 = auto-approve on save
|
||||
* auto_invoice_and_credit_note int 0 = manual, 1 = auto-generate
|
||||
* auto_complete_on_ship int 0 = manual, 1 = auto-complete on shipped
|
||||
* advanced_location int 0 = simple (single location), 1 = zone/aisle/rack
|
||||
* location_label_rack string label for rack / single location field
|
||||
* advanced_location int 0 = simple (single location), 1 = zone/aisle/bin
|
||||
* location_label_bin string label for bin / single location field
|
||||
* location_label_zone string label for zone (advanced only)
|
||||
* location_label_aisle string label for aisle (advanced only)
|
||||
*
|
||||
@@ -27,7 +27,7 @@ class CompanySettingManager
|
||||
'auto_invoice_and_credit_note' => 0, // manual by default
|
||||
'auto_complete_on_ship' => 1, // auto-complete by default
|
||||
'advanced_location' => 0, // simple mode by default
|
||||
'location_label_rack' => 'Location', // single label default
|
||||
'location_label_bin' => 'Location', // single label default
|
||||
'location_label_zone' => 'Zone', // advanced level 1 default
|
||||
'location_label_aisle' => 'Aisle', // advanced level 2 default
|
||||
'posting_open_from' => '', // YYYY-MM-DD; empty = no lower bound
|
||||
|
||||
@@ -17,19 +17,19 @@ require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
|
||||
* - Order items are stored as a JSON array in td_order.items.
|
||||
* No separate child table exists. SKU-level reporting is served by
|
||||
* td_stock_* rows (source='order') rather than querying items JSON.
|
||||
* - confirmOrder() picks racks via FIFO — oldest approved stock-in row
|
||||
* still rack-occupied, ordered by td_stock_<warehouse_id>.date ASC.
|
||||
* - confirmOrder() picks bins via FIFO — oldest approved stock-in row
|
||||
* still bin-occupied, ordered by td_stock_<warehouse_id>.date ASC.
|
||||
* - confirmOrder() creates stock-out rows with status=0 (draft).
|
||||
* Warehouse staff approve them via the existing approve_stock.php
|
||||
* engine, which handles rack release and balance adjustment.
|
||||
* engine, which handles bin release and balance adjustment.
|
||||
* - cancelOrder() sets td_order.status = -1 and soft-deletes ALL linked
|
||||
* stock-out rows (status → -1) across all td_stock_* tables. If linked
|
||||
* stock-out rows were already approved, their rack and balance effects are
|
||||
* stock-out rows were already approved, their bin and balance effects are
|
||||
* reversed before the rows are cancelled.
|
||||
* Cancellation is blocked if any active invoice (status != 4 / void) or
|
||||
* active return (status != -1 / cancelled) is linked to the order.
|
||||
* - Cancel logic stays in the order domain, but uses WarehouseManager for
|
||||
* the same rack and balance reversal rules as manual stock-out deletion.
|
||||
* the same bin and balance reversal rules as manual stock-out deletion.
|
||||
*
|
||||
* Note: Write methods do NOT manage their own DB transactions.
|
||||
* Callers must wrap multi-step operations inside dbTransaction().
|
||||
@@ -78,15 +78,15 @@ class OrderManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* FIFO rack pick — find the oldest approved stock-in row for a SKU
|
||||
* in a given warehouse that is still rack-occupied.
|
||||
* FIFO bin pick — find the oldest approved stock-in row for a SKU
|
||||
* in a given warehouse that is still bin-occupied.
|
||||
*
|
||||
* "Oldest" = smallest date value among status=1 stock-in rows
|
||||
* that have an md_rack row pointing back to them (td_stock_id IS set).
|
||||
* that have an md_bin row pointing back to them (td_stock_id IS set).
|
||||
*
|
||||
* @param int $warehouse_id
|
||||
* @param string $product_sku
|
||||
* @return array|null Full td_stock row + zone/aisle/rack from md_rack,
|
||||
* @return array|null Full td_stock row + zone/aisle/bin from md_bin,
|
||||
* or null if no available stock in this warehouse.
|
||||
*/
|
||||
private function pickFifoRack(int $warehouse_id, string $product_sku): ?array
|
||||
@@ -94,7 +94,7 @@ class OrderManager {
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT s.*, r.zone, r.aisle, r.rack, l.expiry_date,
|
||||
"SELECT s.*, r.zone, r.aisle, r.bin, l.expiry_date,
|
||||
(s.`in` - COALESCE((
|
||||
SELECT SUM(o.`out`)
|
||||
FROM `{$table}` o
|
||||
@@ -104,7 +104,7 @@ class OrderManager {
|
||||
AND o.status != -1
|
||||
), 0)) AS available_qty
|
||||
FROM `{$table}` s
|
||||
INNER JOIN md_rack r
|
||||
INNER JOIN md_bin r
|
||||
ON r.company_id = s.company_id
|
||||
AND r.warehouse = :warehouse_id
|
||||
AND r.td_stock_id = s.id
|
||||
@@ -358,7 +358,7 @@ class OrderManager {
|
||||
if ($stock_out_warehouse_id > 0 && $stock_out_id > 0) {
|
||||
$table = $this->stockTableNameFromWarehouseId($stock_out_warehouse_id);
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, status, zone, aisle, rack, lot_number, serial_number
|
||||
"SELECT id, status, zone, aisle, bin, lot_number, serial_number
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND id = :id
|
||||
@@ -374,7 +374,7 @@ class OrderManager {
|
||||
if ($stock_out) {
|
||||
$item['zone'] = $stock_out['zone'] ?? ($item['zone'] ?? '');
|
||||
$item['aisle'] = $stock_out['aisle'] ?? ($item['aisle'] ?? '');
|
||||
$item['rack'] = $stock_out['rack'] ?? ($item['rack'] ?? '');
|
||||
$item['bin'] = $stock_out['bin'] ?? ($item['bin'] ?? '');
|
||||
$item['lot_number'] = $stock_out['lot_number'] ?? ($item['lot_number'] ?? '');
|
||||
$item['serial_number'] = $stock_out['serial_number'] ?? ($item['serial_number'] ?? '');
|
||||
$item['stock_out_status'] = (int)$stock_out['status'];
|
||||
@@ -599,10 +599,10 @@ class OrderManager {
|
||||
|
||||
/**
|
||||
* Confirm a draft order — create stock-out rows (status=0) for each item
|
||||
* using FIFO rack selection, then advance the order to status=1.
|
||||
* using FIFO bin selection, then advance the order to status=1.
|
||||
*
|
||||
* Flow per item:
|
||||
* 1. pickFifoRack() — find oldest rack-occupied stock-in row for the SKU.
|
||||
* 1. pickFifoRack() — find oldest bin-occupied stock-in row for the SKU.
|
||||
* 2. INSERT into td_stock_<warehouse_id> with type='out', status=0,
|
||||
* source='order', source_id=order_id.
|
||||
* 3. Write back stock_out_id into the item object in td_order.items.
|
||||
@@ -611,7 +611,7 @@ class OrderManager {
|
||||
* 4. UPDATE td_order.items with stock_out_id values written back.
|
||||
* 5. UPDATE td_order.status = 1 (confirmed).
|
||||
*
|
||||
* Rack release and balance adjustment are intentionally deferred —
|
||||
* Bin release and balance adjustment are intentionally deferred —
|
||||
* they happen when warehouse staff approve the stock-out rows via
|
||||
* the existing approve_stock.php engine (StockManager::approveStock).
|
||||
*
|
||||
@@ -621,7 +621,7 @@ class OrderManager {
|
||||
* @param string $uuid UUID prefix — each stock-out row gets uuid_{$i}.
|
||||
* @param array $logging Audit entry appended to td_order.log.
|
||||
* @throws Exception If order not found, not in draft status, or any item
|
||||
* has no available FIFO rack in its assigned warehouse.
|
||||
* has no available FIFO bin in its assigned warehouse.
|
||||
*/
|
||||
public function confirmOrder(int $order_id, string $uuid, array $logging, bool $auto_approve = false): void
|
||||
{
|
||||
@@ -668,22 +668,22 @@ class OrderManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku);
|
||||
if (!$rack_stock) {
|
||||
$bin_stock = $this->pickFifoRack($warehouse_id, $product_sku);
|
||||
if (!$bin_stock) {
|
||||
$preflight_errors[] = "No available stock for \"{$name}\" in the selected warehouse.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$available_qty = (float)($rack_stock['available_qty'] ?? $rack_stock['in'] ?? 0);
|
||||
$available_qty = (float)($bin_stock['available_qty'] ?? $bin_stock['in'] ?? 0);
|
||||
if ($quantity - $available_qty > 0.000001) {
|
||||
$preflight_errors[] =
|
||||
"Insufficient stock for \"{$name}\": " .
|
||||
"{$available_qty} available, {$quantity} requested.";
|
||||
}
|
||||
|
||||
$expiry = $rack_stock['expiry_date'] ?? null;
|
||||
$expiry = $bin_stock['expiry_date'] ?? null;
|
||||
if ($expiry && strtotime($expiry) < strtotime(date('Y-m-d'))) {
|
||||
$lot = $rack_stock['lot_number'] ?? '';
|
||||
$lot = $bin_stock['lot_number'] ?? '';
|
||||
$preflight_errors[] =
|
||||
"Lot {$lot} for \"{$name}\" expired on {$expiry} and cannot be picked.";
|
||||
}
|
||||
@@ -699,8 +699,8 @@ class OrderManager {
|
||||
$product_sku = $item['product_sku'] ?? '';
|
||||
$quantity = (float)($item['quantity'] ?? 0);
|
||||
|
||||
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku);
|
||||
$available_qty = (float)($rack_stock['available_qty'] ?? $rack_stock['in'] ?? 0);
|
||||
$bin_stock = $this->pickFifoRack($warehouse_id, $product_sku);
|
||||
$available_qty = (float)($bin_stock['available_qty'] ?? $bin_stock['in'] ?? 0);
|
||||
|
||||
$table = $this->stockTableNameFromWarehouseId($warehouse_id);
|
||||
$item_uuid = $uuid . '_' . $i;
|
||||
@@ -709,13 +709,13 @@ class OrderManager {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `{$table}`
|
||||
(uuid, company_id, `date`, product_sku, `out`,
|
||||
zone, aisle, rack,
|
||||
zone, aisle, bin,
|
||||
contact_id, `description`, `log`, `type`,
|
||||
ref_id, lot_number, serial_number,
|
||||
source, source_id, price, status)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:zone, :aisle, :rack,
|
||||
:zone, :aisle, :bin,
|
||||
:contact_id, :description, :log, 'out',
|
||||
:ref_id, :lot_number, :serial_number,
|
||||
'order', :source_id, :price, 0)"
|
||||
@@ -725,28 +725,28 @@ class OrderManager {
|
||||
':date' => date('Y-m-d H:i:s'),
|
||||
':product_sku' => $product_sku,
|
||||
':quantity' => $quantity,
|
||||
':zone' => $rack_stock['zone'],
|
||||
':aisle' => $rack_stock['aisle'],
|
||||
':rack' => $rack_stock['rack'],
|
||||
':zone' => $bin_stock['zone'],
|
||||
':aisle' => $bin_stock['aisle'],
|
||||
':bin' => $bin_stock['bin'],
|
||||
':contact_id' => (int)$order['contact_id'],
|
||||
':description' => $order['order_number'],
|
||||
':log' => json_encode($item_log),
|
||||
':ref_id' => (int)$rack_stock['id'],
|
||||
':lot_number' => $rack_stock['lot_number'] ?? '',
|
||||
':serial_number' => $rack_stock['serial_number'] ?? '',
|
||||
':ref_id' => (int)$bin_stock['id'],
|
||||
':lot_number' => $bin_stock['lot_number'] ?? '',
|
||||
':serial_number' => $bin_stock['serial_number'] ?? '',
|
||||
':source_id' => $order_id,
|
||||
':price' => (float)($item['unit_price'] ?? 0),
|
||||
]);
|
||||
|
||||
// Write rack context + stock_out_id back into the item object
|
||||
$item['zone'] = $rack_stock['zone'];
|
||||
$item['aisle'] = $rack_stock['aisle'];
|
||||
$item['rack'] = $rack_stock['rack'];
|
||||
$item['lot_number'] = $rack_stock['lot_number'] ?? '';
|
||||
$item['serial_number'] = $rack_stock['serial_number'] ?? '';
|
||||
// Write bin context + stock_out_id back into the item object
|
||||
$item['zone'] = $bin_stock['zone'];
|
||||
$item['aisle'] = $bin_stock['aisle'];
|
||||
$item['bin'] = $bin_stock['bin'];
|
||||
$item['lot_number'] = $bin_stock['lot_number'] ?? '';
|
||||
$item['serial_number'] = $bin_stock['serial_number'] ?? '';
|
||||
$item['stock_out_id'] = (int)$this->pdo->lastInsertId();
|
||||
|
||||
// Auto-approve: immediately release rack + adjust balance
|
||||
// Auto-approve: immediately release bin + adjust balance
|
||||
if ($auto_approve) {
|
||||
$stock = new StockManager($this->pdo, $this->company_id);
|
||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||
@@ -802,7 +802,7 @@ class OrderManager {
|
||||
* - td_return where order_id = $order_id AND status != -1
|
||||
*
|
||||
* Stock-out rows are children of the order — they follow the parent.
|
||||
* On cancel, approved rows first re-occupy the original source rack and
|
||||
* On cancel, approved rows first re-occupy the original source bin and
|
||||
* reverse the stock-out balance. Then ALL stock-out rows linked to this
|
||||
* order (any status except already -1) are soft-deleted (status → -1)
|
||||
* across all td_stock_* tables.
|
||||
@@ -882,7 +882,7 @@ class OrderManager {
|
||||
$warehouse_id = (int)$matches[1];
|
||||
|
||||
$row_sth = $this->pdo->prepare(
|
||||
"SELECT id, product_sku, `out`, zone, aisle, rack, ref_id, status, `date`
|
||||
"SELECT id, product_sku, `out`, zone, aisle, bin, ref_id, status, `date`
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'order'
|
||||
@@ -903,11 +903,11 @@ class OrderManager {
|
||||
|
||||
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Order stock cancellation');
|
||||
|
||||
$whMgmt->occupyRack(
|
||||
$whMgmt->occupyBin(
|
||||
$warehouse_id,
|
||||
(string)$row['zone'],
|
||||
(string)$row['aisle'],
|
||||
(string)$row['rack'],
|
||||
(string)$row['bin'],
|
||||
(string)$row['product_sku'],
|
||||
(int)$row['ref_id']
|
||||
);
|
||||
|
||||
@@ -534,18 +534,18 @@ class ProductManager {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all rack slots across all warehouses with occupancy status,
|
||||
* Return all bin slots across all warehouses with occupancy status,
|
||||
* warehouse name, and product name.
|
||||
*
|
||||
* Used by the rack occupancy dashboard to visualise which racks are
|
||||
* Used by the bin occupancy dashboard to visualise which bins are
|
||||
* empty vs. occupied, and which product is in each slot.
|
||||
* Results are ordered by warehouse → zone → aisle → rack, with
|
||||
* Results are ordered by warehouse → zone → aisle → bin, with
|
||||
* numeric-first sorting via CAST so e.g. "2" sorts before "10".
|
||||
*
|
||||
* Security fix: was previously using $this->companyId (undefined property),
|
||||
* corrected to $this->company_id.
|
||||
*
|
||||
* @return array All md_rack rows joined to warehouse and product, with 'status' field.
|
||||
* @return array All md_bin rows joined to warehouse and product, with 'status' field.
|
||||
*/
|
||||
public function getRackOccupancy(): array
|
||||
{
|
||||
@@ -554,14 +554,14 @@ class ProductManager {
|
||||
r.id,
|
||||
r.zone,
|
||||
r.aisle,
|
||||
r.rack,
|
||||
r.bin,
|
||||
r.product_sku,
|
||||
r.td_stock_id,
|
||||
mw.warehouse_name,
|
||||
mw.id AS warehouse_id,
|
||||
p.product_name,
|
||||
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
|
||||
FROM md_rack r
|
||||
FROM md_bin r
|
||||
INNER JOIN md_warehouse mw
|
||||
ON mw.company_id = r.company_id
|
||||
AND mw.id = r.warehouse
|
||||
@@ -571,7 +571,7 @@ class ProductManager {
|
||||
WHERE r.company_id = :company_id
|
||||
ORDER BY mw.warehouse_name, r.zone,
|
||||
CAST(r.aisle AS UNSIGNED), r.aisle,
|
||||
CAST(r.rack AS UNSIGNED), r.rack"
|
||||
CAST(r.bin AS UNSIGNED), r.bin"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -25,7 +25,7 @@ require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
|
||||
* Receipt progress (pending/partial/received) is derived via deriveReceiptStatus(),
|
||||
* same pattern as fulfillment_status on SO. Never stored in td_purchase_order.
|
||||
* - cancelPo() is blocked if any linked stock-in rows have been approved (status=1),
|
||||
* because those have already modified rack and balance.
|
||||
* because those have already modified bin and balance.
|
||||
* - Write methods do NOT manage their own DB transactions.
|
||||
* Callers must wrap multi-step operations inside dbTransaction().
|
||||
*
|
||||
@@ -274,7 +274,7 @@ class PurchaseOrderManager {
|
||||
'warehouse_id' => (int)($item['receive_wh'] ?? 0),
|
||||
'zone' => $item['receive_zone'] ?? '',
|
||||
'aisle' => $item['receive_aisle'] ?? '',
|
||||
'rack' => $item['receive_rack'] ?? '',
|
||||
'bin' => $item['receive_bin'] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -492,7 +492,7 @@ class PurchaseOrderManager {
|
||||
*
|
||||
* @param int $po_id td_purchase_order.id
|
||||
* @param array $receive_items Each item: { item_id, product_sku, warehouse_id,
|
||||
* quantity, zone, aisle, rack, lot_number,
|
||||
* quantity, zone, aisle, bin, lot_number,
|
||||
* expiry_date, serial_number, contact_id }
|
||||
* @param string $uuid UUID prefix for stock-in rows.
|
||||
* @param array $logging Audit entry.
|
||||
@@ -573,16 +573,16 @@ class PurchaseOrderManager {
|
||||
$item_uuid = $uuid . '_' . $j;
|
||||
$item_log = array_merge($logging, ['action' => 'receive_item']);
|
||||
|
||||
$rack = $recv['rack'] ?? '';
|
||||
$bin = $recv['bin'] ?? '';
|
||||
$zone = $recv['zone'] ?? '';
|
||||
$aisle = $recv['aisle'] ?? '';
|
||||
|
||||
// Simple location mode: zone and aisle must mirror the rack value
|
||||
// Simple location mode: zone and aisle must mirror the bin value
|
||||
// (same convention as manage_stock_in.php).
|
||||
// occupyRack() looks up md_rack WHERE zone=:zone AND aisle=:aisle AND rack=:rack,
|
||||
// so all three must match — a blank zone/aisle produces "Rack --b does not exist".
|
||||
if ($zone === '' && $rack !== '') $zone = $rack;
|
||||
if ($aisle === '' && $rack !== '') $aisle = $rack;
|
||||
// occupyBin() looks up md_bin WHERE zone=:zone AND aisle=:aisle AND bin=:bin,
|
||||
// so all three must match — a blank zone/aisle produces "Bin --b does not exist".
|
||||
if ($zone === '' && $bin !== '') $zone = $bin;
|
||||
if ($aisle === '' && $bin !== '') $aisle = $bin;
|
||||
|
||||
// Resolve unit_price: prefer the receive-time override, fall back to PO item price
|
||||
$po_item_price = 0;
|
||||
@@ -599,7 +599,7 @@ class PurchaseOrderManager {
|
||||
'price' => $unit_price,
|
||||
'zone' => $zone,
|
||||
'aisle' => $aisle,
|
||||
'rack' => $rack,
|
||||
'bin' => $bin,
|
||||
'lot_number' => $recv['lot_number'] ?? '',
|
||||
'expiry_date' => $recv['expiry_date'] ?? '',
|
||||
'serial_number' => $recv['serial_number'] ?? '',
|
||||
@@ -626,7 +626,7 @@ class PurchaseOrderManager {
|
||||
]);
|
||||
|
||||
// approveStock() handles balance updates. saveStockIn() has
|
||||
// already reserved the rack so draft receipts cannot be reused.
|
||||
// already reserved the bin so draft receipts cannot be reused.
|
||||
if ($auto_approve) {
|
||||
$stock->approveStock($new_stock_in_id, $warehouse_id, 'in', $whMgmt);
|
||||
}
|
||||
@@ -708,7 +708,7 @@ class PurchaseOrderManager {
|
||||
* Cancel a PO.
|
||||
*
|
||||
* Blocked if any linked stock-in rows have already been approved (status=1)
|
||||
* because those have modified rack occupancy and balance.
|
||||
* because those have modified bin occupancy and balance.
|
||||
*
|
||||
* For draft stock-in rows (status=0), they are soft-deleted (status=-1).
|
||||
*
|
||||
@@ -756,7 +756,7 @@ class PurchaseOrderManager {
|
||||
|
||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||
|
||||
// Soft-delete draft stock-in rows and release their rack reservations.
|
||||
// Soft-delete draft stock-in rows and release their bin reservations.
|
||||
foreach ($tables as $table) {
|
||||
if (!preg_match('/^td_stock_(\d+)$/', $table, $m)) {
|
||||
continue;
|
||||
@@ -764,7 +764,7 @@ class PurchaseOrderManager {
|
||||
$warehouse_id = (int)$m[1];
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, zone, aisle, rack
|
||||
"SELECT id, zone, aisle, bin
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'po'
|
||||
@@ -783,11 +783,11 @@ class PurchaseOrderManager {
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
|
||||
$whMgmt->releaseRack(
|
||||
$whMgmt->releaseBin(
|
||||
$warehouse_id,
|
||||
$row['zone'],
|
||||
$row['aisle'],
|
||||
$row['rack']
|
||||
$row['bin']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -894,21 +894,21 @@ class PurchaseOrderManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Release racks from draft stock-in rows and negate them
|
||||
// Release bins from draft stock-in rows and negate them
|
||||
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
||||
foreach ($tables as $table) {
|
||||
if (!preg_match('/^td_stock_(\d+)$/', (string)$table, $matches)) continue;
|
||||
$warehouse_id = (int)$matches[1];
|
||||
|
||||
$row_sth = $this->pdo->prepare(
|
||||
"SELECT id, zone, aisle, rack FROM `{$table}`
|
||||
"SELECT id, zone, aisle, bin FROM `{$table}`
|
||||
WHERE company_id = :cid AND source = 'po' AND source_id = :id AND status = 0"
|
||||
);
|
||||
$row_sth->execute([':cid' => $this->company_id, ':id' => $po_id]);
|
||||
$draft_rows = $row_sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($draft_rows as $row) {
|
||||
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
|
||||
$whMgmt->releaseBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin']);
|
||||
}
|
||||
|
||||
$this->pdo->prepare(
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* Dashboard reports → stats, movement charts, most moved, recent activity
|
||||
* Warehouse detail → capacity, space used, balance, movement, trend, activity
|
||||
* Expiry reports → expired / near-expiry stock
|
||||
* Lot / Rack reports → lot stock log, product lots, rack log, rack occupancy
|
||||
* Lot / Bin reports → lot stock log, product lots, bin log, bin occupancy
|
||||
* Balance summary → getWarehouseBalanceSummary
|
||||
*
|
||||
* Security: All SQL uses PDO prepared statements with bound parameters.
|
||||
@@ -159,25 +159,25 @@ class ReportManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Total rack capacity (all racks across all warehouses) for this company.
|
||||
* Total bin capacity (all bins across all warehouses) for this company.
|
||||
* Used by the reports_stats dashboard tile.
|
||||
*/
|
||||
public function getTotalCapacity(): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*)
|
||||
FROM md_rack
|
||||
FROM md_bin
|
||||
WHERE company_id = :company_id";
|
||||
return (int) $this->fetchScalar($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of occupied racks (product_sku IS NOT NULL) for this company.
|
||||
* Number of occupied bins (product_sku IS NOT NULL) for this company.
|
||||
* Used by the reports_stats dashboard tile.
|
||||
*/
|
||||
public function getSpaceUsed(): int
|
||||
{
|
||||
$sql = "SELECT COUNT(*)
|
||||
FROM md_rack
|
||||
FROM md_bin
|
||||
WHERE company_id = :company_id
|
||||
AND product_sku IS NOT NULL";
|
||||
return (int) $this->fetchScalar($sql);
|
||||
@@ -710,15 +710,15 @@ class ReportManager
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Total rack capacity for a specific warehouse (all racks regardless of occupancy).
|
||||
* Total bin capacity for a specific warehouse (all bins regardless of occupancy).
|
||||
*
|
||||
* @param int $warehouse_id The warehouse to query.
|
||||
* @return int Total rack count.
|
||||
* @return int Total bin count.
|
||||
*/
|
||||
public function getWarehouseCapacity(int $warehouse_id): int
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_rack
|
||||
"SELECT COUNT(*) FROM md_bin
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]);
|
||||
@@ -726,15 +726,15 @@ class ReportManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of occupied racks (product_sku IS NOT NULL) for a specific warehouse.
|
||||
* Number of occupied bins (product_sku IS NOT NULL) for a specific warehouse.
|
||||
*
|
||||
* @param int $warehouse_id The warehouse to query.
|
||||
* @return int Occupied rack count.
|
||||
* @return int Occupied bin count.
|
||||
*/
|
||||
public function getWarehouseSpaceUsed(int $warehouse_id): int
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM md_rack
|
||||
"SELECT COUNT(*) FROM md_bin
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse_id
|
||||
AND product_sku IS NOT NULL"
|
||||
);
|
||||
@@ -1008,14 +1008,14 @@ class ReportManager
|
||||
s.lot_number,
|
||||
s.zone,
|
||||
s.aisle,
|
||||
s.rack,
|
||||
s.bin,
|
||||
ROUND(SUM(s.`in`) - SUM(s.`out`), 2) AS quantity,
|
||||
{$wh['id']} AS warehouse_id
|
||||
FROM `{$table}` s
|
||||
WHERE s.company_id = {$cid}
|
||||
AND s.lot_number IS NOT NULL
|
||||
AND s.status = 1
|
||||
GROUP BY s.product_sku, s.lot_number, s.zone, s.aisle, s.rack
|
||||
GROUP BY s.product_sku, s.lot_number, s.zone, s.aisle, s.bin
|
||||
HAVING quantity > 0";
|
||||
},
|
||||
$warehouses
|
||||
@@ -1026,7 +1026,7 @@ class ReportManager
|
||||
stock.lot_number,
|
||||
stock.zone,
|
||||
stock.aisle,
|
||||
stock.rack,
|
||||
stock.bin,
|
||||
stock.quantity,
|
||||
l.expiry_date,
|
||||
DATEDIFF(l.expiry_date, CURDATE()) AS days_remaining,
|
||||
@@ -1073,7 +1073,7 @@ class ReportManager
|
||||
'lot_number' => $row['lot_number'],
|
||||
'zone' => $row['zone'],
|
||||
'aisle' => $row['aisle'],
|
||||
'rack' => $row['rack'],
|
||||
'bin' => $row['bin'],
|
||||
'quantity' => (float) $row['quantity'],
|
||||
'expiry_date' => $row['expiry_date'],
|
||||
'days_remaining' => $days,
|
||||
@@ -1085,7 +1085,7 @@ class ReportManager
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// REPORT BASIS — Lot / Rack reports
|
||||
// REPORT BASIS — Lot / Bin reports
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1097,7 +1097,7 @@ class ReportManager
|
||||
*
|
||||
* @param string $product_sku SKU to filter by.
|
||||
* @param string $lot_number Lot number to filter by.
|
||||
* @return array Stock rows with date, type, zone/aisle/rack, in/out amounts, warehouse_name.
|
||||
* @return array Stock rows with date, type, zone/aisle/bin, in/out amounts, warehouse_name.
|
||||
*/
|
||||
public function getLotStockLog(string $product_sku, string $lot_number): array
|
||||
{
|
||||
@@ -1123,7 +1123,7 @@ class ReportManager
|
||||
s.type,
|
||||
s.zone,
|
||||
s.aisle,
|
||||
s.rack,
|
||||
s.bin,
|
||||
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
|
||||
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
|
||||
s.serial_number,
|
||||
@@ -1247,21 +1247,21 @@ class ReportManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the action log for a specific rack, with user name.
|
||||
* Return the action log for a specific bin, with user name.
|
||||
*
|
||||
* Used by the rack log dashboard report page.
|
||||
* Used by the bin log dashboard report page.
|
||||
* Joins the wms.user table for the acting user's name.
|
||||
*
|
||||
* Security fix: the database name is now fetched once via a bound query
|
||||
* and stored as $db_name (string), then used as a backtick-quoted identifier
|
||||
* rather than being embedded in the JOIN without escaping.
|
||||
*
|
||||
* @param int $rack_id The md_rack.id to fetch logs for (0 returns empty array).
|
||||
* @param int $bin_id The md_bin.id to fetch logs for (0 returns empty array).
|
||||
* @return array Log rows with dt, action, product_sku, td_stock_id, login, user_name.
|
||||
*/
|
||||
public function getRackLog(int $rack_id): array
|
||||
public function getBinLog(int $bin_id): array
|
||||
{
|
||||
if (!$rack_id) return [];
|
||||
if (!$bin_id) return [];
|
||||
|
||||
$main_db = $this->mainDb;
|
||||
if ($main_db === '' || !ctype_alnum(str_replace('_', '', $main_db))) {
|
||||
@@ -1277,30 +1277,30 @@ class ReportManager
|
||||
rl.td_stock_id,
|
||||
rl.login,
|
||||
u.name AS user_name
|
||||
FROM td_rack_log rl
|
||||
FROM td_bin_log rl
|
||||
LEFT JOIN `{$main_db}`.user u
|
||||
ON u.user_id = rl.user_id
|
||||
WHERE rl.company_id = :company_id
|
||||
AND rl.md_rack_id = :rack_id
|
||||
AND rl.md_bin_id = :bin_id
|
||||
ORDER BY rl.dt DESC"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->companyId,
|
||||
':rack_id' => $rack_id,
|
||||
':bin_id' => $bin_id,
|
||||
]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all rack slots across all warehouses with occupancy status,
|
||||
* Return all bin slots across all warehouses with occupancy status,
|
||||
* warehouse name, and product name.
|
||||
*
|
||||
* Used by the rack occupancy dashboard report page to visualise which
|
||||
* racks are empty vs occupied and which product is in each slot.
|
||||
* Results are ordered by warehouse → zone → aisle → rack with
|
||||
* Used by the bin occupancy dashboard report page to visualise which
|
||||
* bins are empty vs occupied and which product is in each slot.
|
||||
* Results are ordered by warehouse → zone → aisle → bin with
|
||||
* numeric-first sorting via CAST.
|
||||
*
|
||||
* @return array All md_rack rows joined to warehouse and product with 'status' field.
|
||||
* @return array All md_bin rows joined to warehouse and product with 'status' field.
|
||||
*/
|
||||
public function getRackOccupancy(): array
|
||||
{
|
||||
@@ -1309,14 +1309,14 @@ class ReportManager
|
||||
r.id,
|
||||
r.zone,
|
||||
r.aisle,
|
||||
r.rack,
|
||||
r.bin,
|
||||
r.product_sku,
|
||||
r.td_stock_id,
|
||||
mw.warehouse_name,
|
||||
mw.id AS warehouse_id,
|
||||
p.product_name,
|
||||
CASE WHEN r.product_sku IS NOT NULL THEN 'occupied' ELSE 'empty' END AS status
|
||||
FROM md_rack r
|
||||
FROM md_bin r
|
||||
INNER JOIN md_warehouse mw
|
||||
ON mw.company_id = r.company_id
|
||||
AND mw.id = r.warehouse
|
||||
@@ -1326,7 +1326,7 @@ class ReportManager
|
||||
WHERE r.company_id = :company_id
|
||||
ORDER BY mw.warehouse_name, r.zone,
|
||||
CAST(r.aisle AS UNSIGNED), r.aisle,
|
||||
CAST(r.rack AS UNSIGNED), r.rack"
|
||||
CAST(r.bin AS UNSIGNED), r.bin"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -1510,7 +1510,7 @@ class ReportManager
|
||||
s.serial_number,
|
||||
s.zone,
|
||||
s.aisle,
|
||||
s.rack,
|
||||
s.bin,
|
||||
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
|
||||
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
|
||||
CASE
|
||||
|
||||
@@ -17,13 +17,13 @@ require_once __DIR__ . '/../classes_ac/PostingWindowGuard.php';
|
||||
* 1. INSERT td_stock_<warehouse_id> in rows (source='return', source_id=return_id,
|
||||
* status=1 ALWAYS — confirming a return is a final business decision,
|
||||
* no separate warehouse approval step needed).
|
||||
* 2. occupyRack() + adjustBalance() via WarehouseManager.
|
||||
* 2. occupyBin() + adjustBalance() via WarehouseManager.
|
||||
* 3. If auto_invoice_and_credit_note=1 → InvoiceManager::createCreditNote()
|
||||
* auto-generates a CN linked to the original invoice (status=1 issued).
|
||||
* If =0 → user creates CN manually from the return detail page.
|
||||
* - Return items stored as JSON in td_return.items (same pattern as td_order).
|
||||
* - Each item references stock_out_id from the original order so the correct
|
||||
* warehouse/rack/lot context is known for restock.
|
||||
* warehouse/bin/lot context is known for restock.
|
||||
*
|
||||
* Note: Write methods do NOT manage their own DB transactions.
|
||||
* Callers must wrap multi-step operations inside dbTransaction().
|
||||
@@ -242,7 +242,7 @@ class ReturnManager {
|
||||
*
|
||||
* items JSON array — each element mirrors td_order.items with stock_out_id
|
||||
* referencing the original stock-out row so confirmReturn() knows which
|
||||
* rack/warehouse/lot to restock.
|
||||
* bin/warehouse/lot to restock.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
@@ -389,7 +389,7 @@ class ReturnManager {
|
||||
* Flow per item:
|
||||
* 1. INSERT td_stock_<warehouse_id> in row with type='in', status=1 (force confirmed),
|
||||
* source='return', source_id=return_id.
|
||||
* 2. occupyRack() — place returned stock into the user-selected empty rack.
|
||||
* 2. occupyBin() — place returned stock into the user-selected empty bin.
|
||||
* 3. adjustBalance() — update etl_stock_summary.
|
||||
*
|
||||
* After all items:
|
||||
@@ -400,7 +400,7 @@ class ReturnManager {
|
||||
*
|
||||
* Stock-in rows respect $auto_approve (mirrors default_stock_status config).
|
||||
* When false, rows are created as draft (status=0) and warehouse staff approve
|
||||
* them via approve_stock.php; rack occupation and balance are deferred.
|
||||
* them via approve_stock.php; bin occupation and balance are deferred.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
@@ -495,13 +495,13 @@ class ReturnManager {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `{$table}`
|
||||
(uuid, company_id, `date`, product_sku, `in`,
|
||||
zone, aisle, rack,
|
||||
zone, aisle, bin,
|
||||
contact_id, `description`, `log`, `type`,
|
||||
lot_number, serial_number,
|
||||
source, source_id, ref_id, price, status)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:zone, :aisle, :rack,
|
||||
:zone, :aisle, :bin,
|
||||
:contact_id, :description, :log, 'in',
|
||||
:lot_number, :serial_number,
|
||||
'return', :source_id, :ref_id, :price, :status)"
|
||||
@@ -513,7 +513,7 @@ class ReturnManager {
|
||||
':quantity' => $quantity,
|
||||
':zone' => $item['zone'] ?? '',
|
||||
':aisle' => $item['aisle'] ?? '',
|
||||
':rack' => $item['rack'] ?? '',
|
||||
':bin' => $item['bin'] ?? '',
|
||||
':contact_id' => (int)$return['contact_id'],
|
||||
':description' => $return['return_number'],
|
||||
':log' => json_encode($item_log),
|
||||
@@ -528,11 +528,11 @@ class ReturnManager {
|
||||
$stock_in_id = (int)$this->pdo->lastInsertId();
|
||||
|
||||
if ($auto_approve) {
|
||||
$whMgmt->occupyRack(
|
||||
$whMgmt->occupyBin(
|
||||
$warehouse_id,
|
||||
$item['zone'] ?? '',
|
||||
$item['aisle'] ?? '',
|
||||
$item['rack'] ?? '',
|
||||
$item['bin'] ?? '',
|
||||
$product_sku,
|
||||
$stock_in_id,
|
||||
true
|
||||
@@ -583,9 +583,9 @@ class ReturnManager {
|
||||
* and doc_type.
|
||||
*
|
||||
* If the return was confirmed, stock-in rows (source='return', source_id=$return_id)
|
||||
* were created with status=1 and rack/balance were already adjusted.
|
||||
* were created with status=1 and bin/balance were already adjusted.
|
||||
* On cancel we soft-delete those rows (status → -1) and reverse the
|
||||
* rack occupation and balance via WarehouseManager.
|
||||
* bin occupation and balance via WarehouseManager.
|
||||
*
|
||||
* If the return was still draft, no stock rows exist — simple status flip.
|
||||
*
|
||||
@@ -636,7 +636,7 @@ class ReturnManager {
|
||||
}
|
||||
}
|
||||
|
||||
// ── If confirmed: reverse stock-in rows + rack/balance ────────────
|
||||
// ── If confirmed: reverse stock-in rows + bin/balance ────────────
|
||||
if ($status === 1) {
|
||||
$sth2 = $this->pdo->prepare(
|
||||
"SELECT * FROM td_return_item
|
||||
@@ -657,7 +657,7 @@ class ReturnManager {
|
||||
|
||||
// Fetch ALL non-cancelled stock-in rows (status=1 approved or status=0 draft)
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, status, zone, aisle, rack, `date` FROM `{$table}`
|
||||
"SELECT id, status, zone, aisle, bin, `date` FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'return'
|
||||
AND source_id = :return_id
|
||||
@@ -679,11 +679,11 @@ class ReturnManager {
|
||||
|
||||
if ((int)$row['status'] === 1) {
|
||||
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Return stock cancellation');
|
||||
// Approved rows: reverse rack occupation + balance
|
||||
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
|
||||
// Approved rows: reverse bin occupation + balance
|
||||
$whMgmt->releaseBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin']);
|
||||
$whMgmt->adjustBalance('out', $warehouse_id, $product_sku, 0, $quantity);
|
||||
}
|
||||
// Draft (status=0) rows: just soft-delete — rack/balance were never applied
|
||||
// Draft (status=0) rows: just soft-delete — bin/balance were never applied
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ require_once __DIR__ . '/../notify_node.php';
|
||||
* getTransferById, saveStockIn, saveStockOut, saveStockTransfer
|
||||
* Report basis → (none — reporting is handled by ReportManager)
|
||||
*
|
||||
* Write operations delegate rack and balance side-effects to WarehouseManager.
|
||||
* Write operations delegate bin and balance side-effects to WarehouseManager.
|
||||
* Delete operations are handled directly in WarehouseManager (deleteStockIn, etc.).
|
||||
*
|
||||
* Note: Write methods do NOT manage their own DB transactions.
|
||||
@@ -228,16 +228,16 @@ class StockManager {
|
||||
* Insert flow (id = 0):
|
||||
* 1. Upserts md_lot if lot_number + expiry_date are provided.
|
||||
* 2. Inserts the td_stock_<warehouse_id> row.
|
||||
* 3. Calls WarehouseManager::occupyRack() to reserve the rack.
|
||||
* 3. Calls WarehouseManager::occupyBin() to reserve the bin.
|
||||
* etl_stock_summary is updated later by approveStock().
|
||||
*
|
||||
* Update flow (id > 0):
|
||||
* - Updates contact_id, description, and log only.
|
||||
* - Quantity, rack, lot, and serial are immutable after creation.
|
||||
* - Quantity, bin, lot, and serial are immutable after creation.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param array $data Keys: id, warehouse, product_sku, quantity, zone, aisle, rack,
|
||||
* @param array $data Keys: id, warehouse, product_sku, quantity, zone, aisle, bin,
|
||||
* contact_id, description, lot_number, expiry_date, serial_number.
|
||||
* @param array $logging Audit entry to append to the log column.
|
||||
* @param string $uuid UUID for this transaction (shared across transfer pairs).
|
||||
@@ -303,10 +303,10 @@ class StockManager {
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `in`, price, zone, aisle, rack,
|
||||
(uuid, company_id, `date`, product_sku, `in`, price, zone, aisle, bin,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :price, :zone, :aisle, :rack,
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :price, :zone, :aisle, :bin,
|
||||
:contact_id, :description, :log, 'in', :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
@@ -317,7 +317,7 @@ class StockManager {
|
||||
':price' => (float)($data['price'] ?? 0),
|
||||
':zone' => $data['zone'],
|
||||
':aisle' => $data['aisle'],
|
||||
':rack' => $data['rack'],
|
||||
':bin' => $data['bin'],
|
||||
':contact_id' => (int)($data['contact_id'] ?? 0),
|
||||
':description' => $data['description'] ?? '',
|
||||
':log' => json_encode($table_log),
|
||||
@@ -328,13 +328,13 @@ class StockManager {
|
||||
$td_stock_id = (int)$this->pdo->lastInsertId();
|
||||
|
||||
// Reserve the location immediately so draft stock-in rows cannot
|
||||
// leave the same rack available for another receipt. Balance still
|
||||
// leave the same bin available for another receipt. Balance still
|
||||
// changes only when approveStock() runs.
|
||||
$whMgmt->occupyRack(
|
||||
$whMgmt->occupyBin(
|
||||
$warehouse_id,
|
||||
$data['zone'],
|
||||
$data['aisle'],
|
||||
$data['rack'],
|
||||
$data['bin'],
|
||||
$data['product_sku'],
|
||||
$td_stock_id
|
||||
);
|
||||
@@ -347,9 +347,9 @@ class StockManager {
|
||||
* Insert a new stock_out record or update metadata on an existing one.
|
||||
*
|
||||
* Insert flow (id = 0):
|
||||
* 1. Validates the rack is occupied with the correct SKU / lot / serial.
|
||||
* 2. Inserts the td_stock_<warehouse_id> row, copying quantity and lot info from the rack.
|
||||
* 3. Calls WarehouseManager::releaseRack() to free the rack slot.
|
||||
* 1. Validates the bin is occupied with the correct SKU / lot / serial.
|
||||
* 2. Inserts the td_stock_<warehouse_id> row, copying quantity and lot info from the bin.
|
||||
* 3. Calls WarehouseManager::releaseBin() to free the bin slot.
|
||||
* 4. Calls WarehouseManager::adjustBalance() to update etl_stock_summary.
|
||||
*
|
||||
* Update flow (id > 0):
|
||||
@@ -357,11 +357,11 @@ class StockManager {
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param array $data Keys: id, warehouse, product_sku, zone, aisle, rack,
|
||||
* @param array $data Keys: id, warehouse, product_sku, zone, aisle, bin,
|
||||
* contact_id, description, lot_number, serial_number.
|
||||
* @param array $logging Audit entry to append to the log column.
|
||||
* @param string $uuid UUID for this transaction.
|
||||
* @throws Exception If the rack is empty, holds a different SKU/lot/serial.
|
||||
* @throws Exception If the bin is empty, holds a different SKU/lot/serial.
|
||||
*/
|
||||
public function saveStockOut(array $data, array $logging, string $uuid): int
|
||||
{
|
||||
@@ -400,33 +400,33 @@ class StockManager {
|
||||
|
||||
} else {
|
||||
|
||||
// Validate rack holds the expected product / lot / serial
|
||||
$source_stock = $whMgmt->getRackStock(
|
||||
// Validate bin holds the expected product / lot / serial
|
||||
$source_stock = $whMgmt->getBinStock(
|
||||
$warehouse_id,
|
||||
$data['zone'], $data['aisle'], $data['rack']
|
||||
$data['zone'], $data['aisle'], $data['bin']
|
||||
);
|
||||
|
||||
if (!$source_stock) {
|
||||
throw new Exception(
|
||||
"Rack {$data['zone']}-{$data['aisle']}-{$data['rack']} is empty — nothing to take out."
|
||||
"Bin {$data['zone']}-{$data['aisle']}-{$data['bin']} is empty — nothing to take out."
|
||||
);
|
||||
}
|
||||
|
||||
if ($source_stock['product_sku'] !== $data['product_sku']) {
|
||||
throw new Exception(
|
||||
"Rack holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
"Bin holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) {
|
||||
throw new Exception(
|
||||
"Rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
"Bin holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) {
|
||||
throw new Exception(
|
||||
"Rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
"Bin holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,10 +438,10 @@ class StockManager {
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$table`
|
||||
(uuid, company_id, `date`, product_sku, `out`, zone, aisle, rack,
|
||||
(uuid, company_id, `date`, product_sku, `out`, zone, aisle, bin,
|
||||
contact_id, `description`, `log`, `type`, ref_id, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack,
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :bin,
|
||||
:contact_id, :description, :log, 'out', :ref_id, :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
@@ -451,7 +451,7 @@ class StockManager {
|
||||
':quantity' => $quantity,
|
||||
':zone' => $data['zone'],
|
||||
':aisle' => $data['aisle'],
|
||||
':rack' => $data['rack'],
|
||||
':bin' => $data['bin'],
|
||||
':contact_id' => (int)($data['contact_id'] ?? 0),
|
||||
':description' => $data['description'] ?? '',
|
||||
':log' => json_encode($table_log),
|
||||
@@ -462,7 +462,7 @@ class StockManager {
|
||||
|
||||
$out_stock_id = (int)$this->pdo->lastInsertId();
|
||||
|
||||
// releaseRack and adjustBalance deferred — called from approveStock() only.
|
||||
// releaseBin and adjustBalance deferred — called from approveStock() only.
|
||||
return $out_stock_id;
|
||||
}
|
||||
}
|
||||
@@ -475,11 +475,11 @@ class StockManager {
|
||||
* sharing the same UUID and cross-referencing each other via ref_id.
|
||||
*
|
||||
* Insert flow (id = 0):
|
||||
* 1. Validates the source rack holds the correct SKU / lot / serial.
|
||||
* 1. Validates the source bin holds the correct SKU / lot / serial.
|
||||
* 2. Inserts the outbound row in td_stock_<from>.
|
||||
* 3. Inserts the inbound row in td_stock_<to> with ref_id pointing to from.
|
||||
* 4. Back-fills ref_id on the from row so both point at each other.
|
||||
* 5. Releases the source rack, occupies the destination rack.
|
||||
* 5. Releases the source bin, occupies the destination bin.
|
||||
* 6. Adjusts balance on both warehouses (out from source, in to dest).
|
||||
*
|
||||
* Update flow (id > 0):
|
||||
@@ -488,11 +488,11 @@ class StockManager {
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param array $data Keys: id, warehouse_from, warehouse_to, product_sku,
|
||||
* zone_from, aisle_from, rack_from, zone_to, aisle_to, rack_to,
|
||||
* zone_from, aisle_from, bin_from, zone_to, aisle_to, bin_to,
|
||||
* contact_id, description, lot_number, serial_number.
|
||||
* @param array $logging Audit entry to append to the log column on both rows.
|
||||
* @param string $uuid UUID shared by both the from and to rows.
|
||||
* @throws Exception If source rack validation fails or paired record is missing on update.
|
||||
* @throws Exception If source bin validation fails or paired record is missing on update.
|
||||
*/
|
||||
public function saveStockTransfer(array $data, array $logging, string $uuid): int
|
||||
{
|
||||
@@ -566,39 +566,39 @@ class StockManager {
|
||||
return 0; // update — no new row
|
||||
}
|
||||
|
||||
// Insert: validate source rack, then create paired rows
|
||||
// Insert: validate source bin, then create paired rows
|
||||
$from_warehouse = (int)$data['warehouse_from'];
|
||||
$from_zone = $data['zone_from'];
|
||||
$from_aisle = $data['aisle_from'];
|
||||
$from_rack = $data['rack_from'];
|
||||
$from_bin = $data['bin_from'];
|
||||
$to_warehouse = (int)$data['warehouse_to'];
|
||||
$to_zone = $data['zone_to'];
|
||||
$to_aisle = $data['aisle_to'];
|
||||
$to_rack = $data['rack_to'];
|
||||
$to_bin = $data['bin_to'];
|
||||
|
||||
$source_stock = $whMgmt->getRackStock(
|
||||
$from_warehouse, $from_zone, $from_aisle, $from_rack
|
||||
$source_stock = $whMgmt->getBinStock(
|
||||
$from_warehouse, $from_zone, $from_aisle, $from_bin
|
||||
);
|
||||
|
||||
if (!$source_stock) {
|
||||
throw new Exception("Source rack {$from_zone}-{$from_aisle}-{$from_rack} is empty.");
|
||||
throw new Exception("Source bin {$from_zone}-{$from_aisle}-{$from_bin} is empty.");
|
||||
}
|
||||
|
||||
if ($source_stock['product_sku'] !== $data['product_sku']) {
|
||||
throw new Exception(
|
||||
"Source rack holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
"Source bin holds {$source_stock['product_sku']}, not {$data['product_sku']}."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['lot_number']) && $source_stock['lot_number'] !== $data['lot_number']) {
|
||||
throw new Exception(
|
||||
"Source rack holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
"Source bin holds lot '{$source_stock['lot_number']}', not '{$data['lot_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($data['serial_number']) && $source_stock['serial_number'] !== $data['serial_number']) {
|
||||
throw new Exception(
|
||||
"Source rack holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
"Source bin holds serial '{$source_stock['serial_number']}', not '{$data['serial_number']}'."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -615,11 +615,11 @@ class StockManager {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$from_table`
|
||||
(uuid, company_id, `date`, product_sku, `out`,
|
||||
ref_warehouse, zone, aisle, rack,
|
||||
ref_warehouse, zone, aisle, bin,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :zone, :aisle, :rack,
|
||||
:ref_warehouse, :zone, :aisle, :bin,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
@@ -630,7 +630,7 @@ class StockManager {
|
||||
':ref_warehouse' => $to_warehouse,
|
||||
':zone' => $from_zone,
|
||||
':aisle' => $from_aisle,
|
||||
':rack' => $from_rack,
|
||||
':bin' => $from_bin,
|
||||
':contact_id' => $contact_id,
|
||||
':description' => $description,
|
||||
':log' => json_encode($table_log),
|
||||
@@ -643,11 +643,11 @@ class StockManager {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `$to_table`
|
||||
(uuid, company_id, `date`, product_sku, `in`,
|
||||
ref_warehouse, ref_id, zone, aisle, rack,
|
||||
ref_warehouse, ref_id, zone, aisle, bin,
|
||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status, updated_at)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:ref_warehouse, :ref_id, :zone, :aisle, :rack,
|
||||
:ref_warehouse, :ref_id, :zone, :aisle, :bin,
|
||||
:contact_id, :description, :log, 'transfer', :lot_number, :serial_number, 0, NOW())"
|
||||
)->execute([
|
||||
':uuid' => $uuid,
|
||||
@@ -659,7 +659,7 @@ class StockManager {
|
||||
':ref_id' => $from_stock_id,
|
||||
':zone' => $to_zone,
|
||||
':aisle' => $to_aisle,
|
||||
':rack' => $to_rack,
|
||||
':bin' => $to_bin,
|
||||
':contact_id' => $contact_id,
|
||||
':description' => $description,
|
||||
':log' => json_encode($table_log),
|
||||
@@ -678,7 +678,7 @@ class StockManager {
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
|
||||
// releaseRack, occupyRack and adjustBalance deferred — called from approveStock() only.
|
||||
// releaseBin, occupyBin and adjustBalance deferred — called from approveStock() only.
|
||||
return $from_stock_id;
|
||||
}
|
||||
|
||||
@@ -730,13 +730,13 @@ class StockManager {
|
||||
"UPDATE `{$table}` SET status = 1, updated_at = NOW() WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $id, ':company_id' => $this->company_id]);
|
||||
|
||||
// ── Rack state + balance ──────────────────────────────────────────
|
||||
// ── Bin state + balance ──────────────────────────────────────────
|
||||
if ($type === 'in') {
|
||||
|
||||
// Occupy rack now that stock is approved
|
||||
$whMgmt->occupyRack(
|
||||
// Occupy bin now that stock is approved
|
||||
$whMgmt->occupyBin(
|
||||
$warehouse_id,
|
||||
$row['zone'], $row['aisle'], $row['rack'],
|
||||
$row['zone'], $row['aisle'], $row['bin'],
|
||||
$row['product_sku'],
|
||||
$id
|
||||
);
|
||||
@@ -766,9 +766,9 @@ class StockManager {
|
||||
$remaining_qty = (float)$remaining_sth->fetchColumn();
|
||||
}
|
||||
|
||||
// Release the rack only when the source batch is fully consumed.
|
||||
// Release the bin only when the source batch is fully consumed.
|
||||
if ($remaining_qty <= 0.000001) {
|
||||
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
|
||||
$whMgmt->releaseBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin']);
|
||||
}
|
||||
$whMgmt->adjustBalance('out', $warehouse_id, $row['product_sku'], 0, (float)$row['out'],
|
||||
$id, $row['source'] ?? '', (int)($row['source_id'] ?? 0), $row['date'] ?? '');
|
||||
@@ -848,19 +848,19 @@ class StockManager {
|
||||
)->execute([':id' => $inbound_row['id'], ':company_id' => $this->company_id]);
|
||||
}
|
||||
|
||||
// Rack state: release source, occupy destination
|
||||
// Bin state: release source, occupy destination
|
||||
if ($from_row && $from_wh_id) {
|
||||
$whMgmt->releaseRack(
|
||||
$whMgmt->releaseBin(
|
||||
$from_wh_id,
|
||||
$from_row['zone'], $from_row['aisle'], $from_row['rack']
|
||||
$from_row['zone'], $from_row['aisle'], $from_row['bin']
|
||||
);
|
||||
$whMgmt->adjustBalance('out', $from_wh_id, $from_row['product_sku'], 0, (float)$from_row['out'],
|
||||
(int)$from_row['id'], $from_row['source'] ?? '', (int)($from_row['source_id'] ?? 0), $from_row['date'] ?? '');
|
||||
}
|
||||
if ($inbound_row && $paired_wh_id) {
|
||||
$whMgmt->occupyRack(
|
||||
$whMgmt->occupyBin(
|
||||
$paired_wh_id,
|
||||
$inbound_row['zone'], $inbound_row['aisle'], $inbound_row['rack'],
|
||||
$inbound_row['zone'], $inbound_row['aisle'], $inbound_row['bin'],
|
||||
$inbound_row['product_sku'],
|
||||
$inbound_row['id']
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@ class StockSourceManager
|
||||
$warehouse_id = (int)$match[1];
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT s.id, s.type, s.product_sku, s.`in`, s.`out`,
|
||||
s.zone, s.aisle, s.rack, s.lot_number, s.serial_number,
|
||||
s.zone, s.aisle, s.bin, s.lot_number, s.serial_number,
|
||||
s.status, s.date, s.price,
|
||||
p.product_name
|
||||
FROM `{$table}` s
|
||||
|
||||
@@ -104,10 +104,10 @@ class SupplierReturnManager {
|
||||
$sth = $this->pdo->prepare(
|
||||
"INSERT INTO td_supplier_return_item
|
||||
(company_id, return_id, item_id, product_sku, product_name,
|
||||
quantity, unit_price, total_price, tax_amount, tax_rate, warehouse_id, stock_in_id, zone, aisle, rack)
|
||||
quantity, unit_price, total_price, tax_amount, tax_rate, warehouse_id, stock_in_id, zone, aisle, bin)
|
||||
VALUES
|
||||
(:company_id, :return_id, :item_id, :product_sku, :product_name,
|
||||
:quantity, :unit_price, :total_price, :tax_amount, :tax_rate, :warehouse_id, :stock_in_id, :zone, :aisle, :rack)"
|
||||
:quantity, :unit_price, :total_price, :tax_amount, :tax_rate, :warehouse_id, :stock_in_id, :zone, :aisle, :bin)"
|
||||
);
|
||||
foreach ($items as $pos => $item) {
|
||||
$sth->execute([
|
||||
@@ -125,7 +125,7 @@ class SupplierReturnManager {
|
||||
':stock_in_id' => (int)($item['stock_in_id'] ?? 0),
|
||||
':zone' => $item['zone'] ?? '',
|
||||
':aisle' => $item['aisle'] ?? '',
|
||||
':rack' => $item['rack'] ?? '',
|
||||
':bin' => $item['bin'] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -348,7 +348,7 @@ class SupplierReturnManager {
|
||||
* Flow per item:
|
||||
* 1. Validate stock-in row (from PO receive) is approved.
|
||||
* 2. INSERT td_stock_<warehouse_id> out row, status respects $auto_approve.
|
||||
* 3. If $auto_approve: releaseRack() + adjustBalance('out').
|
||||
* 3. If $auto_approve: releaseBin() + adjustBalance('out').
|
||||
* After all items:
|
||||
* 4. UPDATE td_supplier_return.status = 1.
|
||||
*
|
||||
@@ -433,9 +433,9 @@ class SupplierReturnManager {
|
||||
|
||||
$zone = $item['zone'] ?? '';
|
||||
$aisle = $item['aisle'] ?? '';
|
||||
$rack = $item['rack'] ?? '';
|
||||
if ($zone === '' && $rack !== '') $zone = $rack;
|
||||
if ($aisle === '' && $rack !== '') $aisle = $rack;
|
||||
$bin = $item['bin'] ?? '';
|
||||
if ($zone === '' && $bin !== '') $zone = $bin;
|
||||
if ($aisle === '' && $bin !== '') $aisle = $bin;
|
||||
|
||||
$item_uuid = $uuid . '_srn_' . $i;
|
||||
$item_log = [array_merge($logging, ['action' => 'confirm_return_item'])];
|
||||
@@ -443,13 +443,13 @@ class SupplierReturnManager {
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO `{$table}`
|
||||
(uuid, company_id, `date`, product_sku, `out`,
|
||||
zone, aisle, rack,
|
||||
zone, aisle, bin,
|
||||
contact_id, `description`, `log`, `type`,
|
||||
lot_number, serial_number,
|
||||
source, source_id, ref_id, price, status)
|
||||
VALUES
|
||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||
:zone, :aisle, :rack,
|
||||
:zone, :aisle, :bin,
|
||||
:contact_id, :description, :log, 'out',
|
||||
:lot_number, :serial_number,
|
||||
'supplier_return', :source_id, :ref_id, :price, :status)"
|
||||
@@ -461,7 +461,7 @@ class SupplierReturnManager {
|
||||
':quantity' => $quantity,
|
||||
':zone' => $zone,
|
||||
':aisle' => $aisle,
|
||||
':rack' => $rack,
|
||||
':bin' => $bin,
|
||||
':contact_id' => (int)$return['contact_id'],
|
||||
':description' => $return['return_number'],
|
||||
':log' => json_encode($item_log),
|
||||
@@ -475,7 +475,7 @@ class SupplierReturnManager {
|
||||
|
||||
if ($auto_approve) {
|
||||
$stock_out_id = (int)$this->pdo->lastInsertId();
|
||||
$whMgmt->releaseRack($warehouse_id, $zone, $aisle, $rack);
|
||||
$whMgmt->releaseBin($warehouse_id, $zone, $aisle, $bin);
|
||||
$whMgmt->adjustBalance('out', $warehouse_id, $product_sku, 0, $quantity);
|
||||
}
|
||||
}
|
||||
@@ -560,7 +560,7 @@ class SupplierReturnManager {
|
||||
|
||||
// Fetch ALL non-cancelled stock-out rows (status=1 approved or status=0 draft)
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, status, zone, aisle, rack, ref_id, `date` FROM `{$table}`
|
||||
"SELECT id, status, zone, aisle, bin, ref_id, `date` FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'supplier_return'
|
||||
AND source_id = :return_id
|
||||
@@ -582,14 +582,14 @@ class SupplierReturnManager {
|
||||
|
||||
if ((int)$row['status'] === 1) {
|
||||
$whMgmt->assertStockMovementWindow($row['date'] ?? null, 'Supplier return stock cancellation');
|
||||
// Approved rows: re-occupy rack (reverse the release) + reverse balance
|
||||
// Approved rows: re-occupy bin (reverse the release) + reverse balance
|
||||
$ref_id = (int)$row['ref_id'];
|
||||
if ($ref_id > 0) {
|
||||
$whMgmt->occupyRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack'], $product_sku, $ref_id, false);
|
||||
$whMgmt->occupyBin($warehouse_id, $row['zone'], $row['aisle'], $row['bin'], $product_sku, $ref_id, false);
|
||||
}
|
||||
$whMgmt->adjustBalance('in', $warehouse_id, $product_sku, 0, $quantity);
|
||||
}
|
||||
// Draft (status=0) rows: just soft-delete — rack/balance were never applied
|
||||
// Draft (status=0) rows: just soft-delete — bin/balance were never applied
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
||||
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||
$zone = trim($data['zone'] ?? '');
|
||||
$aisle = trim($data['aisle'] ?? '');
|
||||
$rack = trim($data['rack'] ?? '');
|
||||
$bin = trim($data['bin'] ?? '');
|
||||
|
||||
if ($label_action !== 'list') {
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
@@ -56,14 +56,14 @@
|
||||
}
|
||||
|
||||
// Default: create
|
||||
if ($barcode === '' || !$warehouse_id || $rack === '') {
|
||||
if ($barcode === '' || !$warehouse_id || $bin === '') {
|
||||
$answer['message'] = 'Location barcode data is incomplete.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
$output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $rack);
|
||||
$output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $bin);
|
||||
$answer['success'] = 1;
|
||||
$answer['output'] = $output;
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
if (isset($data['warehouse_to']) && $type === 'to') { $warehouse = (int)$data['warehouse_to']; $zone = $data['zone_to'] ?? $zone; $aisle = $data['aisle_to'] ?? $aisle; }
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle);
|
||||
$output = $whMgmt->getBinsAll($warehouse, $zone, $aisle);
|
||||
} elseif ($type === 'to') {
|
||||
$output = $whMgmt->getRacksIn($warehouse, $zone, $aisle);
|
||||
$output = $whMgmt->getBinsIn($warehouse, $zone, $aisle);
|
||||
} elseif ($type === 'from') {
|
||||
$output = $whMgmt->getRacksOut(
|
||||
$output = $whMgmt->getBinsOut(
|
||||
$warehouse,
|
||||
$zone,
|
||||
$aisle,
|
||||
@@ -26,7 +26,7 @@
|
||||
$data['serial_number'] ?? null
|
||||
);
|
||||
} else {
|
||||
$output = $whMgmt->getRacksAll($warehouse, $zone, $aisle);
|
||||
$output = $whMgmt->getBinsAll($warehouse, $zone, $aisle);
|
||||
}
|
||||
|
||||
$answer['output'] = $output;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
// When type=from and a lot is selected, filter warehouses to only those
|
||||
// holding that specific lot in an approved, rack-occupied stock-in row.
|
||||
// holding that specific lot in an approved, bin-occupied stock-in row.
|
||||
if ($type === 'from' && $sku && $lot) {
|
||||
$answer['output'] = $wh->getWarehousesByLot($sku, $lot);
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||
$zone = trim($data['zone'] ?? '');
|
||||
$aisle = trim($data['aisle'] ?? '');
|
||||
$rack = trim($data['rack'] ?? '');
|
||||
$bin = trim($data['bin'] ?? '');
|
||||
$product_sku = trim($data['product_sku'] ?? '');
|
||||
$lot_number = trim($data['lot_number'] ?? '');
|
||||
$serial = trim($data['serial_number'] ?? '');
|
||||
@@ -22,7 +22,7 @@
|
||||
$warehouse_id,
|
||||
$zone,
|
||||
$aisle,
|
||||
$rack,
|
||||
$bin,
|
||||
$product_sku,
|
||||
$lot_number,
|
||||
$serial
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="col-12 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Location Barcode Labels</h1>
|
||||
<p class="mb-0 text-muted">Print or reprint labels from the rack master list.</p>
|
||||
<p class="mb-0 text-muted">Print or reprint labels from the bin master list.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="btn_print" class="btn btn-outline-success" data-min-role="staff" onclick="print_selected_label()" disabled>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="card"><div class="card-body p-5">
|
||||
<div class="position-relative mb-4">
|
||||
<input type="search" id="search_location" class="form-control ps-9"
|
||||
placeholder="Search warehouse, zone, aisle, rack, or SKU...">
|
||||
placeholder="Search warehouse, zone, aisle, bin, or SKU...">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
@@ -51,13 +51,13 @@
|
||||
<th>Warehouse</th>
|
||||
<th class="th-zone">Zone</th>
|
||||
<th class="th-aisle">Aisle</th>
|
||||
<th id="th-rack">Rack</th>
|
||||
<th id="th-bin">Bin</th>
|
||||
<th>Status</th>
|
||||
<th>Label Status</th>
|
||||
<th class="text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rack_tbody">
|
||||
<tbody id="bin_tbody">
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Loading locations...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -79,13 +79,13 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
|
||||
<script>
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
var racks = [];
|
||||
var bins = [];
|
||||
var barcode_status = {};
|
||||
var selected_payload = '';
|
||||
var selected_rack_index = -1;
|
||||
var selected_bin_index = -1;
|
||||
|
||||
function escape_html(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, function(char) {
|
||||
@@ -103,9 +103,9 @@
|
||||
}
|
||||
|
||||
function highlight_selected_row() {
|
||||
$('#rack_tbody tr').removeClass('table-active');
|
||||
if (selected_rack_index >= 0) {
|
||||
$('#rack_tbody tr[data-rack-index="' + selected_rack_index + '"]').addClass('table-active');
|
||||
$('#bin_tbody tr').removeClass('table-active');
|
||||
if (selected_bin_index >= 0) {
|
||||
$('#bin_tbody tr[data-bin-index="' + selected_bin_index + '"]').addClass('table-active');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,26 +119,26 @@
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
$('.th-zone').text(label_zone).toggleClass('d-none', !advanced);
|
||||
$('.th-aisle').text(label_aisle).toggleClass('d-none', !advanced);
|
||||
$('#th-rack').text(label_rack);
|
||||
$('#th-bin').text(label_bin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_racks() {
|
||||
function load_bins() {
|
||||
return ajax_request({
|
||||
url: server_url + 'reports/api/engine_report/rack_occupancy.php',
|
||||
url: server_url + 'reports/api/engine_report/bin_occupancy.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
queueLock: false,
|
||||
onSuccess: function(res) {
|
||||
racks = res.output || [];
|
||||
render_racks();
|
||||
bins = res.output || [];
|
||||
render_bins();
|
||||
highlight_selected_row();
|
||||
}
|
||||
});
|
||||
@@ -157,26 +157,26 @@
|
||||
(res.output || []).forEach(function(item) {
|
||||
barcode_status[item.barcode] = parseInt(item.status) === 1 ? 'active' : 'disabled';
|
||||
});
|
||||
render_racks();
|
||||
render_bins();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function location_payload(item) {
|
||||
return advanced
|
||||
? `LOC|${item.warehouse_id}|${item.zone}|${item.aisle}|${item.rack}`
|
||||
: `LOC|${item.warehouse_id}|${item.rack}`;
|
||||
? `LOC|${item.warehouse_id}|${item.zone}|${item.aisle}|${item.bin}`
|
||||
: `LOC|${item.warehouse_id}|${item.bin}`;
|
||||
}
|
||||
|
||||
function render_racks() {
|
||||
function render_bins() {
|
||||
var keyword = $('#search_location').val().toLowerCase();
|
||||
var rows = racks.filter(function(item) {
|
||||
var rows = bins.filter(function(item) {
|
||||
if (!keyword) return true;
|
||||
return [
|
||||
item.warehouse_name,
|
||||
item.zone,
|
||||
item.aisle,
|
||||
item.rack,
|
||||
item.bin,
|
||||
item.product_sku,
|
||||
item.product_name,
|
||||
item.status
|
||||
@@ -184,13 +184,13 @@
|
||||
});
|
||||
|
||||
if (!rows.length) {
|
||||
$('#rack_tbody').html('<tr><td colspan="7" class="text-center text-muted py-4">No locations found.</td></tr>');
|
||||
$('#bin_tbody').html('<tr><td colspan="7" class="text-center text-muted py-4">No locations found.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
var body = '';
|
||||
rows.forEach(function(item) {
|
||||
var idx = racks.indexOf(item);
|
||||
var idx = bins.indexOf(item);
|
||||
var status = item.status === 'occupied'
|
||||
? '<span class="badge bg-warning text-dark">Occupied</span>'
|
||||
: '<span class="badge bg-success">Empty</span>';
|
||||
@@ -202,11 +202,11 @@
|
||||
if (barcode_status[payload] === 'disabled') {
|
||||
labelStatus = '<span class="badge bg-danger">Disabled</span>';
|
||||
}
|
||||
body += `<tr data-rack-index="${idx}">
|
||||
body += `<tr data-bin-index="${idx}">
|
||||
<td>${escape_html(item.warehouse_name)}</td>
|
||||
<td class="th-zone ${advanced ? '' : 'd-none'}">${escape_html(item.zone)}</td>
|
||||
<td class="th-aisle ${advanced ? '' : 'd-none'}">${escape_html(item.aisle)}</td>
|
||||
<td>${escape_html(item.rack)}</td>
|
||||
<td>${escape_html(item.bin)}</td>
|
||||
<td>${status}</td>
|
||||
<td>${labelStatus}</td>
|
||||
<td class="text-end">
|
||||
@@ -216,7 +216,7 @@
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('#rack_tbody').html(body);
|
||||
$('#bin_tbody').html(body);
|
||||
}
|
||||
|
||||
function render_preview(item, payload) {
|
||||
@@ -224,8 +224,8 @@
|
||||
<div class="barcode-label mx-auto">
|
||||
<div class="fw-bold fs-5">${escape_html(item.warehouse_name)}</div>
|
||||
<div class="small">${advanced
|
||||
? `${escape_html(item.zone)} / ${escape_html(item.aisle)} / ${escape_html(item.rack)}`
|
||||
: escape_html(item.rack)}</div>
|
||||
? `${escape_html(item.zone)} / ${escape_html(item.aisle)} / ${escape_html(item.bin)}`
|
||||
: escape_html(item.bin)}</div>
|
||||
<svg id="barcode_svg"></svg>
|
||||
<div class="small text-muted">${escape_html(payload)}</div>
|
||||
</div>`);
|
||||
@@ -233,7 +233,7 @@
|
||||
}
|
||||
|
||||
function preview_label(index) {
|
||||
var item = racks[index];
|
||||
var item = bins[index];
|
||||
if (!item) return;
|
||||
var payload = location_payload(item);
|
||||
|
||||
@@ -249,13 +249,13 @@
|
||||
warehouse_id: item.warehouse_id,
|
||||
zone: item.zone || '',
|
||||
aisle: item.aisle || '',
|
||||
rack: item.rack
|
||||
bin: item.bin
|
||||
},
|
||||
onSuccess: function() {
|
||||
selected_payload = payload;
|
||||
selected_rack_index = index;
|
||||
selected_bin_index = index;
|
||||
if (!barcode_status[payload]) barcode_status[payload] = 'active';
|
||||
render_racks();
|
||||
render_bins();
|
||||
refresh_header_buttons();
|
||||
highlight_selected_row();
|
||||
render_preview(item, payload);
|
||||
@@ -303,9 +303,9 @@
|
||||
onSuccess: function() {
|
||||
barcode_status[selected_payload] = 'disabled';
|
||||
selected_payload = '';
|
||||
selected_rack_index = -1;
|
||||
selected_bin_index = -1;
|
||||
$('#label_preview').html('Select a location to preview its label.');
|
||||
render_racks();
|
||||
render_bins();
|
||||
refresh_header_buttons();
|
||||
highlight_selected_row();
|
||||
}
|
||||
@@ -316,8 +316,8 @@
|
||||
|
||||
function reenable_selected_label() {
|
||||
if (!selected_payload) return;
|
||||
var index = selected_rack_index;
|
||||
var item = racks[index];
|
||||
var index = selected_bin_index;
|
||||
var item = bins[index];
|
||||
if (!item) return;
|
||||
|
||||
ajax_request({
|
||||
@@ -329,7 +329,7 @@
|
||||
data: { label_action: 'enable', barcode: selected_payload },
|
||||
onSuccess: function() {
|
||||
barcode_status[selected_payload] = 'active';
|
||||
render_racks();
|
||||
render_bins();
|
||||
refresh_header_buttons();
|
||||
highlight_selected_row();
|
||||
render_preview(item, selected_payload);
|
||||
@@ -338,7 +338,7 @@
|
||||
}
|
||||
|
||||
function enable_label_for_row(index) {
|
||||
var item = racks[index];
|
||||
var item = bins[index];
|
||||
if (!item) return;
|
||||
var payload = location_payload(item);
|
||||
|
||||
@@ -352,8 +352,8 @@
|
||||
onSuccess: function() {
|
||||
barcode_status[payload] = 'active';
|
||||
selected_payload = payload;
|
||||
selected_rack_index = index;
|
||||
render_racks();
|
||||
selected_bin_index = index;
|
||||
render_bins();
|
||||
refresh_header_buttons();
|
||||
highlight_selected_row();
|
||||
render_preview(item, payload);
|
||||
@@ -361,11 +361,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
$('#search_location').on('input', render_racks);
|
||||
$('#search_location').on('input', render_bins);
|
||||
|
||||
$(async function() {
|
||||
await load_config();
|
||||
await load_racks();
|
||||
await load_bins();
|
||||
await load_barcode_statuses();
|
||||
});
|
||||
</script>
|
||||
|
||||
+33
-33
@@ -97,10 +97,10 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6" id="group_rack">
|
||||
<label for="rack" class="form-label" id="label_rack">Rack</label>
|
||||
<select name="rack" id="rack" class="form-select" required>
|
||||
<option value="">Please select rack</option>
|
||||
<div class="mb-3 col-lg-6" id="group_bin">
|
||||
<label for="bin" class="form-label" id="label_bin">Bin</label>
|
||||
<select name="bin" id="bin" class="form-select" required>
|
||||
<option value="">Please select bin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
// custom.js globals: set_select_value(), load_location_config(), init_lot_search_inputs()
|
||||
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
var scan_sku_ready = false;
|
||||
@@ -150,15 +150,15 @@
|
||||
$('#group_aisle').show();
|
||||
$('#label_zone').text(label_zone);
|
||||
$('#label_aisle').text(label_aisle);
|
||||
$('#label_rack').text(label_rack);
|
||||
$('#label_bin').text(label_bin);
|
||||
$('#zone').attr('placeholder', 'Please select ' + label_zone.toLowerCase());
|
||||
$('#aisle').attr('placeholder', 'Please select ' + label_aisle.toLowerCase());
|
||||
$('#rack').attr('placeholder', 'Please select ' + label_rack.toLowerCase());
|
||||
$('#bin').attr('placeholder', 'Please select ' + label_bin.toLowerCase());
|
||||
} else {
|
||||
$('#group_zone').hide();
|
||||
$('#group_aisle').hide();
|
||||
$('#label_rack').text(label_rack);
|
||||
$('#rack').attr('placeholder', 'Please select ' + label_rack.toLowerCase());
|
||||
$('#label_bin').text(label_bin);
|
||||
$('#bin').attr('placeholder', 'Please select ' + label_bin.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
$('#warehouse').val('');
|
||||
$('#zone').val('');
|
||||
$('#aisle').val('');
|
||||
$('#rack').val('');
|
||||
$('#bin').val('');
|
||||
$('#product_sku').trigger('blur');
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
set_select_value('#zone', data.zone, data.zone);
|
||||
set_select_value('#aisle', data.aisle, data.aisle);
|
||||
}
|
||||
set_select_value('#rack', data.rack, data.rack);
|
||||
set_select_value('#bin', data.bin, data.bin);
|
||||
scan_location_ready = true;
|
||||
});
|
||||
}
|
||||
@@ -207,7 +207,7 @@
|
||||
warehouse_id: data.warehouse_id,
|
||||
zone: data.zone,
|
||||
aisle: data.aisle,
|
||||
rack: data.rack,
|
||||
bin: data.bin,
|
||||
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
lot_number: $('#lot_number').val(),
|
||||
serial_number: $('#serial_number').val()
|
||||
@@ -230,7 +230,7 @@
|
||||
$('#warehouse').val('');
|
||||
$('#zone').val('');
|
||||
$('#aisle').val('');
|
||||
$('#rack').val('');
|
||||
$('#bin').val('');
|
||||
scan_sku_ready = false;
|
||||
scan_location_ready = false;
|
||||
$('#product_sku').focus();
|
||||
@@ -241,7 +241,7 @@
|
||||
bootbox.alert('Scan the SKU label first.');
|
||||
return;
|
||||
}
|
||||
if (!scan_location_ready && !$('#rack').val()) {
|
||||
if (!scan_location_ready && !$('#bin').val()) {
|
||||
bootbox.alert('Scan the location label before saving.');
|
||||
return;
|
||||
}
|
||||
@@ -250,17 +250,17 @@
|
||||
|
||||
function manage_stock_in() {
|
||||
|
||||
var rack_val = $('#rack').val();
|
||||
var zone_val = advanced ? $('#zone').val() : rack_val;
|
||||
var aisle_val = advanced ? $('#aisle').val() : rack_val;
|
||||
var bin_val = $('#bin').val();
|
||||
var zone_val = advanced ? $('#zone').val() : bin_val;
|
||||
var aisle_val = advanced ? $('#aisle').val() : bin_val;
|
||||
var warehouse_val = $('#warehouse').val();
|
||||
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
|
||||
var quantity_val = $('#quantity').val();
|
||||
|
||||
// Mirror to hidden inputs for autoPrepare consistency (simple mode)
|
||||
if (!advanced) {
|
||||
$('#zone').val(rack_val);
|
||||
$('#aisle').val(rack_val);
|
||||
$('#zone').val(bin_val);
|
||||
$('#aisle').val(bin_val);
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
@@ -275,7 +275,7 @@
|
||||
'serial_number': $('#serial_number').val(),
|
||||
'zone': zone_val,
|
||||
'aisle': aisle_val,
|
||||
'rack': rack_val,
|
||||
'bin': bin_val,
|
||||
'product_sku': sku_val,
|
||||
'quantity': quantity_val,
|
||||
'warehouse': warehouse_val,
|
||||
@@ -398,10 +398,10 @@
|
||||
|
||||
|
||||
|
||||
function retrieve_rack(type, callback) {
|
||||
function retrieve_bin(type, callback) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_rack.php",
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_bin.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
@@ -411,11 +411,11 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>';
|
||||
var option = '<option value="">Please select ' + label_bin.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += '<option value="' + item + '">' + item + '</option>';
|
||||
});
|
||||
$('select#rack').html(option);
|
||||
$('select#bin').html(option);
|
||||
|
||||
if (callback) callback(); // ← add this
|
||||
}
|
||||
@@ -445,7 +445,7 @@
|
||||
|
||||
function populate_fields() {
|
||||
$.each(data, function(key, item) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'rack'].includes(key)) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'bin'].includes(key)) {
|
||||
$('#' + key).val(item);
|
||||
}
|
||||
});
|
||||
@@ -478,16 +478,16 @@
|
||||
$('select#zone').val(data.zone).attr('disabled', true);
|
||||
retrieve_aisle('to', function() {
|
||||
$('#aisle').val(data.aisle).attr('disabled', true);
|
||||
retrieve_rack('to', function() {
|
||||
$('#rack').val(data.rack).attr('disabled', true);
|
||||
retrieve_bin('to', function() {
|
||||
$('#bin').val(data.bin).attr('disabled', true);
|
||||
populate_fields();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Simple mode — skip zone/aisle cascade
|
||||
retrieve_rack('to', function() {
|
||||
$('#rack').val(data.rack).attr('disabled', true);
|
||||
retrieve_bin('to', function() {
|
||||
$('#bin').val(data.bin).attr('disabled', true);
|
||||
populate_fields();
|
||||
});
|
||||
}
|
||||
@@ -536,11 +536,11 @@
|
||||
if (advanced) {
|
||||
retrieve_zone('to', function() {
|
||||
retrieve_aisle('to', function() {
|
||||
retrieve_rack('to');
|
||||
retrieve_bin('to');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
retrieve_rack('to');
|
||||
retrieve_bin('to');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
if (advanced) {
|
||||
retrieve_zone('to');
|
||||
} else {
|
||||
retrieve_rack('to');
|
||||
retrieve_bin('to');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -624,7 +624,7 @@
|
||||
});
|
||||
|
||||
$("select#aisle").change(function() {
|
||||
if (advanced) retrieve_rack('to');
|
||||
if (advanced) retrieve_bin('to');
|
||||
});
|
||||
|
||||
<?php } ?>
|
||||
|
||||
@@ -85,11 +85,11 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Step 7: Rack -->
|
||||
<div class="mb-3 col-lg-6" id="group_rack">
|
||||
<label for="rack" class="form-label" id="label_rack">Rack</label>
|
||||
<select name="rack" id="rack" class="form-select" required disabled>
|
||||
<option value="">Please select rack</option>
|
||||
<!-- Step 7: Bin -->
|
||||
<div class="mb-3 col-lg-6" id="group_bin">
|
||||
<label for="bin" class="form-label" id="label_bin">Bin</label>
|
||||
<select name="bin" id="bin" class="form-select" required disabled>
|
||||
<option value="">Please select bin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
// custom.js globals: set_select_value(), load_location_config()
|
||||
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
var scan_sku_ready = false;
|
||||
@@ -157,7 +157,7 @@
|
||||
set_select_value('#zone', data.zone, data.zone);
|
||||
set_select_value('#aisle', data.aisle, data.aisle);
|
||||
}
|
||||
set_select_value('#rack', data.rack, data.rack);
|
||||
set_select_value('#bin', data.bin, data.bin);
|
||||
scan_location_ready = true;
|
||||
});
|
||||
}
|
||||
@@ -175,7 +175,7 @@
|
||||
warehouse_id: data.warehouse_id,
|
||||
zone: data.zone,
|
||||
aisle: data.aisle,
|
||||
rack: data.rack,
|
||||
bin: data.bin,
|
||||
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
lot_number: $('#lot_number').val(),
|
||||
serial_number: $('#serial_number').val()
|
||||
@@ -204,7 +204,7 @@
|
||||
bootbox.alert('Scan the SKU label first.');
|
||||
return;
|
||||
}
|
||||
if (!scan_location_ready && !$('#rack').val()) {
|
||||
if (!scan_location_ready && !$('#bin').val()) {
|
||||
bootbox.alert('Scan the location label before saving.');
|
||||
return;
|
||||
}
|
||||
@@ -218,22 +218,22 @@
|
||||
$('#group_aisle').show();
|
||||
$('#label_zone').text(label_zone);
|
||||
$('#label_aisle').text(label_aisle);
|
||||
$('#label_rack').text(label_rack);
|
||||
$('#label_bin').text(label_bin);
|
||||
} else {
|
||||
$('#group_zone').hide();
|
||||
$('#group_aisle').hide();
|
||||
$('#label_rack').text(label_rack);
|
||||
$('#label_bin').text(label_bin);
|
||||
}
|
||||
}
|
||||
|
||||
function manage_stock_out() {
|
||||
var rack_val = $('#rack').val();
|
||||
var bin_val = $('#bin').val();
|
||||
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
|
||||
var wh_val = $('#warehouse').val();
|
||||
|
||||
if (!advanced) {
|
||||
$('#zone').val(rack_val);
|
||||
$('#aisle').val(rack_val);
|
||||
$('#zone').val(bin_val);
|
||||
$('#aisle').val(bin_val);
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
@@ -243,9 +243,9 @@
|
||||
debugMode: 0,
|
||||
data: {
|
||||
'contact_id': $('#contact').attr('data-id'),
|
||||
'zone': advanced ? $('#zone').val() : rack_val,
|
||||
'aisle': advanced ? $('#aisle').val() : rack_val,
|
||||
'rack': rack_val,
|
||||
'zone': advanced ? $('#zone').val() : bin_val,
|
||||
'aisle': advanced ? $('#aisle').val() : bin_val,
|
||||
'bin': bin_val,
|
||||
'product_sku': sku_val,
|
||||
'warehouse': wh_val,
|
||||
'lot_number': $('#lot_number').val(),
|
||||
@@ -391,7 +391,7 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Steps 5-7: Zone → Aisle → Rack ───────────────────────────────────
|
||||
// ── Steps 5-7: Zone → Aisle → Bin ───────────────────────────────────
|
||||
function retrieve_zone(callback) {
|
||||
|
||||
return ajax_request({
|
||||
@@ -447,10 +447,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_rack(callback) {
|
||||
function retrieve_bin(callback) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_rack.php",
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_bin.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
@@ -466,11 +466,11 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>';
|
||||
var option = '<option value="">Please select ' + label_bin.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item}'>${item}</option>`;
|
||||
});
|
||||
$('#rack').html(option).prop('disabled', false);
|
||||
$('#bin').html(option).prop('disabled', false);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
@@ -482,7 +482,7 @@
|
||||
$('#warehouse').html('<option value="">Select warehouse...</option>').prop('disabled', true);
|
||||
$('#zone').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true);
|
||||
$('#aisle').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true);
|
||||
$('#rack').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').prop('disabled', true);
|
||||
$('#bin').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
|
||||
}
|
||||
|
||||
|
||||
@@ -513,7 +513,7 @@
|
||||
|
||||
function populate_fields() {
|
||||
$.each(data, function(key, item) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'rack', 'lot_number', 'serial_number'].includes(key)) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'bin', 'lot_number', 'serial_number'].includes(key)) {
|
||||
$('#' + key).val(item);
|
||||
}
|
||||
});
|
||||
@@ -534,16 +534,16 @@
|
||||
$('#zone').val(data.zone).prop('disabled', true);
|
||||
retrieve_aisle(function() {
|
||||
$('#aisle').val(data.aisle).prop('disabled', true);
|
||||
retrieve_rack(function() {
|
||||
$('#rack').val(data.rack).prop('disabled', true);
|
||||
retrieve_bin(function() {
|
||||
$('#bin').val(data.bin).prop('disabled', true);
|
||||
populate_fields();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Simple mode — skip zone/aisle cascade
|
||||
retrieve_rack(function() {
|
||||
$('#rack').val(data.rack).prop('disabled', true);
|
||||
retrieve_bin(function() {
|
||||
$('#bin').val(data.bin).prop('disabled', true);
|
||||
populate_fields();
|
||||
});
|
||||
}
|
||||
@@ -600,12 +600,12 @@
|
||||
retrieve_warehouse();
|
||||
});
|
||||
|
||||
// Warehouse selected → load zone (advanced) or rack directly (simple)
|
||||
// Warehouse selected → load zone (advanced) or bin directly (simple)
|
||||
$('#warehouse').on('change', function() {
|
||||
if (advanced) {
|
||||
retrieve_zone();
|
||||
} else {
|
||||
retrieve_rack();
|
||||
retrieve_bin();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -614,9 +614,9 @@
|
||||
if (advanced) retrieve_aisle();
|
||||
});
|
||||
|
||||
// Aisle selected → load rack (advanced only)
|
||||
// Aisle selected → load bin (advanced only)
|
||||
$('#aisle').on('change', function() {
|
||||
if (advanced) retrieve_rack();
|
||||
if (advanced) retrieve_bin();
|
||||
});
|
||||
|
||||
<?php } ?>
|
||||
|
||||
@@ -87,9 +87,9 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label" id="label_rack_from">From Rack</label>
|
||||
<select name="rack" id="rack_from" role="from" class="form-select" required disabled>
|
||||
<option value="">Please select rack</option>
|
||||
<label class="form-label" id="label_bin_from">From Bin</label>
|
||||
<select name="bin" id="bin_from" role="from" class="form-select" required disabled>
|
||||
<option value="">Please select bin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -119,9 +119,9 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label" id="label_rack_to">To Rack</label>
|
||||
<select name="rack" id="rack_to" role="to" class="form-select" required>
|
||||
<option value="">Please select rack</option>
|
||||
<label class="form-label" id="label_bin_to">To Bin</label>
|
||||
<select name="bin" id="bin_to" role="to" class="form-select" required>
|
||||
<option value="">Please select bin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
var scan_sku_ready = false;
|
||||
@@ -200,7 +200,7 @@
|
||||
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);
|
||||
set_select_value('#bin_' + role, data.bin, data.bin);
|
||||
|
||||
if (role === 'from') {
|
||||
scan_from_ready = true;
|
||||
@@ -214,7 +214,7 @@
|
||||
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);
|
||||
&& String($('#bin_from').val()) === String(data.bin);
|
||||
}
|
||||
|
||||
function validate_scanned_location(role, data, onSuccess) {
|
||||
@@ -230,7 +230,7 @@
|
||||
warehouse_id: data.warehouse_id,
|
||||
zone: data.zone,
|
||||
aisle: data.aisle,
|
||||
rack: data.rack,
|
||||
bin: data.bin,
|
||||
product_sku: $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
lot_number: $('#lot_number').val(),
|
||||
serial_number: $('#serial_number').val()
|
||||
@@ -243,7 +243,7 @@
|
||||
$('#warehouse_to').val('');
|
||||
$('#zone_to').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').val('');
|
||||
$('#aisle_to').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').val('');
|
||||
$('#rack_to').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').val('');
|
||||
$('#bin_to').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').val('');
|
||||
}
|
||||
|
||||
function reset_stock_transfer_inputs() {
|
||||
@@ -269,11 +269,11 @@
|
||||
bootbox.alert('Scan the SKU label first.');
|
||||
return;
|
||||
}
|
||||
if (!scan_from_ready && !$('#rack_from').val()) {
|
||||
if (!scan_from_ready && !$('#bin_from').val()) {
|
||||
bootbox.alert('Scan the from-location label before saving.');
|
||||
return;
|
||||
}
|
||||
if (!scan_to_ready && !$('#rack_to').val()) {
|
||||
if (!scan_to_ready && !$('#bin_to').val()) {
|
||||
bootbox.alert('Scan the to-location label before saving.');
|
||||
return;
|
||||
}
|
||||
@@ -290,19 +290,19 @@
|
||||
$('#label_zone_to').text('To ' + label_zone);
|
||||
$('#label_aisle_from').text('From ' + label_aisle);
|
||||
$('#label_aisle_to').text('To ' + label_aisle);
|
||||
$('#label_rack_from').text('From ' + label_rack);
|
||||
$('#label_rack_to').text('To ' + label_rack);
|
||||
$('#label_bin_from').text('From ' + label_bin);
|
||||
$('#label_bin_to').text('To ' + label_bin);
|
||||
// Update select placeholders
|
||||
$('#zone_from, #zone_to').find('option:first').text('Please select ' + label_zone.toLowerCase());
|
||||
$('#aisle_from, #aisle_to').find('option:first').text('Please select ' + label_aisle.toLowerCase());
|
||||
$('#rack_from, #rack_to').find('option:first').text('Please select ' + label_rack.toLowerCase());
|
||||
$('#bin_from, #bin_to').find('option:first').text('Please select ' + label_bin.toLowerCase());
|
||||
} else {
|
||||
$('#group_zone_from, #group_zone_to').hide();
|
||||
$('#group_aisle_from, #group_aisle_to').hide();
|
||||
$('#zone_from, #zone_to, #aisle_from, #aisle_to').prop('required', false).removeClass('is-invalid is-valid');
|
||||
$('#label_rack_from').text('From ' + label_rack);
|
||||
$('#label_rack_to').text('To ' + label_rack);
|
||||
$('#rack_from, #rack_to').find('option:first').text('Please select ' + label_rack.toLowerCase());
|
||||
$('#label_bin_from').text('From ' + label_bin);
|
||||
$('#label_bin_to').text('To ' + label_bin);
|
||||
$('#bin_from, #bin_to').find('option:first').text('Please select ' + label_bin.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,16 +310,16 @@
|
||||
// ── Submit ────────────────────────────────────────────────────────────────
|
||||
function manage_stock_transfer() {
|
||||
|
||||
var rack_from_val = $('#rack_from').val();
|
||||
var rack_to_val = $('#rack_to').val();
|
||||
var bin_from_val = $('#bin_from').val();
|
||||
var bin_to_val = $('#bin_to').val();
|
||||
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
|
||||
|
||||
// Simple mode — mirror rack value to zone and aisle before submit
|
||||
// Simple mode — mirror bin value to zone and aisle before submit
|
||||
if (!advanced) {
|
||||
$('#zone_from').val(rack_from_val);
|
||||
$('#aisle_from').val(rack_from_val);
|
||||
$('#zone_to').val(rack_to_val);
|
||||
$('#aisle_to').val(rack_to_val);
|
||||
$('#zone_from').val(bin_from_val);
|
||||
$('#aisle_from').val(bin_from_val);
|
||||
$('#zone_to').val(bin_to_val);
|
||||
$('#aisle_to').val(bin_to_val);
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
@@ -330,12 +330,12 @@
|
||||
'contact_id': $('#contact').attr('data-id'),
|
||||
'warehouse_from': $('#warehouse_from').val(),
|
||||
'warehouse_to': $('#warehouse_to').val(),
|
||||
'zone_from': advanced ? $('#zone_from').val() : rack_from_val,
|
||||
'aisle_from': advanced ? $('#aisle_from').val() : rack_from_val,
|
||||
'rack_from': rack_from_val,
|
||||
'zone_to': advanced ? $('#zone_to').val() : rack_to_val,
|
||||
'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val,
|
||||
'rack_to': rack_to_val,
|
||||
'zone_from': advanced ? $('#zone_from').val() : bin_from_val,
|
||||
'aisle_from': advanced ? $('#aisle_from').val() : bin_from_val,
|
||||
'bin_from': bin_from_val,
|
||||
'zone_to': advanced ? $('#zone_to').val() : bin_to_val,
|
||||
'aisle_to': advanced ? $('#aisle_to').val() : bin_to_val,
|
||||
'bin_to': bin_to_val,
|
||||
'product_sku': sku_val,
|
||||
'lot_number': $('#lot_number').val(),
|
||||
'serial_number': $('#serial_number').val(),
|
||||
@@ -467,7 +467,7 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Zone / Aisle / Rack loaders ───────────────────────────────────────────
|
||||
// ── Zone / Aisle / Bin loaders ───────────────────────────────────────────
|
||||
function retrieve_zone(type, callback) {
|
||||
var extra = {
|
||||
'warehouse_from': $('#warehouse_from').val(),
|
||||
@@ -532,7 +532,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_rack(type, callback) {
|
||||
function retrieve_bin(type, callback) {
|
||||
var extra = {
|
||||
'warehouse_from': $('#warehouse_from').val(),
|
||||
'warehouse_to': $('#warehouse_to').val(),
|
||||
@@ -549,7 +549,7 @@
|
||||
});
|
||||
}
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php',
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_bin.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
@@ -557,11 +557,11 @@
|
||||
data: Object.assign({ 'type': type }, extra),
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>';
|
||||
var option = '<option value="">Please select ' + label_bin.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += '<option value="' + item + '">' + item + '</option>';
|
||||
});
|
||||
$('select[name="rack"][role="' + type + '"]').html(option).prop('disabled', false);
|
||||
$('select[name="bin"][role="' + type + '"]').html(option).prop('disabled', false);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
@@ -573,7 +573,7 @@
|
||||
$('#warehouse_from').html('<option value="">Select warehouse...</option>').prop('disabled', true);
|
||||
$('#zone_from').html('<option value="">Please select ' + label_zone.toLowerCase() + '</option>').prop('disabled', true);
|
||||
$('#aisle_from').html('<option value="">Please select ' + label_aisle.toLowerCase() + '</option>').prop('disabled', true);
|
||||
$('#rack_from').html('<option value="">Please select ' + label_rack.toLowerCase() + '</option>').prop('disabled', true);
|
||||
$('#bin_from').html('<option value="">Please select ' + label_bin.toLowerCase() + '</option>').prop('disabled', true);
|
||||
}
|
||||
|
||||
|
||||
@@ -616,23 +616,23 @@
|
||||
$('select[name="zone"][role="from"]').val(data.zone).prop('disabled', true);
|
||||
retrieve_aisle('from', function() {
|
||||
$('select[name="aisle"][role="from"]').val(data.aisle).prop('disabled', true);
|
||||
retrieve_rack('from', function() {
|
||||
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true);
|
||||
retrieve_bin('from', function() {
|
||||
$('select[name="bin"][role="from"]').val(data.bin).prop('disabled', true);
|
||||
populate_to_side(data, to_wh);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Simple mode — skip zone/aisle cascade, load rack directly
|
||||
retrieve_rack('from', function() {
|
||||
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true);
|
||||
// Simple mode — skip zone/aisle cascade, load bin directly
|
||||
retrieve_bin('from', function() {
|
||||
$('select[name="bin"][role="from"]').val(data.bin).prop('disabled', true);
|
||||
populate_to_side_simple(data, to_wh);
|
||||
});
|
||||
}
|
||||
|
||||
// Common fields
|
||||
$.each(data, function(key, item) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'rack', 'lot_number', 'serial_number', 'ref'].includes(key)) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'bin', 'lot_number', 'serial_number', 'ref'].includes(key)) {
|
||||
$('#' + key).val(item);
|
||||
}
|
||||
});
|
||||
@@ -655,16 +655,16 @@
|
||||
$('select[name="zone"][role="to"]').val(data.ref.zone).prop('disabled', true);
|
||||
retrieve_aisle('to', function() {
|
||||
$('select[name="aisle"][role="to"]').val(data.ref.aisle).prop('disabled', true);
|
||||
retrieve_rack('to', function() {
|
||||
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true);
|
||||
retrieve_bin('to', function() {
|
||||
$('select[name="bin"][role="to"]').val(data.ref.bin).prop('disabled', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function populate_to_side_simple(data, to_wh) {
|
||||
retrieve_rack('to', function() {
|
||||
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true);
|
||||
retrieve_bin('to', function() {
|
||||
$('select[name="bin"][role="to"]').val(data.ref.bin).prop('disabled', true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -717,9 +717,9 @@
|
||||
retrieve_warehouse('from');
|
||||
});
|
||||
|
||||
// From warehouse → zone (advanced) or rack directly (simple)
|
||||
// From warehouse → zone (advanced) or bin directly (simple)
|
||||
$('select[name="warehouse"][role="from"]').on('change', function() {
|
||||
if (advanced) { retrieve_zone('from'); } else { retrieve_rack('from'); }
|
||||
if (advanced) { retrieve_zone('from'); } else { retrieve_bin('from'); }
|
||||
});
|
||||
|
||||
// From zone → aisle (advanced only)
|
||||
@@ -727,14 +727,14 @@
|
||||
if (advanced) retrieve_aisle('from');
|
||||
});
|
||||
|
||||
// From aisle → rack (advanced only)
|
||||
// From aisle → bin (advanced only)
|
||||
$('select[name="aisle"][role="from"]').on('change', function() {
|
||||
if (advanced) retrieve_rack('from');
|
||||
if (advanced) retrieve_bin('from');
|
||||
});
|
||||
|
||||
// To warehouse → zone (advanced) or rack directly (simple)
|
||||
// To warehouse → zone (advanced) or bin directly (simple)
|
||||
$('select[name="warehouse"][role="to"]').on('change', function() {
|
||||
if (advanced) { retrieve_zone('to'); } else { retrieve_rack('to'); }
|
||||
if (advanced) { retrieve_zone('to'); } else { retrieve_bin('to'); }
|
||||
});
|
||||
|
||||
// To zone → aisle (advanced only)
|
||||
@@ -742,9 +742,9 @@
|
||||
if (advanced) retrieve_aisle('to');
|
||||
});
|
||||
|
||||
// To aisle → rack (advanced only)
|
||||
// To aisle → bin (advanced only)
|
||||
$('select[name="aisle"][role="to"]').on('change', function() {
|
||||
if (advanced) retrieve_rack('to');
|
||||
if (advanced) retrieve_bin('to');
|
||||
});
|
||||
|
||||
<?php } ?>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<th>Source</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th class="th_loc_bin">Bin</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<script>
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -98,10 +98,10 @@
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
} else {
|
||||
$('.th_loc_zone, .th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +109,9 @@
|
||||
if (advanced) {
|
||||
return `<td class="py-3">${item['zone'] || ''}</td>
|
||||
<td class="py-3">${item['aisle'] || ''}</td>
|
||||
<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
<td class="py-3">${item['bin'] || ''}</td>`;
|
||||
}
|
||||
return `<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
return `<td class="py-3">${item['bin'] || ''}</td>`;
|
||||
}
|
||||
|
||||
function escape_html(value) {
|
||||
@@ -309,7 +309,7 @@
|
||||
var s = res.output || {};
|
||||
auto_approve = s.default_stock_status == 1;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<th>Source</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th class="th_loc_bin">Bin</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
<script>
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -97,10 +97,10 @@
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
} else {
|
||||
$('.th_loc_zone, .th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +108,9 @@
|
||||
if (advanced) {
|
||||
return `<td class="py-3">${item['zone'] || ''}</td>
|
||||
<td class="py-3">${item['aisle'] || ''}</td>
|
||||
<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
<td class="py-3">${item['bin'] || ''}</td>`;
|
||||
}
|
||||
return `<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
return `<td class="py-3">${item['bin'] || ''}</td>`;
|
||||
}
|
||||
|
||||
function escape_html(value) {
|
||||
@@ -329,7 +329,7 @@
|
||||
var s = res.output || {};
|
||||
auto_approve = s.default_stock_status == 1;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div>
|
||||
<span class="mb-4 d-block">Total Capacity</span>
|
||||
<h3 class="fw-bold mb-0 text-info" id="stat_capacity">—</h3>
|
||||
<p class="mb-0 small text-secondary" id="stat_capacity_sub">Racks in warehouse</p>
|
||||
<p class="mb-0 small text-secondary" id="stat_capacity_sub">Bins in warehouse</p>
|
||||
</div>
|
||||
<i class="ti ti-box-seam fs-3 text-muted"></i>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<th style="text-align: right;">Quantity</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th class="th_loc_bin">Bin</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<script>
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -86,10 +86,10 @@
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
} else {
|
||||
$('.th_loc_zone, .th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@
|
||||
if (advanced) {
|
||||
return `<td class="py-3">${item['zone'] || ''}</td>
|
||||
<td class="py-3">${item['aisle'] || ''}</td>
|
||||
<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
<td class="py-3">${item['bin'] || ''}</td>`;
|
||||
}
|
||||
return `<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
return `<td class="py-3">${item['bin'] || ''}</td>`;
|
||||
}
|
||||
|
||||
function retrieve_stock_transfer() {
|
||||
@@ -240,7 +240,7 @@
|
||||
var s = res.output || {};
|
||||
auto_approve = s.default_stock_status == 1;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
<li>
|
||||
<a class="nav-link <?php echo wms_active('occupy_rack'); ?>" href="<?php echo $server_url?>reports/occupy_rack.php">
|
||||
<i class="ti ti-layout-grid"></i>
|
||||
<span class="nav-text">Rack Occupancy</span>
|
||||
<span class="nav-text">Bin Occupancy</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced mode — zone + aisle range + rack range -->
|
||||
<!-- Advanced mode — zone + aisle range + bin range -->
|
||||
<div id="form_advanced" class="d-none">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
@@ -81,16 +81,16 @@
|
||||
<div class="form-text">Must be same type as Aisle From</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="rack_from" class="form-label" id="label_rack_from">Rack From</label>
|
||||
<input type="text" class="form-control" id="rack_from" placeholder="e.g. 1 or A"
|
||||
<label for="bin_from" class="form-label" id="label_bin_from">Bin From</label>
|
||||
<input type="text" class="form-control" id="bin_from" placeholder="e.g. 1 or A"
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter">
|
||||
<div class="form-text">Number (1–99) or letter (A–Z)</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="rack_to" class="form-label" id="label_rack_to">Rack To</label>
|
||||
<input type="text" class="form-control" id="rack_to" placeholder="e.g. 10 or J"
|
||||
<label for="bin_to" class="form-label" id="label_bin_to">Bin To</label>
|
||||
<input type="text" class="form-control" id="bin_to" placeholder="e.g. 10 or J"
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter">
|
||||
<div class="form-text">Must be same type as Rack From</div>
|
||||
<div class="form-text">Must be same type as Bin From</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@
|
||||
<script>
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
@@ -150,13 +150,13 @@
|
||||
$('#label_zone').text(label_zone);
|
||||
$('#label_aisle_from').text(label_aisle + ' From');
|
||||
$('#label_aisle_to').text(label_aisle + ' To');
|
||||
$('#label_rack_from').text(label_rack + ' From');
|
||||
$('#label_rack_to').text(label_rack + ' To');
|
||||
$('#label_bin_from').text(label_bin + ' From');
|
||||
$('#label_bin_to').text(label_bin + ' To');
|
||||
} else {
|
||||
$('#form_advanced').addClass('d-none');
|
||||
$('#form_simple').removeClass('d-none');
|
||||
$('#label_location_from').text(label_rack + ' From');
|
||||
$('#label_location_to').text(label_rack + ' To');
|
||||
$('#label_location_from').text(label_bin + ' From');
|
||||
$('#label_location_to').text(label_bin + ' To');
|
||||
$('#location_from').attr('placeholder', 'e.g. Shelf-A or 1');
|
||||
$('#location_to').attr('placeholder', 'e.g. Shelf-E or 5');
|
||||
}
|
||||
@@ -164,14 +164,14 @@
|
||||
|
||||
// ── Save storage ──────────────────────────────────────────────────────────
|
||||
function manage_storage() {
|
||||
var zone, aisle_from, aisle_to, rack_from, rack_to;
|
||||
var zone, aisle_from, aisle_to, bin_from, bin_to;
|
||||
|
||||
if (advanced) {
|
||||
zone = $('#zone').val().trim();
|
||||
aisle_from = $('#aisle_from').val().trim().toUpperCase();
|
||||
aisle_to = $('#aisle_to').val().trim().toUpperCase();
|
||||
rack_from = $('#rack_from').val().trim().toUpperCase();
|
||||
rack_to = $('#rack_to').val().trim().toUpperCase();
|
||||
bin_from = $('#bin_from').val().trim().toUpperCase();
|
||||
bin_to = $('#bin_to').val().trim().toUpperCase();
|
||||
|
||||
var isNumeric = function(v) { return /^[0-9]+$/.test(v); };
|
||||
var isAlpha = function(v) { return /^[A-Z]$/.test(v); };
|
||||
@@ -182,8 +182,8 @@
|
||||
if (!aisle_from || !aisle_to) {
|
||||
alert('Please enter the full ' + label_aisle + ' range.'); return;
|
||||
}
|
||||
if (!rack_from || !rack_to) {
|
||||
alert('Please enter the full ' + label_rack + ' range.'); return;
|
||||
if (!bin_from || !bin_to) {
|
||||
alert('Please enter the full ' + label_bin + ' range.'); return;
|
||||
}
|
||||
|
||||
if (aisle_from && aisle_to) {
|
||||
@@ -197,25 +197,25 @@
|
||||
alert(label_aisle + ' From must be less than or equal to To.'); return;
|
||||
}
|
||||
}
|
||||
if (rack_from && rack_to) {
|
||||
if (isNumeric(rack_from) !== isNumeric(rack_to)) {
|
||||
alert(label_rack + ' From and To must both be numbers or both be letters.'); return;
|
||||
if (bin_from && bin_to) {
|
||||
if (isNumeric(bin_from) !== isNumeric(bin_to)) {
|
||||
alert(label_bin + ' From and To must both be numbers or both be letters.'); return;
|
||||
}
|
||||
if (isAlpha(rack_from) && rack_from.charCodeAt(0) > rack_to.charCodeAt(0)) {
|
||||
alert(label_rack + ' From must be less than or equal to To.'); return;
|
||||
if (isAlpha(bin_from) && bin_from.charCodeAt(0) > bin_to.charCodeAt(0)) {
|
||||
alert(label_bin + ' From must be less than or equal to To.'); return;
|
||||
}
|
||||
if (isNumeric(rack_from) && parseInt(rack_from) > parseInt(rack_to)) {
|
||||
alert(label_rack + ' From must be less than or equal to To.'); return;
|
||||
if (isNumeric(bin_from) && parseInt(bin_from) > parseInt(bin_to)) {
|
||||
alert(label_bin + ' From must be less than or equal to To.'); return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Simple mode — from/to, same value written to zone/aisle/rack
|
||||
// Simple mode — from/to, same value written to zone/aisle/bin
|
||||
var loc_from = $('#location_from').val().trim();
|
||||
var loc_to = $('#location_to').val().trim();
|
||||
|
||||
if (!loc_from) {
|
||||
alert('Please enter a ' + label_rack + ' From value.'); return;
|
||||
alert('Please enter a ' + label_bin + ' From value.'); return;
|
||||
}
|
||||
// If To is blank, default to same as From (single location)
|
||||
if (!loc_to) loc_to = loc_from;
|
||||
@@ -225,14 +225,14 @@
|
||||
var mf = loc_from.match(prefix_pattern);
|
||||
var mt = loc_to.match(prefix_pattern);
|
||||
if (mf && mt && mf[1] !== mt[1]) {
|
||||
alert(label_rack + ' From and To must share the same prefix (e.g. Shelf-A → Shelf-E).'); return;
|
||||
alert(label_bin + ' From and To must share the same prefix (e.g. Shelf-A → Shelf-E).'); return;
|
||||
}
|
||||
|
||||
zone = loc_from;
|
||||
aisle_from = loc_from;
|
||||
aisle_to = loc_to;
|
||||
rack_from = loc_from;
|
||||
rack_to = loc_to;
|
||||
bin_from = loc_from;
|
||||
bin_to = loc_to;
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
@@ -245,8 +245,8 @@
|
||||
zone: zone,
|
||||
aisle_from: aisle_from,
|
||||
aisle_to: aisle_to,
|
||||
rack_from: rack_from,
|
||||
rack_to: rack_to,
|
||||
bin_from: bin_from,
|
||||
bin_to: bin_to,
|
||||
description: $('#description').val(),
|
||||
},
|
||||
onSuccess: function() {
|
||||
@@ -287,11 +287,11 @@
|
||||
$('#zone').val(d.zone || '');
|
||||
$('#aisle_from').val(d.aisle_from || '');
|
||||
$('#aisle_to').val(d.aisle_to || '');
|
||||
$('#rack_from').val(d.rack_from || '');
|
||||
$('#rack_to').val(d.rack_to || '');
|
||||
$('#bin_from').val(d.bin_from || '');
|
||||
$('#bin_to').val(d.bin_to || '');
|
||||
} else {
|
||||
$('#location_from').val(d.rack_from || '');
|
||||
$('#location_to').val(d.rack_to || '');
|
||||
$('#location_from').val(d.bin_from || '');
|
||||
$('#location_to').val(d.bin_to || '');
|
||||
}
|
||||
$('button[type=submit]').text('Update Storage');
|
||||
$('button[type=reset]').hide();
|
||||
|
||||
+16
-16
@@ -60,7 +60,7 @@
|
||||
<div class="d-flex flex-column">
|
||||
<span class="fs-2 fw-bold mb-3 d-block">—</span>
|
||||
<span class="fs-5">Total Capacity</span>
|
||||
<small class="text-warning">Rack positions available</small>
|
||||
<small class="text-warning">Bin positions available</small>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-warning">
|
||||
@@ -87,7 +87,7 @@
|
||||
<div class="d-flex flex-column">
|
||||
<span class="fs-2 fw-bold mb-3 d-block">—</span>
|
||||
<span class="fs-5">Space Used</span>
|
||||
<small class="text-success">Racks occupied</small>
|
||||
<small class="text-success">Bins occupied</small>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-success">
|
||||
@@ -206,7 +206,7 @@
|
||||
<div class="mb-6 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h2 class="fs-4 mb-2">Storage Locations</h2>
|
||||
<p id="storage_subtitle">Zones, aisles, and rack assignments</p>
|
||||
<p id="storage_subtitle">Zones, aisles, and bin assignments</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<?php echo $server_url?>inventory/manage_storage.php" class="btn btn-secondary" data-min-role="admin">
|
||||
@@ -234,7 +234,7 @@
|
||||
<th class="py-3">Warehouse</th>
|
||||
<th class="py-3" id="th_level1">Zone</th>
|
||||
<th class="py-3" id="th_level2">Aisle Range</th>
|
||||
<th class="py-3" id="th_level3">Rack Range</th>
|
||||
<th class="py-3" id="th_level3">Bin Range</th>
|
||||
<th class="py-3">Items Stored</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -257,7 +257,7 @@
|
||||
<script>
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
@@ -280,26 +280,26 @@
|
||||
|
||||
function apply_location_labels() {
|
||||
// Stats card subtitles
|
||||
$('#total_capacity small').text(label_rack + ' positions available');
|
||||
$('#total_space_used small').text(label_rack + 's occupied');
|
||||
$('#total_capacity small').text(label_bin + ' positions available');
|
||||
$('#total_space_used small').text(label_bin + 's occupied');
|
||||
|
||||
// Storage tab subtitle + search
|
||||
$('#storage_subtitle').text(advanced
|
||||
? label_zone + 's, ' + label_aisle + 's, and ' + label_rack + ' assignments'
|
||||
: label_rack + ' assignments');
|
||||
? label_zone + 's, ' + label_aisle + 's, and ' + label_bin + ' assignments'
|
||||
: label_bin + ' assignments');
|
||||
$('#search_storage').attr('placeholder', advanced
|
||||
? 'Search by warehouse or ' + label_zone.toLowerCase() + '…'
|
||||
: 'Search by warehouse or ' + label_rack.toLowerCase() + '…');
|
||||
: 'Search by warehouse or ' + label_bin.toLowerCase() + '…');
|
||||
|
||||
// Table headers
|
||||
if (advanced) {
|
||||
$('#th_level1').text(label_zone).show();
|
||||
$('#th_level2').text(label_aisle + ' Range').show();
|
||||
$('#th_level3').text(label_rack + ' Range');
|
||||
$('#th_level3').text(label_bin + ' Range');
|
||||
} else {
|
||||
$('#th_level1').hide();
|
||||
$('#th_level2').hide();
|
||||
$('#th_level3').text(label_rack + ' Range');
|
||||
$('#th_level3').text(label_bin + ' Range');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@
|
||||
alasql(`SELECT * FROM storage
|
||||
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
|
||||
OR LOWER(zone) LIKE '%${kw}%'
|
||||
OR LOWER(rack) LIKE '%${kw}%'`) :
|
||||
OR LOWER(bin) LIKE '%${kw}%'`) :
|
||||
alasql(`SELECT * FROM storage`);
|
||||
|
||||
var page = generate_pagination('storage', filtered.length);
|
||||
@@ -452,8 +452,8 @@
|
||||
var location_cells = advanced
|
||||
? `<td class="py-3">${item['zone']}</td>
|
||||
<td class="py-3">${item['aisle_from']} - ${item['aisle_to']}</td>
|
||||
<td class="py-3">${item['rack_from']} - ${item['rack_to']}</td>`
|
||||
: `<td class="py-3">${item['rack_from']} - ${item['rack_to']}</td>`;
|
||||
<td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`
|
||||
: `<td class="py-3">${item['bin_from']} - ${item['bin_to']}</td>`;
|
||||
|
||||
body += `<tr>
|
||||
<td class="py-3">${item['warehouse_name']}</td>
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
* b. The elapsed time since otpTime is ≤ 5 minutes.
|
||||
* Fail either → return "Wrong OTP! Please try again."
|
||||
* 5. On success:
|
||||
* a. Check session_token in DB — if non-NULL, another session is active;
|
||||
* reject login with "account already logged in" message.
|
||||
* a. Force-replace session_token in DB — writing a new token invalidates
|
||||
* any prior session (old device gets kicked out by db_auth.php on its
|
||||
* next request). No block: password + OTP is full 2FA proof of identity.
|
||||
* b. session_regenerate_id(true) — prevents session fixation attack by
|
||||
* issuing a new session ID and deleting the old one.
|
||||
* c. Generate a fresh CSRF token and store in session.
|
||||
@@ -105,31 +106,28 @@ if (empty($_SESSION['skip_otp'])) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 4b: Claim session atomically ─────────────────────────────────────────
|
||||
// The WHERE clause is the concurrency gate: only a free or stale token can be
|
||||
// replaced. session_last_seen is set immediately so a fresh login is live before
|
||||
// the first authenticated heartbeat in db_auth.php.
|
||||
// ── Step 4b: Claim session — always replace existing token ────────────────────
|
||||
// Password + OTP is full 2FA proof of identity, so we always grant the login.
|
||||
// Writing a new token here also invalidates any prior session: db_auth.php
|
||||
// compares session_token in the DB to the one stored in the PHP session, so the
|
||||
// old device is kicked out on its next request. This also fixes the case where
|
||||
// a PHP session expired without clearing the DB token, which would otherwise
|
||||
// permanently block re-login.
|
||||
$session_token = bin2hex(random_bytes(32));
|
||||
$sth_claim = $pdo1->prepare(
|
||||
"UPDATE user
|
||||
SET session_token = :token,
|
||||
session_token_at = NOW(),
|
||||
SET session_token = :token,
|
||||
session_token_at = NOW(),
|
||||
session_last_seen = NOW()
|
||||
WHERE user_id = :uid
|
||||
AND (
|
||||
session_token IS NULL
|
||||
OR session_last_seen IS NULL
|
||||
OR TIMESTAMPDIFF(SECOND, session_last_seen, NOW()) >= :gc_maxlifetime
|
||||
)"
|
||||
WHERE user_id = :uid"
|
||||
);
|
||||
$sth_claim->execute([
|
||||
':token' => $session_token,
|
||||
':uid' => $user_id,
|
||||
':gc_maxlifetime' => (int)ini_get('session.gc_maxlifetime'),
|
||||
':token' => $session_token,
|
||||
':uid' => $user_id,
|
||||
]);
|
||||
|
||||
if ($sth_claim->rowCount() !== 1) {
|
||||
$answer["message"] = "This account is currently logged in on another device. Please wait for the other session to end.";
|
||||
$answer["message"] = "Login failed: user record not found.";
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@
|
||||
warehouse_id: wh,
|
||||
zone: '',
|
||||
aisle: '',
|
||||
rack: '',
|
||||
bin: '',
|
||||
lot_number: '',
|
||||
serial_number: '',
|
||||
quantity: qty,
|
||||
@@ -566,7 +566,7 @@
|
||||
// ── Confirm order ─────────────────────────────────────────────────────────
|
||||
function confirm_order() {
|
||||
bootbox.confirm({
|
||||
message: 'Confirm this order? Stock-out rows will be created for all items using FIFO rack selection.',
|
||||
message: 'Confirm this order? Stock-out rows will be created for all items using FIFO bin selection.',
|
||||
buttons: {
|
||||
confirm: { label: 'Confirm Order', className: 'btn-success' },
|
||||
cancel: { label: 'Back', className: 'btn-secondary' }
|
||||
|
||||
+40
-40
@@ -102,7 +102,7 @@
|
||||
<th class="th_return_wh" style="width:150px;">Warehouse</th>
|
||||
<th class="th_return_zone" style="width:120px;">Zone</th>
|
||||
<th class="th_return_aisle" style="width:120px;">Aisle</th>
|
||||
<th class="th_return_rack" style="width:120px;">Location</th>
|
||||
<th class="th_return_bin" style="width:120px;">Location</th>
|
||||
<th class="text-end" style="width:110px;">Return Qty</th>
|
||||
<th class="text-end" style="width:100px;">Unit Price</th>
|
||||
<th style="width:70px;">Tax%</th>
|
||||
@@ -215,7 +215,7 @@
|
||||
var returnable = []; // returnable_items from order
|
||||
var warehouses = [];
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -276,16 +276,16 @@
|
||||
$('.th_return_wh').text('Warehouse');
|
||||
$('.th_return_zone').text(label_zone).toggleClass('d-none', !advanced);
|
||||
$('.th_return_aisle').text(label_aisle).toggleClass('d-none', !advanced);
|
||||
$('.th_return_rack').text(label_rack);
|
||||
$('.th_return_bin').text(label_bin);
|
||||
$('#loading_row td').attr('colspan', 12);
|
||||
}
|
||||
|
||||
function retrieve_return_zones(item_id, selected_zone, selected_aisle, selected_rack) {
|
||||
function retrieve_return_zones(item_id, selected_zone, selected_aisle, selected_bin) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
var wh = $row.find('.item_return_warehouse').val();
|
||||
$row.find('.item_return_zone').html(`<option value="">Select ${label_zone.toLowerCase()}...</option>`);
|
||||
$row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
|
||||
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
|
||||
$row.find('.item_return_bin').html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
|
||||
if (!wh) return Promise.resolve();
|
||||
|
||||
return ajax_request({
|
||||
@@ -307,7 +307,7 @@
|
||||
opts += `<option value="${escape_html(selected_zone)}">${escape_html(selected_zone)}</option>`;
|
||||
}
|
||||
$row.find('.item_return_zone').html(opts).val(selected_zone || '');
|
||||
if (selected_zone) retrieve_return_aisles(item_id, selected_aisle, selected_rack);
|
||||
if (selected_zone) retrieve_return_aisles(item_id, selected_aisle, selected_bin);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -316,35 +316,35 @@
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
$row.find('.item_return_zone').val('');
|
||||
$row.find('.item_return_aisle').val('');
|
||||
$row.find('.item_return_rack').val('');
|
||||
$row.find('.item_return_bin').val('');
|
||||
|
||||
if (!$row.find('.item_check').is(':checked')) return;
|
||||
if (advanced) {
|
||||
retrieve_return_zones(item_id);
|
||||
} else {
|
||||
retrieve_return_racks(item_id);
|
||||
retrieve_return_bins(item_id);
|
||||
}
|
||||
}
|
||||
|
||||
function change_return_zone(item_id) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
$row.find('.item_return_aisle').val('');
|
||||
$row.find('.item_return_rack').val('');
|
||||
$row.find('.item_return_bin').val('');
|
||||
if ($row.find('.item_check').is(':checked')) retrieve_return_aisles(item_id);
|
||||
}
|
||||
|
||||
function change_return_aisle(item_id) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
$row.find('.item_return_rack').val('');
|
||||
if ($row.find('.item_check').is(':checked')) retrieve_return_racks(item_id);
|
||||
$row.find('.item_return_bin').val('');
|
||||
if ($row.find('.item_check').is(':checked')) retrieve_return_bins(item_id);
|
||||
}
|
||||
|
||||
function retrieve_return_aisles(item_id, selected_aisle, selected_rack) {
|
||||
function retrieve_return_aisles(item_id, selected_aisle, selected_bin) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
var wh = $row.find('.item_return_warehouse').val();
|
||||
var zone = $row.find('.item_return_zone').val();
|
||||
$row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
|
||||
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
|
||||
$row.find('.item_return_bin').html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
|
||||
if (!wh || !zone) return Promise.resolve();
|
||||
|
||||
return ajax_request({
|
||||
@@ -366,21 +366,21 @@
|
||||
opts += `<option value="${escape_html(selected_aisle)}">${escape_html(selected_aisle)}</option>`;
|
||||
}
|
||||
$row.find('.item_return_aisle').html(opts).val(selected_aisle || '');
|
||||
if (selected_aisle) retrieve_return_racks(item_id, selected_rack);
|
||||
if (selected_aisle) retrieve_return_bins(item_id, selected_bin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_return_racks(item_id, selected_rack) {
|
||||
function retrieve_return_bins(item_id, selected_bin) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
var wh = $row.find('.item_return_warehouse').val();
|
||||
var zone = advanced ? $row.find('.item_return_zone').val() : '';
|
||||
var aisle = advanced ? $row.find('.item_return_aisle').val() : '';
|
||||
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
|
||||
$row.find('.item_return_bin').html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
|
||||
if (!wh || (advanced && (!zone || !aisle))) return Promise.resolve();
|
||||
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php',
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_bin.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
@@ -388,16 +388,16 @@
|
||||
action: 'read',
|
||||
data: { type: 'to', warehouse: wh, zone: zone, aisle: aisle, id: 0 },
|
||||
onSuccess: function(res) {
|
||||
var opts = `<option value="">Select ${label_rack.toLowerCase()}...</option>`;
|
||||
var has_selected = !selected_rack;
|
||||
var opts = `<option value="">Select ${label_bin.toLowerCase()}...</option>`;
|
||||
var has_selected = !selected_bin;
|
||||
$.each(res.output || [], function(_, value) {
|
||||
if (String(value) === String(selected_rack)) has_selected = true;
|
||||
if (String(value) === String(selected_bin)) has_selected = true;
|
||||
opts += `<option value="${escape_html(value)}">${escape_html(value)}</option>`;
|
||||
});
|
||||
if (!has_selected) {
|
||||
opts += `<option value="${escape_html(selected_rack)}">${escape_html(selected_rack)}</option>`;
|
||||
opts += `<option value="${escape_html(selected_bin)}">${escape_html(selected_bin)}</option>`;
|
||||
}
|
||||
$row.find('.item_return_rack').html(opts).val(selected_rack || '');
|
||||
$row.find('.item_return_bin').html(opts).val(selected_bin || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -406,19 +406,19 @@
|
||||
var wh = selected.warehouse_id || '';
|
||||
var zone = selected.zone || '';
|
||||
var aisle = selected.aisle || '';
|
||||
var rack = selected.rack || '';
|
||||
var bin = selected.bin || '';
|
||||
|
||||
if (advanced) {
|
||||
return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td>
|
||||
<td><select class="form-select form-select-sm item_return_zone" onchange="change_return_zone(${item_id})"><option value="">Select ${label_zone.toLowerCase()}...</option></select></td>
|
||||
<td><select class="form-select form-select-sm item_return_aisle" onchange="change_return_aisle(${item_id})"><option value="">Select ${label_aisle.toLowerCase()}...</option></select></td>
|
||||
<td><select class="form-select form-select-sm item_return_rack"><option value="">Select ${label_rack.toLowerCase()}...</option></select></td>`;
|
||||
<td><select class="form-select form-select-sm item_return_bin"><option value="">Select ${label_bin.toLowerCase()}...</option></select></td>`;
|
||||
}
|
||||
|
||||
return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td>
|
||||
<td class="d-none"><select class="form-select form-select-sm item_return_zone"><option value=""></option></select></td>
|
||||
<td class="d-none"><select class="form-select form-select-sm item_return_aisle"><option value=""></option></select></td>
|
||||
<td><select class="form-select form-select-sm item_return_rack"><option value="">Select ${label_rack.toLowerCase()}...</option></select></td>`;
|
||||
<td><select class="form-select form-select-sm item_return_bin"><option value="">Select ${label_bin.toLowerCase()}...</option></select></td>`;
|
||||
}
|
||||
|
||||
function hydrate_location_selectors(items, selected_map) {
|
||||
@@ -427,15 +427,15 @@
|
||||
var $row = $(`#ret_item_${item.item_id}`);
|
||||
if (!$row.find('.item_check').is(':checked')) return;
|
||||
|
||||
if (!selected.warehouse_id && !selected.zone && !selected.aisle && !selected.rack) {
|
||||
if (!selected.warehouse_id && !selected.zone && !selected.aisle && !selected.bin) {
|
||||
change_return_warehouse(item.item_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (advanced) {
|
||||
retrieve_return_zones(item.item_id, selected.zone || '', selected.aisle || '', selected.rack || '');
|
||||
} else if (selected.rack) {
|
||||
retrieve_return_racks(item.item_id, selected.rack || '');
|
||||
retrieve_return_zones(item.item_id, selected.zone || '', selected.aisle || '', selected.bin || '');
|
||||
} else if (selected.bin) {
|
||||
retrieve_return_bins(item.item_id, selected.bin || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -498,7 +498,7 @@
|
||||
warehouse_id: si ? (si.warehouse_id || item.warehouse_id || '') : (item.warehouse_id || ''),
|
||||
zone: si ? (si.zone || '') : (item.zone || ''),
|
||||
aisle: si ? (si.aisle || '') : (item.aisle || ''),
|
||||
rack: si ? (si.rack || '') : (item.rack || ''),
|
||||
bin: si ? (si.bin || '') : (item.bin || ''),
|
||||
};
|
||||
|
||||
rows += `<tr class="return-item-row" id="ret_item_${item.item_id}">
|
||||
@@ -514,7 +514,7 @@
|
||||
<input type="hidden" class="item_warehouse_id" value="${item.warehouse_id || 0}">
|
||||
<input type="hidden" class="item_zone" value="${item.zone || ''}">
|
||||
<input type="hidden" class="item_aisle" value="${item.aisle || ''}">
|
||||
<input type="hidden" class="item_rack" value="${item.rack || ''}">
|
||||
<input type="hidden" class="item_bin" value="${item.bin || ''}">
|
||||
<input type="hidden" class="item_lot" value="${item.lot_number || ''}">
|
||||
<input type="hidden" class="item_serial" value="${item.serial_number || ''}">
|
||||
<input type="hidden" class="item_stock_out_id" value="${item.stock_out_id || 0}">
|
||||
@@ -561,7 +561,7 @@
|
||||
function toggle_item_row(item_id) {
|
||||
var $row = $(`#ret_item_${item_id}`);
|
||||
var checked = $row.find('.item_check').is(':checked');
|
||||
$row.find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack').prop('disabled', !checked);
|
||||
$row.find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_bin').prop('disabled', !checked);
|
||||
if (!checked) {
|
||||
$row.find('.item_total').text('—');
|
||||
return;
|
||||
@@ -631,7 +631,7 @@
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_headers();
|
||||
@@ -664,12 +664,12 @@
|
||||
var tax_rate = parseFloat($row.find('.item_tax_rate').val()) || 0;
|
||||
if (qty <= 0) return;
|
||||
var return_wh = parseInt($row.find('.item_return_warehouse').val()) || 0;
|
||||
var return_rack = $row.find('.item_return_rack').val() || '';
|
||||
var return_zone = advanced ? ($row.find('.item_return_zone').val() || '') : return_rack;
|
||||
var return_aisle = advanced ? ($row.find('.item_return_aisle').val() || '') : return_rack;
|
||||
var return_bin = $row.find('.item_return_bin').val() || '';
|
||||
var return_zone = advanced ? ($row.find('.item_return_zone').val() || '') : return_bin;
|
||||
var return_aisle = advanced ? ($row.find('.item_return_aisle').val() || '') : return_bin;
|
||||
|
||||
if (!return_wh || !return_rack || (advanced && (!return_zone || !return_aisle))) {
|
||||
throw new Error('Please select return warehouse and ' + label_rack.toLowerCase() + ' for every checked item.');
|
||||
if (!return_wh || !return_bin || (advanced && (!return_zone || !return_aisle))) {
|
||||
throw new Error('Please select return warehouse and ' + label_bin.toLowerCase() + ' for every checked item.');
|
||||
}
|
||||
|
||||
items.push({
|
||||
@@ -679,7 +679,7 @@
|
||||
warehouse_id: return_wh,
|
||||
zone: return_zone,
|
||||
aisle: return_aisle,
|
||||
rack: return_rack,
|
||||
bin: return_bin,
|
||||
lot_number: $row.find('.item_lot').val(),
|
||||
serial_number: $row.find('.item_serial').val(),
|
||||
stock_out_id: parseInt($row.find('.item_stock_out_id').val()) || 0,
|
||||
@@ -750,7 +750,7 @@
|
||||
$('.item_check').prop('disabled', !is_draft);
|
||||
$('#item_tbody tr.return-item-row').each(function() {
|
||||
var checked = $(this).find('.item_check').is(':checked');
|
||||
$(this).find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack')
|
||||
$(this).find('.item_qty, .item_price, .item_tax_rate, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_bin')
|
||||
.prop('disabled', !is_draft || !checked);
|
||||
});
|
||||
|
||||
|
||||
+21
-21
@@ -124,7 +124,7 @@
|
||||
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||
<div>
|
||||
<h2 class="fs-5 mb-1">Receive Goods</h2>
|
||||
<p class="mb-0 text-muted small">Enter quantities received and assign rack locations for each line</p>
|
||||
<p class="mb-0 text-muted small">Enter quantities received and assign bin locations for each line</p>
|
||||
</div>
|
||||
<button class="btn btn-success" id="btn_receive" onclick="receive_goods()">
|
||||
<i class="ti ti-package-import me-1"></i>Confirm Receipt
|
||||
@@ -255,7 +255,7 @@
|
||||
|
||||
// ── Location config (loaded from company settings) ────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -393,7 +393,7 @@
|
||||
});
|
||||
$(`#recv_zone_${rowId}`).html(opts);
|
||||
$(`#recv_aisle_${rowId}`).html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
|
||||
$(`#recv_rack_${rowId}`).html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
|
||||
$(`#recv_bin_${rowId}`).html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
@@ -414,28 +414,28 @@
|
||||
opts += `<option value="${escape_html(a)}">${escape_html(a)}</option>`;
|
||||
});
|
||||
$(`#recv_aisle_${rowId}`).html(opts);
|
||||
$(`#recv_rack_${rowId}`).html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
|
||||
$(`#recv_bin_${rowId}`).html(`<option value="">Select ${label_bin.toLowerCase()}...</option>`);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function recv_load_racks(rowId, callback) {
|
||||
function recv_load_bins(rowId, callback) {
|
||||
var wh_id = $(`#recv_wh_${rowId}`).val();
|
||||
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
|
||||
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
|
||||
if (!wh_id) return;
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php',
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_bin.php',
|
||||
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
|
||||
action: 'read',
|
||||
data: { type: 'to', warehouse_to: wh_id, zone_to: zone, aisle_to: aisle, id: 0 },
|
||||
onSuccess: function(res) {
|
||||
var opts = `<option value="">Select ${label_rack.toLowerCase()}...</option>`;
|
||||
var opts = `<option value="">Select ${label_bin.toLowerCase()}...</option>`;
|
||||
(res.output || []).forEach(function(r) {
|
||||
opts += `<option value="${escape_html(r)}">${escape_html(r)}</option>`;
|
||||
});
|
||||
$(`#recv_rack_${rowId}`).html(opts);
|
||||
$(`#recv_bin_${rowId}`).html(opts);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
@@ -476,7 +476,7 @@
|
||||
if (advanced) {
|
||||
recv_load_zones(rowId);
|
||||
} else {
|
||||
recv_load_racks(rowId);
|
||||
recv_load_bins(rowId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,13 +507,13 @@
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label small text-muted">${label_aisle}</label>
|
||||
<select id="recv_aisle_${rowId}" class="form-select form-select-sm"
|
||||
onchange="recv_load_racks('${rowId}')">
|
||||
onchange="recv_load_bins('${rowId}')">
|
||||
<option value="">Select ${label_aisle.toLowerCase()}...</option>
|
||||
</select>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
var rack_col_class = advanced ? 'col-lg-4' : 'col-lg-6';
|
||||
var bin_col_class = advanced ? 'col-lg-4' : 'col-lg-6';
|
||||
|
||||
html += `
|
||||
<div class="border rounded p-4 mb-4"
|
||||
@@ -569,10 +569,10 @@
|
||||
|
||||
${zone_aisle_html}
|
||||
|
||||
<div class="${rack_col_class}">
|
||||
<label class="form-label small text-muted">${label_rack} <span class="text-danger">*</span></label>
|
||||
<select id="recv_rack_${rowId}" class="form-select form-select-sm">
|
||||
<option value="">Select ${label_rack.toLowerCase()}...</option>
|
||||
<div class="${bin_col_class}">
|
||||
<label class="form-label small text-muted">${label_bin} <span class="text-danger">*</span></label>
|
||||
<select id="recv_bin_${rowId}" class="form-select form-select-sm">
|
||||
<option value="">Select ${label_bin.toLowerCase()}...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -771,22 +771,22 @@
|
||||
if (qty <= 0) return;
|
||||
|
||||
var wh_id = parseInt($(`#recv_wh_${rowId}`).val()) || 0;
|
||||
var rack = $(`#recv_rack_${rowId}`).val() || '';
|
||||
var bin = $(`#recv_bin_${rowId}`).val() || '';
|
||||
|
||||
if (!wh_id) {
|
||||
errors.push(`${$card.data('sku')}: please select a warehouse.`);
|
||||
return;
|
||||
}
|
||||
if (!rack) {
|
||||
errors.push(`${$card.data('sku')}: please select a ${label_rack.toLowerCase()}.`);
|
||||
if (!bin) {
|
||||
errors.push(`${$card.data('sku')}: please select a ${label_bin.toLowerCase()}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
|
||||
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
|
||||
if (!advanced) {
|
||||
zone = rack;
|
||||
aisle = rack;
|
||||
zone = bin;
|
||||
aisle = bin;
|
||||
}
|
||||
|
||||
// flatpickr with altInput: the real (hidden) input holds the ISO value
|
||||
@@ -802,7 +802,7 @@
|
||||
serial_number: $(`#recv_serial_${rowId}`).val().trim(),
|
||||
zone: zone,
|
||||
aisle: aisle,
|
||||
rack: rack,
|
||||
bin: bin,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
<input type="hidden" class="item_warehouse" value="${item.warehouse_id || 0}">
|
||||
<input type="hidden" class="item_zone" value="${item.zone || ''}">
|
||||
<input type="hidden" class="item_aisle" value="${item.aisle || ''}">
|
||||
<input type="hidden" class="item_rack" value="${item.rack || ''}">
|
||||
<input type="hidden" class="item_bin" value="${item.bin || ''}">
|
||||
<input type="hidden" class="item_stock_in_id" value="${item.stock_in_id || 0}">
|
||||
<input type="hidden" class="item_lot" value="${item.lot_number || ''}">
|
||||
<input type="hidden" class="item_serial" value="${item.serial_number || ''}">
|
||||
@@ -465,7 +465,7 @@
|
||||
warehouse_id: parseInt($row.find('.item_warehouse').val()) || 0,
|
||||
zone: $row.find('.item_zone').val(),
|
||||
aisle: $row.find('.item_aisle').val(),
|
||||
rack: $row.find('.item_rack').val(),
|
||||
bin: $row.find('.item_bin').val(),
|
||||
stock_in_id: parseInt($row.find('.item_stock_in_id').val()) || 0,
|
||||
lot_number: $row.find('.item_lot').val(),
|
||||
serial_number: $row.find('.item_serial').val(),
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
(new UsageGuard($pdo1, $company_id, $packages))->assertFeatureAccessible('reports');
|
||||
require_once '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$rack_id = (int)($data['rack_id'] ?? 0);
|
||||
$bin_id = (int)($data['bin_id'] ?? 0);
|
||||
|
||||
if (!$rack_id) {
|
||||
if (!$bin_id) {
|
||||
$answer['output'] = [];
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id, $db_database);
|
||||
$answer['output'] = $report->getRackLog($rack_id);
|
||||
$answer['output'] = $report->getBinLog($bin_id);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -194,22 +194,22 @@
|
||||
var search_term = '';
|
||||
var filter_status = '';
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function apply_location_labels() {
|
||||
$('.th_location').text(advanced
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
|
||||
: label_rack
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_bin
|
||||
: label_bin
|
||||
);
|
||||
}
|
||||
|
||||
function format_location(item) {
|
||||
if (advanced) {
|
||||
return [item.zone, item.aisle, item.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
||||
return [item.zone, item.aisle, item.bin].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
||||
}
|
||||
return item.rack || '<span class="text-muted">—</span>';
|
||||
return item.bin || '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
function load_company_setting() {
|
||||
@@ -221,7 +221,7 @@
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
|
||||
+50
-50
@@ -18,7 +18,7 @@
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Rack Occupancy</h1>
|
||||
<p class="mb-0">View occupied and available rack positions across all warehouses</p>
|
||||
<p class="mb-0">View occupied and available bin positions across all warehouses</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,13 +112,13 @@
|
||||
<div class="mb-6 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="mb-1 fs-4">All Racks</h3>
|
||||
<p class="mb-0">Complete rack list across all warehouses</p>
|
||||
<p class="mb-0">Complete bin list across all warehouses</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 mb-5">
|
||||
<div class="position-relative">
|
||||
<input type="search" id="search_all" class="form-control ps-9"
|
||||
placeholder="Search by warehouse, zone, aisle, rack, or SKU…">
|
||||
placeholder="Search by warehouse, zone, aisle, bin, or SKU…">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
@@ -131,7 +131,7 @@
|
||||
<th>Warehouse</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th class="th_loc_bin">Rack</th>
|
||||
<th>SKU</th>
|
||||
<th>Product</th>
|
||||
<th>Status</th>
|
||||
@@ -161,7 +161,7 @@
|
||||
<div class="col-lg-12 mb-5">
|
||||
<div class="position-relative">
|
||||
<input type="search" id="search_occupied" class="form-control ps-9"
|
||||
placeholder="Search by warehouse, zone, aisle, rack, or SKU…">
|
||||
placeholder="Search by warehouse, zone, aisle, bin, or SKU…">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
@@ -174,7 +174,7 @@
|
||||
<th>Warehouse</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th class="th_loc_bin">Rack</th>
|
||||
<th>SKU</th>
|
||||
<th>Product</th>
|
||||
<th>Action</th>
|
||||
@@ -203,7 +203,7 @@
|
||||
<div class="col-lg-12 mb-5">
|
||||
<div class="position-relative">
|
||||
<input type="search" id="search_empty" class="form-control ps-9"
|
||||
placeholder="Search by warehouse, zone, aisle, or rack…">
|
||||
placeholder="Search by warehouse, zone, aisle, or bin…">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
@@ -216,7 +216,7 @@
|
||||
<th>Warehouse</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th class="th_loc_bin">Rack</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -231,12 +231,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Rack Log Modal -->
|
||||
<div class="modal fade" id="rackLogModal" tabindex="-1" aria-labelledby="rackLogModalLabel" aria-hidden="true">
|
||||
<!-- Bin Log Modal -->
|
||||
<div class="modal fade" id="binLogModal" tabindex="-1" aria-labelledby="binLogModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="rackLogModalLabel">Rack Log</h5>
|
||||
<h5 class="modal-title" id="binLogModalLabel">Bin Log</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
@@ -249,7 +249,7 @@
|
||||
<th>User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rack_log_body">
|
||||
<tbody id="bin_log_body">
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -267,7 +267,7 @@
|
||||
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -276,21 +276,21 @@
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
} else {
|
||||
$('.th_loc_zone').hide();
|
||||
$('.th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
$('.th_loc_bin').text(label_bin);
|
||||
}
|
||||
var loc_hint = advanced
|
||||
? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_rack.toLowerCase()
|
||||
: label_rack.toLowerCase();
|
||||
? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_bin.toLowerCase()
|
||||
: label_bin.toLowerCase();
|
||||
$('#search_all, #search_occupied').attr('placeholder',
|
||||
'Search by warehouse, ' + loc_hint + ', or SKU…');
|
||||
$('#search_empty').attr('placeholder',
|
||||
'Search by warehouse or ' + loc_hint + '…');
|
||||
$('#rackLogModalLabel').text(label_rack + ' Log');
|
||||
$('#stat_label_racks').text('Total ' + label_rack + 's');
|
||||
$('#binLogModalLabel').text(label_bin + ' Log');
|
||||
$('#stat_label_bins').text('Total ' + label_bin + 's');
|
||||
}
|
||||
|
||||
// Returns 1 or 3 <td> cells depending on mode
|
||||
@@ -298,16 +298,16 @@
|
||||
if (advanced) {
|
||||
return '<td class="py-3">' + (item.zone || '—') + '</td>'
|
||||
+ '<td class="py-3">' + (item.aisle || '—') + '</td>'
|
||||
+ '<td class="py-3">' + (item.rack || '—') + '</td>';
|
||||
+ '<td class="py-3">' + (item.bin || '—') + '</td>';
|
||||
}
|
||||
return '<td class="py-3">' + (item.rack || '—') + '</td>';
|
||||
return '<td class="py-3">' + (item.bin || '—') + '</td>';
|
||||
}
|
||||
|
||||
// Rack log modal label
|
||||
function rack_label(item) {
|
||||
function bin_label(item) {
|
||||
return advanced
|
||||
? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.rack
|
||||
: item.warehouse_name + ' ' + item.rack;
|
||||
? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.bin
|
||||
: item.warehouse_name + ' ' + item.bin;
|
||||
}
|
||||
|
||||
var search_all = '';
|
||||
@@ -316,10 +316,10 @@
|
||||
|
||||
|
||||
// ── Fetch ─────────────────────────────────────────────────────────────
|
||||
function retrieve_rack_occupancy() {
|
||||
function retrieve_bin_occupancy() {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/rack_occupancy.php",
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/bin_occupancy.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
@@ -338,8 +338,8 @@
|
||||
$('#stat_rate').text(rate + '%');
|
||||
|
||||
// Store in alasql
|
||||
alasql('CREATE TABLE IF NOT EXISTS rack_all');
|
||||
alasql.tables.rack_all.data = res.output || [];
|
||||
alasql('CREATE TABLE IF NOT EXISTS bin_all');
|
||||
alasql.tables.bin_all.data = res.output || [];
|
||||
|
||||
change_page_all(1);
|
||||
change_page_occupied(1);
|
||||
@@ -356,14 +356,14 @@
|
||||
var kw = search_all.toLowerCase();
|
||||
|
||||
var filtered = kw ? alasql(
|
||||
`SELECT * FROM rack_all
|
||||
`SELECT * FROM bin_all
|
||||
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
|
||||
OR LOWER(zone) LIKE '%${kw}%'
|
||||
OR LOWER(aisle) LIKE '%${kw}%'
|
||||
OR LOWER(rack) LIKE '%${kw}%'
|
||||
OR LOWER(bin) LIKE '%${kw}%'
|
||||
OR LOWER(product_sku) LIKE '%${kw}%'
|
||||
OR LOWER(product_name) LIKE '%${kw}%'`
|
||||
) : alasql('SELECT * FROM rack_all');
|
||||
) : alasql('SELECT * FROM bin_all');
|
||||
|
||||
$('table#tbl_all tfoot').html(generate_pagination('tbl_all', filtered.length));
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
<td class="py-3">${item.product_sku || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${badge}</td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_bin_log(${item.id}, '${bin_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_all > tbody').html(body);
|
||||
@@ -398,13 +398,13 @@
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var kw = search_occupied.toLowerCase();
|
||||
|
||||
var base = alasql("SELECT * FROM rack_all WHERE status = 'occupied'");
|
||||
var base = alasql("SELECT * FROM bin_all WHERE status = 'occupied'");
|
||||
|
||||
var filtered = kw ? base.filter(function(item) {
|
||||
return (item.warehouse_name || '').toLowerCase().includes(kw)
|
||||
|| (item.zone || '').toLowerCase().includes(kw)
|
||||
|| (item.aisle || '').toLowerCase().includes(kw)
|
||||
|| (item.rack || '').toLowerCase().includes(kw)
|
||||
|| (item.bin || '').toLowerCase().includes(kw)
|
||||
|| (item.product_sku || '').toLowerCase().includes(kw)
|
||||
|| (item.product_name || '').toLowerCase().includes(kw);
|
||||
}) : base;
|
||||
@@ -425,7 +425,7 @@
|
||||
` + location_cells(item) + `
|
||||
<td class="py-3">${item.product_sku}</td>
|
||||
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_bin_log(${item.id}, '${bin_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_occupied > tbody').html(body);
|
||||
@@ -438,13 +438,13 @@
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var kw = search_empty.toLowerCase();
|
||||
|
||||
var base = alasql("SELECT * FROM rack_all WHERE status = 'empty'");
|
||||
var base = alasql("SELECT * FROM bin_all WHERE status = 'empty'");
|
||||
|
||||
var filtered = kw ? base.filter(function(item) {
|
||||
return (item.warehouse_name || '').toLowerCase().includes(kw)
|
||||
|| (item.zone || '').toLowerCase().includes(kw)
|
||||
|| (item.aisle || '').toLowerCase().includes(kw)
|
||||
|| (item.rack || '').toLowerCase().includes(kw);
|
||||
|| (item.bin || '').toLowerCase().includes(kw);
|
||||
}) : base;
|
||||
|
||||
$('table#tbl_empty tfoot').html(generate_pagination('tbl_empty', filtered.length));
|
||||
@@ -461,7 +461,7 @@
|
||||
body += `<tr>
|
||||
<td class="py-3">${item.warehouse_name}</td>
|
||||
` + location_cells(item) + `
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${rack_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_bin_log(${item.id}, '${bin_label(item)}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_empty > tbody').html(body);
|
||||
@@ -501,30 +501,30 @@
|
||||
// Remember last active tab
|
||||
$(document).ready(function() {
|
||||
$('button.nav-link').on('click', function() {
|
||||
sessionStorage.setItem('rack_nav_id', $(this).attr('id'));
|
||||
sessionStorage.setItem('bin_nav_id', $(this).attr('id'));
|
||||
});
|
||||
var nav_id = sessionStorage.getItem('rack_nav_id');
|
||||
var nav_id = sessionStorage.getItem('bin_nav_id');
|
||||
if (nav_id) $(`button#${nav_id}`).trigger('click');
|
||||
});
|
||||
|
||||
|
||||
// ── Rack Log ─────────────────────────────────────────────────────────
|
||||
function view_rack_log(rack_id, label) {
|
||||
// ── Bin Log ──────────────────────────────────────────────────────────
|
||||
function view_bin_log(bin_id, label) {
|
||||
|
||||
$('#rackLogModalLabel').text('Rack Log — ' + label);
|
||||
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>');
|
||||
$('#rackLogModal').modal('show');
|
||||
$('#binLogModalLabel').text('Bin Log — ' + label);
|
||||
$('#bin_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>');
|
||||
$('#binLogModal').modal('show');
|
||||
|
||||
ajax_request({
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/rack_log.php",
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/bin_log.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { "rack_id": rack_id },
|
||||
data: { "bin_id": bin_id },
|
||||
onSuccess: function(res) {
|
||||
|
||||
if (!res.output || res.output.length === 0) {
|
||||
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">No log entries.</td></tr>');
|
||||
$('#bin_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">No log entries.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -544,7 +544,7 @@
|
||||
<td class="py-3">${row.user_name || '<span class="text-muted">—</span>'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('#rack_log_body').html(body);
|
||||
$('#bin_log_body').html(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -554,7 +554,7 @@
|
||||
$(async function() {
|
||||
try {
|
||||
await load_location_config(apply_location_labels);
|
||||
await retrieve_rack_occupancy();
|
||||
await retrieve_bin_occupancy();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
var lot_log_page = 1;
|
||||
var lot_log_limit = 10;
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -202,16 +202,16 @@
|
||||
|
||||
function apply_location_labels() {
|
||||
$('#th_location').text(advanced
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
|
||||
: label_rack
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_bin
|
||||
: label_bin
|
||||
);
|
||||
}
|
||||
|
||||
function format_location(row) {
|
||||
if (advanced) {
|
||||
return [row.zone, row.aisle, row.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
||||
return [row.zone, row.aisle, row.bin].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
||||
}
|
||||
return row.rack || '<span class="text-muted">—</span>';
|
||||
return row.bin || '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
function load_company_setting() {
|
||||
@@ -223,7 +223,7 @@
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
|
||||
// Movement Summary running totals (reset on each search)
|
||||
var run_revenue = 0, run_cogs = 0, run_gp = 0, sum_in = 0, sum_out = 0, sum_txn = 0;
|
||||
var label_rack = 'Location';
|
||||
var label_bin = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
@@ -268,16 +268,16 @@
|
||||
|
||||
function apply_location_labels() {
|
||||
$('#th_location').text(advanced
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
|
||||
: label_rack
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_bin
|
||||
: label_bin
|
||||
);
|
||||
}
|
||||
|
||||
function format_location(row) {
|
||||
if (advanced) {
|
||||
return [row.zone, row.aisle, row.rack].filter(Boolean).join('-');
|
||||
return [row.zone, row.aisle, row.bin].filter(Boolean).join('-');
|
||||
}
|
||||
return row.rack || '';
|
||||
return row.bin || '';
|
||||
}
|
||||
|
||||
function display_location(row) {
|
||||
@@ -293,7 +293,7 @@
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
warehouse_id: 0,
|
||||
zone: '',
|
||||
aisle: '',
|
||||
rack: '',
|
||||
bin: '',
|
||||
lot_number: '',
|
||||
serial_number: '',
|
||||
quantity: qty,
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<p class="text-muted small mb-2">
|
||||
By default, each storage location uses a single <strong>Location</strong> field —
|
||||
ideal for small warehouses. Enable <strong>Advanced</strong> to use a three-level
|
||||
hierarchy (e.g. Zone → Aisle → Rack) for larger operations.
|
||||
hierarchy (e.g. Zone → Aisle → Bin) for larger operations.
|
||||
You can customise the label for each level to match your team's terminology.
|
||||
</p>
|
||||
<div class="form-check form-switch mb-3">
|
||||
@@ -54,7 +54,7 @@
|
||||
<label class="col-form-label text-muted small">Location Label</label>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<input type="text" id="location_label_rack_simple" class="form-control form-control-sm"
|
||||
<input type="text" id="location_label_bin_simple" class="form-control form-control-sm"
|
||||
placeholder="Location" maxlength="30">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@@ -94,11 +94,11 @@
|
||||
<label class="col-form-label text-muted small">Level 3 Label</label>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<input type="text" id="location_label_rack_advanced" class="form-control form-control-sm"
|
||||
placeholder="Rack" maxlength="30">
|
||||
<input type="text" id="location_label_bin_advanced" class="form-control form-control-sm"
|
||||
placeholder="Bin" maxlength="30">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="text-muted small">default: "Rack"</span>
|
||||
<span class="text-muted small">default: "Bin"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,10 +264,10 @@
|
||||
var advanced = (s.advanced_location ?? 0) == 1;
|
||||
$('#advanced_location_toggle').prop('checked', advanced);
|
||||
toggle_location_mode(advanced);
|
||||
$('#location_label_rack_simple').val(s.location_label_rack || '');
|
||||
$('#location_label_bin_simple').val(s.location_label_bin || '');
|
||||
$('#location_label_zone').val(s.location_label_zone || '');
|
||||
$('#location_label_aisle').val(s.location_label_aisle || '');
|
||||
$('#location_label_rack_advanced').val(s.location_label_rack || '');
|
||||
$('#location_label_bin_advanced').val(s.location_label_bin || '');
|
||||
|
||||
// Other settings
|
||||
$('input[name="default_stock_status"][value="' + (s.default_stock_status ?? 1) + '"]').prop('checked', true);
|
||||
@@ -350,15 +350,15 @@
|
||||
}
|
||||
|
||||
// Resolve label values — fall back to defaults if blank
|
||||
var label_rack, label_zone, label_aisle;
|
||||
var label_bin, label_zone, label_aisle;
|
||||
if (advanced) {
|
||||
label_zone = $('#location_label_zone').val().trim() || 'Zone';
|
||||
label_aisle = $('#location_label_aisle').val().trim() || 'Aisle';
|
||||
label_rack = $('#location_label_rack_advanced').val().trim() || 'Rack';
|
||||
label_bin = $('#location_label_bin_advanced').val().trim() || 'Bin';
|
||||
} else {
|
||||
label_rack = $('#location_label_rack_simple').val().trim() || 'Location';
|
||||
label_zone = label_rack;
|
||||
label_aisle = label_rack;
|
||||
label_bin = $('#location_label_bin_simple').val().trim() || 'Location';
|
||||
label_zone = label_bin;
|
||||
label_aisle = label_bin;
|
||||
}
|
||||
|
||||
ajax_request({
|
||||
@@ -370,7 +370,7 @@
|
||||
advanced_location: advanced,
|
||||
location_label_zone: label_zone,
|
||||
location_label_aisle: label_aisle,
|
||||
location_label_rack: label_rack,
|
||||
location_label_bin: label_bin,
|
||||
default_stock_status: stock_status,
|
||||
auto_invoice_and_credit_note: auto_invoice_and_cn,
|
||||
auto_complete_on_ship: auto_complete_on_ship,
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ $active_nav = 'home';
|
||||
<div class="text-container">
|
||||
<h2>Track every item, every movement, in real time</h2>
|
||||
<p>MN3 WMS gives you a live stock overview across all your warehouses. Drill into individual lots, view expiry dates, and spot low-stock alerts before they become a problem.</p>
|
||||
<p>Advanced and simple location modes let you model everything from a single rack to complex zone-aisle-rack layouts.</p>
|
||||
<p>Advanced and simple location modes let you model everything from a single bin to complex zone-aisle-bin layouts.</p>
|
||||
<a class="btn-solid-reg" href="<?= $app_url ?>/login/index.php">See it in action</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -256,8 +256,8 @@ CREATE TABLE IF NOT EXISTS `md_storage` (
|
||||
`zone` text NOT NULL DEFAULT '',
|
||||
`aisle_from` varchar(255) NOT NULL DEFAULT '',
|
||||
`aisle_to` varchar(255) NOT NULL DEFAULT '',
|
||||
`rack_from` varchar(255) NOT NULL DEFAULT '',
|
||||
`rack_to` varchar(255) NOT NULL DEFAULT '',
|
||||
`bin_from` varchar(255) NOT NULL DEFAULT '',
|
||||
`bin_to` varchar(255) NOT NULL DEFAULT '',
|
||||
`description` text NOT NULL DEFAULT '',
|
||||
`status` int(1) NOT NULL DEFAULT 1,
|
||||
`log` mediumtext NOT NULL DEFAULT '{}',
|
||||
@@ -266,7 +266,7 @@ CREATE TABLE IF NOT EXISTS `md_storage` (
|
||||
", 'md_storage');
|
||||
|
||||
run($pdo, "
|
||||
CREATE TABLE IF NOT EXISTS `md_rack` (
|
||||
CREATE TABLE IF NOT EXISTS `md_bin` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`company_id` int(11) NOT NULL,
|
||||
`td_stock_id` int(11) NOT NULL DEFAULT 0,
|
||||
@@ -274,20 +274,20 @@ CREATE TABLE IF NOT EXISTS `md_rack` (
|
||||
`storage_id` int(11) NOT NULL DEFAULT 0,
|
||||
`zone` varchar(10) NOT NULL,
|
||||
`aisle` varchar(10) NOT NULL,
|
||||
`rack` varchar(10) NOT NULL,
|
||||
`bin` varchar(10) NOT NULL,
|
||||
`product_sku` varchar(50) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_rack_location` (`company_id`,`warehouse`,`zone`,`aisle`,`rack`),
|
||||
UNIQUE KEY `uk_bin_location` (`company_id`,`warehouse`,`zone`,`aisle`,`bin`),
|
||||
KEY `idx_storage` (`storage_id`),
|
||||
KEY `idx_sku` (`company_id`,`product_sku`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
", 'md_rack');
|
||||
", 'md_bin');
|
||||
|
||||
run($pdo, "
|
||||
CREATE TABLE IF NOT EXISTS `td_rack_log` (
|
||||
CREATE TABLE IF NOT EXISTS `td_bin_log` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`company_id` int(10) NOT NULL,
|
||||
`md_rack_id` int(10) unsigned NOT NULL,
|
||||
`md_bin_id` int(10) unsigned NOT NULL,
|
||||
`user_id` int(10) unsigned DEFAULT NULL,
|
||||
`dt` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
`login` datetime DEFAULT NULL,
|
||||
@@ -295,10 +295,10 @@ CREATE TABLE IF NOT EXISTS `td_rack_log` (
|
||||
`product_sku` varchar(100) DEFAULT NULL,
|
||||
`td_stock_id` int(10) unsigned DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_rack` (`company_id`,`md_rack_id`),
|
||||
KEY `idx_bin` (`company_id`,`md_bin_id`),
|
||||
KEY `idx_user` (`company_id`,`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
", 'td_rack_log');
|
||||
", 'td_bin_log');
|
||||
|
||||
run($pdo, "
|
||||
CREATE TABLE IF NOT EXISTS `md_product_category` (
|
||||
@@ -401,7 +401,7 @@ CREATE TABLE IF NOT EXISTS `md_barcode` (
|
||||
`warehouse_id` int(10) unsigned DEFAULT NULL,
|
||||
`zone` varchar(100) DEFAULT NULL,
|
||||
`aisle` varchar(100) DEFAULT NULL,
|
||||
`rack` varchar(100) DEFAULT NULL,
|
||||
`bin` varchar(100) DEFAULT NULL,
|
||||
`print_count` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
`last_printed_dt` datetime DEFAULT NULL,
|
||||
`created_dt` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
@@ -410,7 +410,7 @@ CREATE TABLE IF NOT EXISTS `md_barcode` (
|
||||
UNIQUE KEY `company_id_barcode` (`company_id`,`barcode`),
|
||||
KEY `company_id_type` (`company_id`,`barcode_type`),
|
||||
KEY `company_id_product_lot` (`company_id`,`product_sku`,`lot_number`),
|
||||
KEY `company_id_location` (`company_id`,`warehouse_id`,`zone`,`aisle`,`rack`)
|
||||
KEY `company_id_location` (`company_id`,`warehouse_id`,`zone`,`aisle`,`bin`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
|
||||
", 'md_barcode');
|
||||
|
||||
@@ -527,7 +527,7 @@ CREATE TABLE IF NOT EXISTS `td_stock` (
|
||||
`price` decimal(18,4) NOT NULL DEFAULT 0.0000,
|
||||
`zone` varchar(255) NOT NULL DEFAULT '',
|
||||
`aisle` varchar(255) NOT NULL DEFAULT '',
|
||||
`rack` varchar(255) NOT NULL DEFAULT '',
|
||||
`bin` varchar(255) NOT NULL DEFAULT '',
|
||||
`contact_id` int(11) NOT NULL DEFAULT 0,
|
||||
`description` text NOT NULL DEFAULT '',
|
||||
`status` int(3) NOT NULL DEFAULT 0,
|
||||
@@ -972,7 +972,7 @@ CREATE TABLE IF NOT EXISTS `td_supplier_return_item` (
|
||||
`stock_in_id` int(11) NOT NULL DEFAULT 0,
|
||||
`zone` varchar(50) NOT NULL DEFAULT '',
|
||||
`aisle` varchar(50) NOT NULL DEFAULT '',
|
||||
`rack` varchar(50) NOT NULL DEFAULT '',
|
||||
`bin` varchar(50) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_sr_item` (`return_id`,`item_id`),
|
||||
KEY `idx_return` (`company_id`,`return_id`)
|
||||
|
||||
Reference in New Issue
Block a user