Split inventory forecast by current and next lot

This commit is contained in:
gitea_admin committed 2026-09-08 18:48:53 +02:00
1 parent 77f6fee5ac
commit 959586ecc9
5 files changed
+164 -59

No files matched your search

+46 -12
View File
@@ -35,15 +35,25 @@ function lager_forecast_load_current_lots(PDO $pdo): array
{
$stmt = $pdo->query(
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
COALESCE(v.qty_net, 0) + COALESCE((
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0))
FROM stock_lot open_lot
LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id
WHERE open_lot.product_id = p.id AND open_lot.status = 'open'
), 0) AS physical_qty
COALESCE(v.qty_net, 0) AS physical_qty,
next_sl.id AS next_lot_id,
COALESCE(next_v.qty_net, 0) AS next_physical_qty
FROM product p
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current'
LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
LEFT JOIN stock_lot next_sl
ON next_sl.product_id = p.id
AND next_sl.status = 'open'
AND next_sl.next_active = TRUE
AND NULLIF(TRIM(next_sl.lot_number), '') IS NOT NULL
AND next_sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(next_sl.manufacturer_lot_number), '') IS NOT NULL
AND next_sl.mfg_date IS NOT NULL
AND next_sl.expiry_date IS NOT NULL
AND next_sl.purchase_date IS NOT NULL
LEFT JOIN v_stock_lot_balance next_v
ON next_v.stock_lot_id = next_sl.id
AND next_v.qty_net > 0
WHERE p.status = 'active'
ORDER BY p.id"
);
@@ -54,15 +64,25 @@ function lager_forecast_load_current_lot(PDO $pdo, int $productId): ?array
{
$stmt = $pdo->prepare(
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
COALESCE(v.qty_net, 0) + COALESCE((
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0))
FROM stock_lot open_lot
LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id
WHERE open_lot.product_id = p.id AND open_lot.status = 'open'
), 0) AS physical_qty
COALESCE(v.qty_net, 0) AS physical_qty,
next_sl.id AS next_lot_id,
COALESCE(next_v.qty_net, 0) AS next_physical_qty
FROM product p
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current'
LEFT JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
LEFT JOIN stock_lot next_sl
ON next_sl.product_id = p.id
AND next_sl.status = 'open'
AND next_sl.next_active = TRUE
AND NULLIF(TRIM(next_sl.lot_number), '') IS NOT NULL
AND next_sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(next_sl.manufacturer_lot_number), '') IS NOT NULL
AND next_sl.mfg_date IS NOT NULL
AND next_sl.expiry_date IS NOT NULL
AND next_sl.purchase_date IS NOT NULL
LEFT JOIN v_stock_lot_balance next_v
ON next_v.stock_lot_id = next_sl.id
AND next_v.qty_net > 0
WHERE p.status = 'active' AND p.id = :product_id
LIMIT 1"
);
@@ -235,6 +255,13 @@ function refresh_lager_stock_lot_forecast_for_product(PDO $pdo, int $productId):
$dailySales[$productId] ?? []
);
lager_forecast_persist($pdo, $lot, $forecast, $asOfDate);
if ($lot['next_lot_id'] !== null) {
lager_forecast_persist($pdo, [
'lot_id' => $lot['next_lot_id'],
'product_id' => $productId,
'physical_qty' => $lot['next_physical_qty'],
], lager_forecast_calculate($asOfDate, (float) $lot['next_physical_qty'], $dailySales[$productId] ?? []), $asOfDate);
}
}
function run_lager_stock_lot_forecast(PDO $pdo): array
@@ -260,6 +287,13 @@ function run_lager_stock_lot_forecast(PDO $pdo): array
try {
$forecast = lager_forecast_calculate($asOfDate, (float) $lot['physical_qty'], $dailySales[$productId] ?? []);
lager_forecast_persist($pdo, $lot, $forecast, $asOfDate);
if ($lot['next_lot_id'] !== null) {
lager_forecast_persist($pdo, [
'lot_id' => $lot['next_lot_id'],
'product_id' => $productId,
'physical_qty' => $lot['next_physical_qty'],
], lager_forecast_calculate($asOfDate, (float) $lot['next_physical_qty'], $dailySales[$productId] ?? []), $asOfDate);
}
$result['processed_products']++;
} catch (Throwable $error) {
$result['failed_products']++;
+59 -9
View File
@@ -75,13 +75,7 @@ function get_lager_product_overview(PDO $pdo): array
p.name,
p.sku,
p.uom,
COALESCE(SUM(v.qty_net), 0) + COALESCE((
SELECT SUM(open_v.qty_net)
FROM stock_lot open_sl
INNER JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_sl.id
WHERE open_sl.product_id = p.id
AND open_sl.status = 'open'
), 0) AS available_qty,
COALESCE(v.qty_net, 0) AS available_qty,
COALESCE(v.qty_net, 0) AS current_qty,
COALESCE(v.qty_in, 0) AS qty_in,
sl.lot_number,
@@ -94,6 +88,11 @@ function get_lager_product_overview(PDO $pdo): array
sl.purchase_date,
sl.sellout_date,
sl.sellout_forecast_status,
next_sl.id AS next_lot_id,
COALESCE(next_v.qty_net, 0) AS next_qty,
next_sl.sellout_date AS next_sellout_date,
next_sl.sellout_forecast_status AS next_sellout_forecast_status,
next_sl.expiry_date AS next_expiry_date,
COALESCE((
SELECT SUM(a.qty)
FROM sales_order_line_lot_allocation a
@@ -117,10 +116,25 @@ function get_lager_product_overview(PDO $pdo): array
LEFT JOIN party mp ON mp.id = sl.manufacturer_party_id
LEFT JOIN v_stock_lot_balance v
ON v.stock_lot_id = sl.id
LEFT JOIN stock_lot next_sl
ON next_sl.product_id = p.id
AND next_sl.status = 'open'
AND next_sl.next_active = TRUE
AND NULLIF(TRIM(next_sl.lot_number), '') IS NOT NULL
AND next_sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(next_sl.manufacturer_lot_number), '') IS NOT NULL
AND next_sl.mfg_date IS NOT NULL
AND next_sl.expiry_date IS NOT NULL
AND next_sl.purchase_date IS NOT NULL
LEFT JOIN v_stock_lot_balance next_v
ON next_v.stock_lot_id = next_sl.id
AND next_v.qty_net > 0
WHERE p.status = 'active'
GROUP BY p.id, p.name, p.sku, p.uom, sl.id, sl.lot_number, sl.manufacturer_party_id,
sl.manufacturer_lot_number, mp.name, sl.mfg_date, sl.expiry_date, sl.purchase_date,
sl.sellout_date, sl.sellout_forecast_status, v.qty_net, v.qty_in
sl.sellout_date, sl.sellout_forecast_status, v.qty_net, v.qty_in,
next_sl.id, next_v.qty_net, next_sl.sellout_date,
next_sl.sellout_forecast_status, next_sl.expiry_date
ORDER BY p.name ASC, p.id ASC"
);
@@ -143,6 +157,11 @@ function get_lager_product_overview(PDO $pdo): array
'purchase_date' => $row['purchase_date'],
'sellout_date' => $row['sellout_date'],
'sellout_forecast_status' => (string) ($row['sellout_forecast_status'] ?? 'no_demand'),
'next_lot_id' => $row['next_lot_id'] === null ? null : (int) $row['next_lot_id'],
'next_qty' => (float) ($row['next_qty'] ?? 0),
'next_sellout_date' => $row['next_sellout_date'],
'next_sellout_forecast_status' => (string) ($row['next_sellout_forecast_status'] ?? 'no_demand'),
'next_expiry_date' => $row['next_expiry_date'],
'virtual_shortage_qty' => (float) ($row['virtual_shortage_qty'] ?? 0),
'virtual_lot_number' => $row['virtual_lot_number'],
],
@@ -256,8 +275,27 @@ function get_lager_product_available_qty(PDO $pdo, int $productId): float
function get_lager_product_forecast(PDO $pdo, int $productId): array
{
$stmt = $pdo->prepare(
"SELECT sellout_date, sellout_forecast_status, expiry_date
"SELECT sellout_date, sellout_forecast_status, expiry_date,
next_lot.sellout_date AS next_sellout_date,
next_lot.sellout_forecast_status AS next_sellout_forecast_status,
next_lot.expiry_date AS next_expiry_date
FROM stock_lot
LEFT JOIN LATERAL (
SELECT sl.sellout_date, sl.sellout_forecast_status, sl.expiry_date
FROM stock_lot sl
INNER JOIN v_stock_lot_balance v ON v.stock_lot_id = sl.id
WHERE sl.product_id = stock_lot.product_id
AND sl.status = 'open'
AND sl.next_active = TRUE
AND NULLIF(TRIM(sl.lot_number), '') IS NOT NULL
AND sl.manufacturer_party_id IS NOT NULL
AND NULLIF(TRIM(sl.manufacturer_lot_number), '') IS NOT NULL
AND sl.mfg_date IS NOT NULL
AND sl.expiry_date IS NOT NULL
AND sl.purchase_date IS NOT NULL
AND v.qty_net > 0
LIMIT 1
) next_lot ON TRUE
WHERE product_id = :product_id
AND status = 'current'
LIMIT 1"
@@ -271,6 +309,18 @@ function get_lager_product_forecast(PDO $pdo, int $productId): array
'date' => $forecast['sellout_date'],
'status' => (string) $forecast['sellout_forecast_status'],
'expiryDate' => $forecast['expiry_date'],
'current' => [
'date' => $forecast['sellout_date'],
'status' => (string) $forecast['sellout_forecast_status'],
'expiryDate' => $forecast['expiry_date'],
],
'next' => $forecast['next_sellout_date'] === null && $forecast['next_expiry_date'] === null
? null
: [
'date' => $forecast['next_sellout_date'],
'status' => (string) ($forecast['next_sellout_forecast_status'] ?? 'no_demand'),
'expiryDate' => $forecast['next_expiry_date'],
],
];
}