Anchor next lot forecast after current lot

This commit is contained in:
gitea_admin committed 2026-09-08 19:24:34 +02:00
1 parent 959586ecc9
commit 3ae9951b13
2 files changed
+23 -7

No files matched your search

@@ -23,7 +23,7 @@ Direkt ausgeführter Lager-Prozess mit zwei Triggern: synchron nach jeder erfolg
4. `allocated`- und `reserved`-Mengen nach Bestell- beziehungsweise OTC-Verkaufsdatum je Produkt und Kalendertag aggregieren. 4. `allocated`- und `reserved`-Mengen nach Bestell- beziehungsweise OTC-Verkaufsdatum je Produkt und Kalendertag aggregieren.
5. Stornierte Auftrags- und Positionsmengen aus der Nachfrage herausrechnen. 5. Stornierte Auftrags- und Positionsmengen aus der Nachfrage herausrechnen.
6. Den gewichteten Tagesabsatz mit einer Halbwertszeit von 60 Tagen berechnen. 6. Den gewichteten Tagesabsatz mit einer Halbwertszeit von 60 Tagen berechnen.
7. Das Leer-Datum der aktuellen Charge und – sofern vorhanden – der nächsten Charge getrennt aus dem jeweiligen physischen Bestand und derselben gewichteten Nachfrage berechnen. 7. Das Leer-Datum der aktuellen Charge aus ihrem physischen Bestand und der gewichteten Nachfrage berechnen. Das Leer-Datum der nächsten Charge wird mit derselben Nachfrage ab dem Leer-Datum der aktuellen Charge weitergerechnet; bei bereits leerer aktueller Charge beginnt es am aktuellen Datum.
8. Jede erfolgreiche Berechnung je Charge in `stock_lot_forecast_history` speichern und den jeweiligen Snapshot in `stock_lot` aktualisieren. 8. Jede erfolgreiche Berechnung je Charge in `stock_lot_forecast_history` speichern und den jeweiligen Snapshot in `stock_lot` aktualisieren.
9. Im Cron Fehler einzelner Produkte sammeln und mit den übrigen Produkten fortfahren. 9. Im Cron Fehler einzelner Produkte sammeln und mit den übrigen Produkten fortfahren.
+22 -6
View File
@@ -124,7 +124,12 @@ function lager_forecast_load_daily_sales(PDO $pdo, DateTimeImmutable $asOfDate,
return $sales; return $sales;
} }
function lager_forecast_calculate(DateTimeImmutable $asOfDate, float $physicalQty, array $dailySales): array function lager_forecast_calculate(
DateTimeImmutable $asOfDate,
float $physicalQty,
array $dailySales,
?DateTimeImmutable $forecastStartDate = null
): array
{ {
$lookbackDays = 365; $lookbackDays = 365;
$halfLifeDays = 60; $halfLifeDays = 60;
@@ -176,8 +181,9 @@ function lager_forecast_calculate(DateTimeImmutable $asOfDate, float $physicalQt
? 'high' ? 'high'
: ($salesDays >= 10 ? 'medium' : 'low'); : ($salesDays >= 10 ? 'medium' : 'low');
$daysUntilDepleted = (int) ceil($physicalQty / $dailyDemand); $daysUntilDepleted = (int) ceil($physicalQty / $dailyDemand);
$forecastStartDate ??= $asOfDate;
return [ return [
'forecast_date' => $asOfDate->modify('+' . $daysUntilDepleted . ' days')->format('Y-m-d'), 'forecast_date' => $forecastStartDate->modify('+' . $daysUntilDepleted . ' days')->format('Y-m-d'),
'forecast_status' => 'forecast', 'confidence' => $confidence, 'daily_demand' => $dailyDemand, 'forecast_status' => 'forecast', 'confidence' => $confidence, 'daily_demand' => $dailyDemand,
'sales_units' => $salesUnits, 'sales_days' => $salesDays, 'observed_span_days' => $observedSpanDays, 'sales_units' => $salesUnits, 'sales_days' => $salesDays, 'observed_span_days' => $observedSpanDays,
'lookback_days' => $lookbackDays, 'half_life_days' => $halfLifeDays, 'lookback_days' => $lookbackDays, 'half_life_days' => $halfLifeDays,
@@ -255,12 +261,17 @@ function refresh_lager_stock_lot_forecast_for_product(PDO $pdo, int $productId):
$dailySales[$productId] ?? [] $dailySales[$productId] ?? []
); );
lager_forecast_persist($pdo, $lot, $forecast, $asOfDate); lager_forecast_persist($pdo, $lot, $forecast, $asOfDate);
if ($lot['next_lot_id'] !== null) { if ($lot['next_lot_id'] !== null && $forecast['forecast_date'] !== null) {
lager_forecast_persist($pdo, [ lager_forecast_persist($pdo, [
'lot_id' => $lot['next_lot_id'], 'lot_id' => $lot['next_lot_id'],
'product_id' => $productId, 'product_id' => $productId,
'physical_qty' => $lot['next_physical_qty'], 'physical_qty' => $lot['next_physical_qty'],
], lager_forecast_calculate($asOfDate, (float) $lot['next_physical_qty'], $dailySales[$productId] ?? []), $asOfDate); ], lager_forecast_calculate(
$asOfDate,
(float) $lot['next_physical_qty'],
$dailySales[$productId] ?? [],
new DateTimeImmutable($forecast['forecast_date'])
), $asOfDate);
} }
} }
@@ -287,12 +298,17 @@ function run_lager_stock_lot_forecast(PDO $pdo): array
try { try {
$forecast = lager_forecast_calculate($asOfDate, (float) $lot['physical_qty'], $dailySales[$productId] ?? []); $forecast = lager_forecast_calculate($asOfDate, (float) $lot['physical_qty'], $dailySales[$productId] ?? []);
lager_forecast_persist($pdo, $lot, $forecast, $asOfDate); lager_forecast_persist($pdo, $lot, $forecast, $asOfDate);
if ($lot['next_lot_id'] !== null) { if ($lot['next_lot_id'] !== null && $forecast['forecast_date'] !== null) {
lager_forecast_persist($pdo, [ lager_forecast_persist($pdo, [
'lot_id' => $lot['next_lot_id'], 'lot_id' => $lot['next_lot_id'],
'product_id' => $productId, 'product_id' => $productId,
'physical_qty' => $lot['next_physical_qty'], 'physical_qty' => $lot['next_physical_qty'],
], lager_forecast_calculate($asOfDate, (float) $lot['next_physical_qty'], $dailySales[$productId] ?? []), $asOfDate); ], lager_forecast_calculate(
$asOfDate,
(float) $lot['next_physical_qty'],
$dailySales[$productId] ?? [],
new DateTimeImmutable($forecast['forecast_date'])
), $asOfDate);
} }
$result['processed_products']++; $result['processed_products']++;
} catch (Throwable $error) { } catch (Throwable $error) {