Reports
This commit is contained in:
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user