Fix timestamps, delete requests, invoice dates and GR quantities

Apply the configured timezone to PHP and both DB connections, wrap
unwrapped ajax payloads so delete buttons reach their engines, normalise
and validate invoice due dates, reject stock quantities below the stored
4dp scale, and list stock movements across all warehouses.
This commit is contained in:
Thanakorn
2026-09-17 09:00:15 +07:00
parent f14c850c70
commit f70f226bd1
14 changed files with 464 additions and 60 deletions
+36
View File
@@ -257,6 +257,31 @@
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
var quantity_val = $('#quantity').val();
// Quantities are stored as decimal(18,4). Anything finer is rounded away
// on insert, so 0.0000001 used to become a movement of 0.0000 that still
// looked like a successful stock-in. Reject it here with a message that
// names the limit; StockManager enforces the same rule server-side.
<?php if (empty($_GET["id"])) { ?>
var quantity_num = parseFloat(quantity_val);
if (!(quantity_num > 0)) {
bootbox.alert('Please enter a quantity greater than zero.');
return Promise.resolve();
}
if (round_dp(quantity_num, 4) < 0.0001) {
bootbox.alert('Quantity ' + quantity_num + ' is smaller than the minimum the system records (0.0001).');
return Promise.resolve();
}
quantity_val = round_dp(quantity_num, 4);
// Expiry dates are stored on the lot, so one entered without a lot number
// has nowhere to go and would be dropped without warning.
if ($('#expiry_date').val() && !$('#lot_number').val().trim()) {
bootbox.alert('Enter a lot number — expiry dates are recorded against a lot.');
return Promise.resolve();
}
<?php } ?>
// Mirror to hidden inputs for autoPrepare consistency (simple mode)
if (!advanced) {
$('#zone').val(bin_val);
@@ -462,7 +487,18 @@
.val(data.expiry_date);
if (expiryPicker && expiryPicker.altInput) { expiryPicker.altInput.disabled = true; }
} else {
// No lot number, so there is nothing for an expiry date to hang
// off: expiry lives on md_lot, keyed by lot. Leaving the field
// empty but editable invited entering one that would be silently
// discarded on update, so say why it is unavailable instead.
if (expiryPicker) { expiryPicker.clear(); }
$('#expiry_date').prop('disabled', true)
.attr('title', 'Expiry dates are recorded against a lot — this movement has no lot number')
.attr('placeholder', 'Not tracked — no lot number');
if (expiryPicker && expiryPicker.altInput) {
expiryPicker.altInput.disabled = true;
expiryPicker.altInput.placeholder = 'Not tracked — no lot number';
}
}
$('#product_sku').attr('secondary', data.product_sku);
$('#product_sku, #quantity, #price').prop('disabled', true);
+17 -9
View File
@@ -32,7 +32,7 @@
<input class="form-control" value='Warehouse' disabled>
</div>
<div class="mb-3 col-lg-2">
<select name="warehouse" id="warehouse" class="form-select" required></select>
<select name="warehouse" id="warehouse" class="form-select"></select>
</div>
<div class="mb-3 col-lg-2">
<input class="form-control" value='Source' disabled>
@@ -56,6 +56,7 @@
<tr>
<th>Date</th>
<th>Reference</th>
<th>Warehouse</th>
<th>Product</th>
<th>Lot Number</th>
<th>Serial Number</th>
@@ -210,18 +211,21 @@
var body = ``;
var warehouse = $(`select#warehouse`).val();
$.each(page_data, function(key, item) {
// The row's own warehouse, not the filter — with "All Warehouses" the
// filter has no value, and each row lives in its own td_stock_<id> table.
var warehouse = item['warehouse_id'];
body += `<tr>
<td class="py-3">${format_date(item["date"])}</td>
<td class="py-3 fw-semibold">${item["stock_reference"] || "—"}</td>
<td class="py-3">${escape_html(item['warehouse_name'] || '—')}</td>
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
<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">
<div class="d-flex justify-content-end align-items-baseline gap-1">
<span>${format_number(item['quantity'], 2)}</span>
<span>${format_quantity(item['quantity'])}</span>
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
</div>
</td>
@@ -234,7 +238,7 @@
<td class="py-3">
<a href="<?php echo $server_url?>ics/manage_stock_in.php?id=${item['id']}&wh=${warehouse}" class=""><i class="ti ti-eye fs-5"></i></a>
<a href="javascript:void(0);" class="link-danger"
onclick="delete_stock_in($(this),${item['id']})">
onclick="delete_stock_in($(this),${item['id']},${warehouse})">
<i class="ti ti-trash ms-2 fs-5"></i>
</a>
</td>
@@ -255,9 +259,13 @@
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var option = ``;
// Default to every warehouse. Stock is stored one table per warehouse,
// so defaulting to a single one made a receipt that was split across
// warehouses look incomplete — a 4-line PO showed only the 3 lines that
// landed in the selected warehouse.
var option = `<option value=''>All Warehouses</option>`;
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
option += `<option value='${item.id}'>${escape_html(item.warehouse_name)}</option>`;
})
$(`select#warehouse`).html(option);
}
@@ -265,7 +273,7 @@
}
function delete_stock_in(element, id) {
function delete_stock_in(element, id, warehouse_id) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/delete_stock_in.php",
@@ -274,7 +282,7 @@
action: 'delete',
data : {
id: id,
wh: $(`select#warehouse`).val()
wh: warehouse_id
},
onSuccess: function(res) {
element.closest('tr').remove();
+16 -9
View File
@@ -32,7 +32,7 @@
<input class="form-control" value='Warehouse' disabled>
</div>
<div class="mb-3 col-lg-2">
<select name="warehouse" id="warehouse" class="form-select" required></select>
<select name="warehouse" id="warehouse" class="form-select"></select>
</div>
<div class="mb-3 col-lg-2">
<input class="form-control" value='Source' disabled>
@@ -56,6 +56,7 @@
<tr>
<th>Date</th>
<th>Reference</th>
<th>Warehouse</th>
<th>Product</th>
<th>Lot Number</th>
<th>Serial Number</th>
@@ -151,7 +152,7 @@
}
var delete_btn = (!source)
? `<a href="javascript:void(0);" class="link-danger" onclick="delete_stock_out($(this),${item['id']})">
? `<a href="javascript:void(0);" class="link-danger" onclick="delete_stock_out($(this),${item['id']},${warehouse})">
<i class="ti ti-trash ms-2 fs-5"></i>
</a>`
: '';
@@ -237,18 +238,21 @@
var body = ``;
var warehouse = $(`select#warehouse`).val();
$.each(page_data, function(key, item) {
// The row's own warehouse, not the filter — with "All Warehouses" the
// filter has no value, and each row lives in its own td_stock_<id> table.
var warehouse = item['warehouse_id'];
body += `<tr>
<td class="py-3">${format_date(item["date"])}</td>
<td class="py-3 fw-semibold">${item["stock_reference"] || "—"}</td>
<td class="py-3">${escape_html(item['warehouse_name'] || '—')}</td>
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
<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">
<div class="d-flex justify-content-end align-items-baseline gap-1">
<span>${format_number(item['quantity'], 2)}</span>
<span>${format_quantity(item['quantity'])}</span>
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
</div>
</td>
@@ -275,9 +279,12 @@
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var option = ``;
// Default to every warehouse — stock is stored one table per
// warehouse, so a single-warehouse default hides movements that went
// elsewhere and makes a document look only partly processed.
var option = `<option value=''>All Warehouses</option>`;
$.each(res.output, function(key, item) {
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
option += `<option value='${item.id}'>${escape_html(item.warehouse_name)}</option>`;
})
$(`select#warehouse`).html(option);
}
@@ -285,7 +292,7 @@
}
function delete_stock_out(element, id) {
function delete_stock_out(element, id, warehouse_id) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/delete_stock_out.php",
@@ -294,7 +301,7 @@
action: 'delete',
data : {
id: id,
wh: $(`select#warehouse`).val()
wh: warehouse_id
},
onSuccess: function(res) {
element.closest('tr').remove();