stockmovement: add summary views
This commit is contained in:
@@ -90,6 +90,10 @@
|
|||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep logo-area pinned at top when sidebar nav scrolls */
|
||||||
|
.sidebar { padding-top: 0; }
|
||||||
|
.sidebar .logo-area { position: sticky; top: 0; z-index: 1; }
|
||||||
|
|
||||||
.logo-area > a > img:first-child {
|
.logo-area > a > img:first-child {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
function escape_html(value) {
|
||||||
|
return String(value ?? '').replace(/[&<>"']/g, function(c) {
|
||||||
|
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** =========================
|
/** =========================
|
||||||
* SIDEBAR ACTIVE STATE
|
* SIDEBAR ACTIVE STATE
|
||||||
* Override: activate the parent listing page for manage_* sub-pages.
|
* Override: activate the parent listing page for manage_* sub-pages.
|
||||||
|
|||||||
@@ -919,25 +919,24 @@ class ReportManager
|
|||||||
function ($wh) use ($cid) {
|
function ($wh) use ($cid) {
|
||||||
$table = $this->stockTableNameFromWarehouseId((int)$wh['id']);
|
$table = $this->stockTableNameFromWarehouseId((int)$wh['id']);
|
||||||
return "SELECT
|
return "SELECT
|
||||||
s.id,
|
|
||||||
s.product_sku,
|
s.product_sku,
|
||||||
s.lot_number,
|
s.lot_number,
|
||||||
s.zone,
|
s.zone,
|
||||||
s.aisle,
|
s.aisle,
|
||||||
s.rack,
|
s.rack,
|
||||||
ROUND(s.`in`, 2) AS quantity,
|
ROUND(SUM(s.`in`) - SUM(s.`out`), 2) AS quantity,
|
||||||
{$wh['id']} AS warehouse_id
|
{$wh['id']} AS warehouse_id
|
||||||
FROM `{$table}` s
|
FROM `{$table}` s
|
||||||
WHERE s.company_id = {$cid}
|
WHERE s.company_id = {$cid}
|
||||||
AND s.type = 'in'
|
|
||||||
AND s.lot_number IS NOT NULL
|
AND s.lot_number IS NOT NULL
|
||||||
AND s.status = 1";
|
AND s.status = 1
|
||||||
|
GROUP BY s.product_sku, s.lot_number, s.zone, s.aisle, s.rack
|
||||||
|
HAVING quantity > 0";
|
||||||
},
|
},
|
||||||
$warehouses
|
$warehouses
|
||||||
));
|
));
|
||||||
|
|
||||||
$sql = "SELECT
|
$sql = "SELECT
|
||||||
stock.id,
|
|
||||||
stock.product_sku,
|
stock.product_sku,
|
||||||
stock.lot_number,
|
stock.lot_number,
|
||||||
stock.zone,
|
stock.zone,
|
||||||
@@ -982,7 +981,6 @@ class ReportManager
|
|||||||
else { continue; }
|
else { continue; }
|
||||||
|
|
||||||
$items[] = [
|
$items[] = [
|
||||||
'id' => (int) $row['id'],
|
|
||||||
'product_sku' => $row['product_sku'],
|
'product_sku' => $row['product_sku'],
|
||||||
'product_name' => $row['product_name'],
|
'product_name' => $row['product_name'],
|
||||||
'category_name' => $row['category_name'],
|
'category_name' => $row['category_name'],
|
||||||
@@ -1411,13 +1409,17 @@ class ReportManager
|
|||||||
$product_uom = $product_row['uom'] ?? '';
|
$product_uom = $product_row['uom'] ?? '';
|
||||||
|
|
||||||
// All transactions for this SKU in range, sorted by date then id.
|
// All transactions for this SKU in range, sorted by date then id.
|
||||||
// For stock-out rows, si.price is the purchase cost from the source stock-in (via ref_id).
|
// Pricing rules per movement type:
|
||||||
// p.cost_price is the product-level fallback when si.price is 0 or ref_id is absent.
|
// PO in — s.price = actual purchase cost (user-defined on PO)
|
||||||
|
// sales out — s.price = selling price; unit_cost from linked stock-in via ref_id
|
||||||
|
// return in — price derived from linked SO stock-out via ref_id; no unit_cost needed
|
||||||
|
// others — s.price = standard cost from md_product.cost_price
|
||||||
$sth = $this->pdo->prepare(
|
$sth = $this->pdo->prepare(
|
||||||
"SELECT
|
"SELECT
|
||||||
s.id,
|
s.id,
|
||||||
s.date,
|
s.date,
|
||||||
s.type,
|
s.type,
|
||||||
|
s.source,
|
||||||
s.product_sku,
|
s.product_sku,
|
||||||
s.lot_number,
|
s.lot_number,
|
||||||
s.serial_number,
|
s.serial_number,
|
||||||
@@ -1426,14 +1428,19 @@ class ReportManager
|
|||||||
s.rack,
|
s.rack,
|
||||||
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
|
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
|
||||||
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
|
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
|
||||||
ROUND(s.price, 4) AS price,
|
CASE
|
||||||
|
WHEN s.source = 'return' AND s.`in` > 0
|
||||||
|
THEN ROUND(COALESCE(NULLIF(si.price, 0), s.price, 0), 4)
|
||||||
|
ELSE
|
||||||
|
ROUND(s.price, 4)
|
||||||
|
END AS price,
|
||||||
ROUND(COALESCE(NULLIF(si.price, 0), p.cost_price, 0), 4) AS unit_cost,
|
ROUND(COALESCE(NULLIF(si.price, 0), p.cost_price, 0), 4) AS unit_cost,
|
||||||
c.contact_name,
|
c.contact_name,
|
||||||
s.description
|
s.description
|
||||||
FROM `{$table}` s
|
FROM `{$table}` s
|
||||||
LEFT JOIN `{$table}` si
|
LEFT JOIN `{$table}` si
|
||||||
ON si.id = s.ref_id
|
ON si.id = s.ref_id
|
||||||
AND s.`out` > 0
|
AND (s.`out` > 0 OR s.source = 'return')
|
||||||
LEFT JOIN md_product p
|
LEFT JOIN md_product p
|
||||||
ON p.company_id = s.company_id
|
ON p.company_id = s.company_id
|
||||||
AND p.sku = s.product_sku
|
AND p.sku = s.product_sku
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ class ReturnManager {
|
|||||||
|
|
||||||
$stock_out_table = $this->stockTableNameFromWarehouseId($stock_out_wh);
|
$stock_out_table = $this->stockTableNameFromWarehouseId($stock_out_wh);
|
||||||
$stock_out_sth = $this->pdo->prepare(
|
$stock_out_sth = $this->pdo->prepare(
|
||||||
"SELECT status, lot_number, serial_number
|
"SELECT status, lot_number, serial_number, price
|
||||||
FROM `{$stock_out_table}`
|
FROM `{$stock_out_table}`
|
||||||
WHERE company_id = :company_id
|
WHERE company_id = :company_id
|
||||||
AND id = :id
|
AND id = :id
|
||||||
@@ -341,19 +341,21 @@ class ReturnManager {
|
|||||||
$quantity = (float)($item['quantity'] ?? 0);
|
$quantity = (float)($item['quantity'] ?? 0);
|
||||||
|
|
||||||
// INSERT stock-in row — status=1 forced
|
// INSERT stock-in row — status=1 forced
|
||||||
|
// price comes from the linked SO stock-out row (selling price), not the return form.
|
||||||
|
// ref_id links back to the stock-out row so the report can trace the original sale.
|
||||||
$this->pdo->prepare(
|
$this->pdo->prepare(
|
||||||
"INSERT INTO `{$table}`
|
"INSERT INTO `{$table}`
|
||||||
(uuid, company_id, `date`, product_sku, `in`,
|
(uuid, company_id, `date`, product_sku, `in`,
|
||||||
zone, aisle, rack,
|
zone, aisle, rack,
|
||||||
contact_id, `description`, `log`, `type`,
|
contact_id, `description`, `log`, `type`,
|
||||||
lot_number, serial_number,
|
lot_number, serial_number,
|
||||||
source, source_id, price, status)
|
source, source_id, ref_id, price, status)
|
||||||
VALUES
|
VALUES
|
||||||
(:uuid, :company_id, :date, :product_sku, :quantity,
|
(:uuid, :company_id, :date, :product_sku, :quantity,
|
||||||
:zone, :aisle, :rack,
|
:zone, :aisle, :rack,
|
||||||
:contact_id, :description, :log, 'in',
|
:contact_id, :description, :log, 'in',
|
||||||
:lot_number, :serial_number,
|
:lot_number, :serial_number,
|
||||||
'return', :source_id, :price, 1)"
|
'return', :source_id, :ref_id, :price, 1)"
|
||||||
)->execute([
|
)->execute([
|
||||||
':uuid' => $item_uuid,
|
':uuid' => $item_uuid,
|
||||||
':company_id' => $this->company_id,
|
':company_id' => $this->company_id,
|
||||||
@@ -369,7 +371,8 @@ class ReturnManager {
|
|||||||
':lot_number' => $stock_out['lot_number'] ?? ($item['lot_number'] ?? ''),
|
':lot_number' => $stock_out['lot_number'] ?? ($item['lot_number'] ?? ''),
|
||||||
':serial_number' => $stock_out['serial_number'] ?? ($item['serial_number'] ?? ''),
|
':serial_number' => $stock_out['serial_number'] ?? ($item['serial_number'] ?? ''),
|
||||||
':source_id' => $return_id,
|
':source_id' => $return_id,
|
||||||
':price' => (float)($item['price'] ?? 0),
|
':ref_id' => $stock_out_id,
|
||||||
|
':price' => (float)$stock_out['price'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$stock_in_id = (int)$this->pdo->lastInsertId();
|
$stock_in_id = (int)$this->pdo->lastInsertId();
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
<th>Product</th>
|
<th>Product</th>
|
||||||
<th>SKU</th>
|
<th>SKU</th>
|
||||||
<th>Lot Number</th>
|
<th>Lot Number</th>
|
||||||
<th class="th_location">Location</th>
|
<th class="th_location">Location</th>
|
||||||
<th>Expiry Date</th>
|
<th>Expiry Date</th>
|
||||||
<th>Days Overdue</th>
|
<th>Days Overdue</th>
|
||||||
<th>Quantity</th>
|
<th>Quantity</th>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
<th>Product</th>
|
<th>Product</th>
|
||||||
<th>SKU</th>
|
<th>SKU</th>
|
||||||
<th>Lot Number</th>
|
<th>Lot Number</th>
|
||||||
<th class="th_location">Location</th>
|
<th class="th_location">Location</th>
|
||||||
<th>Expiry Date</th>
|
<th>Expiry Date</th>
|
||||||
<th>Days Remaining</th>
|
<th>Days Remaining</th>
|
||||||
<th>Quantity</th>
|
<th>Quantity</th>
|
||||||
@@ -190,44 +190,44 @@
|
|||||||
<?php require "../include_ending.php";?>
|
<?php require "../include_ending.php";?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var all_items = [];
|
var all_items = [];
|
||||||
var search_term = '';
|
var search_term = '';
|
||||||
var filter_status = '';
|
var filter_status = '';
|
||||||
var advanced = false;
|
var advanced = false;
|
||||||
var label_rack = 'Location';
|
var label_rack = 'Location';
|
||||||
var label_zone = 'Zone';
|
var label_zone = 'Zone';
|
||||||
var label_aisle = 'Aisle';
|
var label_aisle = 'Aisle';
|
||||||
|
|
||||||
function apply_location_labels() {
|
function apply_location_labels() {
|
||||||
$('.th_location').text(advanced
|
$('.th_location').text(advanced
|
||||||
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
|
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
|
||||||
: label_rack
|
: label_rack
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function format_location(item) {
|
function format_location(item) {
|
||||||
if (advanced) {
|
if (advanced) {
|
||||||
return [item.zone, item.aisle, item.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
return [item.zone, item.aisle, item.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
||||||
}
|
}
|
||||||
return item.rack || '<span class="text-muted">—</span>';
|
return item.rack || '<span class="text-muted">—</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_company_setting() {
|
function load_company_setting() {
|
||||||
return ajax_request({
|
return ajax_request({
|
||||||
url: server_url + 'setting/api/engine/company_setting.php',
|
url: server_url + 'setting/api/engine/company_setting.php',
|
||||||
autoPrepare: true,
|
autoPrepare: true,
|
||||||
checkRequired: 0,
|
checkRequired: 0,
|
||||||
action: 'read',
|
action: 'read',
|
||||||
onSuccess: function(res) {
|
onSuccess: function(res) {
|
||||||
var s = res.output || {};
|
var s = res.output || {};
|
||||||
advanced = s.advanced_location == 1;
|
advanced = s.advanced_location == 1;
|
||||||
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
|
||||||
label_zone = s.location_label_zone || 'Zone';
|
label_zone = s.location_label_zone || 'Zone';
|
||||||
label_aisle = s.location_label_aisle || 'Aisle';
|
label_aisle = s.location_label_aisle || 'Aisle';
|
||||||
apply_location_labels();
|
apply_location_labels();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Fetch + populate ─────────────────────────────────────────────────
|
// ── Fetch + populate ─────────────────────────────────────────────────
|
||||||
@@ -264,14 +264,14 @@
|
|||||||
// ── Render both tables from filtered data ────────────────────────────
|
// ── Render both tables from filtered data ────────────────────────────
|
||||||
function render_tables() {
|
function render_tables() {
|
||||||
|
|
||||||
var kw = search_term;
|
var kw = search_term.toLowerCase();
|
||||||
var fs = filter_status;
|
var fs = filter_status;
|
||||||
|
|
||||||
var filtered = all_items.filter(function(item) {
|
var filtered = all_items.filter(function(item) {
|
||||||
var match_search = !kw ||
|
var match_search = !kw ||
|
||||||
item.product_name || ''.includes(kw) ||
|
(item.product_name || '').toLowerCase().includes(kw) ||
|
||||||
item.product_sku || ''.includes(kw) ||
|
(item.product_sku || '').toLowerCase().includes(kw) ||
|
||||||
item.lot_number || ''.includes(kw);
|
(item.lot_number || '').toLowerCase().includes(kw);
|
||||||
var match_status = !fs || item.status === fs;
|
var match_status = !fs || item.status === fs;
|
||||||
return match_search && match_status;
|
return match_search && match_status;
|
||||||
});
|
});
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
<td class="py-3">${item.product_name}</td>
|
<td class="py-3">${item.product_name}</td>
|
||||||
<td class="py-3">${item.product_sku}</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.lot_number || '<span class="text-muted">—</span>'}</td>
|
||||||
<td class="py-3">${format_location(item)}</td>
|
<td class="py-3">${format_location(item)}</td>
|
||||||
<td class="py-3">${item.expiry_date}</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 text-danger fw-semibold">${Math.abs(item.days_remaining)} days</td>
|
||||||
<td class="py-3">${item.quantity}</td>
|
<td class="py-3">${item.quantity}</td>
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
<td class="py-3">${item.product_name}</td>
|
<td class="py-3">${item.product_name}</td>
|
||||||
<td class="py-3">${item.product_sku}</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.lot_number || '<span class="text-muted">—</span>'}</td>
|
||||||
<td class="py-3">${format_location(item)}</td>
|
<td class="py-3">${format_location(item)}</td>
|
||||||
<td class="py-3">${item.expiry_date}</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 text-warning fw-semibold">${item.days_remaining} days</td>
|
||||||
<td class="py-3">${item.quantity}</td>
|
<td class="py-3">${item.quantity}</td>
|
||||||
@@ -382,11 +382,11 @@
|
|||||||
|
|
||||||
|
|
||||||
// ── Boot ─────────────────────────────────────────────────────────────
|
// ── Boot ─────────────────────────────────────────────────────────────
|
||||||
$(async function() {
|
$(async function() {
|
||||||
try {
|
try {
|
||||||
await load_company_setting();
|
await load_company_setting();
|
||||||
await retrieve_warehouse();
|
await retrieve_warehouse();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -398,4 +398,4 @@
|
|||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -115,6 +115,23 @@
|
|||||||
</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 -->
|
<!-- Table -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -165,6 +182,65 @@
|
|||||||
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -177,6 +253,9 @@
|
|||||||
var is_loading = false;
|
var is_loading = false;
|
||||||
var search_params = {}; // warehouse_id, from_month, to_month — fixed per search
|
var search_params = {}; // warehouse_id, from_month, to_month — fixed per search
|
||||||
var advanced = false;
|
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_rack = 'Location';
|
||||||
var label_zone = 'Zone';
|
var label_zone = 'Zone';
|
||||||
var label_aisle = 'Aisle';
|
var label_aisle = 'Aisle';
|
||||||
@@ -263,7 +342,11 @@
|
|||||||
sku_index = 0;
|
sku_index = 0;
|
||||||
is_loading = false;
|
is_loading = false;
|
||||||
export_data = [];
|
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 };
|
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
|
// Reset UI
|
||||||
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-4 text-muted">Loading…</td></tr>');
|
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-4 text-muted">Loading…</td></tr>');
|
||||||
@@ -458,6 +541,66 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('#tbl_body').append(header + bf_row + body);
|
$('#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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user