This commit is contained in:
Thanakorn S
2026-04-29 17:04:11 +07:00
parent f6dc9a3278
commit db5c47b6ca
15 changed files with 679 additions and 43 deletions
+179
View File
@@ -1250,4 +1250,183 @@ class ReportManager
$sth->execute([':company_id' => $this->companyId]);
return $sth->fetch(PDO::FETCH_ASSOC) ?: [];
}
/**
* Return paginated raw stock movement transactions from td_stock_<warehouse>
* within a date range (by month), with a balance brought forward.
*
* Each row is one individual transaction — no grouping. Full datetime is
* returned as stored in the td_stock_* table.
*
* Brought-forward balance is sourced from warehouse_balance for all months
* strictly before from_month, giving the cumulative net before the range.
*
* Used by reports/stock_movement.php.
*
* @param int $warehouse_id Warehouse to scope the report to.
* @param string $from_month Start of range in YYYY-MM format (inclusive).
* @param string $to_month End of range in YYYY-MM format (inclusive).
* @param int $page Current page number (1-based).
* @param int $per_page Rows per page (default 20).
* @return array Keys: rows (array), total_rows (int), brought_forward (float).
*/
/**
* Return summary stats + ordered SKU list for the stock movement report.
*
* Called once when the user clicks Search. Returns:
* - stats tile values (total_transactions, total_in, total_out, net)
* - ordered list of distinct product_skus present in the date range,
* used by the client to drive lazy-load requests one SKU at a time.
*
* @param int $warehouse_id
* @param string $from_month YYYY-MM inclusive start
* @param string $to_month YYYY-MM inclusive end
* @return array Keys: skus (string[]), total_transactions, total_in, total_out, net
*/
public function getStockMovementSummary(
int $warehouse_id,
string $from_month,
string $to_month
): array {
$table = $this->resolveWarehouseTable($warehouse_id);
if (!$table) return ['skus' => [], 'total_transactions' => 0, 'total_in' => 0, 'total_out' => 0, 'net' => 0];
$date_from = $from_month . '-01 00:00:00';
$date_to = date('Y-m-t 23:59:59', strtotime($to_month . '-01'));
// Ordered distinct SKUs in the range — drives lazy-load sequence
$sth = $this->pdo->prepare(
"SELECT DISTINCT product_sku
FROM `{$table}`
WHERE company_id = :company_id
AND date BETWEEN :date_from AND :date_to
ORDER BY product_sku ASC"
);
$sth->execute([':company_id' => $this->companyId, ':date_from' => $date_from, ':date_to' => $date_to]);
$skus = $sth->fetchAll(PDO::FETCH_COLUMN);
// Summary stats across full range
$sth = $this->pdo->prepare(
"SELECT
COUNT(*) AS total_transactions,
ROUND(SUM(COALESCE(`in`, 0)), 2) AS total_in,
ROUND(SUM(COALESCE(`out`, 0)), 2) AS total_out
FROM `{$table}`
WHERE company_id = :company_id
AND date BETWEEN :date_from AND :date_to"
);
$sth->execute([':company_id' => $this->companyId, ':date_from' => $date_from, ':date_to' => $date_to]);
$stats = $sth->fetch(PDO::FETCH_ASSOC);
return [
'skus' => $skus,
'total_transactions' => (int) ($stats['total_transactions'] ?? 0),
'total_in' => (float) ($stats['total_in'] ?? 0),
'total_out' => (float) ($stats['total_out'] ?? 0),
'net' => round(($stats['total_in'] ?? 0) - ($stats['total_out'] ?? 0), 2),
];
}
/**
* Return all transactions for a single product_sku within the date range,
* with a brought-forward balance and a running total column.
*
* Called once per SKU by the lazy-load scroll handler.
*
* Brought-forward: sum of (in - out) for this SKU in this warehouse
* for all months strictly before from_month.
*
* Running total: cumulative (brought_forward + in - out) across rows,
* computed in PHP after the DB fetch so no window function is needed.
*
* @param int $warehouse_id
* @param string $product_sku
* @param string $from_month YYYY-MM inclusive start
* @param string $to_month YYYY-MM inclusive end
* @return array Keys: sku, product_name, brought_forward (array with in/out/balance),
* rows (each row has running_total appended)
*/
public function getStockMovementBySku(
int $warehouse_id,
string $product_sku,
string $from_month,
string $to_month
): array {
$table = $this->resolveWarehouseTable($warehouse_id);
if (!$table) return ['sku' => $product_sku, 'rows' => [], 'brought_forward' => []];
$date_from = $from_month . '-01 00:00:00';
$date_to = date('Y-m-t 23:59:59', strtotime($to_month . '-01'));
// Brought-forward: all transactions for this SKU before the range
$sth = $this->pdo->prepare(
"SELECT
ROUND(COALESCE(SUM(COALESCE(`in`, 0)), 0), 2) AS bf_in,
ROUND(COALESCE(SUM(COALESCE(`out`, 0)), 0), 2) AS bf_out
FROM `{$table}`
WHERE company_id = :company_id
AND product_sku = :sku
AND date < :date_from"
);
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku, ':date_from' => $date_from]);
$bf = $sth->fetch(PDO::FETCH_ASSOC);
$bf_in = (float)($bf['bf_in'] ?? 0);
$bf_out = (float)($bf['bf_out'] ?? 0);
$bf_bal = round($bf_in - $bf_out, 2);
// Product name
$sth = $this->pdo->prepare(
"SELECT product_name FROM md_product
WHERE company_id = :company_id AND sku = :sku LIMIT 1"
);
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku]);
$product_name = $sth->fetchColumn() ?: $product_sku;
// All transactions for this SKU in range, sorted by date then id
$sth = $this->pdo->prepare(
"SELECT
s.id,
s.date,
s.type,
s.product_sku,
s.lot_number,
s.serial_number,
s.zone,
s.aisle,
s.rack,
ROUND(COALESCE(s.`in`, 0), 2) AS stock_in,
ROUND(COALESCE(s.`out`, 0), 2) AS stock_out,
c.contact_name,
s.description
FROM `{$table}` s
LEFT JOIN md_contact c
ON c.company_id = s.company_id
AND c.id = s.contact_id
WHERE s.company_id = :company_id
AND s.product_sku = :sku
AND s.date BETWEEN :date_from AND :date_to
ORDER BY s.date ASC, s.id ASC"
);
$sth->execute([':company_id' => $this->companyId, ':sku' => $product_sku, ':date_from' => $date_from, ':date_to' => $date_to]);
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);
// Append running_total to each row (carried forward from brought-forward balance)
$running = $bf_bal;
foreach ($rows as &$row) {
$running += (float)$row['stock_in'] - (float)$row['stock_out'];
$row['running_total'] = round($running, 2);
}
unset($row);
return [
'sku' => $product_sku,
'product_name' => $product_name,
'brought_forward' => [
'in' => $bf_in,
'out' => $bf_out,
'balance' => $bf_bal,
],
'rows' => $rows,
];
}
}
+22 -18
View File
@@ -254,14 +254,15 @@ class StockManager {
*/
public function saveStockIn(array $data, array $logging, string $uuid): void
{
$id = (int)$data['id'];
$warehouse_id = (int)$data['warehouse'];
$id = (int)($data['id'] ?? 0);
$warehouse_id = (int)($data['warehouse'] ?? 0);
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
$row = $ctx['row'] ?? [];
$table = $ctx['table'];
$table_log = json_decode($row['log'] ?? '[]', true) ?: [];
$raw_log = $row['log'] ?? [];
$table_log = is_array($raw_log) ? $raw_log : (json_decode($raw_log, true) ?: []);
$table_log[] = $logging;
if ($id > 0) {
@@ -276,8 +277,8 @@ class StockManager {
)->execute([
':id' => $id,
':company_id' => $this->company_id,
':contact_id' => (int)$data['contact_id'],
':description' => $data['description'],
':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '',
':log' => json_encode($table_log),
]);
@@ -316,11 +317,11 @@ class StockManager {
':zone' => $data['zone'],
':aisle' => $data['aisle'],
':rack' => $data['rack'],
':contact_id' => (int)$data['contact_id'],
':description' => $data['description'],
':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '',
':log' => json_encode($table_log),
':lot_number' => $lot_number,
':serial_number' => $data['serial_number'] ?: null,
':serial_number' => ($data['serial_number'] ?? null) ?: null,
]);
$td_stock_id = (int)$this->pdo->lastInsertId();
@@ -366,14 +367,15 @@ class StockManager {
*/
public function saveStockOut(array $data, array $logging, string $uuid): void
{
$id = (int)$data['id'];
$warehouse_id = (int)$data['warehouse'];
$id = (int)($data['id'] ?? 0);
$warehouse_id = (int)($data['warehouse'] ?? 0);
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
$ctx = $whMgmt->getStockContext($warehouse_id, $id);
$row = $ctx['row'] ?? [];
$table = $ctx['table'];
$table_log = json_decode($row['log'] ?? '[]', true) ?: [];
$raw_log = $row['log'] ?? [];
$table_log = is_array($raw_log) ? $raw_log : (json_decode($raw_log, true) ?: []);
$table_log[] = $logging;
if ($id > 0) {
@@ -388,8 +390,8 @@ class StockManager {
)->execute([
':id' => $id,
':company_id' => $this->company_id,
':contact_id' => (int)$data['contact_id'],
':description' => $data['description'],
':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '',
':log' => json_encode($table_log),
]);
@@ -447,8 +449,8 @@ class StockManager {
':zone' => $data['zone'],
':aisle' => $data['aisle'],
':rack' => $data['rack'],
':contact_id' => (int)$data['contact_id'],
':description' => $data['description'],
':contact_id' => (int)($data['contact_id'] ?? 0),
':description' => $data['description'] ?? '',
':log' => json_encode($table_log),
':ref_id' => $ref_id,
':lot_number' => $lot_number,
@@ -504,7 +506,7 @@ class StockManager {
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
$contact_id = (int)($data['contact_id'] ?? 0);
$description = ($data['description'] ?? '');
$description = ($data['description'] ?? '' ?? '');
if ($id > 0) {
@@ -527,9 +529,11 @@ class StockManager {
throw new Exception("Paired destination record missing — data integrity issue.");
}
$from_log = json_decode($from_row['log'] ?? '[]', true) ?: [];
$raw_from = $from_row['log'] ?? [];
$from_log = is_array($raw_from) ? $raw_from : (json_decode($raw_from, true) ?: []);
$from_log[] = $logging;
$to_log = json_decode($to_row['log'] ?? '[]', true) ?: [];
$raw_to = $to_row['log'] ?? [];
$to_log = is_array($raw_to) ? $raw_to : (json_decode($raw_to, true) ?: []);
$to_log[] = $logging;
$this->pdo->prepare(