Files
wms-app/app/ics/stock_out.php
T
Thanakorn f70f226bd1 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.
2026-09-17 09:00:15 +07:00

376 lines
12 KiB
PHP

<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<div class="row ">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<h1 class="fs-3 mb-0">Stock Out
</h1>
<a href="<?php echo $server_url?>ics/manage_stock_out.php" class="btn btn-primary">
<span><i class="ti ti-plus"></i></span>
<span>ADD Stock Out</span></a>
</div>
</div>
</div>
<div class="row g-5">
<div class="mb-3 col-lg-2">
<input class="form-control" value='Warehouse' disabled>
</div>
<div class="mb-3 col-lg-2">
<select name="warehouse" id="warehouse" class="form-select"></select>
</div>
<div class="mb-3 col-lg-2">
<input class="form-control" value='Source' disabled>
</div>
<div class="mb-3 col-lg-2">
<select name="source" id="source" class="form-select">
<option value="">All Sources</option>
</select>
</div>
<div class="mb-3 col-lg-2">
<button class='btn btn-secondary' onclick='retrieve_stock_out()'>Search</button>
</div>
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<h2 class="mb-4 fs-5">Item List</h2>
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered" id="stock_out">
<thead class="table-primary border-light">
<tr>
<th>Date</th>
<th>Reference</th>
<th>Warehouse</th>
<th>Product</th>
<th>Lot Number</th>
<th>Serial Number</th>
<th style="text-align: right;">Quantity</th>
<th>Source</th>
<th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th>
<th class="th_loc_bin">Bin</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot class="">
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<script>
var advanced = false;
var label_bin = '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_bin').text(label_bin);
} else {
$('.th_loc_zone, .th_loc_aisle').hide();
$('.th_loc_bin').text(label_bin);
}
}
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['bin'] || ''}</td>`;
}
return `<td class="py-3">${item['bin'] || ''}</td>`;
}
function escape_html(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[char];
});
}
function source_label(source) {
source = source || '';
if (!source) return 'Manual';
return source.split('_').map(function(w) {
return w.charAt(0).toUpperCase() + w.slice(1);
}).join(' ');
}
var source_view_urls = {
'supplier_return': '<?php echo $server_url?>po/manage_supplier_return.php',
'order': '<?php echo $server_url?>order/manage_order.php',
'return': '<?php echo $server_url?>order/manage_return.php',
};
function action_cell(item, warehouse) {
var source = item.source || '';
var source_id = parseInt(item.source_id) || 0;
var view_url;
if (source && source_view_urls[source] && source_id > 0) {
view_url = source_view_urls[source] + '?id=' + source_id;
} else {
view_url = '<?php echo $server_url?>ics/manage_stock_out.php?id=' + item['id'] + '&wh=' + warehouse;
}
var delete_btn = (!source)
? `<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>`
: '';
return `<a href="${view_url}"><i class="ti ti-eye fs-5"></i></a>${delete_btn}`;
}
function source_cell(source) {
var label = escape_html(source_label(source));
return source
? `<span class="badge bg-info-subtle text-info">${label}</span>`
: `<span class="badge bg-secondary-subtle text-secondary">${label}</span>`;
}
function populate_source_filter(rows) {
var current = $('#source').val() || '';
var sources = {};
$.each(rows, function(_, item) {
sources[item.source || '__manual__'] = item.source || '';
});
var option = `<option value="">All Sources</option>`;
$.each(Object.keys(sources).sort(), function(_, key) {
var value = sources[key];
var option_value = value || '__manual__';
option += `<option value="${escape_html(option_value)}">${escape_html(source_label(value))}</option>`;
});
$('#source').html(option).val(current);
if ($('#source').val() !== current) $('#source').val('');
}
function apply_source_filter() {
var selected = $('#source').val() || '';
var rows = alasql.tables.stock_out_all ? alasql.tables.stock_out_all.data : [];
if (!selected) {
alasql.tables.stock_out.data = rows;
} else {
alasql.tables.stock_out.data = rows.filter(function(item) {
var source = item.source || '';
return selected === '__manual__' ? !source : source === selected;
});
}
var table_id = 'stock_out';
var total_records = alasql.tables.stock_out.data.length;
var page = generate_pagination(table_id, total_records);
$(`table#${table_id} tfoot`).html(page);
change_page_stock_out(1);
}
function retrieve_stock_out() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/stock_out.php",
autoPrepare: true,
checkRequired: 0,
debugMode: 0,
action: 'read',
onSuccess: function(res) {
// Register the array as a table name
alasql('CREATE TABLE IF NOT EXISTS stock_out_all');
alasql('CREATE TABLE IF NOT EXISTS stock_out');
alasql.tables.stock_out_all.data = res.output || [];
populate_source_filter(alasql.tables.stock_out_all.data);
apply_source_filter();
}
});
}
function change_page_stock_out(page_num) {
var offset = (page_num - 1) * prop_limit;
var page_data = alasql(`SELECT * FROM stock_out LIMIT ${prop_limit} OFFSET ${offset}`);
var body = ``;
$.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_quantity(item['quantity'])}</span>
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
</div>
</td>
<td class="py-3">${source_cell(item['source'])}</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>
<td class="py-3">${action_cell(item, warehouse)}</td>
</tr>`;
})
$("table#stock_out > tbody").html(body);
}
function retrieve_warehouse() {
return ajax_request({
url: "<?php echo $server_url?>inventory/api/engine/warehouse.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
// 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}'>${escape_html(item.warehouse_name)}</option>`;
})
$(`select#warehouse`).html(option);
}
});
}
function delete_stock_out(element, id, warehouse_id) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/delete_stock_out.php",
autoPrepare: true,
checkRequired: 0,
action: 'delete',
data : {
id: id,
wh: warehouse_id
},
onSuccess: function(res) {
element.closest('tr').remove();
}
});
}
$(async function() {
try {
await load_company_setting();
await retrieve_warehouse();
await retrieve_stock_out();
} catch (e) {
console.log(e);
}
});
$(document).on('change', '#source', apply_source_filter);
$(document).on('change', '#warehouse', retrieve_stock_out);
// ── Stock approval ────────────────────────────────────────────────────────
var auto_approve = false; // set from company_setting on load
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 || {};
auto_approve = s.default_stock_status == 1;
advanced = s.advanced_location == 1;
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
}
});
}
function approve_stock(id, warehouse_id, type) {
bootbox.confirm({
message: 'Approve this stock record?',
buttons: { confirm: { label: 'Approve', className: 'btn-success' }, cancel: { label: 'Cancel', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: server_url + 'ics/api/engine/approve_stock.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
id: id,
warehouse: warehouse_id,
type: type
},
onSuccess: function(res) {
retrieve_stock_out();
}
});
}
});
}
</script>
</body>
</html>