cost/price/margin
This commit is contained in:
@@ -360,6 +360,7 @@ class ProductManager {
|
|||||||
':sku' => $data['sku'],
|
':sku' => $data['sku'],
|
||||||
':uom' => $data['uom'] ?? 'pcs',
|
':uom' => $data['uom'] ?? 'pcs',
|
||||||
':price' => $data['price'],
|
':price' => $data['price'],
|
||||||
|
':cost_price' => (float)($data['cost_price'] ?? 0),
|
||||||
':min_stock' => $data['min_stock'],
|
':min_stock' => $data['min_stock'],
|
||||||
':reorder_point' => $data['reorder_point'],
|
':reorder_point' => $data['reorder_point'],
|
||||||
':category' => $data['category'],
|
':category' => $data['category'],
|
||||||
@@ -377,6 +378,7 @@ class ProductManager {
|
|||||||
sku = :sku,
|
sku = :sku,
|
||||||
uom = :uom,
|
uom = :uom,
|
||||||
price = :price,
|
price = :price,
|
||||||
|
cost_price = :cost_price,
|
||||||
min_stock = :min_stock,
|
min_stock = :min_stock,
|
||||||
reorder_point = :reorder_point,
|
reorder_point = :reorder_point,
|
||||||
category = :category,
|
category = :category,
|
||||||
@@ -389,10 +391,10 @@ class ProductManager {
|
|||||||
} else {
|
} else {
|
||||||
$this->pdo->prepare(
|
$this->pdo->prepare(
|
||||||
"INSERT INTO md_product
|
"INSERT INTO md_product
|
||||||
(company_id, product_name, sku, uom, price, min_stock, reorder_point,
|
(company_id, product_name, sku, uom, price, cost_price, min_stock, reorder_point,
|
||||||
category, product_image, `description`, `status`, `log`)
|
category, product_image, `description`, `status`, `log`)
|
||||||
VALUES
|
VALUES
|
||||||
(:company_id, :product_name, :sku, :uom, :price, :min_stock, :reorder_point,
|
(:company_id, :product_name, :sku, :uom, :price, :cost_price, :min_stock, :reorder_point,
|
||||||
:category, :product_image, :description, :status, :log)"
|
:category, :product_image, :description, :status, :log)"
|
||||||
)->execute($params);
|
)->execute($params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,11 +366,19 @@ class PurchaseOrderManager {
|
|||||||
if ($zone === '' && $rack !== '') $zone = $rack;
|
if ($zone === '' && $rack !== '') $zone = $rack;
|
||||||
if ($aisle === '' && $rack !== '') $aisle = $rack;
|
if ($aisle === '' && $rack !== '') $aisle = $rack;
|
||||||
|
|
||||||
|
// Resolve unit_price: prefer the receive-time override, fall back to PO item price
|
||||||
|
$po_item_price = 0;
|
||||||
|
if ($item_id >= 0 && isset($po_items_by_id[$item_id])) {
|
||||||
|
$po_item_price = (float)($po_items[$po_items_by_id[$item_id]]['unit_price'] ?? 0);
|
||||||
|
}
|
||||||
|
$unit_price = (float)($recv['unit_price'] ?? $po_item_price);
|
||||||
|
|
||||||
$stock_data = [
|
$stock_data = [
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
'warehouse' => $warehouse_id,
|
'warehouse' => $warehouse_id,
|
||||||
'product_sku' => $product_sku,
|
'product_sku' => $product_sku,
|
||||||
'quantity' => $quantity,
|
'quantity' => $quantity,
|
||||||
|
'price' => $unit_price,
|
||||||
'zone' => $zone,
|
'zone' => $zone,
|
||||||
'aisle' => $aisle,
|
'aisle' => $aisle,
|
||||||
'rack' => $rack,
|
'rack' => $rack,
|
||||||
|
|||||||
@@ -276,13 +276,14 @@ class ReportManager
|
|||||||
a.product_sku,
|
a.product_sku,
|
||||||
SUM(a.total_in) AS total_in,
|
SUM(a.total_in) AS total_in,
|
||||||
SUM(a.total_out) AS total_out,
|
SUM(a.total_out) AS total_out,
|
||||||
b.min_stock
|
b.min_stock,
|
||||||
|
b.cost_price
|
||||||
FROM warehouse_balance a
|
FROM warehouse_balance a
|
||||||
LEFT JOIN md_product b
|
LEFT JOIN md_product b
|
||||||
ON a.company_id = b.company_id
|
ON a.company_id = b.company_id
|
||||||
AND a.product_sku = b.sku
|
AND a.product_sku = b.sku
|
||||||
WHERE a.company_id = :company_id
|
WHERE a.company_id = :company_id
|
||||||
GROUP BY a.product_sku, b.min_stock";
|
GROUP BY a.product_sku, b.min_stock, b.cost_price";
|
||||||
return $this->fetchAll($sql);
|
return $this->fetchAll($sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +325,7 @@ class ReportManager
|
|||||||
mp.min_stock,
|
mp.min_stock,
|
||||||
mp.reorder_point,
|
mp.reorder_point,
|
||||||
mp.product_image,
|
mp.product_image,
|
||||||
|
mp.cost_price,
|
||||||
ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance
|
ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance
|
||||||
FROM warehouse_balance wb
|
FROM warehouse_balance wb
|
||||||
INNER JOIN md_product mp
|
INNER JOIN md_product mp
|
||||||
@@ -336,7 +338,7 @@ class ReportManager
|
|||||||
AND mp.reorder_point > 0
|
AND mp.reorder_point > 0
|
||||||
GROUP BY
|
GROUP BY
|
||||||
wb.warehouse_id, wb.product_sku, mw.warehouse_name,
|
wb.warehouse_id, wb.product_sku, mw.warehouse_name,
|
||||||
mp.product_name, mp.min_stock, mp.reorder_point, mp.product_image
|
mp.product_name, mp.min_stock, mp.reorder_point, mp.product_image, mp.cost_price
|
||||||
HAVING balance <= mp.reorder_point
|
HAVING balance <= mp.reorder_point
|
||||||
ORDER BY mp.product_name ASC, mw.warehouse_name ASC";
|
ORDER BY mp.product_name ASC, mw.warehouse_name ASC";
|
||||||
|
|
||||||
@@ -352,6 +354,7 @@ class ReportManager
|
|||||||
'product_sku' => $row['product_sku'],
|
'product_sku' => $row['product_sku'],
|
||||||
'product_name' => $row['product_name'],
|
'product_name' => $row['product_name'],
|
||||||
'product_image' => $row['product_image'],
|
'product_image' => $row['product_image'],
|
||||||
|
'cost_price' => (float) ($row['cost_price'] ?? 0),
|
||||||
'balance' => max(0, round($balance, 2)),
|
'balance' => max(0, round($balance, 2)),
|
||||||
'min_stock' => (float) $row['min_stock'],
|
'min_stock' => (float) $row['min_stock'],
|
||||||
'reorder_point' => (float) $row['reorder_point'],
|
'reorder_point' => (float) $row['reorder_point'],
|
||||||
@@ -1407,7 +1410,9 @@ class ReportManager
|
|||||||
$product_name = $product_row['product_name'] ?: $product_sku;
|
$product_name = $product_row['product_name'] ?: $product_sku;
|
||||||
$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).
|
||||||
|
// p.cost_price is the product-level fallback when si.price is 0 or ref_id is absent.
|
||||||
$sth = $this->pdo->prepare(
|
$sth = $this->pdo->prepare(
|
||||||
"SELECT
|
"SELECT
|
||||||
s.id,
|
s.id,
|
||||||
@@ -1421,9 +1426,17 @@ 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,
|
||||||
|
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
|
||||||
|
ON si.id = s.ref_id
|
||||||
|
AND s.`out` > 0
|
||||||
|
LEFT JOIN md_product p
|
||||||
|
ON p.company_id = s.company_id
|
||||||
|
AND p.sku = s.product_sku
|
||||||
LEFT JOIN md_contact c
|
LEFT JOIN md_contact c
|
||||||
ON c.company_id = s.company_id
|
ON c.company_id = s.company_id
|
||||||
AND c.id = s.contact_id
|
AND c.id = s.contact_id
|
||||||
@@ -1436,11 +1449,18 @@ class ReportManager
|
|||||||
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku, ':date_from' => $date_from, ':date_to' => $date_to]);
|
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku, ':date_from' => $date_from, ':date_to' => $date_to]);
|
||||||
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Append running_total to each row (carried forward from brought-forward balance)
|
// Append running_total and margin to each row.
|
||||||
|
// Margin is only meaningful for stock-out rows with both sell price and cost.
|
||||||
$running = $bf_bal;
|
$running = $bf_bal;
|
||||||
foreach ($rows as &$row) {
|
foreach ($rows as &$row) {
|
||||||
$running += (float)$row['stock_in'] - (float)$row['stock_out'];
|
$running += (float)$row['stock_in'] - (float)$row['stock_out'];
|
||||||
$row['running_total'] = round($running, 2);
|
$row['running_total'] = round($running, 2);
|
||||||
|
|
||||||
|
$sell = (float)$row['price'];
|
||||||
|
$cost = (float)$row['unit_cost'];
|
||||||
|
$row['margin'] = ($row['stock_out'] > 0 && $sell > 0)
|
||||||
|
? round(($sell - $cost) / $sell * 100, 2)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
unset($row);
|
unset($row);
|
||||||
|
|
||||||
|
|||||||
@@ -283,10 +283,10 @@ class StockManager {
|
|||||||
|
|
||||||
$this->pdo->prepare(
|
$this->pdo->prepare(
|
||||||
"INSERT INTO `$table`
|
"INSERT INTO `$table`
|
||||||
(uuid, company_id, `date`, product_sku, `in`, zone, aisle, rack,
|
(uuid, company_id, `date`, product_sku, `in`, price, zone, aisle, rack,
|
||||||
contact_id, `description`, `log`, `type`, lot_number, serial_number, status)
|
contact_id, `description`, `log`, `type`, lot_number, serial_number, status)
|
||||||
VALUES
|
VALUES
|
||||||
(:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack,
|
(:uuid, :company_id, :date, :product_sku, :quantity, :price, :zone, :aisle, :rack,
|
||||||
:contact_id, :description, :log, 'in', :lot_number, :serial_number, 0)"
|
:contact_id, :description, :log, 'in', :lot_number, :serial_number, 0)"
|
||||||
)->execute([
|
)->execute([
|
||||||
':uuid' => $uuid,
|
':uuid' => $uuid,
|
||||||
@@ -294,6 +294,7 @@ class StockManager {
|
|||||||
':date' => date('Y-m-d H:i:s'),
|
':date' => date('Y-m-d H:i:s'),
|
||||||
':product_sku' => $data['product_sku'],
|
':product_sku' => $data['product_sku'],
|
||||||
':quantity' => $data['quantity'],
|
':quantity' => $data['quantity'],
|
||||||
|
':price' => (float)($data['price'] ?? 0),
|
||||||
':zone' => $data['zone'],
|
':zone' => $data['zone'],
|
||||||
':aisle' => $data['aisle'],
|
':aisle' => $data['aisle'],
|
||||||
':rack' => $data['rack'],
|
':rack' => $data['rack'],
|
||||||
|
|||||||
@@ -254,7 +254,12 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="py-3 text-secondary">${item.product_sku}</td>
|
<td class="py-3 text-secondary">${item.product_sku}</td>
|
||||||
<td class="py-3">${item.warehouse_name}</td>
|
<td class="py-3">${item.warehouse_name}</td>
|
||||||
<td class="py-3 ${balance_cls}">${item.balance} <span class="text-muted fw-normal small">${item.uom || 'pcs'}</span></td>
|
<td class="py-3 ${balance_cls}">
|
||||||
|
${item.balance} <span class="text-muted fw-normal small">${item.uom || 'pcs'}</span>
|
||||||
|
${item.cost_price > 0
|
||||||
|
? `<div class="text-muted small fw-normal">Value: ${format_number(item.balance * item.cost_price, 2)}</div>`
|
||||||
|
: ''}
|
||||||
|
</td>
|
||||||
<td class="py-3">${item.min_stock}</td>
|
<td class="py-3">${item.min_stock}</td>
|
||||||
<td class="py-3">${item.reorder_point}</td>
|
<td class="py-3">${item.reorder_point}</td>
|
||||||
<td class="py-3">${badge}</td>
|
<td class="py-3">${badge}</td>
|
||||||
|
|||||||
@@ -39,11 +39,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SKU or Product Code -->
|
<!-- SKU or Product Code -->
|
||||||
<div class="mb-3 col-lg-6">
|
<div class="mb-3 col-lg-3">
|
||||||
<label for="quantity" class="form-label">Quantity</label>
|
<label for="quantity" class="form-label">Quantity</label>
|
||||||
<input type="number" class="form-control" id="quantity" placeholder="Enter quantity" required>
|
<input type="number" class="form-control" id="quantity" placeholder="Enter quantity" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Unit Price / COGS -->
|
||||||
|
<div class="mb-3 col-lg-3">
|
||||||
|
<label for="price" class="form-label">Unit Price <span class="text-muted fw-normal">(optional)</span></label>
|
||||||
|
<input type="number" class="form-control" id="price" placeholder="0.00" step="0.01" min="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Lot Number -->
|
<!-- Lot Number -->
|
||||||
<div class="mb-3 col-lg-6">
|
<div class="mb-3 col-lg-6">
|
||||||
<label for="lot_number" class="form-label">Lot Number <span class="text-muted fw-normal">(optional)</span></label>
|
<label for="lot_number" class="form-label">Lot Number <span class="text-muted fw-normal">(optional)</span></label>
|
||||||
@@ -368,7 +374,7 @@
|
|||||||
if (expiryPicker) { expiryPicker.clear(); }
|
if (expiryPicker) { expiryPicker.clear(); }
|
||||||
}
|
}
|
||||||
$('#product_sku').attr('secondary', data.product_sku);
|
$('#product_sku').attr('secondary', data.product_sku);
|
||||||
$('#product_sku, #quantity').prop('disabled', true);
|
$('#product_sku, #quantity, #price').prop('disabled', true);
|
||||||
$('button[type=submit]').text('Update');
|
$('button[type=submit]').text('Update');
|
||||||
$('button[type=reset]').hide();
|
$('button[type=reset]').hide();
|
||||||
if (data.status == 0) {
|
if (data.status == 0) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
<th>Lot Number</th>
|
<th>Lot Number</th>
|
||||||
<th>Serial Number</th>
|
<th>Serial Number</th>
|
||||||
<th style="text-align: right;">Quantity</th>
|
<th style="text-align: right;">Quantity</th>
|
||||||
|
<th style="text-align: right;">Unit Price</th>
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th class="th_loc_zone">Zone</th>
|
<th class="th_loc_zone">Zone</th>
|
||||||
<th class="th_loc_aisle">Aisle</th>
|
<th class="th_loc_aisle">Aisle</th>
|
||||||
@@ -222,6 +223,7 @@
|
|||||||
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
|
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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>
|
<td class="py-3">${source_cell(item['source'])}</td>
|
||||||
${location_cells(item)}
|
${location_cells(item)}
|
||||||
<td class="py-3">
|
<td class="py-3">
|
||||||
|
|||||||
@@ -39,10 +39,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<label for="price" class="form-label">Price</label>
|
<label for="price" class="form-label">Price</label>
|
||||||
<input type="number" class="form-control" id="price" placeholder="0.00" step="0.01" required>
|
<input type="number" class="form-control" id="price" placeholder="0.00" step="0.01" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label for="cost_price" class="form-label">Cost Price <span class="text-muted fw-normal">(COGS)</span></label>
|
||||||
|
<input type="number" class="form-control" id="cost_price" placeholder="0.00" step="0.01" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label">Margin</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="margin_display" placeholder="—" disabled>
|
||||||
|
<span class="input-group-text">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-2 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<label for="uom" class="form-label">UOM</label>
|
<label for="uom" class="form-label">UOM</label>
|
||||||
<input type="text" class="form-control" id="uom" placeholder="pcs"
|
<input type="text" class="form-control" id="uom" placeholder="pcs"
|
||||||
@@ -62,7 +73,7 @@
|
|||||||
<option value="bottle">
|
<option value="bottle">
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<label for="min_stock" class="form-label">Min Stock</label>
|
<label for="min_stock" class="form-label">Min Stock</label>
|
||||||
<input type="number" class="form-control" id="min_stock" placeholder="0" min="0" required>
|
<input type="number" class="form-control" id="min_stock" placeholder="0" min="0" required>
|
||||||
<div class="invalid-feedback" id="min_stock_error"></div>
|
<div class="invalid-feedback" id="min_stock_error"></div>
|
||||||
@@ -138,6 +149,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
formData.append('keep_files', $('#current_file_label').val())
|
formData.append('keep_files', $('#current_file_label').val())
|
||||||
|
formData.append('cost_price', $('#cost_price').val() || 0)
|
||||||
formData.append('uom', $('#uom').val() || 'pcs')
|
formData.append('uom', $('#uom').val() || 'pcs')
|
||||||
|
|
||||||
return ajax_request({
|
return ajax_request({
|
||||||
@@ -209,8 +221,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$(`button[type=submit]`).text('Update');
|
$(`button[type=submit]`).text('Update');
|
||||||
|
|
||||||
$(`button[type=reset]`).hide();
|
$(`button[type=reset]`).hide();
|
||||||
|
update_margin();
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -267,6 +279,19 @@
|
|||||||
// Live feedback as the user types
|
// Live feedback as the user types
|
||||||
$('#min_stock, #reorder_point').on('input', validate_stock_thresholds);
|
$('#min_stock, #reorder_point').on('input', validate_stock_thresholds);
|
||||||
|
|
||||||
|
function update_margin() {
|
||||||
|
var price = parseFloat($('#price').val()) || 0;
|
||||||
|
var cost_price = parseFloat($('#cost_price').val()) || 0;
|
||||||
|
if (price > 0) {
|
||||||
|
var margin = (price - cost_price) / price * 100;
|
||||||
|
$('#margin_display').val(margin.toFixed(2));
|
||||||
|
} else {
|
||||||
|
$('#margin_display').val('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#price, #cost_price').on('input', update_margin);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -197,6 +197,8 @@
|
|||||||
<th width="75px"></th>
|
<th width="75px"></th>
|
||||||
<th>Product</th>
|
<th>Product</th>
|
||||||
<th>SKU</th>
|
<th>SKU</th>
|
||||||
|
<th style="text-align: right;">Price / Cost</th>
|
||||||
|
<th style="text-align: right;">Margin</th>
|
||||||
<th>UOM</th>
|
<th>UOM</th>
|
||||||
<th style="text-align: right;">Current Stock</th>
|
<th style="text-align: right;">Current Stock</th>
|
||||||
<th style="text-align: right;">Min Stock</th>
|
<th style="text-align: right;">Min Stock</th>
|
||||||
@@ -345,7 +347,7 @@
|
|||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
$("table#product > tbody").html(
|
$("table#product > tbody").html(
|
||||||
'<tr><td colspan="9" class="text-center py-5 text-muted">No products match your search.</td></tr>'
|
'<tr><td colspan="11" class="text-center py-5 text-muted">No products match your search.</td></tr>'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -362,6 +364,17 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="py-3">${item['product_name']}</td>
|
<td class="py-3">${item['product_name']}</td>
|
||||||
<td class="py-3">${item['sku']}</td>
|
<td class="py-3">${item['sku']}</td>
|
||||||
|
<td class="py-3 text-end">
|
||||||
|
<div>${format_number(item['price'], 2)}</div>
|
||||||
|
<small class="text-muted">${format_number(item['cost_price'], 2)}</small>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 text-end">
|
||||||
|
${(item['price'] > 0 && item['cost_price'] > 0)
|
||||||
|
? `<span class="${((item['price'] - item['cost_price']) / item['price'] * 100) >= 0 ? 'text-success' : 'text-danger'} fw-semibold">
|
||||||
|
${format_number((item['price'] - item['cost_price']) / item['price'] * 100, 1)}%
|
||||||
|
</span>`
|
||||||
|
: '<span class="text-muted">—</span>'}
|
||||||
|
</td>
|
||||||
<td class="py-3"><span class="badge bg-light text-secondary border">${item['uom'] || 'pcs'}</span></td>
|
<td class="py-3"><span class="badge bg-light text-secondary border">${item['uom'] || 'pcs'}</span></td>
|
||||||
<td class="py-3">
|
<td class="py-3">
|
||||||
<div class="d-flex justify-content-end align-items-baseline gap-1">
|
<div class="d-flex justify-content-end align-items-baseline gap-1">
|
||||||
|
|||||||
@@ -139,6 +139,8 @@
|
|||||||
<th class="text-end">Stock In</th>
|
<th class="text-end">Stock In</th>
|
||||||
<th class="text-end">Stock Out</th>
|
<th class="text-end">Stock Out</th>
|
||||||
<th>UOM</th>
|
<th>UOM</th>
|
||||||
|
<th class="text-end">Price / Cost</th>
|
||||||
|
<th class="text-end">Margin</th>
|
||||||
<th class="text-end">Running Total</th>
|
<th class="text-end">Running Total</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -291,7 +293,7 @@
|
|||||||
.addClass(net >= 0 ? 'text-success' : 'text-danger');
|
.addClass(net >= 0 ? 'text-success' : 'text-danger');
|
||||||
|
|
||||||
if (!d.skus || d.skus.length === 0) {
|
if (!d.skus || d.skus.length === 0) {
|
||||||
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-5 text-muted">No movement data found.</td></tr>');
|
$('#tbl_body').html('<tr><td colspan="12" class="text-center py-5 text-muted">No movement data found.</td></tr>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +393,7 @@
|
|||||||
|
|
||||||
// SKU header row
|
// SKU header row
|
||||||
var header = `<tr class="table-secondary">
|
var header = `<tr class="table-secondary">
|
||||||
<td colspan="10" class="py-2 fw-semibold">
|
<td colspan="12" class="py-2 fw-semibold">
|
||||||
<code>${d.sku}</code>
|
<code>${d.sku}</code>
|
||||||
<span class="text-muted fw-normal ms-2">${d.product_name}</span>
|
<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>
|
<span class="badge bg-light text-secondary border ms-2 small">${d.uom || 'pcs'}</span>
|
||||||
@@ -407,6 +409,8 @@
|
|||||||
<td class="py-2 text-end text-success">${bf_in_val}</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-end text-danger">${bf_out_val}</td>
|
||||||
<td class="py-2 text-muted small">${d.uom || 'pcs'}</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>
|
<td class="py-2 text-end fw-semibold ${bf_bal_cls}">${fmt(bf.balance)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
@@ -414,13 +418,28 @@
|
|||||||
var body = '';
|
var body = '';
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
body = `<tr>
|
body = `<tr>
|
||||||
<td colspan="9" class="text-center py-2 text-muted fst-italic">No transactions in range.</td>
|
<td colspan="12" class="text-center py-2 text-muted fst-italic">No transactions in range.</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
} else {
|
} else {
|
||||||
$.each(rows, function(i, row) {
|
$.each(rows, function(i, row) {
|
||||||
var in_val = row.stock_in > 0 ? fmt(row.stock_in) : '<span class="text-muted">—</span>';
|
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 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 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>
|
body += `<tr>
|
||||||
<td class="py-2">${row.date}</td>
|
<td class="py-2">${row.date}</td>
|
||||||
<td class="py-2">${type_badge[row.type] || row.type}</td>
|
<td class="py-2">${type_badge[row.type] || row.type}</td>
|
||||||
@@ -431,6 +450,8 @@
|
|||||||
<td class="py-2 text-end text-success">${in_val}</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-end text-danger">${out_val}</td>
|
||||||
<td class="py-2 text-muted small">${d.uom || 'pcs'}</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>
|
<td class="py-2 text-end fw-semibold ${rt_cls}">${fmt(row.running_total)}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user