Files
wms-app/app/reports/stock_movement.php
T
2026-05-23 15:07:11 +07:00

710 lines
29 KiB
PHP

<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row mb-5">
<div class="col-12">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
<div>
<h1 class="fs-4 mb-0">Stock Movement</h1>
</div>
<div class="d-flex align-items-center gap-2 flex-nowrap">
<select id="warehouse" class="form-select form-select-sm" style="min-width:140px;max-width:180px;">
<option value="">Warehouse...</option>
</select>
<input type="month" id="from_month" class="form-control form-control-sm" style="width:150px;">
<span class="text-muted small text-nowrap">to</span>
<input type="month" id="to_month" class="form-control form-control-sm" style="width:150px;">
<button class="btn btn-secondary btn-sm text-nowrap" onclick="do_search();">
<i class="ti ti-search"></i> Search
</button>
<div class="dropdown">
<a href="#" role="button" class="btn btn-outline-secondary btn-sm text-nowrap" data-bs-toggle="dropdown" aria-expanded="false">
<i class="ti ti-download me-1"></i> Export
</a>
<div class="dropdown-menu dropdown-menu-end p-0" style="min-width: 160px;">
<div>
<div class="d-flex gap-3 align-items-center border-dashed border-bottom px-3 py-3">
<i class="ti ti-download"></i>
<div>
<h4 class="mb-0 small">Export</h4>
</div>
</div>
<div class="p-3 d-flex flex-column gap-1 small lh-lg">
<a href="javascript:;" class="wms-export-link" onclick="export_report('csv')">
<span><i class="ti ti-file-type-csv me-2"></i>CSV</span>
</a>
<a href="javascript:;" class="wms-export-link" onclick="export_report('xlsx')">
<span><i class="ti ti-file-type-xls me-2"></i>Excel</span>
</a>
<a href="javascript:;" class="wms-export-link" onclick="export_report('pdf')">
<span><i class="ti ti-file-type-pdf me-2"></i>PDF</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Summary stats -->
<div class="row mb-4 g-3">
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-4 d-flex align-items-center gap-3">
<span class="icon-shape icon-sm bg-primary text-white rounded-2">
<i class="ti ti-list fs-5"></i>
</span>
<div>
<div class="fs-3 fw-bold mb-0" id="stat_transactions">—</div>
<div class="text-muted small">Total Transactions</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-4 d-flex align-items-center gap-3">
<span class="icon-shape icon-sm bg-success text-white rounded-2">
<i class="ti ti-package-import fs-5"></i>
</span>
<div>
<div class="fs-3 fw-bold mb-0 text-success" id="stat_in">—</div>
<div class="text-muted small">Total Stock In</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-4 d-flex align-items-center gap-3">
<span class="icon-shape icon-sm bg-danger text-white rounded-2">
<i class="ti ti-package-export fs-5"></i>
</span>
<div>
<div class="fs-3 fw-bold mb-0 text-danger" id="stat_out">—</div>
<div class="text-muted small">Total Stock Out</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 col-12">
<div class="card">
<div class="card-body p-4 d-flex align-items-center gap-3">
<span class="icon-shape icon-sm bg-warning text-white rounded-2">
<i class="ti ti-arrows-diff fs-5"></i>
</span>
<div>
<div class="fs-3 fw-bold mb-0" id="stat_net">—</div>
<div class="text-muted small">Net Movement</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<ul class="nav nav-pills mb-3 bg-light p-2 rounded-2 d-inline-flex" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-movement-tab" data-bs-toggle="pill"
data-bs-target="#pills-movement" type="button" role="tab"
aria-controls="pills-movement" aria-selected="true">Detail</button>
</li>
<li class="nav-item nav-item-secondary" role="presentation">
<button class="nav-link" id="pills-summary-tab" data-bs-toggle="pill"
data-bs-target="#pills-summary" type="button" role="tab"
aria-controls="pills-summary" aria-selected="false">Summary</button>
</li>
</ul>
<!-- Tab 1: Stock Movement -->
<div class="tab-pane fade show active" id="pills-movement" role="tabpanel" aria-labelledby="pills-movement-tab">
<!-- Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-4 mb-0">Movement Detail</h2>
<span id="result_summary" class="text-muted small"></span>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered table-sm" id="tbl_movement">
<thead class="table-light border-light">
<tr>
<th>Date</th>
<th>Type</th>
<th>SKU</th>
<th>Lot</th>
<th id="th_location">Location</th>
<th>Contact</th>
<th class="text-end">Stock In</th>
<th class="text-end">Stock Out</th>
<th>UOM</th>
<th class="text-end">Price / Cost</th>
<th class="text-end">Margin</th>
<th class="text-end">Running Total</th>
</tr>
</thead>
<tbody id="tbl_body">
<tr>
<td colspan="9" class="text-center py-5 text-muted">
Select a warehouse and date range, then click Search.
</td>
</tr>
</tbody>
</table>
</div>
<!-- Lazy load sentinel + loader -->
<div id="lazy_sentinel" class="py-3 text-center text-muted small" style="display:none;">
<div id="lazy_loader"><i class="ti ti-loader-2 me-1"></i>Loading...</div>
<div id="lazy_done" style="display:none;">All products loaded.</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- end tab 1 -->
<!-- Tab 2: Movement Summary -->
<div class="tab-pane fade" id="pills-summary" role="tabpanel" aria-labelledby="pills-summary-tab">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-4 mb-0">Movement Summary</h2>
<span id="summary_note" class="text-muted small">Search to load data.</span>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered table-sm">
<thead class="table-light border-light">
<tr>
<th>SKU</th>
<th>Product</th>
<th>UOM</th>
<th class="text-end">Transactions</th>
<th class="text-end">Total In</th>
<th class="text-end">Total Out</th>
<th class="text-end">Revenue</th>
<th class="text-end">COGS</th>
<th class="text-end">Gross Profit</th>
<th class="text-end">Margin</th>
<th class="text-end">Run. Revenue</th>
<th class="text-end">Run. COGS</th>
<th class="text-end">Run. GP</th>
</tr>
</thead>
<tbody id="tbl_summary_body">
<tr>
<td colspan="12" class="text-center py-5 text-muted">
Select a warehouse and date range, then click Search.
</td>
</tr>
</tbody>
<tfoot id="tbl_summary_foot" style="display:none;">
<tr class="table-secondary fw-semibold">
<td colspan="3">Total</td>
<td class="text-end" id="ssum_txn">—</td>
<td class="text-end" id="ssum_total_in">—</td>
<td class="text-end" id="ssum_total_out">—</td>
<td class="text-end" id="ssum_revenue">—</td>
<td class="text-end" id="ssum_cogs">—</td>
<td class="text-end" id="ssum_gp">—</td>
<td class="text-end" id="ssum_margin">—</td>
<td colspan="3"></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div><!-- end tab 2 -->
</div>
</main>
<?php require "../include_ending.php";?>
<script>
// ── State ─────────────────────────────────────────────────────────────
var sku_queue = []; // ordered list of SKUs returned by summary call
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;
// Movement Summary running totals (reset on each search)
var run_revenue = 0, run_cogs = 0, run_gp = 0, sum_in = 0, sum_out = 0, sum_txn = 0;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
var type_badge = {
'in': '<span class="badge bg-success">In</span>',
'out': '<span class="badge bg-danger">Out</span>',
'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();
var month = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
$('#from_month').val(month);
$('#to_month').val(month);
})();
// ── Warehouse dropdown ────────────────────────────────────────────────
function retrieve_warehouse() {
return ajax_request({
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
autoPrepare: true, checkRequired: 0, action: 'read',
onSuccess: function(res) {
var options = '';
$.each(res.output, function(i, item) {
options += `<option value="${item.id}">${item.warehouse_name}</option>`;
});
$('#warehouse').html(options);
if (res.output && res.output.length > 0) {
$('#warehouse').val(res.output[0].id);
do_search();
}
}
});
}
// ── Search — fetch summary, reset state, start lazy load ─────────────
function do_search() {
var warehouse_id = $('#warehouse').val();
var from_month = $('#from_month').val();
var to_month = $('#to_month').val();
if (!warehouse_id || !from_month || !to_month) return;
// Reset state
sku_queue = [];
sku_index = 0;
is_loading = false;
export_data = [];
run_revenue = run_cogs = run_gp = sum_in = sum_out = sum_txn = 0;
search_params = { warehouse_id: warehouse_id, from_month: from_month, to_month: to_month };
$('#tbl_summary_body').html('<tr><td colspan="13" class="text-center py-4 text-muted">Loading…</td></tr>');
$('#tbl_summary_foot').hide();
$('#summary_note').text('');
// Reset UI
$('#tbl_body').html('<tr><td colspan="13" class="text-center py-4 text-muted">Loading…</td></tr>');
$('#lazy_sentinel').hide();
$('#lazy_loader').hide();
$('#lazy_done').hide();
$('#result_summary').text('');
// Reset stats
$('#stat_transactions, #stat_in, #stat_out, #stat_net').text('—');
return ajax_request({
url: "<?php echo $server_url?>reports/api/engine_report/stock_movement.php",
autoPrepare: true, checkRequired: 0, action: 'read',
data: { warehouse_id: warehouse_id, from_month: from_month, to_month: to_month },
onSuccess: function(res) {
var d = res.output;
// Populate stats
$('#stat_transactions').text(d.total_transactions);
$('#stat_in').text(fmt(d.total_in));
$('#stat_out').text(fmt(d.total_out));
var net = d.net;
$('#stat_net')
.text((net >= 0 ? '+' : '') + fmt(net))
.removeClass('text-success text-danger')
.addClass(net >= 0 ? 'text-success' : 'text-danger');
if (!d.skus || d.skus.length === 0) {
$('#tbl_body').html('<tr><td colspan="12" class="text-center py-5 text-muted">No movement data found.</td></tr>');
return;
}
sku_queue = d.skus;
sku_index = 0;
$('#tbl_body').html('');
$('#result_summary').text(d.skus.length + ' product' + (d.skus.length > 1 ? 's' : ''));
$('#lazy_sentinel').show();
// Start loading — sentinel_is_visible() will keep pulling until window is filled
load_next_sku();
}
});
}
// ── Load one SKU group, then keep going if sentinel still visible ─────
function load_next_sku() {
if (is_loading || sku_index >= sku_queue.length) {
if (sku_index >= sku_queue.length) {
$('#lazy_loader').hide();
$('#lazy_done').show();
}
return;
}
is_loading = true;
$('#lazy_loader').show();
var sku = sku_queue[sku_index];
return ajax_request({
url: "<?php echo $server_url?>reports/api/engine_report/stock_movement_sku.php",
autoPrepare: true, checkRequired: 0, action: 'read',
queueLock: false,
data: {
warehouse_id : search_params.warehouse_id,
product_sku : sku,
from_month : search_params.from_month,
to_month : search_params.to_month,
},
onSuccess: function(res) {
append_sku_group(res.output);
sku_index++;
is_loading = false;
if (sku_index >= sku_queue.length) {
$('#lazy_loader').hide();
$('#lazy_done').show();
return;
}
// If sentinel is still within the viewport keep loading immediately.
// Otherwise hide the spinner and wait for the IntersectionObserver to
// fire when the user scrolls close to the sentinel.
if (sentinel_is_visible()) {
load_next_sku();
} else {
$('#lazy_loader').hide();
}
},
onError: function() {
is_loading = false;
$('#lazy_loader').hide();
}
});
}
// ── Returns true if the sentinel is within one window-height of the fold ─
function sentinel_is_visible() {
var el = document.getElementById('lazy_sentinel');
var rect = el.getBoundingClientRect();
var vh = window.innerHeight || document.documentElement.clientHeight;
return rect.top <= vh * 2;
}
// ── Render one SKU group into the table ───────────────────────────────
function append_sku_group(d) {
var bf = d.brought_forward;
var rows = d.rows;
// Populate export_data — brought forward row first, then transactions
export_data.push({
sku: d.sku, product: d.product_name, date: 'Brought Forward', type: '',
lot: '', location: '', contact: '',
stock_in: bf.in > 0 ? bf.in : '', stock_out: bf.out > 0 ? bf.out : '',
uom: d.uom || 'pcs',
running_total: bf.balance,
});
$.each(rows, function(i, row) {
export_data.push({
sku: row.product_sku,
product: d.product_name,
date: row.date,
type: row.type,
lot: row.lot_number || '',
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 : '',
uom: d.uom || 'pcs',
running_total: row.running_total,
});
});
// SKU header row
var header = `<tr class="table-secondary">
<td colspan="12" class="py-2 fw-semibold">
<code>${d.sku}</code>
<span class="text-muted fw-normal ms-2">${d.product_name}</span>
<span class="badge bg-light text-secondary border ms-2 small">${d.uom || 'pcs'}</span>
</td>
</tr>`;
// Brought-forward row
var bf_in_val = bf.in > 0 ? fmt(bf.in) : '<span class="text-muted">—</span>';
var bf_out_val = bf.out > 0 ? fmt(bf.out) : '<span class="text-muted">—</span>';
var bf_bal_cls = bf.balance >= 0 ? 'text-success' : 'text-danger';
var bf_row = `<tr class="table-light text-muted fst-italic">
<td class="py-2" colspan="6">Brought Forward</td>
<td class="py-2 text-end text-success">${bf_in_val}</td>
<td class="py-2 text-end text-danger">${bf_out_val}</td>
<td class="py-2 text-muted small">${d.uom || 'pcs'}</td>
<td class="py-2"></td>
<td class="py-2"></td>
<td class="py-2 text-end fw-semibold ${bf_bal_cls}">${fmt(bf.balance)}</td>
</tr>`;
// Transaction rows
var body = '';
if (rows.length === 0) {
body = `<tr>
<td colspan="12" class="text-center py-2 text-muted fst-italic">No transactions in range.</td>
</tr>`;
} else {
$.each(rows, function(i, row) {
var in_val = row.stock_in > 0 ? fmt(row.stock_in) : '<span class="text-muted">—</span>';
var out_val = row.stock_out > 0 ? fmt(row.stock_out) : '<span class="text-muted">—</span>';
var rt_cls = row.running_total >= 0 ? 'text-success' : 'text-danger';
var price_cell = '<span class="text-muted">—</span>';
if (row.stock_in > 0 && parseFloat(row.price) > 0) {
price_cell = `<span class="text-success">${fmt(row.price)}</span>`;
} else if (row.stock_out > 0 && parseFloat(row.price) > 0) {
price_cell = `<div>${fmt(row.price)}</div>
<small class="text-muted">${fmt(row.unit_cost)}</small>`;
}
var margin_cell = '<span class="text-muted">—</span>';
if (row.margin !== null && row.margin !== undefined) {
var m_cls = row.margin >= 0 ? 'text-success' : 'text-danger';
margin_cell = `<span class="${m_cls} fw-semibold">${parseFloat(row.margin).toFixed(1)}%</span>`;
}
body += `<tr>
<td class="py-2">${row.date}</td>
<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">${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>
<td class="py-2 text-muted small">${d.uom || 'pcs'}</td>
<td class="py-2 text-end">${price_cell}</td>
<td class="py-2 text-end">${margin_cell}</td>
<td class="py-2 text-end fw-semibold ${rt_cls}">${fmt(row.running_total)}</td>
</tr>`;
});
}
$('#tbl_body').append(header + bf_row + body);
// ── Movement Summary row ──────────────────────────────────────────────
var sku_txn = rows.length;
var sku_in = 0, sku_out = 0, sku_rev = 0, sku_cost = 0;
$.each(rows, function(i, row) {
sku_in += parseFloat(row.stock_in || 0);
if (parseFloat(row.stock_out) > 0) {
sku_out += parseFloat(row.stock_out);
sku_rev += parseFloat(row.stock_out) * parseFloat(row.price || 0);
sku_cost += parseFloat(row.stock_out) * parseFloat(row.unit_cost || 0);
}
});
var sku_gp = sku_rev - sku_cost;
var sku_mgn = sku_rev > 0 ? (sku_gp / sku_rev * 100) : null;
sum_txn += sku_txn;
sum_in += sku_in;
sum_out += sku_out;
run_revenue += sku_rev;
run_cogs += sku_cost;
run_gp += sku_gp;
var mgn_html = sku_mgn !== null
? `<span class="${sku_mgn >= 0 ? 'text-success' : 'text-danger'}">${sku_mgn.toFixed(1)}%</span>`
: '<span class="text-muted">—</span>';
var gp_cls = sku_gp >= 0 ? 'text-success' : 'text-danger';
var run_gp_cls = run_gp >= 0 ? 'text-success' : 'text-danger';
if ($('#tbl_summary_body tr[data-sku]').length === 0) $('#tbl_summary_body').html('');
$('#tbl_summary_body').append(`<tr data-sku="${escape_html(d.sku)}">
<td><code>${escape_html(d.sku)}</code></td>
<td>${escape_html(d.product_name)}</td>
<td class="text-muted small">${escape_html(d.uom || 'pcs')}</td>
<td class="text-end">${sku_txn}</td>
<td class="text-end text-success">${fmt(sku_in)}</td>
<td class="text-end text-danger">${fmt(sku_out)}</td>
<td class="text-end">${sku_rev > 0 ? fmt(sku_rev) : '<span class="text-muted">—</span>'}</td>
<td class="text-end">${sku_cost > 0 ? fmt(sku_cost) : '<span class="text-muted">—</span>'}</td>
<td class="text-end fw-semibold ${gp_cls}">${fmt(sku_gp)}</td>
<td class="text-end">${mgn_html}</td>
<td class="text-end text-muted small">${fmt(run_revenue)}</td>
<td class="text-end text-muted small">${fmt(run_cogs)}</td>
<td class="text-end small fw-semibold ${run_gp_cls}">${fmt(run_gp)}</td>
</tr>`);
// Update totals footer
var tot_mgn = run_revenue > 0 ? (run_gp / run_revenue * 100) : null;
var tot_mgn_html = tot_mgn !== null
? `<span class="${tot_mgn >= 0 ? 'text-success' : 'text-danger'}">${tot_mgn.toFixed(1)}%</span>`
: '—';
$('#ssum_txn').text(sum_txn);
$('#ssum_total_in').text(fmt(sum_in));
$('#ssum_total_out').text(fmt(sum_out));
$('#ssum_revenue').text(fmt(run_revenue));
$('#ssum_cogs').text(fmt(run_cogs));
$('#ssum_gp').html(`<span class="${run_gp >= 0 ? 'text-success' : 'text-danger'}">${fmt(run_gp)}</span>`);
$('#ssum_margin').html(tot_mgn_html);
$('#tbl_summary_foot').show();
$('#summary_note').text((sku_index + 1) + ' of ' + sku_queue.length + ' SKUs loaded');
}
// ── Intersection Observer for lazy load ───────────────────────────────
var observer = new IntersectionObserver(function(entries) {
if (entries[0].isIntersecting && !is_loading && sku_index < sku_queue.length) {
load_next_sku();
}
}, { rootMargin: '200px' });
observer.observe(document.getElementById('lazy_sentinel'));
// ── Helpers ───────────────────────────────────────────────────────────
function fmt(n) {
return format_number(n, 2);
}
// ── Export ───────────────────────────────────────────────────────────
// export_data holds all rows accumulated across all loaded SKU groups.
// It is populated by append_sku_group() alongside the table render.
var export_data = [];
function export_report(format) {
if (export_data.length === 0) {
alert('No data to export. Please search first.');
return;
}
var warehouse = $('#warehouse option:selected').text();
var from = $('#from_month').val();
var to = $('#to_month').val();
var filename = 'stock_movement_' + warehouse + '_' + from + '_' + to;
if (format === 'csv') {
alasql('SELECT * INTO CSV("' + filename + '.csv", {headers:true}) FROM ?', [export_data]);
} else if (format === 'xlsx') {
alasql('SELECT * INTO XLSX("' + filename + '.xlsx", {headers:true}) FROM ?', [export_data]);
} else if (format === 'pdf') {
var { jsPDF } = window.jspdf;
var doc = new jsPDF({ orientation: 'landscape' });
doc.setFontSize(12);
doc.text('Stock Movement — ' + warehouse + ' (' + from + ' to ' + to + ')', 14, 15);
var headers = [['SKU', 'Product', 'Date', 'Type', 'Lot', 'Location', 'Contact', 'Stock In', 'Stock Out', 'UOM', 'Running Total']];
var rows = export_data.map(function(r) {
return [
r.sku || '',
r.product || '',
r.date || '',
r.type || '',
r.lot || '',
r.location || '',
r.contact || '',
r.stock_in || '',
r.stock_out || '',
r.uom || 'pcs',
r.running_total || '',
];
});
doc.autoTable({
head: headers,
body: rows,
startY: 22,
styles: { fontSize: 7 },
headStyles: { fillColor: [41, 128, 185] },
});
doc.save(filename + '.pdf');
}
}
$(async function() {
try {
await load_company_setting();
await retrieve_warehouse();
} catch(e) { console.log(e); }
});
</script>
<!-- SheetJS — required by AlaSQL for Excel (.xlsx) export -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<!-- jsPDF + autoTable — for PDF export on stock movement report -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
</body>
</html>