Files
wms-app/app/ics/stock_in.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

357 lines
11 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 in
</h1>
<a href="<?php echo $server_url?>ics/manage_stock_in.php" class="btn btn-primary">
<span><i class="ti ti-plus"></i></span>
<span>ADD Stock In</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_in()'>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_in">
<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 style="text-align: right;">Unit Price</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.charAt(0).toUpperCase() + source.slice(1).replace(/_/g, ' ');
}
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_in_all ? alasql.tables.stock_in_all.data : [];
if (!selected) {
alasql.tables.stock_in.data = rows;
} else {
alasql.tables.stock_in.data = rows.filter(function(item) {
var source = item.source || '';
return selected === '__manual__' ? !source : source === selected;
});
}
var table_id = 'stock_in';
var total_records = alasql.tables.stock_in.data.length;
var page = generate_pagination(table_id, total_records);
$(`table#${table_id} tfoot`).html(page);
change_page_stock_in(1);
}
function retrieve_stock_in() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/stock_in.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_in_all');
alasql('CREATE TABLE IF NOT EXISTS stock_in');
alasql.tables.stock_in_all.data = res.output || [];
populate_source_filter(alasql.tables.stock_in_all.data);
apply_source_filter();
}
});
}
function change_page_stock_in(page_num) {
var offset = (page_num - 1) * prop_limit;
var page_data = alasql(`SELECT * FROM stock_in 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 text-end">${item['price'] > 0 ? format_number(item['price'], 2) : '<span class="text-muted">—</span>'}</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">
<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']},${warehouse})">
<i class="ti ti-trash ms-2 fs-5"></i>
</a>
</td>
</tr>`;
})
$("table#stock_in > 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 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}'>${escape_html(item.warehouse_name)}</option>`;
})
$(`select#warehouse`).html(option);
}
});
}
function delete_stock_in(element, id, warehouse_id) {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/delete_stock_in.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_in();
} catch (e) {
console.log(e);
}
});
$(document).on('change', '#source', apply_source_filter);
$(document).on('change', '#warehouse', retrieve_stock_in);
// ── 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_in();
}
});
}
});
}
</script>
</body>
</html>