warehouse layers [switchable]
This commit is contained in:
@@ -698,6 +698,21 @@ function format_date(iso_string) {
|
||||
return (datePart) ? `${datePart[2]}/${datePart[1]}/${datePart[0]} ${split[1]}` : iso_string;
|
||||
}
|
||||
|
||||
|
||||
function to_iso_date(dateStr) {
|
||||
if (!dateStr) return null;
|
||||
|
||||
const parts = dateStr.split('/');
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
let [day, month, year] = parts;
|
||||
day = day.padStart(2, '0');
|
||||
month = month.padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
|
||||
function format_number(value, decimal) {
|
||||
var n = Number(value);
|
||||
if (isNaN(n)) return '--';
|
||||
|
||||
@@ -5,28 +5,37 @@
|
||||
* Table: company_setting (company_id, setting_key, value, log, updated_at)
|
||||
*
|
||||
* Current keys:
|
||||
* default_stock_status int 0 = draft (manual approve required)
|
||||
* 1 = auto-approve on save
|
||||
* auto_invoice_and_credit_note int 0 = manual
|
||||
* 1 = auto-generate invoice on confirm order
|
||||
* + auto-generate credit note on confirm return
|
||||
* 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
|
||||
* location_label_zone string label for zone (advanced only)
|
||||
* location_label_aisle string label for aisle (advanced only)
|
||||
*
|
||||
* Designed to accept new keys without schema changes.
|
||||
*/
|
||||
class CompanySettingManager
|
||||
{
|
||||
private PDO $pdo;
|
||||
private ?PDO $transactionPdo;
|
||||
private int $companyId;
|
||||
|
||||
// ── Known keys with their defaults ───────────────────────────────────────
|
||||
private const DEFAULTS = [
|
||||
'default_stock_status' => 1, // auto-approve by default
|
||||
'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_zone' => 'Zone', // advanced level 1 default
|
||||
'location_label_aisle' => 'Aisle', // advanced level 2 default
|
||||
];
|
||||
|
||||
public function __construct(PDO $pdo, int $companyId)
|
||||
public function __construct(PDO $pdo, int $companyId, ?PDO $transactionPdo = null)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
$this->transactionPdo = $transactionPdo;
|
||||
$this->companyId = $companyId;
|
||||
}
|
||||
|
||||
@@ -80,6 +89,32 @@ class CompanySettingManager
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return settings that are locked by existing transaction data.
|
||||
*
|
||||
* @return array Locked key => reason pairs.
|
||||
*/
|
||||
public function getTransactionLocks(): array
|
||||
{
|
||||
$locked = [];
|
||||
|
||||
if (!$this->transactionPdo || $this->isLocalhostRequest()) {
|
||||
return $locked;
|
||||
}
|
||||
|
||||
if ($this->hasStockTransactions()) {
|
||||
$locked['default_stock_status'] = 'stock transactions already exist';
|
||||
$locked['advanced_location'] = 'stock transactions already exist';
|
||||
}
|
||||
|
||||
if ($this->hasOrderTransactions()) {
|
||||
$locked['auto_complete_on_ship'] = 'order, invoice, or return transactions already exist';
|
||||
$locked['auto_invoice_and_credit_note'] = 'order, invoice, or return transactions already exist';
|
||||
}
|
||||
|
||||
return $locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert a single setting value.
|
||||
*
|
||||
@@ -114,6 +149,144 @@ class CompanySettingManager
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return keys whose value cannot be changed because related transactions exist.
|
||||
*
|
||||
* @param array $incoming Requested key => value pairs.
|
||||
* @return array Blocked key => reason pairs.
|
||||
*/
|
||||
private function blockedChanges(array $incoming): array
|
||||
{
|
||||
if (!$this->transactionPdo || $this->isLocalhostRequest()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$current = $this->getAll();
|
||||
$blocked = [];
|
||||
|
||||
$stock_keys = [
|
||||
'default_stock_status',
|
||||
'advanced_location',
|
||||
];
|
||||
|
||||
$order_keys = [
|
||||
'auto_complete_on_ship',
|
||||
'auto_invoice_and_credit_note',
|
||||
];
|
||||
|
||||
$needs_stock_check = false;
|
||||
foreach ($stock_keys as $key) {
|
||||
if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) {
|
||||
$needs_stock_check = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$needs_order_check = false;
|
||||
foreach ($order_keys as $key) {
|
||||
if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) {
|
||||
$needs_order_check = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$has_stock_transactions = $needs_stock_check ? $this->hasStockTransactions() : false;
|
||||
$has_order_transactions = $needs_order_check ? $this->hasOrderTransactions() : false;
|
||||
|
||||
if ($has_stock_transactions) {
|
||||
foreach ($stock_keys as $key) {
|
||||
if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) {
|
||||
$blocked[$key] = 'stock transactions already exist';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($has_order_transactions) {
|
||||
foreach ($order_keys as $key) {
|
||||
if (array_key_exists($key, $incoming) && (string)$current[$key] !== (string)$incoming[$key]) {
|
||||
$blocked[$key] = 'order, invoice, or return transactions already exist';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $blocked;
|
||||
}
|
||||
|
||||
private function hasOrderTransactions(): bool
|
||||
{
|
||||
foreach (['td_order', 'td_invoice', 'td_return'] as $table) {
|
||||
if ($this->tableHasCompanyRows($table)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function hasStockTransactions(): bool
|
||||
{
|
||||
if ($this->tableHasCompanyRows('warehouse_balance')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->tableHasCompanyRows('md_lot')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$sth = $this->transactionPdo->prepare(
|
||||
"SELECT warehouse_name FROM md_warehouse WHERE company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
$warehouses = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
foreach ($warehouses as $warehouse_name) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', (string)$warehouse_name);
|
||||
if ($safe !== '' && $this->tableHasCompanyRows('td_stock_' . $safe)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function tableHasCompanyRows(string $table): bool
|
||||
{
|
||||
if (!$this->tableExists($table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$safe_table = str_replace('`', '``', $table);
|
||||
$sth = $this->transactionPdo->prepare(
|
||||
"SELECT COUNT(*) FROM `{$safe_table}` WHERE company_id = :company_id LIMIT 1"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
|
||||
return (int)$sth->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
$sth = $this->transactionPdo->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = :table_name"
|
||||
);
|
||||
$sth->execute([':table_name' => $table]);
|
||||
|
||||
return (int)$sth->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
private function isLocalhostRequest(): bool
|
||||
{
|
||||
$host = strtolower($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '');
|
||||
$host = preg_replace('/:\d+$/', '', $host);
|
||||
|
||||
return in_array($host, ['localhost', '127.0.0.1', '::1'], true)
|
||||
|| ($_SERVER['REMOTE_ADDR'] ?? '') === '127.0.0.1'
|
||||
|| ($_SERVER['REMOTE_ADDR'] ?? '') === '::1';
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP handler — read or update settings via AJAX.
|
||||
*
|
||||
@@ -127,14 +300,48 @@ class CompanySettingManager
|
||||
$action = $data['action'] ?? '';
|
||||
|
||||
if ($action === 'read') {
|
||||
echo json_encode(['success' => 1, 'output' => $this->getAll()]);
|
||||
echo json_encode([
|
||||
'success' => 1,
|
||||
'output' => $this->getAll(),
|
||||
'locked' => $this->getTransactionLocks(),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'update') {
|
||||
$allowed = array_keys(self::DEFAULTS);
|
||||
$incoming = [];
|
||||
foreach ($allowed as $key) {
|
||||
$this->set($key, $data[$key] ?? self::DEFAULTS[$key]);
|
||||
$incoming[$key] = $data[$key] ?? self::DEFAULTS[$key];
|
||||
}
|
||||
|
||||
$blocked = $this->blockedChanges($incoming);
|
||||
if (!empty($blocked)) {
|
||||
$labels = [
|
||||
'default_stock_status' => 'Default Stock Status',
|
||||
'advanced_location' => 'Advanced Location',
|
||||
'auto_invoice_and_credit_note' => 'Invoice & Credit Note Generation',
|
||||
'auto_complete_on_ship' => 'Auto-Complete on Shipped',
|
||||
];
|
||||
|
||||
$names = array_map(
|
||||
fn($key) => $labels[$key] ?? $key,
|
||||
array_keys($blocked)
|
||||
);
|
||||
|
||||
http_response_code(409);
|
||||
echo json_encode([
|
||||
'success' => 0,
|
||||
'message' => 'Cannot change ' . implode(', ', $names) . ' because relevant transactions already exist.',
|
||||
'blocked' => $blocked,
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
foreach ($incoming as $key => $value) {
|
||||
if ((string)$this->get($key) !== (string)$value) {
|
||||
$this->set($key, $value);
|
||||
}
|
||||
}
|
||||
echo json_encode(['success' => 1, 'message' => 'Settings saved.']);
|
||||
exit;
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
*
|
||||
* Method order:
|
||||
* Transaction basis → getOrderList, getOrderById, generateOrderNumber,
|
||||
* saveOrder, confirmOrder, cancelOrder
|
||||
* saveOrder, confirmOrder, cancelOrder,
|
||||
* updateFulfillmentStatus
|
||||
*
|
||||
* Key design decisions:
|
||||
* - Order items are stored as a JSON array in td_order.items.
|
||||
@@ -19,10 +20,10 @@
|
||||
* - 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.
|
||||
* - cancelOrder() sets td_order.status = -1 and flips all linked
|
||||
* draft stock-out rows (status=0) to status=-1 (cancelled).
|
||||
* Approved stock-out rows block cancellation — caller must handle
|
||||
* these manually before cancelling.
|
||||
* - cancelOrder() sets td_order.status = -1 and soft-deletes ALL linked
|
||||
* stock-out rows (status → -1) across all td_stock_* tables.
|
||||
* Cancellation is blocked if any active invoice (status != -1) or
|
||||
* active return (status != -1) is linked to the order.
|
||||
* - Cancel logic is intentionally self-contained here. status=-1 is
|
||||
* an order-domain concept with no rack/balance side effects, so
|
||||
* WarehouseManager and StockManager are not involved.
|
||||
@@ -240,7 +241,7 @@ class OrderManager {
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param array $data Keys: id, contact_id, order_date, items (array),
|
||||
* discount, tax, shipping_fee, notes.
|
||||
* discount, tax, shipping_fee, shipping_tracking_number, notes.
|
||||
* @param array $logging Audit entry to append to log column.
|
||||
* @return int New td_order.id on insert, 0 on update.
|
||||
* @throws Exception If updating a non-draft order.
|
||||
@@ -257,6 +258,7 @@ class OrderManager {
|
||||
$discount = (float)($data['discount'] ?? 0);
|
||||
$tax = (float)($data['tax'] ?? 0);
|
||||
$shipping_fee = (float)($data['shipping_fee'] ?? 0);
|
||||
$tracking_no = trim((string)($data['shipping_tracking_number'] ?? ''));
|
||||
$grand_total = $subtotal - $discount + $tax + $shipping_fee;
|
||||
|
||||
if ($id > 0) {
|
||||
@@ -288,6 +290,7 @@ class OrderManager {
|
||||
discount = :discount,
|
||||
tax = :tax,
|
||||
shipping_fee = :shipping_fee,
|
||||
shipping_tracking_number = :shipping_tracking_number,
|
||||
grand_total = :grand_total,
|
||||
notes = :notes,
|
||||
`log` = :log
|
||||
@@ -300,6 +303,7 @@ class OrderManager {
|
||||
':discount' => $discount,
|
||||
':tax' => $tax,
|
||||
':shipping_fee' => $shipping_fee,
|
||||
':shipping_tracking_number' => $tracking_no,
|
||||
':grand_total' => $grand_total,
|
||||
':notes' => $data['notes'] ?? '',
|
||||
':log' => json_encode($log),
|
||||
@@ -317,11 +321,11 @@ class OrderManager {
|
||||
"INSERT INTO td_order
|
||||
(company_id, uuid, order_number, contact_id, order_date,
|
||||
status, payment_status, subtotal, discount, tax,
|
||||
shipping_fee, grand_total, items, notes, `log`, created_at)
|
||||
shipping_fee, shipping_tracking_number, grand_total, items, notes, `log`, created_at)
|
||||
VALUES
|
||||
(:company_id, :uuid, :order_number, :contact_id, :order_date,
|
||||
0, 0, :subtotal, :discount, :tax,
|
||||
:shipping_fee, :grand_total, :items, :notes, :log, :created_at)"
|
||||
:shipping_fee, :shipping_tracking_number, :grand_total, :items, :notes, :log, :created_at)"
|
||||
)->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':uuid' => bin2hex(random_bytes(16)),
|
||||
@@ -332,6 +336,7 @@ class OrderManager {
|
||||
':discount' => $discount,
|
||||
':tax' => $tax,
|
||||
':shipping_fee' => $shipping_fee,
|
||||
':shipping_tracking_number' => $tracking_no,
|
||||
':grand_total' => $grand_total,
|
||||
':items' => json_encode($items, JSON_UNESCAPED_UNICODE),
|
||||
':notes' => $data['notes'] ?? '',
|
||||
@@ -484,28 +489,27 @@ class OrderManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an order and flip all linked draft stock-out rows to status=-1.
|
||||
* Cancel an order and soft-delete all linked stock-out rows (status → -1).
|
||||
*
|
||||
* Flow:
|
||||
* 1. Load the order — must be status=0 (draft) or status=1 (confirmed).
|
||||
* Orders at status=2 (processing) or status=3 (completed) cannot be
|
||||
* cancelled here; they require manual stock reversal first.
|
||||
* 2. Check that no linked stock-out rows are already approved (status=1).
|
||||
* If any are approved, throw — those must be deleted via the existing
|
||||
* deleteStockOut() flow before cancellation can proceed.
|
||||
* 3. UPDATE all td_stock_<wh> rows where
|
||||
* source='order' AND source_id=order_id AND status=0 → status=-1.
|
||||
* 4. UPDATE td_order.status = -1.
|
||||
* Business rule:
|
||||
* An order can be cancelled (Draft or Confirmed) as long as no active
|
||||
* downstream documents exist. Active means status != -1 (not voided /
|
||||
* not cancelled).
|
||||
*
|
||||
* Cancel logic is intentionally self-contained — status=-1 rows have no
|
||||
* rack/balance side effects so WarehouseManager is not involved.
|
||||
* Downstream documents that block cancellation:
|
||||
* - td_invoice where order_id = $order_id AND status != -1
|
||||
* - td_return where order_id = $order_id AND status != -1
|
||||
*
|
||||
* Stock-out rows are children of the order — they follow the parent
|
||||
* and are never independently approved. On cancel, ALL stock-out rows
|
||||
* linked to this order (any status except already -1) are soft-deleted
|
||||
* (status → -1) across all td_stock_* tables.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param int $order_id td_order.id to cancel.
|
||||
* @param array $logging Audit entry appended to td_order.log.
|
||||
* @throws Exception If order not found, already cancelled, in a non-cancellable
|
||||
* status, or has approved stock-out rows.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function cancelOrder(int $order_id, array $logging): void
|
||||
{
|
||||
@@ -528,14 +532,41 @@ class OrderManager {
|
||||
}
|
||||
if ($status >= 2) {
|
||||
throw new Exception(
|
||||
"Cannot cancel an order that is processing or completed. " .
|
||||
"Please reverse stock movements manually first."
|
||||
"Cannot cancel an order that is processing or completed."
|
||||
);
|
||||
}
|
||||
|
||||
// ── Guard: no approved stock-out rows ─────────────────────────────
|
||||
// Discover all td_stock_* tables and check for approved rows
|
||||
// linked to this order before making any changes.
|
||||
// ── Guard: no active invoice ──────────────────────────────────────
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM td_invoice
|
||||
WHERE company_id = :company_id
|
||||
AND order_id = :order_id
|
||||
AND status != -1"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':order_id' => $order_id]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot cancel — this order has an active invoice. " .
|
||||
"Please void the invoice first."
|
||||
);
|
||||
}
|
||||
|
||||
// ── Guard: no active return ───────────────────────────────────────
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM td_return
|
||||
WHERE company_id = :company_id
|
||||
AND order_id = :order_id
|
||||
AND status != -1"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':order_id' => $order_id]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot cancel — this order has an active return. " .
|
||||
"Please cancel the return first."
|
||||
);
|
||||
}
|
||||
|
||||
// ── Soft-delete all linked stock-out rows ─────────────────────────
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE()
|
||||
@@ -545,33 +576,13 @@ class OrderManager {
|
||||
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM `{$table}`
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'order'
|
||||
AND source_id = :order_id
|
||||
AND status = 1"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':order_id' => $order_id,
|
||||
]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot cancel — some stock-out rows for this order are already " .
|
||||
"approved. Please delete them from the Stock Out page first."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flip draft stock-out rows to cancelled ────────────────────────
|
||||
foreach ($tables as $table) {
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$table}` SET status = -1
|
||||
"UPDATE `{$safe}` SET status = -1
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'order'
|
||||
AND source_id = :order_id
|
||||
AND status = 0"
|
||||
AND status != -1"
|
||||
)->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':order_id' => $order_id,
|
||||
@@ -593,4 +604,109 @@ class OrderManager {
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the fulfillment sub-status of a confirmed order.
|
||||
*
|
||||
* Fulfillment status values:
|
||||
* 1 = Picking — warehouse staff are picking items
|
||||
* 2 = Packed — items are packed, ready to ship
|
||||
* 3 = Shipped — goods have left the warehouse
|
||||
*
|
||||
* Rules:
|
||||
* - Only allowed on orders with status >= 1 (confirmed) and not cancelled.
|
||||
* - Fulfillment must move forward only (no going backwards).
|
||||
* - If auto_complete_on_ship = true AND fulfillment_status = 3 (Shipped),
|
||||
* order status is automatically advanced to 3 (Completed).
|
||||
* - If auto_complete_on_ship = false, order status is set to 2 (Processing)
|
||||
* on first fulfillment update, and stays there until manually completed.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param int $order_id td_order.id
|
||||
* @param int $fulfillment_status 1=Picking, 2=Packed, 3=Shipped
|
||||
* @param string $tracking_number Shipping carrier tracking number.
|
||||
* @param array $logging Audit entry.
|
||||
* @param bool $auto_complete Whether Shipped auto-drives order to Completed.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateFulfillmentStatus(
|
||||
int $order_id,
|
||||
int $fulfillment_status,
|
||||
string $tracking_number,
|
||||
array $logging,
|
||||
bool $auto_complete = true
|
||||
): void {
|
||||
// ── Load order ────────────────────────────────────────────────────
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM td_order
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $order_id]);
|
||||
$order = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$order) {
|
||||
throw new Exception("Order not found.");
|
||||
}
|
||||
|
||||
$status = (int)$order['status'];
|
||||
$current_fulfillment = (int)($order['fulfillment_status'] ?? 0);
|
||||
$current_tracking = trim((string)($order['shipping_tracking_number'] ?? ''));
|
||||
$tracking_number = trim($tracking_number);
|
||||
|
||||
if ($status === -1) {
|
||||
throw new Exception("Cannot update a cancelled order.");
|
||||
}
|
||||
if ($status < 1) {
|
||||
throw new Exception("Confirm the order before updating fulfillment.");
|
||||
}
|
||||
if ($status === 3) {
|
||||
throw new Exception("Order is already completed.");
|
||||
}
|
||||
if (!in_array($fulfillment_status, [1, 2, 3], true)) {
|
||||
throw new Exception("Invalid fulfillment status.");
|
||||
}
|
||||
if ($fulfillment_status < $current_fulfillment) {
|
||||
throw new Exception("Fulfillment status can only move forward.");
|
||||
}
|
||||
if ($fulfillment_status === $current_fulfillment && $tracking_number === $current_tracking) {
|
||||
throw new Exception("No fulfillment or tracking changes to save.");
|
||||
}
|
||||
|
||||
// ── Determine new order status ────────────────────────────────────
|
||||
$new_order_status = $status;
|
||||
|
||||
if ($fulfillment_status > $current_fulfillment && $fulfillment_status === 3 && $auto_complete) {
|
||||
// Shipped + auto-complete → Completed
|
||||
$new_order_status = 3;
|
||||
} elseif ($fulfillment_status > $current_fulfillment && $status === 1) {
|
||||
// First fulfillment update → Processing
|
||||
$new_order_status = 2;
|
||||
}
|
||||
|
||||
// ── Persist ───────────────────────────────────────────────────────
|
||||
$log = json_decode($order['log'] ?? '[]', true) ?: [];
|
||||
$log[] = array_merge($logging, [
|
||||
'action' => 'update_fulfillment',
|
||||
'fulfillment_status' => $fulfillment_status,
|
||||
'shipping_tracking_number' => $tracking_number,
|
||||
'order_status' => $new_order_status,
|
||||
]);
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE td_order SET
|
||||
fulfillment_status = :fulfillment_status,
|
||||
shipping_tracking_number = :shipping_tracking_number,
|
||||
status = :status,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':fulfillment_status' => $fulfillment_status,
|
||||
':shipping_tracking_number' => $tracking_number,
|
||||
':status' => $new_order_status,
|
||||
':log' => json_encode($log),
|
||||
':id' => $order_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* Method order:
|
||||
* Transaction basis → getReturnList, getReturnById, generateReturnNumber,
|
||||
* saveReturn, confirmReturn
|
||||
* saveReturn, confirmReturn, cancelReturn
|
||||
*
|
||||
* Key design decisions:
|
||||
* - status: -1=cancelled, 0=draft, 1=confirmed (consistent with td_order)
|
||||
@@ -386,4 +386,144 @@ class ReturnManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a return (Draft or Confirmed) and soft-delete linked stock-in rows.
|
||||
*
|
||||
* Business rule:
|
||||
* A return can be cancelled as long as no active downstream document exists.
|
||||
* Active means status != -1 (not voided).
|
||||
*
|
||||
* Downstream document that blocks cancellation:
|
||||
* - td_invoice (credit note) where ref_invoice_id links back via
|
||||
* order_id = return.order_id AND doc_type = 'credit_note' AND status != -1.
|
||||
* Credit notes store order_id (not return_id), so we match via order_id
|
||||
* 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.
|
||||
* On cancel we soft-delete those rows (status → -1) and reverse the
|
||||
* rack occupation and balance via WarehouseManager.
|
||||
*
|
||||
* If the return was still draft, no stock rows exist — simple status flip.
|
||||
*
|
||||
* Must be called inside dbTransaction() by the caller.
|
||||
*
|
||||
* @param int $return_id td_return.id to cancel.
|
||||
* @param array $logging Audit entry appended to td_return.log.
|
||||
* @param object $whMgmt WarehouseManager instance (needed for confirmed reversal).
|
||||
* @throws Exception
|
||||
*/
|
||||
public function cancelReturn(int $return_id, array $logging, $whMgmt): void
|
||||
{
|
||||
// ── Load return ───────────────────────────────────────────────────
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT * FROM td_return
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':id' => $return_id]);
|
||||
$return = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$return) {
|
||||
throw new Exception("Return not found.");
|
||||
}
|
||||
|
||||
$status = (int)$return['status'];
|
||||
|
||||
if ($status === -1) {
|
||||
throw new Exception("Return is already cancelled.");
|
||||
}
|
||||
|
||||
// ── Guard: no active credit note ──────────────────────────────────
|
||||
$order_id = (int)$return['order_id'];
|
||||
|
||||
if ($order_id > 0) {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(*) FROM td_invoice
|
||||
WHERE company_id = :company_id
|
||||
AND order_id = :order_id
|
||||
AND doc_type = 'credit_note'
|
||||
AND status != -1"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':order_id' => $order_id]);
|
||||
if ((int)$sth->fetchColumn() > 0) {
|
||||
throw new Exception(
|
||||
"Cannot cancel — this return has an active credit note. " .
|
||||
"Please void the credit note first."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── If confirmed: reverse stock-in rows + rack/balance ────────────
|
||||
if ($status === 1) {
|
||||
$items = json_decode($return['items'] ?? '[]', true) ?: [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$warehouse_id = (int)($item['warehouse_id'] ?? 0);
|
||||
$product_sku = $item['product_sku'] ?? '';
|
||||
$quantity = (float)($item['quantity'] ?? 0);
|
||||
|
||||
if (!$warehouse_id || !$product_sku) continue;
|
||||
|
||||
$table = $this->resolveStockTable($warehouse_id);
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
|
||||
|
||||
// Find the stock-in row created by confirmReturn()
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT id, zone, aisle, rack FROM `{$safe}`
|
||||
WHERE company_id = :company_id
|
||||
AND source = 'return'
|
||||
AND source_id = :return_id
|
||||
AND product_sku = :sku
|
||||
AND status = 1
|
||||
LIMIT 1"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':return_id' => $return_id,
|
||||
':sku' => $product_sku,
|
||||
]);
|
||||
$stock_row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$stock_row) continue;
|
||||
|
||||
// Soft-delete the stock-in row
|
||||
$this->pdo->prepare(
|
||||
"UPDATE `{$safe}` SET status = -1
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':id' => $stock_row['id'],
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
|
||||
// Release rack occupation
|
||||
$whMgmt->releaseRack(
|
||||
$warehouse_id,
|
||||
$stock_row['zone'],
|
||||
$stock_row['aisle'],
|
||||
$stock_row['rack'],
|
||||
$product_sku,
|
||||
$stock_row['id']
|
||||
);
|
||||
|
||||
// Reverse balance
|
||||
$whMgmt->adjustBalance('out', $warehouse_id, $product_sku, 0, $quantity);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cancel the return ─────────────────────────────────────────────
|
||||
$log = json_decode($return['log'] ?? '[]', true) ?: [];
|
||||
$log[] = array_merge($logging, ['action' => 'cancel']);
|
||||
|
||||
$this->pdo->prepare(
|
||||
"UPDATE td_return SET
|
||||
status = -1,
|
||||
`log` = :log
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([
|
||||
':log' => json_encode($log),
|
||||
':id' => $return_id,
|
||||
':company_id' => $this->company_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,12 @@ class WarehouseManager {
|
||||
$from = trim($from);
|
||||
$to = trim($to);
|
||||
|
||||
// Numeric range
|
||||
// Same value — single location, any format allowed (e.g. "Shelf-A", "Bin-3")
|
||||
if ($from === $to) {
|
||||
return [$from];
|
||||
}
|
||||
|
||||
// Numeric range e.g. 1 → 10
|
||||
if (is_numeric($from) && is_numeric($to)) {
|
||||
$f = (int) $from;
|
||||
$t = (int) $to;
|
||||
@@ -86,15 +91,62 @@ class WarehouseManager {
|
||||
return array_map('strval', range($f, $t));
|
||||
}
|
||||
|
||||
// Single-letter alpha range
|
||||
// Single-letter alpha range e.g. A → Z
|
||||
if (ctype_alpha($from) && ctype_alpha($to)
|
||||
&& strlen($from) === 1 && strlen($to) === 1) {
|
||||
if (ord($from) > ord($to)) throw new Exception("Range start ($from) must be <= end ($to)");
|
||||
return range($from, $to); // PHP range() handles 'A' → 'Z' natively
|
||||
if (ord(strtoupper($from)) > ord(strtoupper($to))) {
|
||||
throw new Exception("Range start ($from) must be <= end ($to)");
|
||||
}
|
||||
return range(strtoupper($from), strtoupper($to));
|
||||
}
|
||||
|
||||
// Prefix + suffix range e.g. Shelf-A → Shelf-E or Bin-1 → Bin-5
|
||||
// Split on the last sequence of digits or single letter at the end
|
||||
$pattern = '/^(.*?)([A-Za-z]|[0-9]+)$/';
|
||||
if (preg_match($pattern, $from, $mf) && preg_match($pattern, $to, $mt)) {
|
||||
$prefix_f = $mf[1];
|
||||
$prefix_t = $mt[1];
|
||||
$suffix_f = $mf[2];
|
||||
$suffix_t = $mt[2];
|
||||
|
||||
// Prefixes must match
|
||||
if ($prefix_f !== $prefix_t) {
|
||||
throw new Exception(
|
||||
"Location prefix mismatch: '$prefix_f' vs '$prefix_t'. " .
|
||||
"From and To must share the same prefix (e.g. Shelf-A → Shelf-E)."
|
||||
);
|
||||
}
|
||||
|
||||
$prefix = $prefix_f;
|
||||
|
||||
// Suffix: numeric range
|
||||
if (is_numeric($suffix_f) && is_numeric($suffix_t)) {
|
||||
$f = (int) $suffix_f;
|
||||
$t = (int) $suffix_t;
|
||||
if ($f > $t) throw new Exception("Range start ($from) must be <= end ($to)");
|
||||
return array_map(fn($n) => $prefix . $n, range($f, $t));
|
||||
}
|
||||
|
||||
// Suffix: single-letter range
|
||||
if (ctype_alpha($suffix_f) && ctype_alpha($suffix_t)
|
||||
&& strlen($suffix_f) === 1 && strlen($suffix_t) === 1) {
|
||||
$sf = strtoupper($suffix_f);
|
||||
$st = strtoupper($suffix_t);
|
||||
if (ord($sf) > ord($st)) {
|
||||
throw new Exception("Range start ($from) must be <= end ($to)");
|
||||
}
|
||||
return array_map(fn($c) => $prefix . $c, range($sf, $st));
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
"Aisle/rack values must be either numbers (e.g. 1–99) or single uppercase letters (e.g. A–Z). Got: '$from' → '$to'"
|
||||
"Location suffix must be a number (e.g. 1–99) or single letter (e.g. A–Z). " .
|
||||
"Got: '$suffix_f' → '$suffix_t'"
|
||||
);
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
"Invalid location range: '$from' → '$to'. " .
|
||||
"Use formats like: 1–10, A–Z, Shelf-1–Shelf-10, or Shelf-A–Shelf-Z."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -848,6 +900,16 @@ class WarehouseManager {
|
||||
*/
|
||||
public function getRacksAll(int $warehouse_id, string $zone, string $aisle): array
|
||||
{
|
||||
if ($zone === '' || $aisle === '') {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT rack FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse
|
||||
ORDER BY CAST(rack AS UNSIGNED), rack"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->company_id, ':warehouse' => $warehouse_id]);
|
||||
return array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
|
||||
}
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT rack FROM md_rack
|
||||
WHERE company_id = :company_id AND warehouse = :warehouse AND zone = :zone AND aisle = :aisle
|
||||
@@ -1703,6 +1765,21 @@ class WarehouseManager {
|
||||
*/
|
||||
public function getRacksIn(int $warehouse_id, string $zone, string $aisle): array
|
||||
{
|
||||
if ($zone === '' || $aisle === '') {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT rack FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
AND warehouse = :warehouse
|
||||
AND product_sku IS NULL
|
||||
ORDER BY CAST(rack AS UNSIGNED), rack"
|
||||
);
|
||||
$sth->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':warehouse' => $warehouse_id,
|
||||
]);
|
||||
return array_column($sth->fetchAll(PDO::FETCH_ASSOC), 'rack');
|
||||
}
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT rack FROM md_rack
|
||||
WHERE company_id = :company_id
|
||||
@@ -1746,17 +1823,20 @@ class WarehouseManager {
|
||||
[$lot_cond, $serial_cond, $params] = $this->buildLotSerialCondition(
|
||||
$warehouse_id, $product_sku, $lot_number, $serial_number
|
||||
);
|
||||
$location_cond = '';
|
||||
if ($zone !== '' && $aisle !== '') {
|
||||
$location_cond = "AND r.zone = :zone AND r.aisle = :aisle";
|
||||
$params[':zone'] = $zone;
|
||||
$params[':aisle'] = $aisle;
|
||||
}
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT r.rack
|
||||
FROM md_rack r
|
||||
WHERE r.company_id = :company_id
|
||||
AND r.warehouse = :warehouse
|
||||
AND r.zone = :zone
|
||||
AND r.aisle = :aisle
|
||||
AND r.product_sku = :product_sku
|
||||
{$location_cond}
|
||||
AND r.td_stock_id IN (
|
||||
SELECT s.id FROM `{$table}` s
|
||||
WHERE s.company_id = :company_id2
|
||||
|
||||
+113
-59
@@ -68,22 +68,22 @@
|
||||
<select name="warehouse" id="warehouse" class="form-select" required></select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label for="zone" class="form-label">Zone</label>
|
||||
<select name="zone" id="zone" class="form-select" required>
|
||||
<div class="mb-3 col-lg-6" id="group_zone">
|
||||
<label for="zone" class="form-label" id="label_zone">Zone</label>
|
||||
<select name="zone" id="zone" class="form-select">
|
||||
<option value="">Please select zone</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label for="aisle" class="form-label">Aisle</label>
|
||||
<select name="aisle" id="aisle" class="form-select" required>
|
||||
<div class="mb-3 col-lg-6" id="group_aisle">
|
||||
<label for="aisle" class="form-label" id="label_aisle">Aisle</label>
|
||||
<select name="aisle" id="aisle" class="form-select">
|
||||
<option value="">Please select aisle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label for="rack" class="form-label">Rack</label>
|
||||
<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>
|
||||
</select>
|
||||
@@ -118,24 +118,72 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function load_location_config(callback) {
|
||||
return ajax_request({
|
||||
url: server_url + 'setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('#group_zone').show();
|
||||
$('#group_aisle').show();
|
||||
$('#label_zone').text(label_zone);
|
||||
$('#label_aisle').text(label_aisle);
|
||||
$('#label_rack').text(label_rack);
|
||||
$('#zone').attr('placeholder', 'Please select ' + label_zone.toLowerCase());
|
||||
$('#aisle').attr('placeholder', 'Please select ' + label_aisle.toLowerCase());
|
||||
$('#rack').attr('placeholder', 'Please select ' + label_rack.toLowerCase());
|
||||
} else {
|
||||
$('#group_zone').hide();
|
||||
$('#group_aisle').hide();
|
||||
$('#label_rack').text(label_rack);
|
||||
$('#rack').attr('placeholder', 'Please select ' + label_rack.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
function manage_stock_in() {
|
||||
|
||||
// In simple mode, mirror rack value to zone and aisle
|
||||
if (!advanced) {
|
||||
var rack_val = $('#rack').val();
|
||||
$('#zone').val(rack_val);
|
||||
$('#aisle').val(rack_val);
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/manage_stock_in.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/manage_stock_in.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 1,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
"contact_id": $("#contact").attr('data-id'),
|
||||
"lot_number": $("#lot_number").val(),
|
||||
"expiry_date": $("#expiry_date").val(),
|
||||
"serial_number": $("#serial_number").val()
|
||||
'contact_id': $('#contact').attr('data-id'),
|
||||
'lot_number': $('#lot_number').val(),
|
||||
'expiry_date': $('#expiry_date').val(),
|
||||
'serial_number': $('#serial_number').val(),
|
||||
'zone': $('#zone').val(),
|
||||
'aisle': $('#aisle').val(),
|
||||
},
|
||||
action: 'manage',
|
||||
onSuccess: function(res) {
|
||||
|
||||
window.location.href = "<?php echo $server_url?>ics/stock_in.php";
|
||||
|
||||
onSuccess: function() {
|
||||
window.location.href = '<?php echo $server_url?>ics/stock_in.php';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -179,11 +227,11 @@
|
||||
debugMode: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = `<option value="">Please select zone</option>`;
|
||||
var option = '<option value="">Please select ' + label_zone.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item.zone}'>${item.zone}</option>`;
|
||||
})
|
||||
$(`select#zone`).html(option);
|
||||
option += '<option value="' + item.zone + '">' + item.zone + '</option>';
|
||||
});
|
||||
$('select#zone').html(option);
|
||||
|
||||
if (callback) callback(); // ← add this
|
||||
}
|
||||
@@ -205,11 +253,11 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = `<option value="">Please select aisle</option>`;
|
||||
var option = '<option value="">Please select ' + label_aisle.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item}'>${item}</option>`;
|
||||
})
|
||||
$(`select#aisle`).html(option);
|
||||
option += '<option value="' + item + '">' + item + '</option>';
|
||||
});
|
||||
$('select#aisle').html(option);
|
||||
|
||||
if (callback) callback(); // ← add this
|
||||
}
|
||||
@@ -231,11 +279,11 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = `<option value="">Please select rack</option>`;
|
||||
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item}'>${item}</option>`;
|
||||
})
|
||||
$(`select#rack`).html(option);
|
||||
option += '<option value="' + item + '">' + item + '</option>';
|
||||
});
|
||||
$('select#rack').html(option);
|
||||
|
||||
if (callback) callback(); // ← add this
|
||||
}
|
||||
@@ -261,55 +309,53 @@
|
||||
|
||||
var data = res.output;
|
||||
|
||||
// 1. Set warehouse (already populated from page load)
|
||||
$('#warehouse').val('<?php echo $_GET["wh"];?>').attr('disabled', true);
|
||||
|
||||
$(`#warehouse`).val('<?php echo $_GET["wh"];?>').attr('disabled', true); // set warehouse BEFORE retrieve_zone
|
||||
|
||||
// 2. Retrieve zone (autoPrepare reads warehouse value)
|
||||
retrieve_zone('to', function() {
|
||||
$(`select#zone`).val(data.zone).attr('disabled', true); // set zone BEFORE retrieve_aisle
|
||||
|
||||
// 3. Retrieve aisle (autoPrepare reads zone value)
|
||||
retrieve_aisle('to', function() {
|
||||
$(`#aisle`).val(data.aisle).attr('disabled', true); // set aisle BEFORE retrieve_rack
|
||||
|
||||
// 4. Retrieve rack (autoPrepare reads aisle value)
|
||||
retrieve_rack('to', function() {
|
||||
$(`#rack`).val(data.rack).attr('disabled', true);
|
||||
|
||||
// retrieve for edit
|
||||
function populate_fields() {
|
||||
$.each(data, function(key, item) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'rack'].includes(key)) {
|
||||
$(`#${key}`).val(item);
|
||||
$('#' + key).val(item);
|
||||
}
|
||||
});
|
||||
|
||||
$('#contact').val(data.contact_name)
|
||||
.attr('secondary', data.contact_name)
|
||||
.attr('data-id', data.contact_id);
|
||||
|
||||
$('#product_name').val(data.product_name);
|
||||
$('#lot_number').val(data.lot_number || '').prop('disabled', true);
|
||||
$('#serial_number').val(data.serial_number || '').prop('disabled', true);
|
||||
if (data.lot_number) {
|
||||
if (expiryPicker) { expiryPicker.setDate(data.expiry_date || '', true, 'Y-m-d'); }
|
||||
$('#expiry_date').prop('disabled', true).attr('title', 'Expiry date is locked for existing lots')
|
||||
$('#expiry_date').prop('disabled', true)
|
||||
.attr('title', 'Expiry date is locked for existing lots')
|
||||
.val(data.expiry_date);
|
||||
if (expiryPicker && expiryPicker.altInput) { expiryPicker.altInput.disabled = true; }
|
||||
} else {
|
||||
if (expiryPicker) { expiryPicker.clear(); }
|
||||
}
|
||||
$('#product_sku').attr('secondary', data.product_sku);
|
||||
$('#product_sku, #quantity').prop('disabled', true);
|
||||
$('button[type=submit]').text('Update');
|
||||
$('button[type=reset]').hide();
|
||||
}
|
||||
|
||||
$('#product_sku,#quantity').prop('disabled', true);
|
||||
|
||||
$(`button[type=submit]`).text('Update');
|
||||
$(`button[type=reset]`).hide();
|
||||
|
||||
if (advanced) {
|
||||
retrieve_zone('to', function() {
|
||||
$('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);
|
||||
populate_fields();
|
||||
});
|
||||
});
|
||||
});
|
||||
// Set re
|
||||
} else {
|
||||
// Simple mode — skip zone/aisle cascade
|
||||
retrieve_rack('to', function() {
|
||||
$('#rack').val(data.rack).attr('disabled', true);
|
||||
populate_fields();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -351,13 +397,16 @@
|
||||
if (wh) {
|
||||
$('#warehouse').val(wh).prop('disabled', true);
|
||||
|
||||
// Chain-load location selects for the locked warehouse.
|
||||
// Unlike edit mode, we DON'T preselect zone/aisle/rack — the user picks.
|
||||
// Chain-load location selects — skip zone/aisle in simple mode
|
||||
if (advanced) {
|
||||
retrieve_zone('to', function() {
|
||||
retrieve_aisle('to', function() {
|
||||
retrieve_rack('to');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
retrieve_rack('to');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -366,6 +415,7 @@
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
await load_location_config(apply_location_labels);
|
||||
await retrieve_warehouse('to');
|
||||
await retrieve_for_edit();
|
||||
|
||||
@@ -395,15 +445,19 @@
|
||||
|
||||
<?php if(empty($_GET["id"])){ ?>
|
||||
$("select#warehouse").change(function() {
|
||||
if (advanced) {
|
||||
retrieve_zone('to');
|
||||
} else {
|
||||
retrieve_rack('to');
|
||||
}
|
||||
});
|
||||
|
||||
$("select#zone").change(function() {
|
||||
retrieve_aisle('to');
|
||||
if (advanced) retrieve_aisle('to');
|
||||
});
|
||||
|
||||
$("select#aisle").change(function() {
|
||||
retrieve_rack('to');
|
||||
if (advanced) retrieve_rack('to');
|
||||
});
|
||||
|
||||
<?php } ?>
|
||||
|
||||
@@ -61,24 +61,24 @@
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Zone -->
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label for="zone" class="form-label">Zone</label>
|
||||
<select name="zone" id="zone" class="form-select" required disabled>
|
||||
<div class="mb-3 col-lg-6" id="group_zone">
|
||||
<label for="zone" class="form-label" id="label_zone">Zone</label>
|
||||
<select name="zone" id="zone" class="form-select" disabled>
|
||||
<option value="">Please select zone</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Step 6: Aisle -->
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label for="aisle" class="form-label">Aisle</label>
|
||||
<select name="aisle" id="aisle" class="form-select" required disabled>
|
||||
<div class="mb-3 col-lg-6" id="group_aisle">
|
||||
<label for="aisle" class="form-label" id="label_aisle">Aisle</label>
|
||||
<select name="aisle" id="aisle" class="form-select" disabled>
|
||||
<option value="">Please select aisle</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Step 7: Rack -->
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label for="rack" class="form-label">Rack</label>
|
||||
<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>
|
||||
</select>
|
||||
@@ -111,19 +111,63 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function load_location_config(callback) {
|
||||
return ajax_request({
|
||||
url: server_url + 'setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('#group_zone').show();
|
||||
$('#group_aisle').show();
|
||||
$('#label_zone').text(label_zone);
|
||||
$('#label_aisle').text(label_aisle);
|
||||
$('#label_rack').text(label_rack);
|
||||
} else {
|
||||
$('#group_zone').hide();
|
||||
$('#group_aisle').hide();
|
||||
$('#label_rack').text(label_rack);
|
||||
}
|
||||
}
|
||||
|
||||
function manage_stock_out() {
|
||||
if (!advanced) {
|
||||
var rack_val = $('#rack').val();
|
||||
$('#zone').val(rack_val);
|
||||
$('#aisle').val(rack_val);
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/manage_stock_out.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/manage_stock_out.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 1,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
"contact_id": $("#contact").attr('data-id')
|
||||
'contact_id': $('#contact').attr('data-id'),
|
||||
'zone': $('#zone').val(),
|
||||
'aisle': $('#aisle').val(),
|
||||
},
|
||||
action: 'manage',
|
||||
onSuccess: function(res) {
|
||||
window.location.href = "<?php echo $server_url?>ics/stock_out.php";
|
||||
onSuccess: function() {
|
||||
window.location.href = '<?php echo $server_url?>ics/stock_out.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -227,7 +271,7 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select zone</option>';
|
||||
var option = '<option value="">Please select ' + label_zone.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item.zone}'>${item.zone}</option>`;
|
||||
});
|
||||
@@ -252,7 +296,7 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select aisle</option>';
|
||||
var option = '<option value="">Please select ' + label_aisle.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item}'>${item}</option>`;
|
||||
});
|
||||
@@ -277,7 +321,7 @@
|
||||
},
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select rack</option>';
|
||||
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item}'>${item}</option>`;
|
||||
});
|
||||
@@ -291,9 +335,9 @@
|
||||
// ── Reset location dropdowns ──────────────────────────────────────────
|
||||
function reset_location() {
|
||||
$('#warehouse').html('<option value="">Select warehouse...</option>').prop('disabled', true);
|
||||
$('#zone').html('<option value="">Please select zone</option>').prop('disabled', true);
|
||||
$('#aisle').html('<option value="">Please select aisle</option>').prop('disabled', true);
|
||||
$('#rack').html('<option value="">Please select rack</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);
|
||||
}
|
||||
|
||||
|
||||
@@ -322,34 +366,39 @@
|
||||
|
||||
$('#warehouse').val('<?php echo $_GET["wh"];?>').prop('disabled', true);
|
||||
|
||||
retrieve_zone(function() {
|
||||
$('#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);
|
||||
|
||||
function populate_fields() {
|
||||
$.each(data, function(key, item) {
|
||||
if (!['warehouse', 'zone', 'aisle', 'rack', 'lot_number', 'serial_number'].includes(key)) {
|
||||
$(`#${key}`).val(item);
|
||||
$('#' + key).val(item);
|
||||
}
|
||||
});
|
||||
|
||||
$('#contact').val(data.contact_name)
|
||||
.attr('secondary', data.contact_name)
|
||||
.attr('data-id', data.contact_id);
|
||||
|
||||
$('#product_name').val(data.product_name);
|
||||
$('#product_sku').attr('secondary', data.product_sku);
|
||||
$('#product_sku').prop('disabled', true);
|
||||
|
||||
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
|
||||
$('button[type=submit]').text('Update');
|
||||
$('button[type=reset]').hide();
|
||||
}
|
||||
|
||||
if (advanced) {
|
||||
retrieve_zone(function() {
|
||||
$('#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);
|
||||
populate_fields();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Simple mode — skip zone/aisle cascade
|
||||
retrieve_rack(function() {
|
||||
$('#rack').val(data.rack).prop('disabled', true);
|
||||
populate_fields();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -358,6 +407,7 @@
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await load_location_config(apply_location_labels);
|
||||
await retrieve_warehouse();
|
||||
await retrieve_for_edit();
|
||||
} catch(e) {
|
||||
@@ -388,19 +438,23 @@
|
||||
retrieve_warehouse();
|
||||
});
|
||||
|
||||
// Warehouse selected → load zone
|
||||
// Warehouse selected → load zone (advanced) or rack directly (simple)
|
||||
$('#warehouse').on('change', function() {
|
||||
if (advanced) {
|
||||
retrieve_zone();
|
||||
});
|
||||
|
||||
// Zone selected → load aisle
|
||||
$('#zone').on('change', function() {
|
||||
retrieve_aisle();
|
||||
});
|
||||
|
||||
// Aisle selected → load rack
|
||||
$('#aisle').on('change', function() {
|
||||
} else {
|
||||
retrieve_rack();
|
||||
}
|
||||
});
|
||||
|
||||
// Zone selected → load aisle (advanced only)
|
||||
$('#zone').on('change', function() {
|
||||
if (advanced) retrieve_aisle();
|
||||
});
|
||||
|
||||
// Aisle selected → load rack (advanced only)
|
||||
$('#aisle').on('change', function() {
|
||||
if (advanced) retrieve_rack();
|
||||
});
|
||||
|
||||
<?php } ?>
|
||||
|
||||
+177
-126
@@ -65,20 +65,20 @@
|
||||
<option value="">Select warehouse...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label">From Zone</label>
|
||||
<div class="mb-3 col-lg-3" id="group_zone_from">
|
||||
<label class="form-label" id="label_zone_from">From Zone</label>
|
||||
<select name="zone" id="zone_from" role="from" class="form-select" required disabled>
|
||||
<option value="">Please select zone</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label">From Aisle</label>
|
||||
<div class="mb-3 col-lg-3" id="group_aisle_from">
|
||||
<label class="form-label" id="label_aisle_from">From Aisle</label>
|
||||
<select name="aisle" id="aisle_from" role="from" class="form-select" required disabled>
|
||||
<option value="">Please select aisle</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label">From Rack</label>
|
||||
<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>
|
||||
</select>
|
||||
@@ -97,20 +97,20 @@
|
||||
<option value="">Select warehouse...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label">To Zone</label>
|
||||
<div class="mb-3 col-lg-3" id="group_zone_to">
|
||||
<label class="form-label" id="label_zone_to">To Zone</label>
|
||||
<select name="zone" id="zone_to" role="to" class="form-select" required>
|
||||
<option value="">Please select zone</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label">To Aisle</label>
|
||||
<div class="mb-3 col-lg-3" id="group_aisle_to">
|
||||
<label class="form-label" id="label_aisle_to">To Aisle</label>
|
||||
<select name="aisle" id="aisle_to" role="to" class="form-select" required>
|
||||
<option value="">Please select aisle</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 col-lg-3">
|
||||
<label class="form-label">To Rack</label>
|
||||
<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>
|
||||
</select>
|
||||
@@ -143,43 +143,97 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function load_location_config(callback) {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('#group_zone_from, #group_zone_to').show();
|
||||
$('#group_aisle_from, #group_aisle_to').show();
|
||||
$('#label_zone_from').text('From ' + label_zone);
|
||||
$('#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);
|
||||
// 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());
|
||||
} else {
|
||||
$('#group_zone_from, #group_zone_to').hide();
|
||||
$('#group_aisle_from, #group_aisle_to').hide();
|
||||
$('#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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Submit ────────────────────────────────────────────────────────────────
|
||||
function manage_stock_transfer() {
|
||||
|
||||
// Simple mode — mirror rack value to zone and aisle before submit
|
||||
if (!advanced) {
|
||||
var rack_from_val = $('#rack_from').val();
|
||||
var rack_to_val = $('#rack_to').val();
|
||||
$('#zone_from').val(rack_from_val);
|
||||
$('#aisle_from').val(rack_from_val);
|
||||
$('#zone_to').val(rack_to_val);
|
||||
$('#aisle_to').val(rack_to_val);
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/manage_stock_transfer.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/manage_stock_transfer.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 1,
|
||||
debugMode: 0,
|
||||
data: {
|
||||
"contact_id": $("#contact").attr('data-id')
|
||||
},
|
||||
data: { 'contact_id': $('#contact').attr('data-id') },
|
||||
action: 'manage',
|
||||
onSuccess: function(res) {
|
||||
window.location.href = "<?php echo $server_url?>ics/stock_transfer.php";
|
||||
onSuccess: function() {
|
||||
window.location.href = '<?php echo $server_url?>ics/stock_transfer.php';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Step 1: Product selected → load active lots + to-warehouses ───────
|
||||
// ── Step 1: Product selected → load active lots + from-warehouses ─────────
|
||||
function retrieve_active_lot() {
|
||||
|
||||
var sku = $('#product_sku').attr('secondary') || $('#product_sku').val();
|
||||
if (!sku) return;
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_active_lot.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_active_lot.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
data: { "product_sku": sku },
|
||||
data: { 'product_sku': sku },
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Select lot... (optional)</option>';
|
||||
$.each(res.output, function(i, item) {
|
||||
var label = item.lot_number;
|
||||
if (item.expiry_date) label += ' — exp: ' + item.expiry_date;
|
||||
option += `<option value="${item.lot_number}">${label}</option>`;
|
||||
option += '<option value="' + item.lot_number + '">' + label + '</option>';
|
||||
});
|
||||
$('#lot_number').html(option).prop('disabled', false);
|
||||
$('#serial_number').html('<option value="">Select serial... (optional)</option>').prop('disabled', false);
|
||||
@@ -189,26 +243,21 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Step 2: Lot selected → load active serials ────────────────────────
|
||||
// ── Step 2: Lot selected → load active serials ────────────────────────────
|
||||
function retrieve_active_serial() {
|
||||
|
||||
var sku = $('#product_sku').attr('secondary') || $('#product_sku').val();
|
||||
if (!sku) return;
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_active_serial.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_active_serial.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
action: 'read',
|
||||
data: {
|
||||
"product_sku": sku,
|
||||
"lot_number": $('#lot_number').val()
|
||||
},
|
||||
data: { 'product_sku': sku, 'lot_number': $('#lot_number').val() },
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Select serial... (optional)</option>';
|
||||
$.each(res.output, function(i, serial) {
|
||||
option += `<option value="${serial}">${serial}</option>`;
|
||||
option += '<option value="' + serial + '">' + serial + '</option>';
|
||||
});
|
||||
$('#serial_number').html(option).prop('disabled', false);
|
||||
reset_from_location();
|
||||
@@ -217,199 +266,205 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Warehouse loaders ─────────────────────────────────────────────────
|
||||
// ── Warehouse loaders ─────────────────────────────────────────────────────
|
||||
function retrieve_warehouse(type) {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
data: { "type": type, "lot_number": type === 'from' ? $('#lot_number').val() : '' },
|
||||
data: { 'type': type, 'lot_number': type === 'from' ? $('#lot_number').val() : '' },
|
||||
action: 'read',
|
||||
debugMode: 0,
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Select warehouse...</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
|
||||
option += '<option value="' + item.id + '">' + item.warehouse_name + '</option>';
|
||||
});
|
||||
$(`select[name='warehouse'][role='${type}']`).html(option).prop('disabled', false);
|
||||
$('select[name="warehouse"][role="' + type + '"]').html(option).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Zone / Aisle / Rack — From (type=from, filtered by sku+lot+serial) ─
|
||||
// ── Zone / Aisle / Rack loaders ───────────────────────────────────────────
|
||||
function retrieve_zone(type, callback) {
|
||||
|
||||
var extra = {};
|
||||
if (type === 'from') {
|
||||
extra = {
|
||||
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
"lot_number": $('#lot_number').val(),
|
||||
"serial_number": $('#serial_number').val()
|
||||
'product_sku': $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
'lot_number': $('#lot_number').val(),
|
||||
'serial_number': $('#serial_number').val()
|
||||
};
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_zone.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_zone.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
data: Object.assign({ "type": type }, extra),
|
||||
data: Object.assign({ 'type': type }, extra),
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select zone</option>';
|
||||
var option = '<option value="">Please select ' + label_zone.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item.zone}'>${item.zone}</option>`;
|
||||
option += '<option value="' + item.zone + '">' + item.zone + '</option>';
|
||||
});
|
||||
$(`select[name='zone'][role='${type}']`).html(option).prop('disabled', false);
|
||||
$('select[name="zone"][role="' + type + '"]').html(option).prop('disabled', false);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_aisle(type, callback) {
|
||||
|
||||
var extra = {};
|
||||
if (type === 'from') {
|
||||
extra = {
|
||||
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
"lot_number": $('#lot_number').val(),
|
||||
"serial_number": $('#serial_number').val()
|
||||
'product_sku': $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
'lot_number': $('#lot_number').val(),
|
||||
'serial_number': $('#serial_number').val()
|
||||
};
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_aisle.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_aisle.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
data: Object.assign({ "type": type }, extra),
|
||||
data: Object.assign({ 'type': type }, extra),
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select aisle</option>';
|
||||
var option = '<option value="">Please select ' + label_aisle.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item}'>${item}</option>`;
|
||||
option += '<option value="' + item + '">' + item + '</option>';
|
||||
});
|
||||
$(`select[name='aisle'][role='${type}']`).html(option).prop('disabled', false);
|
||||
$('select[name="aisle"][role="' + type + '"]').html(option).prop('disabled', false);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function retrieve_rack(type, callback) {
|
||||
|
||||
var extra = {};
|
||||
if (type === 'from') {
|
||||
extra = {
|
||||
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
"lot_number": $('#lot_number').val(),
|
||||
"serial_number": $('#serial_number').val()
|
||||
'product_sku': $('#product_sku').attr('secondary') || $('#product_sku').val(),
|
||||
'lot_number': $('#lot_number').val(),
|
||||
'serial_number': $('#serial_number').val()
|
||||
};
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_rack.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
noLoading: true,
|
||||
data: Object.assign({ "type": type }, extra),
|
||||
data: Object.assign({ 'type': type }, extra),
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '<option value="">Please select rack</option>';
|
||||
var option = '<option value="">Please select ' + label_rack.toLowerCase() + '</option>';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item}'>${item}</option>`;
|
||||
option += '<option value="' + item + '">' + item + '</option>';
|
||||
});
|
||||
$(`select[name='rack'][role='${type}']`).html(option).prop('disabled', false);
|
||||
$('select[name="rack"][role="' + type + '"]').html(option).prop('disabled', false);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Reset from-location dropdowns ─────────────────────────────────────
|
||||
// ── Reset from-location dropdowns ─────────────────────────────────────────
|
||||
function reset_from_location() {
|
||||
$('#warehouse_from').html('<option value="">Select warehouse...</option>').prop('disabled', true);
|
||||
$('#zone_from').html('<option value="">Please select zone</option>').prop('disabled', true);
|
||||
$('#aisle_from').html('<option value="">Please select aisle</option>').prop('disabled', true);
|
||||
$('#rack_from').html('<option value="">Please select rack</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);
|
||||
}
|
||||
|
||||
|
||||
// ── Edit mode ─────────────────────────────────────────────────────────
|
||||
// ── Edit mode ─────────────────────────────────────────────────────────────
|
||||
function retrieve_for_edit() {
|
||||
|
||||
<?php if(empty($_GET["id"])){ ?>
|
||||
<?php if(empty($_GET['id'])){ ?>
|
||||
return false;
|
||||
<?php } ?>
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_stock_transfer.php",
|
||||
url: '<?php echo $server_url?>ics/api/engine/retrieve_stock_transfer.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
debugMode: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
|
||||
var data = res.output;
|
||||
var from_wh = data.ref.ref_warehouse;
|
||||
var to_wh = data.ref_warehouse;
|
||||
|
||||
// Lock lot/serial
|
||||
$('#lot_number').html(`<option value="${data.lot_number || ''}">${data.lot_number || '—'}</option>`)
|
||||
$('#lot_number').html('<option value="' + (data.lot_number || '') + '">' + (data.lot_number || '—') + '</option>')
|
||||
.val(data.lot_number || '').prop('disabled', true);
|
||||
$('#serial_number').html(`<option value="${data.serial_number || ''}">${data.serial_number || '—'}</option>`)
|
||||
$('#serial_number').html('<option value="' + (data.serial_number || '') + '">' + (data.serial_number || '—') + '</option>')
|
||||
.val(data.serial_number || '').prop('disabled', true);
|
||||
|
||||
// From side
|
||||
$("select[name='warehouse'][role='from']").val(from_wh).prop('disabled', true);
|
||||
if (advanced) {
|
||||
// Full cascade for From
|
||||
$('select[name="warehouse"][role="from"]').val(from_wh).prop('disabled', true);
|
||||
retrieve_zone('from', function() {
|
||||
$("select[name='zone'][role='from']").val(data.zone).prop('disabled', true);
|
||||
$('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);
|
||||
$('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);
|
||||
|
||||
// To side
|
||||
$("select[name='warehouse'][role='to']").val(to_wh).prop('disabled', true);
|
||||
retrieve_zone('to', function() {
|
||||
$("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);
|
||||
});
|
||||
});
|
||||
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true);
|
||||
populate_to_side(data, to_wh);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Simple mode — skip zone/aisle cascade, load rack directly
|
||||
$('select[name="warehouse"][role="from"]').val(from_wh).prop('disabled', true);
|
||||
retrieve_rack('from', function() {
|
||||
$('select[name="rack"][role="from"]').val(data.rack).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)) {
|
||||
$(`#${key}`).val(item);
|
||||
$('#' + key).val(item);
|
||||
}
|
||||
});
|
||||
|
||||
$('#contact').val(data.contact_name)
|
||||
.attr('secondary', data.contact_name)
|
||||
.attr('data-id', data.contact_id);
|
||||
|
||||
$('#product_name').val(data.product_name);
|
||||
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
|
||||
|
||||
$('button[type=submit]').text('Update');
|
||||
$('button[type=reset]').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function populate_to_side(data, to_wh) {
|
||||
$('select[name="warehouse"][role="to"]').val(to_wh).prop('disabled', true);
|
||||
retrieve_zone('to', function() {
|
||||
$('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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
function populate_to_side_simple(data, to_wh) {
|
||||
$('select[name="warehouse"][role="to"]').val(to_wh).prop('disabled', true);
|
||||
retrieve_rack('to', function() {
|
||||
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await load_location_config(apply_location_labels);
|
||||
await retrieve_warehouse('to');
|
||||
await retrieve_for_edit();
|
||||
} catch(e) {
|
||||
@@ -418,10 +473,9 @@
|
||||
});
|
||||
|
||||
|
||||
// ── Change handlers (create mode only) ───────────────────────────────
|
||||
<?php if(empty($_GET["id"])){ ?>
|
||||
// ── Change handlers (create mode only) ───────────────────────────────────
|
||||
<?php if(empty($_GET['id'])){ ?>
|
||||
|
||||
// SKU selected → load lots + from-warehouse
|
||||
$('input#product_sku').on('blur', function() {
|
||||
var sku = $(this).attr('secondary');
|
||||
if (!sku) return;
|
||||
@@ -429,50 +483,47 @@
|
||||
retrieve_warehouse('from');
|
||||
});
|
||||
|
||||
// Lot selected → reload serials + reset from-location
|
||||
$('#lot_number').on('change', function() {
|
||||
retrieve_active_serial();
|
||||
retrieve_warehouse('from');
|
||||
});
|
||||
|
||||
// Serial selected → reload from-warehouse
|
||||
$('#serial_number').on('change', function() {
|
||||
retrieve_warehouse('from');
|
||||
});
|
||||
|
||||
// From warehouse → from zone
|
||||
$("select[name='warehouse'][role='from']").on('change', function() {
|
||||
retrieve_zone('from');
|
||||
// From warehouse → zone (advanced) or rack directly (simple)
|
||||
$('select[name="warehouse"][role="from"]').on('change', function() {
|
||||
if (advanced) { retrieve_zone('from'); } else { retrieve_rack('from'); }
|
||||
});
|
||||
|
||||
// From zone → from aisle
|
||||
$("select[name='zone'][role='from']").on('change', function() {
|
||||
retrieve_aisle('from');
|
||||
// From zone → aisle (advanced only)
|
||||
$('select[name="zone"][role="from"]').on('change', function() {
|
||||
if (advanced) retrieve_aisle('from');
|
||||
});
|
||||
|
||||
// From aisle → from rack
|
||||
$("select[name='aisle'][role='from']").on('change', function() {
|
||||
retrieve_rack('from');
|
||||
// From aisle → rack (advanced only)
|
||||
$('select[name="aisle"][role="from"]').on('change', function() {
|
||||
if (advanced) retrieve_rack('from');
|
||||
});
|
||||
|
||||
// To warehouse → to zone
|
||||
$("select[name='warehouse'][role='to']").on('change', function() {
|
||||
retrieve_zone('to');
|
||||
// To warehouse → zone (advanced) or rack directly (simple)
|
||||
$('select[name="warehouse"][role="to"]').on('change', function() {
|
||||
if (advanced) { retrieve_zone('to'); } else { retrieve_rack('to'); }
|
||||
});
|
||||
|
||||
// To zone → to aisle
|
||||
$("select[name='zone'][role='to']").on('change', function() {
|
||||
retrieve_aisle('to');
|
||||
// To zone → aisle (advanced only)
|
||||
$('select[name="zone"][role="to"]').on('change', function() {
|
||||
if (advanced) retrieve_aisle('to');
|
||||
});
|
||||
|
||||
// To aisle → to rack
|
||||
$("select[name='aisle'][role='to']").on('change', function() {
|
||||
retrieve_rack('to');
|
||||
// To aisle → rack (advanced only)
|
||||
$('select[name="aisle"][role="to"]').on('change', function() {
|
||||
if (advanced) retrieve_rack('to');
|
||||
});
|
||||
|
||||
<?php } ?>
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+36
-7
@@ -51,9 +51,9 @@
|
||||
<th>Lot Number</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Quantity</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -78,6 +78,31 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
} else {
|
||||
$('.th_loc_zone, .th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
}
|
||||
}
|
||||
|
||||
function location_cells(item) {
|
||||
if (advanced) {
|
||||
return `<td class="py-3">${item['zone'] || ''}</td>
|
||||
<td class="py-3">${item['aisle'] || ''}</td>
|
||||
<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
}
|
||||
return `<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
}
|
||||
|
||||
function retrieve_stock_in() {
|
||||
|
||||
return ajax_request({
|
||||
@@ -129,9 +154,7 @@
|
||||
<td class="py-3">${item['lot_number'] || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item['serial_number'] || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item['quantity']}</td>
|
||||
<td class="py-3">${item['zone']}</td>
|
||||
<td class="py-3">${item['aisle']}</td>
|
||||
<td class="py-3">${item['rack']}</td>
|
||||
${location_cells(item)}
|
||||
<td class="py-3">
|
||||
${item['status'] == 1 ? '<span class="badge bg-success">Approved</span>' : '<span class="badge bg-warning text-dark">Draft</span>'}
|
||||
</td>
|
||||
@@ -213,7 +236,13 @@
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
auto_approve = res.output.default_stock_status == 1;
|
||||
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_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+36
-7
@@ -51,9 +51,9 @@
|
||||
<th>Lot Number</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Quantity</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -78,6 +78,31 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
} else {
|
||||
$('.th_loc_zone, .th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
}
|
||||
}
|
||||
|
||||
function location_cells(item) {
|
||||
if (advanced) {
|
||||
return `<td class="py-3">${item['zone'] || ''}</td>
|
||||
<td class="py-3">${item['aisle'] || ''}</td>
|
||||
<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
}
|
||||
return `<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
}
|
||||
|
||||
function retrieve_stock_out() {
|
||||
|
||||
return ajax_request({
|
||||
@@ -129,9 +154,7 @@
|
||||
<td class="py-3">${item['lot_number'] || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item['serial_number'] || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item['quantity']}</td>
|
||||
<td class="py-3">${item['zone']}</td>
|
||||
<td class="py-3">${item['aisle']}</td>
|
||||
<td class="py-3">${item['rack']}</td>
|
||||
${location_cells(item)}
|
||||
<td class="py-3">
|
||||
${item['status'] == 1 ? '<span class="badge bg-success">Approved</span>' : '<span class="badge bg-warning text-dark">Draft</span>'}
|
||||
</td>
|
||||
@@ -213,7 +236,13 @@
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
auto_approve = res.output.default_stock_status == 1;
|
||||
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_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
<th>Date</th>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -76,6 +76,31 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
} else {
|
||||
$('.th_loc_zone, .th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
}
|
||||
}
|
||||
|
||||
function location_cells(item) {
|
||||
if (advanced) {
|
||||
return `<td class="py-3">${item['zone'] || ''}</td>
|
||||
<td class="py-3">${item['aisle'] || ''}</td>
|
||||
<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
}
|
||||
return `<td class="py-3">${item['rack'] || ''}</td>`;
|
||||
}
|
||||
|
||||
function retrieve_stock_transfer() {
|
||||
|
||||
return ajax_request({
|
||||
@@ -125,9 +150,7 @@
|
||||
<td class="py-3">${format_date(item["date"])}</td>
|
||||
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
|
||||
<td class="py-3">${item['quantity']}</td>
|
||||
<td class="py-3">${item['zone']}</td>
|
||||
<td class="py-3">${item['aisle']}</td>
|
||||
<td class="py-3">${item['rack']}</td>
|
||||
${location_cells(item)}
|
||||
<td class="py-3">
|
||||
${item['status'] == 1 ? '<span class="badge bg-success">Approved</span>' : '<span class="badge bg-warning text-dark">Draft</span>'}
|
||||
</td>
|
||||
@@ -211,7 +234,13 @@
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
auto_approve = res.output.default_stock_status == 1;
|
||||
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_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,59 +28,84 @@
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
|
||||
<!-- Warehouse — always shown -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="warehouse" class="form-label">Warehouse</label>
|
||||
<select class="form-select" id="warehouse" required> </select>
|
||||
<select class="form-select" id="warehouse" required></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simple mode — from/to location fields -->
|
||||
<div id="form_simple">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="location_from" class="form-label" id="label_location_from">Location From</label>
|
||||
<input type="text" class="form-control" id="location_from"
|
||||
placeholder="e.g. Shelf-A or 1">
|
||||
<div class="form-text">Single value or range start e.g. Shelf-A, Bin-1, 1</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="location_to" class="form-label" id="label_location_to">Location To</label>
|
||||
<input type="text" class="form-control" id="location_to"
|
||||
placeholder="e.g. Shelf-E or 5">
|
||||
<div class="form-text">Same as From for a single location</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced mode — zone + aisle range + rack range -->
|
||||
<div id="form_advanced" class="d-none">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="zone" class="form-label">Zone</label>
|
||||
<input type="text" class="form-control" id="zone" placeholder="Enter zone" required>
|
||||
<label for="zone" class="form-label" id="label_zone">Zone</label>
|
||||
<input type="text" class="form-control" id="zone" placeholder="e.g. A">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="aisle_from" class="form-label">Aisle From</label>
|
||||
<input type="text" class="form-control" id="aisle_from" placeholder="e.g. 1 or A" required
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Enter a number (e.g. 1) or a single letter (e.g. A)">
|
||||
<label for="aisle_from" class="form-label" id="label_aisle_from">Aisle From</label>
|
||||
<input type="text" class="form-control" id="aisle_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="aisle_to" class="form-label">Aisle To</label>
|
||||
<input type="text" class="form-control" id="aisle_to" placeholder="e.g. 5 or E" required
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Enter a number (e.g. 5) or a single letter (e.g. E)">
|
||||
<label for="aisle_to" class="form-label" id="label_aisle_to">Aisle To</label>
|
||||
<input type="text" class="form-control" id="aisle_to" placeholder="e.g. 5 or E"
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter">
|
||||
<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">Rack From</label>
|
||||
<input type="text" class="form-control" id="rack_from" placeholder="e.g. 1 or A" required
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Enter a number (e.g. 1) or a single letter (e.g. A)">
|
||||
<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"
|
||||
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">Rack To</label>
|
||||
<input type="text" class="form-control" id="rack_to" placeholder="e.g. 10 or J" required
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Enter a number (e.g. 10) or a single letter (e.g. J)">
|
||||
<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"
|
||||
pattern="^([0-9]+|[A-Za-z])$" title="Number or single letter">
|
||||
<div class="form-text">Must be same type as Rack From</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Description — always shown -->
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" rows="4"
|
||||
placeholder="Enter warehouse description"></textarea>
|
||||
placeholder="Enter storage description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary" onclick="manage_storage();">Add Storage</button>
|
||||
<button type="reset" class="btn btn-secondary" onclick="reset_input('#storageForm')">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -91,116 +116,188 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
function manage_storage() {
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
// Validate that aisle from/to and rack from/to are same type
|
||||
var af = $('#aisle_from').val().trim().toUpperCase();
|
||||
var at = $('#aisle_to').val().trim().toUpperCase();
|
||||
var rf = $('#rack_from').val().trim().toUpperCase();
|
||||
var rt = $('#rack_to').val().trim().toUpperCase();
|
||||
function load_company_setting() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('#form_simple').addClass('d-none');
|
||||
$('#form_advanced').removeClass('d-none');
|
||||
$('#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');
|
||||
} 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');
|
||||
$('#location_from').attr('placeholder', 'e.g. Shelf-A or 1');
|
||||
$('#location_to').attr('placeholder', 'e.g. Shelf-E or 5');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save storage ──────────────────────────────────────────────────────────
|
||||
function manage_storage() {
|
||||
var zone, aisle_from, aisle_to, rack_from, rack_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();
|
||||
|
||||
var isNumeric = function(v) { return /^[0-9]+$/.test(v); };
|
||||
var isAlpha = function(v) { return /^[A-Z]$/.test(v); };
|
||||
|
||||
if (af && at) {
|
||||
if (isNumeric(af) !== isNumeric(at)) {
|
||||
alert('Aisle From and Aisle To must both be numbers or both be letters.');
|
||||
return;
|
||||
if (aisle_from && aisle_to) {
|
||||
if (isNumeric(aisle_from) !== isNumeric(aisle_to)) {
|
||||
alert(label_aisle + ' From and To must both be numbers or both be letters.'); return;
|
||||
}
|
||||
if (isAlpha(af) && isAlpha(at) && af.charCodeAt(0) > at.charCodeAt(0)) {
|
||||
alert('Aisle From must be less than or equal to Aisle To.');
|
||||
return;
|
||||
if (isAlpha(aisle_from) && aisle_from.charCodeAt(0) > aisle_to.charCodeAt(0)) {
|
||||
alert(label_aisle + ' From must be less than or equal to To.'); return;
|
||||
}
|
||||
if (isNumeric(af) && isNumeric(at) && parseInt(af) > parseInt(at)) {
|
||||
alert('Aisle From must be less than or equal to Aisle To.');
|
||||
return;
|
||||
if (isNumeric(aisle_from) && parseInt(aisle_from) > parseInt(aisle_to)) {
|
||||
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 (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 (isNumeric(rack_from) && parseInt(rack_from) > parseInt(rack_to)) {
|
||||
alert(label_rack + ' From must be less than or equal to To.'); return;
|
||||
}
|
||||
}
|
||||
|
||||
if (rf && rt) {
|
||||
if (isNumeric(rf) !== isNumeric(rt)) {
|
||||
alert('Rack From and Rack To must both be numbers or both be letters.');
|
||||
return;
|
||||
} else {
|
||||
// Simple mode — from/to, same value written to zone/aisle/rack
|
||||
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;
|
||||
}
|
||||
if (isAlpha(rf) && isAlpha(rt) && rf.charCodeAt(0) > rt.charCodeAt(0)) {
|
||||
alert('Rack From must be less than or equal to Rack To.');
|
||||
return;
|
||||
}
|
||||
if (isNumeric(rf) && isNumeric(rt) && parseInt(rf) > parseInt(rt)) {
|
||||
alert('Rack From must be less than or equal to Rack To.');
|
||||
return;
|
||||
// If To is blank, default to same as From (single location)
|
||||
if (!loc_to) loc_to = loc_from;
|
||||
|
||||
// Frontend prefix validation for better UX
|
||||
var prefix_pattern = /^(.*?)([A-Za-z]|[0-9]+)$/;
|
||||
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;
|
||||
}
|
||||
|
||||
zone = loc_from;
|
||||
aisle_from = loc_from;
|
||||
aisle_to = loc_to;
|
||||
rack_from = loc_from;
|
||||
rack_to = loc_to;
|
||||
}
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>inventory/api/engine/manage_storage.php",
|
||||
url: '<?php echo $server_url?>inventory/api/engine/manage_storage.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
debugMode: 0,
|
||||
action: 'manage',
|
||||
onSuccess: function(res) {
|
||||
|
||||
window.location.href = "<?php echo $server_url?>inventory/warehouse.php";
|
||||
|
||||
data: {
|
||||
warehouse: $('#warehouse').val(),
|
||||
zone: zone,
|
||||
aisle_from: aisle_from,
|
||||
aisle_to: aisle_to,
|
||||
rack_from: rack_from,
|
||||
rack_to: rack_to,
|
||||
description: $('#description').val(),
|
||||
},
|
||||
onSuccess: function() {
|
||||
window.location.href = '<?php echo $server_url?>inventory/warehouse.php';
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ── Retrieve warehouses ───────────────────────────────────────────────────
|
||||
function retrieve_warehouse() {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>inventory/api/engine/warehouse.php",
|
||||
url: '<?php echo $server_url?>inventory/api/engine/warehouse.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = ``;
|
||||
var option = '';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
|
||||
})
|
||||
$(`select#warehouse`).html(option);
|
||||
option += '<option value="' + item.id + '">' + item.warehouse_name + '</option>';
|
||||
});
|
||||
$('select#warehouse').html(option);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ── Load for edit ─────────────────────────────────────────────────────────
|
||||
function retrieve_for_edit() {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>inventory/api/engine/retrieve_storage.php",
|
||||
url: '<?php echo $server_url?>inventory/api/engine/retrieve_storage.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
|
||||
$.each(res.output, function(key, item) {
|
||||
$(`#${key}`).val(item);
|
||||
});
|
||||
|
||||
$(`button[type=submit]`).text('Update Storage');
|
||||
|
||||
$(`button[type=reset]`).hide();
|
||||
|
||||
var d = res.output;
|
||||
$('#warehouse').val(d.warehouse);
|
||||
$('#description').val(d.description || '');
|
||||
if (advanced) {
|
||||
$('#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 || '');
|
||||
} else {
|
||||
$('#location_from').val(d.rack_from || '');
|
||||
$('#location_to').val(d.rack_to || '');
|
||||
}
|
||||
$('button[type=submit]').text('Update Storage');
|
||||
$('button[type=reset]').hide();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── Boot ──────────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await load_company_setting();
|
||||
await retrieve_warehouse();
|
||||
<?php if(!empty($_GET["id"])){ ?>
|
||||
<?php if (!empty($_GET['id'])) { ?>
|
||||
await retrieve_for_edit();
|
||||
<?php } ?>
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
<div class="mb-6 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h2 class="fs-4 mb-2">Storage Locations</h2>
|
||||
<p>Zones, aisles, and rack assignments</p>
|
||||
<p id="storage_subtitle">Zones, aisles, and rack assignments</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<?php echo $server_url?>inventory/manage_storage.php" class="btn btn-secondary">
|
||||
@@ -228,9 +228,9 @@
|
||||
<thead class="table-secondary">
|
||||
<tr>
|
||||
<th class="py-3">Warehouse</th>
|
||||
<th class="py-3">Zone</th>
|
||||
<th class="py-3">Aisle Range</th>
|
||||
<th class="py-3">Rack Range</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">Items Stored</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@@ -251,6 +251,54 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function load_company_setting() {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apply_location_labels() {
|
||||
// Stats card subtitles
|
||||
$('#total_capacity small').text(label_rack + ' positions available');
|
||||
$('#total_space_used small').text(label_rack + 's occupied');
|
||||
|
||||
// Storage tab subtitle + search
|
||||
$('#storage_subtitle').text(advanced
|
||||
? label_zone + 's, ' + label_aisle + 's, and ' + label_rack + ' assignments'
|
||||
: label_rack + ' assignments');
|
||||
$('#search_storage').attr('placeholder', advanced
|
||||
? 'Search by warehouse or ' + label_zone.toLowerCase() + '…'
|
||||
: 'Search by warehouse or ' + label_rack.toLowerCase() + '…');
|
||||
|
||||
// Table headers
|
||||
if (advanced) {
|
||||
$('#th_level1').text(label_zone).show();
|
||||
$('#th_level2').text(label_aisle + ' Range').show();
|
||||
$('#th_level3').text(label_rack + ' Range');
|
||||
} else {
|
||||
$('#th_level1').hide();
|
||||
$('#th_level2').hide();
|
||||
$('#th_level3').text(label_rack + ' Range');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Current search terms (read by change_page_* when paginating) ─────
|
||||
var search_term_warehouse = '';
|
||||
var search_term_storage = '';
|
||||
@@ -379,7 +427,8 @@
|
||||
var filtered = kw ?
|
||||
alasql(`SELECT * FROM storage
|
||||
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
|
||||
OR LOWER(zone) LIKE '%${kw}%'`) :
|
||||
OR LOWER(zone) LIKE '%${kw}%'
|
||||
OR LOWER(rack) LIKE '%${kw}%'`) :
|
||||
alasql(`SELECT * FROM storage`);
|
||||
|
||||
var page = generate_pagination('storage', filtered.length);
|
||||
@@ -396,11 +445,15 @@
|
||||
|
||||
var body = '';
|
||||
$.each(page_data, function(key, item) {
|
||||
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>`;
|
||||
|
||||
body += `<tr>
|
||||
<td class="py-3">${item['warehouse_name']}</td>
|
||||
<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>
|
||||
${location_cells}
|
||||
<td class="py-3"></td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>inventory/manage_storage.php?id=${item['id']}" class=""><i class="ti ti-eye fs-5"></i></a>
|
||||
@@ -477,6 +530,7 @@
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await load_company_setting();
|
||||
await retrieve_stats();
|
||||
await retrieve_warehouse();
|
||||
await retrieve_storage();
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReturnManager.php';
|
||||
require '../../../assets/utils/classes/WarehouseManager.php';
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
$answer['message'] = 'Invalid ID.';
|
||||
$answer['message'] = 'Invalid return ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
|
||||
$sth = $pdo->prepare(
|
||||
"SELECT status FROM td_return
|
||||
WHERE company_id = :company_id AND id = :id"
|
||||
);
|
||||
$sth->execute([':company_id' => $company_id, ':id' => $id]);
|
||||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) throw new Exception("Return not found.");
|
||||
if ((int)$row['status'] !== 0) throw new Exception("Only draft returns can be cancelled.");
|
||||
|
||||
$pdo->prepare(
|
||||
"UPDATE td_return SET status = -1
|
||||
WHERE id = :id AND company_id = :company_id"
|
||||
)->execute([':id' => $id, ':company_id' => $company_id]);
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging) {
|
||||
$ret = new ReturnManager($pdo, $company_id);
|
||||
$whMgmt = new WarehouseManager($pdo, $company_id);
|
||||
$ret->cancelReturn($id, $logging, $whMgmt);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/OrderManager.php';
|
||||
require '../../../assets/utils/classes/CompanySettingManager.php';
|
||||
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$fulfillment_status = (int)($data['fulfillment_status'] ?? 0);
|
||||
$tracking_number = trim((string)($data['shipping_tracking_number'] ?? ''));
|
||||
|
||||
if (!$id) {
|
||||
$answer['message'] = 'Invalid order ID.';
|
||||
http_response_code(400);
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$csm = new CompanySettingManager($pdo1, $company_id);
|
||||
$auto_complete = (int)$csm->get('auto_complete_on_ship') === 1;
|
||||
|
||||
try {
|
||||
dbTransaction($pdo2, function($pdo) use ($id, $fulfillment_status, $tracking_number, $company_id, $logging, $auto_complete) {
|
||||
$order = new OrderManager($pdo, $company_id);
|
||||
$order->updateFulfillmentStatus($id, $fulfillment_status, $tracking_number, $logging, $auto_complete);
|
||||
});
|
||||
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Order updated.';
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$answer['message'] = 'Database error, please try again.';
|
||||
http_response_code(500);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$answer['message'] = $e->getMessage();
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -201,7 +201,7 @@
|
||||
var status_hint = !is_invoice ? 'Status cannot be changed on credit/debit notes'
|
||||
: status === 0 ? 'Issue the invoice first before changing status'
|
||||
: status === 4 ? 'Invoice is void' : '';
|
||||
set_btn_state('#invoice_status_group button', can_change_status, status_hint);
|
||||
|
||||
$('#invoice_status_select').prop('disabled', !can_change_status);
|
||||
|
||||
}
|
||||
|
||||
+61
-16
@@ -60,6 +60,12 @@
|
||||
placeholder="DD/MM/YYYY" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Shipping Tracking Number</label>
|
||||
<input type="text" id="shipping_tracking_number" class="form-control"
|
||||
placeholder="Carrier tracking number..." maxlength="100" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-12">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea id="notes" class="form-control" rows="2"
|
||||
@@ -112,8 +118,10 @@
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<span id="badge_status"></span>
|
||||
<span id="badge_payment"></span>
|
||||
<span id="badge_fulfillment"></span>
|
||||
</div>
|
||||
<p class="mb-0 small text-muted" id="order_number_display"></p>
|
||||
<p class="mb-0 small text-muted d-none" id="tracking_number_display"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -172,7 +180,21 @@
|
||||
<i class="ti ti-file-invoice me-1"></i>Proceed to Invoice
|
||||
</button>
|
||||
|
||||
<!-- Update payment status (confirmed and above) -->
|
||||
<!-- Update fulfillment status (confirmed and above) -->
|
||||
<div id="fulfillment_status_group">
|
||||
<label class="form-label small text-muted mb-1">Fulfillment Status</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="fulfillment_status_select" class="form-select form-select-sm">
|
||||
<option value="1">Picking</option>
|
||||
<option value="2">Packed</option>
|
||||
<option value="3">Shipped</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary text-nowrap"
|
||||
onclick="update_fulfillment_status()">
|
||||
<i class="ti ti-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="payment_status_group">
|
||||
<label class="form-label small text-muted mb-1">Payment Status</label>
|
||||
<div class="d-flex gap-2">
|
||||
@@ -242,6 +264,15 @@
|
||||
return map[String(status)] ?? '—';
|
||||
}
|
||||
|
||||
function fulfillment_status_badge(status) {
|
||||
const map = {
|
||||
'1': '<span class="badge bg-warning text-dark"><i class="ti ti-list-check me-1"></i>Picking</span>',
|
||||
'2': '<span class="badge bg-info text-white"><i class="ti ti-package me-1"></i>Packed</span>',
|
||||
'3': '<span class="badge bg-success"><i class="ti ti-truck me-1"></i>Shipped</span>',
|
||||
};
|
||||
return map[String(status)] ?? '';
|
||||
}
|
||||
|
||||
|
||||
// ── Recalculate totals ────────────────────────────────────────────────────
|
||||
function recalc_totals() {
|
||||
@@ -346,37 +377,45 @@
|
||||
|
||||
|
||||
// ── Set view/edit mode ────────────────────────────────────────────────────
|
||||
function set_mode(status, has_active_invoice, returnable_count) {
|
||||
function set_mode(status, has_active_invoice, returnable_count, fulfillment_status) {
|
||||
status = parseInt(status);
|
||||
has_active_invoice = has_active_invoice || false;
|
||||
returnable_count = returnable_count ?? 1;
|
||||
fulfillment_status = parseInt(fulfillment_status) || 0;
|
||||
var is_draft = status === 0;
|
||||
var can_confirm = status === 0;
|
||||
var can_cancel = status === 0 || status === 1;
|
||||
var is_cancelled = status === -1;
|
||||
var is_completed = status === 3;
|
||||
var can_fulfill = status >= 1 && !is_cancelled && !is_completed;
|
||||
|
||||
// Edit controls
|
||||
$('#btn_add_item, #contact, #order_date, #notes, #discount, #tax, #shipping_fee')
|
||||
.prop('disabled', !is_draft);
|
||||
$('#shipping_tracking_number').prop('disabled', !(is_draft || can_fulfill));
|
||||
$('.item_sku, .item_qty, .item_price, .item_warehouse').prop('disabled', !is_draft);
|
||||
$('a[onclick^="remove_item"]').toggleClass('d-none', !is_draft);
|
||||
|
||||
// Action buttons — set_btn_state handles class swap + disabled + title
|
||||
// Action buttons
|
||||
set_btn_state('#btn_save', is_draft, 'Only draft orders can be edited');
|
||||
set_btn_state('#btn_confirm', can_confirm, 'Order must be in draft to confirm');
|
||||
set_btn_state('#btn_cancel', can_cancel && !is_cancelled,
|
||||
is_cancelled ? 'Order is already cancelled' : 'Only draft or confirmed orders can be cancelled');
|
||||
|
||||
// Order status selector — confirmed and above, not cancelled
|
||||
var can_change_status = status >= 1 && !is_cancelled;
|
||||
$('#order_status_group').toggleClass('d-none', !can_change_status);
|
||||
set_btn_state('#order_status_group button', can_change_status,
|
||||
is_cancelled ? 'Order is cancelled' : 'Confirm the order first');
|
||||
$('#order_status_select').prop('disabled', !can_change_status);
|
||||
// Fulfillment status — confirmed and above, not cancelled, not completed
|
||||
$('#fulfillment_status_group').toggleClass('d-none', !can_fulfill);
|
||||
if (can_fulfill) {
|
||||
// Only show options ahead of current fulfillment
|
||||
$('#fulfillment_status_select option').each(function() {
|
||||
$(this).prop('disabled', parseInt($(this).val()) <= fulfillment_status);
|
||||
});
|
||||
// Default to next step
|
||||
var next = fulfillment_status + 1;
|
||||
if (next <= 3) $('#fulfillment_status_select').val(next);
|
||||
else $('#fulfillment_status_select').val(fulfillment_status);
|
||||
}
|
||||
|
||||
// Payment status selector
|
||||
set_btn_state('#payment_status_group button', status >= 1 && !is_cancelled,
|
||||
'Confirm the order before updating payment status');
|
||||
$('#payment_status_select').prop('disabled', status < 1 || is_cancelled);
|
||||
|
||||
// Create Return
|
||||
@@ -421,12 +460,17 @@
|
||||
$('#status_card').removeClass('d-none');
|
||||
$('#badge_status').html(order_status_badge(o.status));
|
||||
$('#badge_payment').html(payment_status_badge(o.payment_status));
|
||||
$('#badge_fulfillment').html(fulfillment_status_badge(o.fulfillment_status));
|
||||
$('#order_number_display').text(o.order_number);
|
||||
$('#tracking_number_display')
|
||||
.toggleClass('d-none', !o.shipping_tracking_number)
|
||||
.text(o.shipping_tracking_number ? 'Tracking: ' + o.shipping_tracking_number : '');
|
||||
|
||||
// Fields
|
||||
$('#contact').val(o.contact_name || '');
|
||||
$('#contact_id').val(o.contact_id);
|
||||
$('#order_date').val(o.order_date ? format_date(o.order_date) : '');
|
||||
$('#shipping_tracking_number').val(o.shipping_tracking_number || '');
|
||||
$('#notes').val(o.notes || '');
|
||||
$('#discount').val(o.discount || 0);
|
||||
$('#tax').val(o.tax || 0);
|
||||
@@ -437,9 +481,8 @@
|
||||
(o.items || []).forEach(function(item) { add_item_row(item); });
|
||||
|
||||
recalc_totals();
|
||||
set_mode(o.status, res.has_active_invoice, (res.returnable_items || []).length);
|
||||
set_mode(o.status, res.has_active_invoice, (res.returnable_items || []).length, o.fulfillment_status);
|
||||
$('#payment_status_select').val(o.payment_status ?? 0);
|
||||
$('#order_status_select').val(o.status);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -470,6 +513,7 @@
|
||||
discount: $('#discount').val() || 0,
|
||||
tax: $('#tax').val() || 0,
|
||||
shipping_fee: $('#shipping_fee').val() || 0,
|
||||
shipping_tracking_number: $('#shipping_tracking_number').val().trim(),
|
||||
notes: $('#notes').val(),
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
@@ -508,8 +552,8 @@
|
||||
}
|
||||
|
||||
|
||||
// ── Update order status ───────────────────────────────────────────────────
|
||||
function update_order_status() {
|
||||
// ── Update fulfillment status ─────────────────────────────────────────────
|
||||
function update_fulfillment_status() {
|
||||
ajax_request({
|
||||
url: '<?php echo $server_url?>order/api/engine/update_order_status.php',
|
||||
autoPrepare: true,
|
||||
@@ -517,7 +561,8 @@
|
||||
action: 'update',
|
||||
data: {
|
||||
id: order_id,
|
||||
status: $('#order_status_select').val(),
|
||||
fulfillment_status: $('#fulfillment_status_select').val(),
|
||||
shipping_tracking_number: $('#shipping_tracking_number').val().trim(),
|
||||
},
|
||||
onSuccess: function() {
|
||||
retrieve_order();
|
||||
|
||||
@@ -376,8 +376,8 @@
|
||||
status === -1 ? 'Return is cancelled' : 'Return has already been confirmed');
|
||||
set_btn_state('#btn_confirm', is_draft,
|
||||
status === -1 ? 'Return is cancelled' : 'Return is already confirmed');
|
||||
set_btn_state('#btn_cancel', is_draft,
|
||||
status === -1 ? 'Return is already cancelled' : 'Only draft returns can be cancelled');
|
||||
set_btn_state('#btn_cancel', is_draft || status === 1,
|
||||
status === -1 ? 'Return is already cancelled' : 'Return is completed and cannot be cancelled');
|
||||
}
|
||||
|
||||
|
||||
|
||||
+16
-2
@@ -95,7 +95,7 @@
|
||||
<div class="col-lg-5">
|
||||
<div class="position-relative">
|
||||
<input type="text" id="search_input" class="form-control ps-9"
|
||||
placeholder="Search order number or contact...">
|
||||
placeholder="Search order number, contact, or tracking...">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
@@ -144,6 +144,8 @@
|
||||
<th class="py-3">Items</th>
|
||||
<th class="py-3">Grand Total</th>
|
||||
<th class="py-3">Status</th>
|
||||
<th class="py-3">Fulfillment</th>
|
||||
<th class="py-3">Tracking #</th>
|
||||
<th class="py-3">Payment</th>
|
||||
<th class="py-3">Action</th>
|
||||
</tr>
|
||||
@@ -188,6 +190,15 @@
|
||||
return map[status] ?? '<span class="badge bg-light text-dark">—</span>';
|
||||
}
|
||||
|
||||
function fulfillment_status_badge(status) {
|
||||
const map = {
|
||||
'1': '<span class="badge bg-warning text-dark"><i class="ti ti-list-check me-1"></i>Picking</span>',
|
||||
'2': '<span class="badge bg-info text-white"><i class="ti ti-package me-1"></i>Packed</span>',
|
||||
'3': '<span class="badge bg-success"><i class="ti ti-truck me-1"></i>Shipped</span>',
|
||||
};
|
||||
return map[String(status)] ?? '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
|
||||
// ── Retrieve orders ───────────────────────────────────────────────────────
|
||||
function retrieve_orders() {
|
||||
@@ -208,7 +219,8 @@
|
||||
var data = all.filter(function(o) {
|
||||
var match_kw = !keyword ||
|
||||
(o.order_number || '').toLowerCase().includes(keyword) ||
|
||||
(o.contact_name || '').toLowerCase().includes(keyword);
|
||||
(o.contact_name || '').toLowerCase().includes(keyword) ||
|
||||
(o.shipping_tracking_number || '').toLowerCase().includes(keyword);
|
||||
var match_st = fil_status === '' || String(o.status) === fil_status;
|
||||
var match_pm = fil_payment === '' || String(o.payment_status) === fil_payment;
|
||||
return match_kw && match_st && match_pm;
|
||||
@@ -254,6 +266,8 @@
|
||||
<td class="py-3">${item_count} item${item_count !== 1 ? 's' : ''}</td>
|
||||
<td class="py-3 fw-semibold">${format_number(o.grand_total, 2)}</td>
|
||||
<td class="py-3">${order_status_badge(o.status)}</td>
|
||||
<td class="py-3">${fulfillment_status_badge(o.fulfillment_status)}</td>
|
||||
<td class="py-3">${o.shipping_tracking_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${payment_status_badge(o.payment_status)}</td>
|
||||
<td class="py-3">
|
||||
<a href="<?php echo $server_url?>order/manage_order.php?id=${o.id}" title="View">
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
$.each(page_data, function(i, r) {
|
||||
var items = JSON.parse(r.items || '[]');
|
||||
var can_cancel = parseInt(r.status) === 0;
|
||||
var can_cancel = parseInt(r.status) >= 0 && parseInt(r.status) <= 1;
|
||||
|
||||
body += `<tr>
|
||||
<td class="py-3 fw-semibold">${r.return_number}</td>
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Lot Number</th>
|
||||
<th>Location</th>
|
||||
<th class="th_location">Location</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Days Overdue</th>
|
||||
<th>Quantity</th>
|
||||
@@ -167,7 +167,7 @@
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Lot Number</th>
|
||||
<th>Location</th>
|
||||
<th class="th_location">Location</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Days Remaining</th>
|
||||
<th>Quantity</th>
|
||||
@@ -193,6 +193,41 @@
|
||||
var all_items = [];
|
||||
var search_term = '';
|
||||
var filter_status = '';
|
||||
var advanced = false;
|
||||
var label_rack = '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
|
||||
);
|
||||
}
|
||||
|
||||
function format_location(item) {
|
||||
if (advanced) {
|
||||
return [item.zone, item.aisle, item.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
||||
}
|
||||
return item.rack || '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
function load_company_setting() {
|
||||
return ajax_request({
|
||||
url: server_url + 'setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Fetch + populate ─────────────────────────────────────────────────
|
||||
@@ -270,7 +305,7 @@
|
||||
<td class="py-3">${item.product_name}</td>
|
||||
<td class="py-3">${item.product_sku}</td>
|
||||
<td class="py-3">${item.lot_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item.zone}-${item.aisle}-${item.rack}</td>
|
||||
<td class="py-3">${format_location(item)}</td>
|
||||
<td class="py-3">${item.expiry_date}</td>
|
||||
<td class="py-3 text-danger fw-semibold">${Math.abs(item.days_remaining)} days</td>
|
||||
<td class="py-3">${item.quantity}</td>
|
||||
@@ -294,7 +329,7 @@
|
||||
<td class="py-3">${item.product_name}</td>
|
||||
<td class="py-3">${item.product_sku}</td>
|
||||
<td class="py-3">${item.lot_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item.zone}-${item.aisle}-${item.rack}</td>
|
||||
<td class="py-3">${format_location(item)}</td>
|
||||
<td class="py-3">${item.expiry_date}</td>
|
||||
<td class="py-3 text-warning fw-semibold">${item.days_remaining} days</td>
|
||||
<td class="py-3">${item.quantity}</td>
|
||||
@@ -349,6 +384,7 @@
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await load_company_setting();
|
||||
await retrieve_warehouse();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
|
||||
+78
-21
@@ -129,9 +129,9 @@
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Warehouse</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th>SKU</th>
|
||||
<th>Product</th>
|
||||
<th>Status</th>
|
||||
@@ -172,9 +172,9 @@
|
||||
<thead class="table-danger border-light">
|
||||
<tr>
|
||||
<th>Warehouse</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th>SKU</th>
|
||||
<th>Product</th>
|
||||
<th>Action</th>
|
||||
@@ -214,9 +214,9 @@
|
||||
<thead class="table-success border-light">
|
||||
<tr>
|
||||
<th>Warehouse</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th class="th_loc_zone">Zone</th>
|
||||
<th class="th_loc_aisle">Aisle</th>
|
||||
<th class="th_loc_rack">Rack</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -263,6 +263,68 @@
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
|
||||
// ── Location config ───────────────────────────────────────────────────────
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
function load_location_config(callback) {
|
||||
return ajax_request({
|
||||
url: '<?php echo $server_url?>setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function apply_location_labels() {
|
||||
if (advanced) {
|
||||
$('.th_loc_zone').text(label_zone).show();
|
||||
$('.th_loc_aisle').text(label_aisle).show();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
} else {
|
||||
$('.th_loc_zone').hide();
|
||||
$('.th_loc_aisle').hide();
|
||||
$('.th_loc_rack').text(label_rack);
|
||||
}
|
||||
var loc_hint = advanced
|
||||
? label_zone.toLowerCase() + ', ' + label_aisle.toLowerCase() + ', ' + label_rack.toLowerCase()
|
||||
: label_rack.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');
|
||||
}
|
||||
|
||||
// Returns 1 or 3 <td> cells depending on mode
|
||||
function location_cells(item) {
|
||||
if (advanced) {
|
||||
return '<td class="py-3">' + (item.zone || '—') + '</td>'
|
||||
+ '<td class="py-3">' + (item.aisle || '—') + '</td>'
|
||||
+ '<td class="py-3">' + (item.rack || '—') + '</td>';
|
||||
}
|
||||
return '<td class="py-3">' + (item.rack || '—') + '</td>';
|
||||
}
|
||||
|
||||
// Rack log modal label
|
||||
function rack_label(item) {
|
||||
return advanced
|
||||
? item.warehouse_name + ' ' + item.zone + '-' + item.aisle + '-' + item.rack
|
||||
: item.warehouse_name + ' ' + item.rack;
|
||||
}
|
||||
|
||||
var search_all = '';
|
||||
var search_occupied = '';
|
||||
var search_empty = '';
|
||||
@@ -334,13 +396,11 @@
|
||||
: '<span class="badge bg-success">Available</span>';
|
||||
body += `<tr>
|
||||
<td class="py-3">${item.warehouse_name}</td>
|
||||
<td class="py-3">${item.zone}</td>
|
||||
<td class="py-3">${item.aisle}</td>
|
||||
<td class="py-3">${item.rack}</td>
|
||||
` + location_cells(item) + `
|
||||
<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}, '${item.warehouse_name} ${item.zone}-${item.aisle}-${item.rack}')"><i class="ti ti-history fs-5"></i></a></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>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_all > tbody').html(body);
|
||||
@@ -377,12 +437,10 @@
|
||||
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
||||
body += `<tr>
|
||||
<td class="py-3">${item.warehouse_name}</td>
|
||||
<td class="py-3">${item.zone}</td>
|
||||
<td class="py-3">${item.aisle}</td>
|
||||
<td class="py-3">${item.rack}</td>
|
||||
` + 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}, '${item.warehouse_name} ${item.zone}-${item.aisle}-${item.rack}')"><i class="ti ti-history fs-5"></i></a></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>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_occupied > tbody').html(body);
|
||||
@@ -417,10 +475,8 @@
|
||||
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
||||
body += `<tr>
|
||||
<td class="py-3">${item.warehouse_name}</td>
|
||||
<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"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${item.warehouse_name} ${item.zone}-${item.aisle}-${item.rack}')"><i class="ti ti-history fs-5"></i></a></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>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_empty > tbody').html(body);
|
||||
@@ -512,6 +568,7 @@
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await load_location_config(apply_location_labels);
|
||||
await retrieve_rack_occupancy();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Warehouse</th>
|
||||
<th>Location</th>
|
||||
<th id="th_location">Location</th>
|
||||
<th>In</th>
|
||||
<th>Out</th>
|
||||
<th>Serial</th>
|
||||
@@ -182,6 +182,10 @@
|
||||
var lot_log_data = [];
|
||||
var lot_log_page = 1;
|
||||
var lot_log_limit = 10;
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
var type_badge = {
|
||||
'in': '<span class="badge bg-success">Stock In</span>',
|
||||
@@ -196,6 +200,37 @@
|
||||
'expired': '<span class="badge bg-secondary">Expired</span>',
|
||||
};
|
||||
|
||||
function apply_location_labels() {
|
||||
$('#th_location').text(advanced
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
|
||||
: label_rack
|
||||
);
|
||||
}
|
||||
|
||||
function format_location(row) {
|
||||
if (advanced) {
|
||||
return [row.zone, row.aisle, row.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
||||
}
|
||||
return row.rack || '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
function load_company_setting() {
|
||||
return ajax_request({
|
||||
url: server_url + 'setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Fetch ─────────────────────────────────────────────────────────────
|
||||
function retrieve_product_lot() {
|
||||
@@ -296,7 +331,7 @@
|
||||
<td class="py-3">${format_date(row.date)}</td>
|
||||
<td class="py-3">${type_badge[row.type] || row.type}</td>
|
||||
<td class="py-3">${row.warehouse_name}</td>
|
||||
<td class="py-3">${row.zone}-${row.aisle}-${row.rack}</td>
|
||||
<td class="py-3">${format_location(row)}</td>
|
||||
<td class="py-3 text-success">${parseFloat(row.stock_in) > 0 ? '+' + Number(row.stock_in).toLocaleString() : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3 text-danger">${parseFloat(row.stock_out) > 0 ? '-' + Number(row.stock_out).toLocaleString() : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${row.serial_number || '<span class="text-muted">—</span>'}</td>
|
||||
@@ -356,6 +391,7 @@
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await load_company_setting();
|
||||
await retrieve_product_lot();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
<th>Type</th>
|
||||
<th>SKU</th>
|
||||
<th>Lot</th>
|
||||
<th>Location</th>
|
||||
<th id="th_location">Location</th>
|
||||
<th>Contact</th>
|
||||
<th class="text-end">Stock In</th>
|
||||
<th class="text-end">Stock Out</th>
|
||||
@@ -173,6 +173,10 @@
|
||||
var sku_index = 0; // next SKU to load
|
||||
var is_loading = false;
|
||||
var search_params = {}; // warehouse_id, from_month, to_month — fixed per search
|
||||
var advanced = false;
|
||||
var label_rack = 'Location';
|
||||
var label_zone = 'Zone';
|
||||
var label_aisle = 'Aisle';
|
||||
|
||||
var type_badge = {
|
||||
'in': '<span class="badge bg-success">In</span>',
|
||||
@@ -180,6 +184,41 @@
|
||||
'transfer': '<span class="badge bg-info text-dark">Transfer</span>',
|
||||
};
|
||||
|
||||
function apply_location_labels() {
|
||||
$('#th_location').text(advanced
|
||||
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
|
||||
: label_rack
|
||||
);
|
||||
}
|
||||
|
||||
function format_location(row) {
|
||||
if (advanced) {
|
||||
return [row.zone, row.aisle, row.rack].filter(Boolean).join('-');
|
||||
}
|
||||
return row.rack || '';
|
||||
}
|
||||
|
||||
function display_location(row) {
|
||||
return format_location(row) || '<span class="text-muted">—</span>';
|
||||
}
|
||||
|
||||
function load_company_setting() {
|
||||
return ajax_request({
|
||||
url: server_url + 'setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output || {};
|
||||
advanced = s.advanced_location == 1;
|
||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||
label_zone = s.location_label_zone || 'Zone';
|
||||
label_aisle = s.location_label_aisle || 'Aisle';
|
||||
apply_location_labels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Default months ────────────────────────────────────────────────────
|
||||
(function() {
|
||||
var now = new Date();
|
||||
@@ -339,7 +378,7 @@
|
||||
date: row.date,
|
||||
type: row.type,
|
||||
lot: row.lot_number || '',
|
||||
location: row.zone + '-' + row.aisle + '-' + row.rack,
|
||||
location: format_location(row),
|
||||
contact: row.contact_name || '',
|
||||
stock_in: row.stock_in > 0 ? row.stock_in : '',
|
||||
stock_out: row.stock_out > 0 ? row.stock_out : '',
|
||||
@@ -382,7 +421,7 @@
|
||||
<td class="py-2">${type_badge[row.type] || row.type}</td>
|
||||
<td class="py-2"><code>${row.product_sku}</code></td>
|
||||
<td class="py-2">${row.lot_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-2">${row.zone}-${row.aisle}-${row.rack}</td>
|
||||
<td class="py-2">${display_location(row)}</td>
|
||||
<td class="py-2">${row.contact_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-2 text-end text-success">${in_val}</td>
|
||||
<td class="py-2 text-end text-danger">${out_val}</td>
|
||||
@@ -474,7 +513,10 @@
|
||||
|
||||
|
||||
$(async function() {
|
||||
try { await retrieve_warehouse(); } catch(e) { console.log(e); }
|
||||
try {
|
||||
await load_company_setting();
|
||||
await retrieve_warehouse();
|
||||
} catch(e) { console.log(e); }
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@ session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/CompanySettingManager.php';
|
||||
|
||||
$csm = new CompanySettingManager($pdo1, $company_id);
|
||||
$csm = new CompanySettingManager($pdo1, $company_id, $pdo2);
|
||||
$csm->handle($data);
|
||||
@@ -23,6 +23,96 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warehouse Location Settings -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card border-0 shadow-sm mb-5">
|
||||
<div class="card-header bg-transparent border-bottom">
|
||||
<h6 class="mb-0"><i class="ti ti-map-pin me-2"></i>Warehouse Location Settings</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Advanced Location</label>
|
||||
<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.
|
||||
You can customise the label for each level to match your team's terminology.
|
||||
</p>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="advanced_location_toggle" value="1">
|
||||
<label class="form-check-label" for="advanced_location_toggle">
|
||||
Enable Advanced Location
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Simple mode — single label -->
|
||||
<div id="location_simple">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-auto">
|
||||
<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"
|
||||
placeholder="Location" maxlength="30">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="text-muted small">default: "Location"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced mode — three labels -->
|
||||
<div id="location_advanced" class="d-none">
|
||||
<div class="row g-3 align-items-center mb-2">
|
||||
<div class="col-auto" style="width:100px">
|
||||
<label class="col-form-label text-muted small">Level 1 Label</label>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<input type="text" id="location_label_zone" class="form-control form-control-sm"
|
||||
placeholder="Zone" maxlength="30">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="text-muted small">default: "Zone"</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 align-items-center mb-2">
|
||||
<div class="col-auto" style="width:100px">
|
||||
<label class="col-form-label text-muted small">Level 2 Label</label>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<input type="text" id="location_label_aisle" class="form-control form-control-sm"
|
||||
placeholder="Aisle" maxlength="30">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="text-muted small">default: "Aisle"</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-auto" style="width:100px">
|
||||
<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">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="text-muted small">default: "Rack"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="save_config();">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock Settings -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-7">
|
||||
@@ -67,6 +157,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Fulfillment Settings -->
|
||||
<div class="card border-0 shadow-sm mb-5">
|
||||
<div class="card-header bg-transparent border-bottom">
|
||||
<h6 class="mb-0"><i class="ti ti-truck me-2"></i>Order Fulfillment Settings</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Auto-Complete on Shipped</label>
|
||||
<p class="text-muted small mb-2">
|
||||
Controls whether an order is automatically marked as <strong>Completed</strong>
|
||||
when its fulfillment status is set to <strong>Shipped</strong>.
|
||||
When <strong>Manual</strong>, the order stays in Processing and staff
|
||||
must mark it Completed themselves.
|
||||
</p>
|
||||
<div class="d-flex flex-column gap-2" id="auto_complete_on_ship_group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="auto_complete_on_ship"
|
||||
id="complete_auto" value="1">
|
||||
<label class="form-check-label" for="complete_auto">
|
||||
<span class="fw-semibold">Auto</span>
|
||||
<span class="text-muted ms-1 small">— order is completed automatically when shipped</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="auto_complete_on_ship"
|
||||
id="complete_manual" value="0">
|
||||
<label class="form-check-label" for="complete_manual">
|
||||
<span class="fw-semibold">Manual</span>
|
||||
<span class="text-muted ms-1 small">— staff mark the order as completed manually</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="save_config();">
|
||||
<i class="ti ti-device-floppy me-1"></i>Save
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice & Credit Note Settings -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-transparent border-bottom">
|
||||
@@ -127,32 +259,123 @@
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var s = res.output;
|
||||
|
||||
// Location settings
|
||||
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_zone').val(s.location_label_zone || '');
|
||||
$('#location_label_aisle').val(s.location_label_aisle || '');
|
||||
$('#location_label_rack_advanced').val(s.location_label_rack || '');
|
||||
|
||||
// Other settings
|
||||
$('input[name="default_stock_status"][value="' + (s.default_stock_status ?? 1) + '"]').prop('checked', true);
|
||||
$('input[name="auto_complete_on_ship"][value="' + (s.auto_complete_on_ship ?? 1) + '"]').prop('checked', true);
|
||||
$('input[name="auto_invoice_and_credit_note"][value="' + (s.auto_invoice_and_credit_note ?? 0) + '"]').prop('checked', true);
|
||||
|
||||
apply_setting_locks(res.locked || {});
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle advanced/simple panels on switch change
|
||||
$('#advanced_location_toggle').on('change', function() {
|
||||
toggle_location_mode($(this).is(':checked'));
|
||||
});
|
||||
});
|
||||
|
||||
function toggle_location_mode(advanced) {
|
||||
if (advanced) {
|
||||
$('#location_simple').addClass('d-none');
|
||||
$('#location_advanced').removeClass('d-none');
|
||||
} else {
|
||||
$('#location_simple').removeClass('d-none');
|
||||
$('#location_advanced').addClass('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function apply_setting_locks(locked) {
|
||||
set_setting_lock(
|
||||
'advanced_location',
|
||||
'#advanced_location_toggle',
|
||||
locked.advanced_location,
|
||||
'Advanced Location cannot be changed because stock transactions already exist. Location labels can still be renamed.'
|
||||
);
|
||||
set_setting_lock(
|
||||
'default_stock_status',
|
||||
'input[name="default_stock_status"]',
|
||||
locked.default_stock_status,
|
||||
'Default Stock Status cannot be changed because stock transactions already exist.'
|
||||
);
|
||||
set_setting_lock(
|
||||
'auto_complete_on_ship',
|
||||
'input[name="auto_complete_on_ship"]',
|
||||
locked.auto_complete_on_ship,
|
||||
'Auto-Complete on Shipped cannot be changed because order, invoice, or return transactions already exist.'
|
||||
);
|
||||
set_setting_lock(
|
||||
'auto_invoice_and_credit_note',
|
||||
'input[name="auto_invoice_and_credit_note"]',
|
||||
locked.auto_invoice_and_credit_note,
|
||||
'Invoice & Credit Note Generation cannot be changed because order, invoice, or return transactions already exist.'
|
||||
);
|
||||
}
|
||||
|
||||
function set_setting_lock(key, selector, is_locked, message) {
|
||||
var $inputs = $(selector);
|
||||
$inputs.prop('disabled', !!is_locked);
|
||||
|
||||
var $container = $inputs.first().closest('.mb-4');
|
||||
$container.find('.setting-lock-note[data-lock-key="' + key + '"]').remove();
|
||||
|
||||
if (is_locked) {
|
||||
$container.append(
|
||||
'<div class="setting-lock-note text-warning small mt-2" data-lock-key="' + key + '">' +
|
||||
'<i class="ti ti-lock me-1"></i>' + message +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────
|
||||
function save_config() {
|
||||
var advanced = $('#advanced_location_toggle').is(':checked') ? 1 : 0;
|
||||
var stock_status = $('input[name="default_stock_status"]:checked').val();
|
||||
var auto_invoice_and_cn = $('input[name="auto_invoice_and_credit_note"]:checked').val();
|
||||
var auto_complete_on_ship = $('input[name="auto_complete_on_ship"]:checked').val();
|
||||
|
||||
if (stock_status === undefined || auto_invoice_and_cn === undefined) {
|
||||
if (stock_status === undefined || auto_invoice_and_cn === undefined || auto_complete_on_ship === undefined) {
|
||||
bootbox.alert('Please select all settings before saving.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve label values — fall back to defaults if blank
|
||||
var label_rack, 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';
|
||||
} else {
|
||||
label_rack = $('#location_label_rack_simple').val().trim() || 'Location';
|
||||
label_zone = label_rack;
|
||||
label_aisle = label_rack;
|
||||
}
|
||||
|
||||
ajax_request({
|
||||
url: server_url + 'setting/api/engine/company_setting.php',
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'update',
|
||||
data: {
|
||||
advanced_location: advanced,
|
||||
location_label_zone: label_zone,
|
||||
location_label_aisle: label_aisle,
|
||||
location_label_rack: label_rack,
|
||||
default_stock_status: stock_status,
|
||||
auto_invoice_and_credit_note: auto_invoice_and_cn,
|
||||
auto_complete_on_ship: auto_complete_on_ship,
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
onSuccess: function() {
|
||||
bootbox.alert('Settings saved.');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user