From adfd5ef01742c838b8abeb2639a1c21e91d82fef Mon Sep 17 00:00:00 2001 From: Thanakorn S Date: Mon, 11 May 2026 16:16:26 +0700 Subject: [PATCH] stockmovement: add summary views --- app/assets/css/custom.css | 4 + app/assets/js/custom.js | 6 + app/assets/utils/classes/ReportManager.php | 29 +++-- app/assets/utils/classes/ReturnManager.php | 11 +- app/reports/expired_stock.php | 104 +++++++-------- app/reports/stock_movement.php | 143 +++++++++++++++++++++ 6 files changed, 230 insertions(+), 67 deletions(-) diff --git a/app/assets/css/custom.css b/app/assets/css/custom.css index 8687314..7371c17 100644 --- a/app/assets/css/custom.css +++ b/app/assets/css/custom.css @@ -90,6 +90,10 @@ 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 { width: 40px; height: 40px; diff --git a/app/assets/js/custom.js b/app/assets/js/custom.js index 7b23855..9e66a3f 100644 --- a/app/assets/js/custom.js +++ b/app/assets/js/custom.js @@ -1,3 +1,9 @@ +function escape_html(value) { + return String(value ?? '').replace(/[&<>"']/g, function(c) { + return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; + }); +} + /** ========================= * SIDEBAR ACTIVE STATE * Override: activate the parent listing page for manage_* sub-pages. diff --git a/app/assets/utils/classes/ReportManager.php b/app/assets/utils/classes/ReportManager.php index f7dbc34..e1b417c 100644 --- a/app/assets/utils/classes/ReportManager.php +++ b/app/assets/utils/classes/ReportManager.php @@ -919,25 +919,24 @@ class ReportManager function ($wh) use ($cid) { $table = $this->stockTableNameFromWarehouseId((int)$wh['id']); return "SELECT - s.id, s.product_sku, s.lot_number, s.zone, s.aisle, s.rack, - ROUND(s.`in`, 2) AS quantity, + ROUND(SUM(s.`in`) - SUM(s.`out`), 2) AS quantity, {$wh['id']} AS warehouse_id FROM `{$table}` s WHERE s.company_id = {$cid} - AND s.type = 'in' 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 )); $sql = "SELECT - stock.id, stock.product_sku, stock.lot_number, stock.zone, @@ -982,7 +981,6 @@ class ReportManager else { continue; } $items[] = [ - 'id' => (int) $row['id'], 'product_sku' => $row['product_sku'], 'product_name' => $row['product_name'], 'category_name' => $row['category_name'], @@ -1411,13 +1409,17 @@ class ReportManager $product_uom = $product_row['uom'] ?? ''; // 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. + // Pricing rules per movement type: + // 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( "SELECT s.id, s.date, s.type, + s.source, s.product_sku, s.lot_number, s.serial_number, @@ -1426,14 +1428,19 @@ class ReportManager s.rack, ROUND(COALESCE(s.`in`, 0), 2) AS stock_in, 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, c.contact_name, s.description FROM `{$table}` s LEFT JOIN `{$table}` si - ON si.id = s.ref_id - AND s.`out` > 0 + ON si.id = s.ref_id + AND (s.`out` > 0 OR s.source = 'return') LEFT JOIN md_product p ON p.company_id = s.company_id AND p.sku = s.product_sku diff --git a/app/assets/utils/classes/ReturnManager.php b/app/assets/utils/classes/ReturnManager.php index 6b4db83..5610a23 100644 --- a/app/assets/utils/classes/ReturnManager.php +++ b/app/assets/utils/classes/ReturnManager.php @@ -313,7 +313,7 @@ class ReturnManager { $stock_out_table = $this->stockTableNameFromWarehouseId($stock_out_wh); $stock_out_sth = $this->pdo->prepare( - "SELECT status, lot_number, serial_number + "SELECT status, lot_number, serial_number, price FROM `{$stock_out_table}` WHERE company_id = :company_id AND id = :id @@ -341,19 +341,21 @@ class ReturnManager { $quantity = (float)($item['quantity'] ?? 0); // 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( "INSERT INTO `{$table}` (uuid, company_id, `date`, product_sku, `in`, zone, aisle, rack, contact_id, `description`, `log`, `type`, lot_number, serial_number, - source, source_id, price, status) + source, source_id, ref_id, price, status) VALUES (:uuid, :company_id, :date, :product_sku, :quantity, :zone, :aisle, :rack, :contact_id, :description, :log, 'in', :lot_number, :serial_number, - 'return', :source_id, :price, 1)" + 'return', :source_id, :ref_id, :price, 1)" )->execute([ ':uuid' => $item_uuid, ':company_id' => $this->company_id, @@ -369,7 +371,8 @@ class ReturnManager { ':lot_number' => $stock_out['lot_number'] ?? ($item['lot_number'] ?? ''), ':serial_number' => $stock_out['serial_number'] ?? ($item['serial_number'] ?? ''), ':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(); diff --git a/app/reports/expired_stock.php b/app/reports/expired_stock.php index ad2f7a9..c2fa6ca 100644 --- a/app/reports/expired_stock.php +++ b/app/reports/expired_stock.php @@ -138,7 +138,7 @@ Product SKU Lot Number - Location + Location Expiry Date Days Overdue Quantity @@ -167,7 +167,7 @@ Product SKU Lot Number - Location + Location Expiry Date Days Remaining Quantity @@ -190,44 +190,44 @@