stock aggregate table
This commit is contained in:
@@ -229,7 +229,7 @@ class CompanySettingManager
|
||||
|
||||
private function hasStockTransactions(): bool
|
||||
{
|
||||
if ($this->tableHasCompanyRows('warehouse_balance')) {
|
||||
if ($this->tableHasCompanyRows('etl_stock_summary')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
/**
|
||||
* EtlStockManager
|
||||
*
|
||||
* Manages etl_stock_summary — the materialized aggregate of td_stock_<warehouse_id>
|
||||
* by (company_id, warehouse_id, product_sku, month). Mirrors EtlManager for GL.
|
||||
*
|
||||
* etl_stock_summary is maintained two ways:
|
||||
* 1. Write-through — adjustBalance() in WarehouseManager updates it on every approve/delete.
|
||||
* 2. Batch repair — checkAndRepair() scans td_stock_* for drift and rebuilds stale months.
|
||||
*
|
||||
* Gap detection is timestamp-based (identical to GL):
|
||||
* A month is stale when any td_stock_N row (updated_at) is newer than the
|
||||
* oldest etl_stock_summary entry for that warehouse+month (source_updated_at).
|
||||
*
|
||||
* checkAndRepair() — scan all warehouses, rebuild stale months, save status
|
||||
* getGaps() — return [['warehouse_id'=>N,'period'=>'YYYY-MM'], ...]
|
||||
* rebuildPeriod(wh_id, period) — DELETE + reinsert one (warehouse, month) slice
|
||||
* rebuildFull(wh_id) — DELETE + rebuild all months for one warehouse
|
||||
* getDistinctPeriods(wh_id) — distinct approved months in td_stock_N
|
||||
* getStatus() — read etl_stock_* keys from company_setting
|
||||
*/
|
||||
class EtlStockManager
|
||||
{
|
||||
private PDO $pdo;
|
||||
private PDO $pdo1;
|
||||
private int $companyId;
|
||||
|
||||
public function __construct(PDO $pdo, PDO $pdo1, int $company_id)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
$this->pdo1 = $pdo1;
|
||||
$this->companyId = $company_id;
|
||||
}
|
||||
|
||||
// ── Public ───────────────────────────────────────────────────────────────
|
||||
|
||||
public function checkAndRepair(): array
|
||||
{
|
||||
$gaps = $this->getGaps();
|
||||
|
||||
foreach ($gaps as $gap) {
|
||||
$this->rebuildPeriod($gap['warehouse_id'], $gap['period']);
|
||||
}
|
||||
|
||||
$gaps_found = count($gaps);
|
||||
$periods_rebuilt = $gaps_found;
|
||||
$this->saveStatus('ok', $gaps_found, $periods_rebuilt);
|
||||
|
||||
return ['gaps_found' => $gaps_found, 'periods_rebuilt' => $periods_rebuilt];
|
||||
}
|
||||
|
||||
public function getGaps(): array
|
||||
{
|
||||
$stock_tables = $this->discoverStockTables();
|
||||
$gaps = [];
|
||||
|
||||
foreach ($stock_tables as $table) {
|
||||
$warehouse_id = (int)substr($table, strlen('td_stock_'));
|
||||
|
||||
$sth = $this->pdo->prepare("
|
||||
SELECT DISTINCT DATE_FORMAT(s.date, '%Y-%m') AS period
|
||||
FROM `{$table}` s
|
||||
LEFT JOIN (
|
||||
SELECT month, MAX(source_updated_at) AS etl_ts
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :cid2 AND warehouse_id = :wh2
|
||||
GROUP BY month
|
||||
) etl ON etl.month = DATE_FORMAT(s.date, '%Y-%m')
|
||||
WHERE s.company_id = :cid
|
||||
AND s.status = 1
|
||||
AND (etl.month IS NULL OR s.updated_at > etl.etl_ts)
|
||||
ORDER BY period
|
||||
");
|
||||
$sth->execute([
|
||||
':cid' => $this->companyId,
|
||||
':cid2' => $this->companyId,
|
||||
':wh2' => $warehouse_id,
|
||||
]);
|
||||
|
||||
foreach ($sth->fetchAll(PDO::FETCH_COLUMN) as $period) {
|
||||
$gaps[] = ['warehouse_id' => $warehouse_id, 'period' => $period];
|
||||
}
|
||||
}
|
||||
|
||||
return $gaps;
|
||||
}
|
||||
|
||||
public function rebuildPeriod(int $warehouse_id, string $period): void
|
||||
{
|
||||
$table = 'td_stock_' . $warehouse_id;
|
||||
|
||||
$this->pdo->prepare(
|
||||
"DELETE FROM etl_stock_summary
|
||||
WHERE company_id = :cid AND warehouse_id = :wh AND month = :month"
|
||||
)->execute([':cid' => $this->companyId, ':wh' => $warehouse_id, ':month' => $period]);
|
||||
|
||||
$this->pdo->prepare("
|
||||
INSERT INTO etl_stock_summary
|
||||
(company_id, warehouse_id, product_sku, month, total_in, total_out, source_updated_at)
|
||||
SELECT
|
||||
company_id,
|
||||
:wh,
|
||||
product_sku,
|
||||
DATE_FORMAT(`date`, '%Y-%m') AS month,
|
||||
ROUND(SUM(`in`), 4) AS total_in,
|
||||
ROUND(SUM(`out`), 4) AS total_out,
|
||||
MAX(updated_at) AS source_updated_at
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :cid
|
||||
AND status = 1
|
||||
AND DATE_FORMAT(`date`, '%Y-%m') = :month
|
||||
GROUP BY company_id, product_sku, DATE_FORMAT(`date`, '%Y-%m')
|
||||
")->execute([':cid' => $this->companyId, ':wh' => $warehouse_id, ':month' => $period]);
|
||||
}
|
||||
|
||||
public function rebuildFull(int $warehouse_id): array
|
||||
{
|
||||
$table = 'td_stock_' . $warehouse_id;
|
||||
|
||||
$this->pdo->prepare(
|
||||
"DELETE FROM etl_stock_summary WHERE company_id = :cid AND warehouse_id = :wh"
|
||||
)->execute([':cid' => $this->companyId, ':wh' => $warehouse_id]);
|
||||
|
||||
$this->pdo->prepare("
|
||||
INSERT INTO etl_stock_summary
|
||||
(company_id, warehouse_id, product_sku, month, total_in, total_out, source_updated_at)
|
||||
SELECT
|
||||
company_id,
|
||||
:wh,
|
||||
product_sku,
|
||||
DATE_FORMAT(`date`, '%Y-%m') AS month,
|
||||
ROUND(SUM(`in`), 4) AS total_in,
|
||||
ROUND(SUM(`out`), 4) AS total_out,
|
||||
MAX(updated_at) AS source_updated_at
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :cid AND status = 1
|
||||
GROUP BY company_id, product_sku, DATE_FORMAT(`date`, '%Y-%m')
|
||||
")->execute([':cid' => $this->companyId, ':wh' => $warehouse_id]);
|
||||
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT COUNT(DISTINCT month) FROM etl_stock_summary
|
||||
WHERE company_id = :cid AND warehouse_id = :wh"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId, ':wh' => $warehouse_id]);
|
||||
$periods_rebuilt = (int)$sth->fetchColumn();
|
||||
|
||||
$this->saveStatus('ok', 0, $periods_rebuilt);
|
||||
|
||||
return ['periods_rebuilt' => $periods_rebuilt];
|
||||
}
|
||||
|
||||
public function getDistinctPeriods(int $warehouse_id): array
|
||||
{
|
||||
$table = 'td_stock_' . $warehouse_id;
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT DATE_FORMAT(`date`, '%Y-%m') AS period
|
||||
FROM `{$table}`
|
||||
WHERE company_id = :cid AND status = 1
|
||||
ORDER BY period"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId]);
|
||||
return $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
public function getStatus(): array
|
||||
{
|
||||
$keys = [
|
||||
'etl_stock_last_ran',
|
||||
'etl_stock_last_result',
|
||||
'etl_stock_gaps_found',
|
||||
'etl_stock_periods_rebuilt',
|
||||
];
|
||||
$sth = $this->pdo1->prepare(
|
||||
"SELECT setting_key, value FROM company_setting
|
||||
WHERE company_id = :cid AND setting_key IN ('" . implode("','", $keys) . "')"
|
||||
);
|
||||
$sth->execute([':cid' => $this->companyId]);
|
||||
$rows = $sth->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
$slot_hour = $this->companyId % 24;
|
||||
$last_ran = $rows['etl_stock_last_ran'] ?? null;
|
||||
$next_run = $last_ran
|
||||
? date('Y-m-d', strtotime($last_ran . ' +1 day')) . sprintf(' %02d:30', $slot_hour)
|
||||
: date('Y-m-d') . sprintf(' %02d:30', $slot_hour);
|
||||
|
||||
return [
|
||||
'slot_hour' => $slot_hour,
|
||||
'last_ran' => $last_ran,
|
||||
'last_result' => $rows['etl_stock_last_result'] ?? null,
|
||||
'gaps_found' => (int)($rows['etl_stock_gaps_found'] ?? 0),
|
||||
'periods_rebuilt' => (int)($rows['etl_stock_periods_rebuilt'] ?? 0),
|
||||
'next_run' => $next_run,
|
||||
];
|
||||
}
|
||||
|
||||
// ── Private ──────────────────────────────────────────────────────────────
|
||||
|
||||
private function discoverStockTables(): array
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name LIKE 'td_stock_%'"
|
||||
);
|
||||
$sth->execute();
|
||||
return $sth->fetchAll(PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
private function saveStatus(string $result, int $gaps_found, int $periods_rebuilt): void
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$data = [
|
||||
'etl_stock_last_ran' => $now,
|
||||
'etl_stock_last_result' => $result,
|
||||
'etl_stock_gaps_found' => (string)$gaps_found,
|
||||
'etl_stock_periods_rebuilt' => (string)$periods_rebuilt,
|
||||
];
|
||||
$sth = $this->pdo1->prepare(
|
||||
"INSERT INTO company_setting (company_id, setting_key, value, updated_at)
|
||||
VALUES (:cid, :key, :val, :ts)
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value), updated_at = VALUES(updated_at)"
|
||||
);
|
||||
foreach ($data as $key => $val) {
|
||||
$sth->execute([':cid' => $this->companyId, ':key' => $key, ':val' => $val, ':ts' => $now]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,7 @@ class ProductManager {
|
||||
* Return all products with their current aggregate warehouse balance.
|
||||
*
|
||||
* The balance is the sum of (total_in - total_out) across all warehouses
|
||||
* from the warehouse_balance table. Products with no balance rows show 0.
|
||||
* from the etl_stock_summary table. Products with no balance rows show 0.
|
||||
*
|
||||
* Used to populate the product listing page.
|
||||
*
|
||||
@@ -285,7 +285,7 @@ class ProductManager {
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT a.*, IFNULL(SUM(b.total_in - b.total_out), 0) AS product_balance
|
||||
FROM md_product a
|
||||
LEFT JOIN warehouse_balance b
|
||||
LEFT JOIN etl_stock_summary b
|
||||
ON a.company_id = b.company_id
|
||||
AND a.sku = b.product_sku
|
||||
WHERE a.company_id = :company_id
|
||||
|
||||
@@ -197,13 +197,13 @@ class ReportManager
|
||||
|
||||
/**
|
||||
* Number of distinct SKUs with a positive running balance across all warehouses.
|
||||
* "In stock" means (total_in - total_out) > 0 in warehouse_balance.
|
||||
* "In stock" means (total_in - total_out) > 0 in etl_stock_summary.
|
||||
* Used by the reports_stats dashboard tile.
|
||||
*/
|
||||
public function getTotalProductInStock(): int
|
||||
{
|
||||
$sql = "SELECT COUNT(DISTINCT product_sku)
|
||||
FROM warehouse_balance
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :company_id
|
||||
AND (total_in - total_out) > 0";
|
||||
return (int) $this->fetchScalar($sql);
|
||||
@@ -265,7 +265,7 @@ class ReportManager
|
||||
|
||||
/**
|
||||
* Return aggregated total_in, total_out, and min_stock for every SKU
|
||||
* across all warehouses from warehouse_balance.
|
||||
* across all warehouses from etl_stock_summary.
|
||||
*
|
||||
* Used internally by getLowStockCount and as a general balance query.
|
||||
*
|
||||
@@ -279,7 +279,7 @@ class ReportManager
|
||||
SUM(a.total_out) AS total_out,
|
||||
b.min_stock,
|
||||
b.cost_price
|
||||
FROM warehouse_balance a
|
||||
FROM etl_stock_summary a
|
||||
LEFT JOIN md_product b
|
||||
ON a.company_id = b.company_id
|
||||
AND a.product_sku = b.sku
|
||||
@@ -314,7 +314,7 @@ class ReportManager
|
||||
"SELECT
|
||||
ROUND(COALESCE(SUM(total_in), 0), 2) AS total_in,
|
||||
ROUND(COALESCE(SUM(total_out), 0), 2) AS total_out
|
||||
FROM warehouse_balance
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
@@ -412,7 +412,7 @@ class ReportManager
|
||||
mp.product_image,
|
||||
mp.cost_price,
|
||||
ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance
|
||||
FROM warehouse_balance wb
|
||||
FROM etl_stock_summary wb
|
||||
INNER JOIN md_product mp
|
||||
ON wb.company_id = mp.company_id
|
||||
AND wb.product_sku = mp.sku
|
||||
@@ -493,7 +493,7 @@ class ReportManager
|
||||
ROUND(COALESCE(SUM(total_in), 0), 2) AS total_in,
|
||||
ROUND(COALESCE(SUM(total_out), 0), 2) AS total_out,
|
||||
COUNT(DISTINCT product_sku) AS active_products
|
||||
FROM warehouse_balance
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :company_id
|
||||
AND month = :month"
|
||||
);
|
||||
@@ -517,7 +517,7 @@ class ReportManager
|
||||
"SELECT wb.product_sku,
|
||||
ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance,
|
||||
mp.min_stock
|
||||
FROM warehouse_balance wb
|
||||
FROM etl_stock_summary wb
|
||||
INNER JOIN md_product mp
|
||||
ON mp.company_id = wb.company_id
|
||||
AND mp.sku = wb.product_sku
|
||||
@@ -537,7 +537,7 @@ class ReportManager
|
||||
* Return monthly stock_in and stock_out totals for a rolling N-month window.
|
||||
*
|
||||
* Produces chart-ready arrays with labels (e.g. "Apr 2025"), stock_in, and stock_out.
|
||||
* Months with no data return 0. Data is sourced from warehouse_balance (all warehouses).
|
||||
* Months with no data return 0. Data is sourced from etl_stock_summary (all warehouses).
|
||||
*
|
||||
* @param int $months Number of months to include (default 12).
|
||||
* @return array Keys: labels (array), stock_in (array), stock_out (array).
|
||||
@@ -553,7 +553,7 @@ class ReportManager
|
||||
month,
|
||||
ROUND(SUM(total_in), 2) AS stock_in,
|
||||
ROUND(SUM(total_out), 2) AS stock_out
|
||||
FROM warehouse_balance
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :company_id
|
||||
AND month BETWEEN :start AND :end
|
||||
GROUP BY month
|
||||
@@ -602,7 +602,7 @@ class ReportManager
|
||||
ROUND(SUM(wb.total_out), 2) AS total_out,
|
||||
p.product_name,
|
||||
pc.category AS category_name
|
||||
FROM warehouse_balance wb
|
||||
FROM etl_stock_summary wb
|
||||
INNER JOIN md_product p
|
||||
ON p.company_id = wb.company_id
|
||||
AND p.sku = wb.product_sku
|
||||
@@ -623,7 +623,7 @@ class ReportManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all months for which warehouse_balance data exists, newest first.
|
||||
* Return all months for which etl_stock_summary data exists, newest first.
|
||||
*
|
||||
* Used to populate the month selector on the dashboard and reports pages.
|
||||
*
|
||||
@@ -633,7 +633,7 @@ class ReportManager
|
||||
{
|
||||
$sth = $this->pdo->prepare(
|
||||
"SELECT DISTINCT month
|
||||
FROM warehouse_balance
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :company_id
|
||||
ORDER BY month DESC"
|
||||
);
|
||||
@@ -743,7 +743,7 @@ class ReportManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Total in and out balance for a specific warehouse from warehouse_balance.
|
||||
* Total in and out balance for a specific warehouse from etl_stock_summary.
|
||||
*
|
||||
* @param int $warehouse_id The warehouse to query.
|
||||
* @return array Keys: total_in, total_out.
|
||||
@@ -754,7 +754,7 @@ class ReportManager
|
||||
"SELECT
|
||||
ROUND(COALESCE(SUM(total_in), 0), 2) AS total_in,
|
||||
ROUND(COALESCE(SUM(total_out), 0), 2) AS total_out
|
||||
FROM warehouse_balance
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :company_id AND warehouse_id = :warehouse_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId, ':warehouse_id' => $warehouse_id]);
|
||||
@@ -764,7 +764,7 @@ class ReportManager
|
||||
/**
|
||||
* Count SKUs below min_stock threshold for a specific warehouse.
|
||||
*
|
||||
* Scoped to warehouse_balance rows for this warehouse only.
|
||||
* Scoped to etl_stock_summary rows for this warehouse only.
|
||||
*
|
||||
* @param int $warehouse_id The warehouse to query.
|
||||
* @return int Number of SKUs below min_stock.
|
||||
@@ -775,7 +775,7 @@ class ReportManager
|
||||
"SELECT wb.product_sku,
|
||||
ROUND(SUM(wb.total_in) - SUM(wb.total_out), 2) AS balance,
|
||||
mp.min_stock
|
||||
FROM warehouse_balance wb
|
||||
FROM etl_stock_summary wb
|
||||
INNER JOIN md_product mp
|
||||
ON mp.company_id = wb.company_id
|
||||
AND mp.sku = wb.product_sku
|
||||
@@ -795,7 +795,7 @@ class ReportManager
|
||||
/**
|
||||
* Return monthly stock_in and stock_out totals for a warehouse over the last 12 months.
|
||||
*
|
||||
* Queries the per-warehouse td_stock_<warehouse_id> table directly (not warehouse_balance)
|
||||
* Queries the per-warehouse td_stock_<warehouse_id> table directly (not etl_stock_summary)
|
||||
* for per-warehouse granularity. Produces chart-ready arrays with month labels.
|
||||
*
|
||||
* @param int $warehouse_id The warehouse to query.
|
||||
@@ -1173,7 +1173,7 @@ class ReportManager
|
||||
$wh_list = $warehouses->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Build per-lot balance by summing approved td_stock rows across all warehouses.
|
||||
// warehouse_balance is per-product, not per-lot, so we query td_stock directly.
|
||||
// etl_stock_summary is per-product, not per-lot, so we query td_stock directly.
|
||||
$lot_balance = [];
|
||||
$cid = (int) $this->companyId;
|
||||
|
||||
@@ -1350,7 +1350,7 @@ class ReportManager
|
||||
SUM(total_in) AS total_in,
|
||||
SUM(total_out) AS total_out,
|
||||
COUNT(DISTINCT warehouse_id) AS total_warehouse
|
||||
FROM warehouse_balance
|
||||
FROM etl_stock_summary
|
||||
WHERE company_id = :company_id"
|
||||
);
|
||||
$sth->execute([':company_id' => $this->companyId]);
|
||||
@@ -1364,7 +1364,7 @@ class ReportManager
|
||||
* 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
|
||||
* Brought-forward balance is sourced from etl_stock_summary for all months
|
||||
* strictly before from_month, giving the cumulative net before the range.
|
||||
*
|
||||
* Used by reports/stock_movement.php.
|
||||
|
||||
@@ -401,7 +401,7 @@ class ReturnManager {
|
||||
* 1. INSERT td_stock_<warehouse_id> in row with type='in', status=1 (force confirmed),
|
||||
* source='return', source_id=return_id.
|
||||
* 2. occupyRack() — place returned stock into the user-selected empty rack.
|
||||
* 3. adjustBalance() — update warehouse_balance.
|
||||
* 3. adjustBalance() — update etl_stock_summary.
|
||||
*
|
||||
* After all items:
|
||||
* 4. UPDATE td_return.status = 1 (confirmed).
|
||||
|
||||
@@ -217,7 +217,7 @@ class StockManager {
|
||||
* 1. Upserts md_lot if lot_number + expiry_date are provided.
|
||||
* 2. Inserts the td_stock_<warehouse_id> row.
|
||||
* 3. Calls WarehouseManager::occupyRack() to reserve the rack.
|
||||
* warehouse_balance is updated later by approveStock().
|
||||
* etl_stock_summary is updated later by approveStock().
|
||||
*
|
||||
* Update flow (id > 0):
|
||||
* - Updates contact_id, description, and log only.
|
||||
@@ -338,7 +338,7 @@ class StockManager {
|
||||
* 1. Validates the rack is occupied with the correct SKU / lot / serial.
|
||||
* 2. Inserts the td_stock_<warehouse_id> row, copying quantity and lot info from the rack.
|
||||
* 3. Calls WarehouseManager::releaseRack() to free the rack slot.
|
||||
* 4. Calls WarehouseManager::adjustBalance() to update warehouse_balance.
|
||||
* 4. Calls WarehouseManager::adjustBalance() to update etl_stock_summary.
|
||||
*
|
||||
* Update flow (id > 0):
|
||||
* - Updates contact_id, description, and log only.
|
||||
@@ -675,7 +675,7 @@ class StockManager {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Approve a draft td_stock row (status 0 → 1) and update warehouse_balance.
|
||||
* Approve a draft td_stock row (status 0 → 1) and update etl_stock_summary.
|
||||
*
|
||||
* This is the single entry point for balance updates — both auto-approve
|
||||
* (called immediately after save) and manual approve go through here.
|
||||
@@ -729,7 +729,7 @@ class StockManager {
|
||||
$id
|
||||
);
|
||||
$whMgmt->adjustBalance('in', $warehouse_id, $row['product_sku'], 0, (float)$row['in'],
|
||||
$id, $row['source'] ?? '', (int)($row['source_id'] ?? 0));
|
||||
$id, $row['source'] ?? '', (int)($row['source_id'] ?? 0), $row['date'] ?? '');
|
||||
|
||||
} elseif ($type === 'out') {
|
||||
|
||||
@@ -759,7 +759,7 @@ class StockManager {
|
||||
$whMgmt->releaseRack($warehouse_id, $row['zone'], $row['aisle'], $row['rack']);
|
||||
}
|
||||
$whMgmt->adjustBalance('out', $warehouse_id, $row['product_sku'], 0, (float)$row['out'],
|
||||
$id, $row['source'] ?? '', (int)($row['source_id'] ?? 0));
|
||||
$id, $row['source'] ?? '', (int)($row['source_id'] ?? 0), $row['date'] ?? '');
|
||||
|
||||
} elseif ($type === 'transfer') {
|
||||
|
||||
@@ -843,7 +843,7 @@ class StockManager {
|
||||
$from_row['zone'], $from_row['aisle'], $from_row['rack']
|
||||
);
|
||||
$whMgmt->adjustBalance('out', $from_wh_id, $from_row['product_sku'], 0, (float)$from_row['out'],
|
||||
(int)$from_row['id'], $from_row['source'] ?? '', (int)($from_row['source_id'] ?? 0));
|
||||
(int)$from_row['id'], $from_row['source'] ?? '', (int)($from_row['source_id'] ?? 0), $from_row['date'] ?? '');
|
||||
}
|
||||
if ($inbound_row && $paired_wh_id) {
|
||||
$whMgmt->occupyRack(
|
||||
@@ -853,7 +853,7 @@ class StockManager {
|
||||
$inbound_row['id']
|
||||
);
|
||||
$whMgmt->adjustBalance('in', $paired_wh_id, $inbound_row['product_sku'], 0, (float)$inbound_row['in'],
|
||||
(int)$inbound_row['id'], $inbound_row['source'] ?? '', (int)($inbound_row['source_id'] ?? 0));
|
||||
(int)$inbound_row['id'], $inbound_row['source'] ?? '', (int)($inbound_row['source_id'] ?? 0), $inbound_row['date'] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1831,7 +1831,7 @@ class WarehouseManager {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Adjust the running balance for a (warehouse, product_sku) pair in warehouse_balance.
|
||||
* Adjust the running balance for a (warehouse, product_sku) pair in etl_stock_summary.
|
||||
*
|
||||
* Uses an INSERT ... ON DUPLICATE KEY UPDATE to atomically upsert the balance row,
|
||||
* partitioned by month (YYYY-MM) for efficient monthly reporting queries.
|
||||
@@ -1851,19 +1851,21 @@ class WarehouseManager {
|
||||
*/
|
||||
public function adjustBalance(
|
||||
$type, $warehouse_id, $product_sku, $old_qty, $new_qty,
|
||||
int $stock_id = 0, string $source = '', int $source_id = 0
|
||||
int $stock_id = 0, string $source = '', int $source_id = 0,
|
||||
string $stock_date = ''
|
||||
): void {
|
||||
$column = $type === 'in' ? 'total_in' : 'total_out';
|
||||
$delta = $new_qty - $old_qty;
|
||||
$month = date('Y-m');
|
||||
$month = $stock_date ? date('Y-m', strtotime($stock_date)) : date('Y-m');
|
||||
|
||||
$this->pdo->prepare(
|
||||
"INSERT INTO warehouse_balance
|
||||
(company_id, warehouse_id, product_sku, month, `{$column}`)
|
||||
"INSERT INTO etl_stock_summary
|
||||
(company_id, warehouse_id, product_sku, month, `{$column}`, source_updated_at)
|
||||
VALUES
|
||||
(:company_id, :warehouse_id, :product_sku, :month, :delta)
|
||||
(:company_id, :warehouse_id, :product_sku, :month, :delta, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`{$column}` = `{$column}` + VALUES(`{$column}`)"
|
||||
`{$column}` = `{$column}` + VALUES(`{$column}`),
|
||||
source_updated_at = NOW()"
|
||||
)->execute([
|
||||
':company_id' => $this->company_id,
|
||||
':warehouse_id' => $warehouse_id,
|
||||
@@ -2173,7 +2175,7 @@ class WarehouseManager {
|
||||
);
|
||||
|
||||
if ((int)$row['status'] === 1) {
|
||||
$this->adjustBalance('in', $warehouse_id, $product_sku, (int)$row['in'], 0);
|
||||
$this->adjustBalance('in', $warehouse_id, $product_sku, (int)$row['in'], 0, 0, '', 0, $row['date'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2235,7 +2237,7 @@ class WarehouseManager {
|
||||
[(int)$row['ref_id']],
|
||||
$row['date']
|
||||
);
|
||||
$this->adjustBalance('out', $warehouse_id, $product_sku, (int)$row['out'], 0);
|
||||
$this->adjustBalance('out', $warehouse_id, $product_sku, (int)$row['out'], 0, 0, '', 0, $row['date'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2334,8 +2336,8 @@ class WarehouseManager {
|
||||
|
||||
// Reverse balances on both sides
|
||||
$quantity = (int)$from_row['out'];
|
||||
$this->adjustBalance('out', $from_warehouse_id, $product_sku, $quantity, 0);
|
||||
$this->adjustBalance('in', $to_warehouse, $product_sku, $quantity, 0);
|
||||
$this->adjustBalance('out', $from_warehouse_id, $product_sku, $quantity, 0, 0, '', 0, $from_row['date'] ?? '');
|
||||
$this->adjustBalance('in', $to_warehouse, $product_sku, $quantity, 0, 0, '', 0, $from_row['date'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user