Refresh stock forecast after each sale

This commit is contained in:
gitea_admin committed 2026-09-06 09:37:27 +02:00
1 parent 5dd41a4bb2
commit 03ea914d04
3 files changed
+76 -21

No files matched your search

+56 -10
View File
@@ -45,11 +45,27 @@ function lager_forecast_load_current_lots(PDO $pdo): array
return $stmt->fetchAll();
}
function lager_forecast_load_daily_sales(PDO $pdo, DateTimeImmutable $asOfDate, int $lookbackDays): array
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) AS 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
WHERE p.status = 'active' AND p.id = :product_id
LIMIT 1"
);
$stmt->execute([':product_id' => $productId]);
$row = $stmt->fetch();
return is_array($row) ? $row : null;
}
function lager_forecast_load_daily_sales(PDO $pdo, DateTimeImmutable $asOfDate, int $lookbackDays, ?int $productId = null): array
{
$startDate = $asOfDate->modify('-' . ($lookbackDays - 1) . ' days')->format('Y-m-d');
$endDate = $asOfDate->modify('+1 day')->format('Y-m-d');
$stmt = $pdo->prepare(
$sql =
"SELECT a.product_id, so.order_date::DATE AS sale_date,
SUM(a.qty * GREATEST(sol.qty - sol.qty_cancelled, 0) / NULLIF(sol.qty, 0)) AS units
FROM sales_order_line_lot_allocation a
@@ -61,11 +77,15 @@ function lager_forecast_load_daily_sales(PDO $pdo, DateTimeImmutable $asOfDate,
AND sol.line_status <> 'cancelled'
AND so.order_status <> 'cancelled'
AND so.order_date >= :start_date::DATE
AND so.order_date < :end_date::DATE
GROUP BY a.product_id, so.order_date::DATE
ORDER BY a.product_id, sale_date"
);
$stmt->execute([':start_date' => $startDate, ':end_date' => $endDate]);
AND so.order_date < :end_date::DATE";
$params = [':start_date' => $startDate, ':end_date' => $endDate];
if ($productId !== null) {
$sql .= ' AND a.product_id = :product_id';
$params[':product_id'] = $productId;
}
$sql .= ' GROUP BY a.product_id, so.order_date::DATE ORDER BY a.product_id, sale_date';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$sales = [];
foreach ($stmt->fetchAll() as $row) {
@@ -136,7 +156,10 @@ function lager_forecast_calculate(DateTimeImmutable $asOfDate, float $physicalQt
function lager_forecast_persist(PDO $pdo, array $lot, array $forecast, DateTimeImmutable $asOfDate): void
{
$pdo->beginTransaction();
$startedTransaction = !$pdo->inTransaction();
if ($startedTransaction) {
$pdo->beginTransaction();
}
try {
$stmt = $pdo->prepare(
"INSERT INTO stock_lot_forecast_history (
@@ -172,15 +195,38 @@ function lager_forecast_persist(PDO $pdo, array $lot, array $forecast, DateTimeI
':confidence' => $forecast['confidence'], ':daily_demand' => $forecast['daily_demand'],
':lot_id' => (int) $lot['lot_id'],
]);
$pdo->commit();
if ($startedTransaction) {
$pdo->commit();
}
} catch (Throwable $error) {
if ($pdo->inTransaction()) {
if ($startedTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $error;
}
}
function refresh_lager_stock_lot_forecast_for_product(PDO $pdo, int $productId): void
{
if ($productId <= 0) {
throw new InvalidArgumentException('Produkt-ID fuer Forecast fehlt');
}
$lot = lager_forecast_load_current_lot($pdo, $productId);
if ($lot === null || $lot['lot_id'] === null) {
throw new RuntimeException("Keine aktuelle Charge fuer Produkt {$productId} vorhanden");
}
$asOfDate = new DateTimeImmutable('today', new DateTimeZone('Europe/Zurich'));
$dailySales = lager_forecast_load_daily_sales($pdo, $asOfDate, 365, $productId);
$forecast = lager_forecast_calculate(
$asOfDate,
(float) $lot['physical_qty'],
$dailySales[$productId] ?? []
);
lager_forecast_persist($pdo, $lot, $forecast, $asOfDate);
}
function run_lager_stock_lot_forecast(PDO $pdo): array
{
$runId = lager_forecast_start_run($pdo);