Split inventory forecast by current and next lot
This commit is contained in:
1 parent
77f6fee5ac
commit
959586ecc9
5 files changed
+164
-59
No files matched your search
@@ -4,7 +4,7 @@ Status: Verbindliche Prozess-Spezifikation
|
|||||||
|
|
||||||
## 1. Zweck
|
## 1. Zweck
|
||||||
|
|
||||||
Tägliche Berechnung und Speicherung des voraussichtlichen Leer-Datums jeder aktuellen Charge aktiver Produkte.
|
Tägliche Berechnung und Speicherung des voraussichtlichen Leer-Datums der aktuellen Charge und – sofern vorhanden – der qualifizierten, als nächste Charge markierten offenen Charge aktiver Produkte.
|
||||||
|
|
||||||
## 2. Prozess-Einbettung
|
## 2. Prozess-Einbettung
|
||||||
|
|
||||||
@@ -13,18 +13,18 @@ Direkt ausgeführter Lager-Prozess mit zwei Triggern: synchron nach jeder erfolg
|
|||||||
## 3. Input
|
## 3. Input
|
||||||
|
|
||||||
- optional eine einzelne `product_id` bei synchronem Verkaufstrigger
|
- optional eine einzelne `product_id` bei synchronem Verkaufstrigger
|
||||||
- aktive Produkte, aktuelle Chargen, eingetragene Mengen aller offenen Folgechargen, physischer Chargenbestand und gültige Verkaufsallokationen aus dem eigenen Datenbestand
|
- aktive Produkte, aktuelle Charge, qualifizierte als nächste Charge markierte offene Charge, physische Chargenbestände und gültige Verkaufsallokationen aus dem eigenen Datenbestand
|
||||||
|
|
||||||
## 4. Exakter Prozessablauf
|
## 4. Exakter Prozessablauf
|
||||||
|
|
||||||
1. Bei einem Verkaufstrigger die betroffenen Produkt-IDs aus der erfolgreichen Allokation übernehmen; beim Cron-Trigger alle aktiven Produkte bestimmen.
|
1. Bei einem Verkaufstrigger die betroffenen Produkt-IDs aus der erfolgreichen Allokation übernehmen; beim Cron-Trigger alle aktiven Produkte bestimmen.
|
||||||
2. Aktive Produkte, ihre aktuelle Charge und die Nettobestände aller bereits angelegten offenen Folgechargen aus `product`, `stock_lot` und `v_stock_lot_balance` laden. Je offene Folgecharge wird nur eine positive Menge berücksichtigt.
|
2. Aktive Produkte, ihre aktuelle Charge und die qualifizierte, als nächste Charge markierte offene Charge aus `product`, `stock_lot` und `v_stock_lot_balance` laden. Für jede Charge wird nur ihr eigener positiver Nettobestand berücksichtigt.
|
||||||
3. Verkaufsallokationen der letzten 365 Tage aus `sales_order_line_lot_allocation`, `sales_order_line` und `sales_order` laden.
|
3. Verkaufsallokationen der letzten 365 Tage aus `sales_order_line_lot_allocation`, `sales_order_line` und `sales_order` laden.
|
||||||
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. Für jede aktuelle Charge das Leer-Datum aus ihrem physischen Bestand zuzüglich der eingetragenen Mengen aller offenen Folgechargen und der gewichteten Nachfrage 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.
|
||||||
8. Jede erfolgreiche Berechnung in `stock_lot_forecast_history` speichern und den aktuellen 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.
|
||||||
|
|
||||||
## 5. Batch, Betriebsmodell und Einbettung
|
## 5. Batch, Betriebsmodell und Einbettung
|
||||||
@@ -37,7 +37,8 @@ Ein normalisiertes JSON-Ergebnis mit Gesamtzahl, erfolgreich verarbeiteten Produ
|
|||||||
|
|
||||||
## 7. Erfolgskriterien
|
## 7. Erfolgskriterien
|
||||||
|
|
||||||
- Jede aktuelle Charge erhält täglich einen aktuellen Prognosesnapshot, der eingetragene Mengen aller offenen Folgechargen berücksichtigt.
|
- Jede aktuelle Charge erhält täglich einen Prognosesnapshot nur für ihren eigenen Bestand.
|
||||||
|
- Eine qualifizierte, als nächste Charge markierte offene Charge erhält einen separaten Prognosesnapshot nur für ihren eigenen Bestand.
|
||||||
- Verkäufe ohne Bestand über virtuelle `reserved`-Allokationen beeinflussen die Nachfrage.
|
- Verkäufe ohne Bestand über virtuelle `reserved`-Allokationen beeinflussen die Nachfrage.
|
||||||
- Die vollständige Berechnungshistorie bleibt erhalten.
|
- Die vollständige Berechnungshistorie bleibt erhalten.
|
||||||
- Ein einzelner Produktfehler verhindert keine weiteren Berechnungen.
|
- Ein einzelner Produktfehler verhindert keine weiteren Berechnungen.
|
||||||
|
|||||||
@@ -35,15 +35,25 @@ function lager_forecast_load_current_lots(PDO $pdo): array
|
|||||||
{
|
{
|
||||||
$stmt = $pdo->query(
|
$stmt = $pdo->query(
|
||||||
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
|
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
|
||||||
COALESCE(v.qty_net, 0) + COALESCE((
|
COALESCE(v.qty_net, 0) AS physical_qty,
|
||||||
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0))
|
next_sl.id AS next_lot_id,
|
||||||
FROM stock_lot open_lot
|
COALESCE(next_v.qty_net, 0) AS next_physical_qty
|
||||||
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
|
|
||||||
FROM product p
|
FROM product p
|
||||||
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current'
|
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 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'
|
WHERE p.status = 'active'
|
||||||
ORDER BY p.id"
|
ORDER BY p.id"
|
||||||
);
|
);
|
||||||
@@ -54,15 +64,25 @@ function lager_forecast_load_current_lot(PDO $pdo, int $productId): ?array
|
|||||||
{
|
{
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
|
"SELECT p.id AS product_id, p.sku, sl.id AS lot_id,
|
||||||
COALESCE(v.qty_net, 0) + COALESCE((
|
COALESCE(v.qty_net, 0) AS physical_qty,
|
||||||
SELECT SUM(GREATEST(COALESCE(open_v.qty_net, 0), 0))
|
next_sl.id AS next_lot_id,
|
||||||
FROM stock_lot open_lot
|
COALESCE(next_v.qty_net, 0) AS next_physical_qty
|
||||||
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
|
|
||||||
FROM product p
|
FROM product p
|
||||||
LEFT JOIN stock_lot sl ON sl.product_id = p.id AND sl.status = 'current'
|
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 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
|
WHERE p.status = 'active' AND p.id = :product_id
|
||||||
LIMIT 1"
|
LIMIT 1"
|
||||||
);
|
);
|
||||||
@@ -235,6 +255,13 @@ 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) {
|
||||||
|
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
|
function run_lager_stock_lot_forecast(PDO $pdo): array
|
||||||
@@ -260,6 +287,13 @@ 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) {
|
||||||
|
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']++;
|
$result['processed_products']++;
|
||||||
} catch (Throwable $error) {
|
} catch (Throwable $error) {
|
||||||
$result['failed_products']++;
|
$result['failed_products']++;
|
||||||
|
|||||||
@@ -75,13 +75,7 @@ function get_lager_product_overview(PDO $pdo): array
|
|||||||
p.name,
|
p.name,
|
||||||
p.sku,
|
p.sku,
|
||||||
p.uom,
|
p.uom,
|
||||||
COALESCE(SUM(v.qty_net), 0) + COALESCE((
|
COALESCE(v.qty_net, 0) AS available_qty,
|
||||||
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 current_qty,
|
COALESCE(v.qty_net, 0) AS current_qty,
|
||||||
COALESCE(v.qty_in, 0) AS qty_in,
|
COALESCE(v.qty_in, 0) AS qty_in,
|
||||||
sl.lot_number,
|
sl.lot_number,
|
||||||
@@ -94,6 +88,11 @@ function get_lager_product_overview(PDO $pdo): array
|
|||||||
sl.purchase_date,
|
sl.purchase_date,
|
||||||
sl.sellout_date,
|
sl.sellout_date,
|
||||||
sl.sellout_forecast_status,
|
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((
|
COALESCE((
|
||||||
SELECT SUM(a.qty)
|
SELECT SUM(a.qty)
|
||||||
FROM sales_order_line_lot_allocation a
|
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 party mp ON mp.id = sl.manufacturer_party_id
|
||||||
LEFT JOIN v_stock_lot_balance v
|
LEFT JOIN v_stock_lot_balance v
|
||||||
ON v.stock_lot_id = sl.id
|
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'
|
WHERE p.status = 'active'
|
||||||
GROUP BY p.id, p.name, p.sku, p.uom, sl.id, sl.lot_number, sl.manufacturer_party_id,
|
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.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"
|
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'],
|
'purchase_date' => $row['purchase_date'],
|
||||||
'sellout_date' => $row['sellout_date'],
|
'sellout_date' => $row['sellout_date'],
|
||||||
'sellout_forecast_status' => (string) ($row['sellout_forecast_status'] ?? 'no_demand'),
|
'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_shortage_qty' => (float) ($row['virtual_shortage_qty'] ?? 0),
|
||||||
'virtual_lot_number' => $row['virtual_lot_number'],
|
'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
|
function get_lager_product_forecast(PDO $pdo, int $productId): array
|
||||||
{
|
{
|
||||||
$stmt = $pdo->prepare(
|
$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
|
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
|
WHERE product_id = :product_id
|
||||||
AND status = 'current'
|
AND status = 'current'
|
||||||
LIMIT 1"
|
LIMIT 1"
|
||||||
@@ -271,6 +309,18 @@ function get_lager_product_forecast(PDO $pdo, int $productId): array
|
|||||||
'date' => $forecast['sellout_date'],
|
'date' => $forecast['sellout_date'],
|
||||||
'status' => (string) $forecast['sellout_forecast_status'],
|
'status' => (string) $forecast['sellout_forecast_status'],
|
||||||
'expiryDate' => $forecast['expiry_date'],
|
'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'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,30 @@ function auth_render_bestellungen_content(array $bestellungenTable): string
|
|||||||
|
|
||||||
function auth_render_lager_table(string $title, array $rows, array $columns, string $tableKey): string
|
function auth_render_lager_table(string $title, array $rows, array $columns, string $tableKey): string
|
||||||
{
|
{
|
||||||
|
$renderForecast = static function (?string $date, string $status, ?string $expiryDate, bool $available): array {
|
||||||
|
if (!$available) {
|
||||||
|
return ['text' => '-', 'warning' => false, 'mhdWarning' => false];
|
||||||
|
}
|
||||||
|
if ($date === null || $date === '') {
|
||||||
|
return ['text' => 'Keine Prognose', 'warning' => false, 'mhdWarning' => false];
|
||||||
|
}
|
||||||
|
$dateObject = new DateTimeImmutable($date);
|
||||||
|
$text = $dateObject->format('d.m.Y');
|
||||||
|
if ($status === 'depleted') {
|
||||||
|
return ['text' => $text . ' (BESTAND ENDE)', 'warning' => true, 'mhdWarning' => false];
|
||||||
|
}
|
||||||
|
$warning = false;
|
||||||
|
$mhdWarning = false;
|
||||||
|
if ($expiryDate !== null && $expiryDate !== '') {
|
||||||
|
$expiryObject = new DateTimeImmutable($expiryDate);
|
||||||
|
$warning = $dateObject >= $expiryObject->modify('-3 months') && $dateObject <= $expiryObject;
|
||||||
|
$mhdWarning = $dateObject > $expiryObject;
|
||||||
|
if ($warning || $mhdWarning) {
|
||||||
|
$text .= ' (MHD: ' . $expiryObject->format('d.m.Y') . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['text' => $text, 'warning' => $warning, 'mhdWarning' => $mhdWarning];
|
||||||
|
};
|
||||||
$html = [];
|
$html = [];
|
||||||
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
|
$html[] = '<article class="sg-card sg-large-table sg-large-table--lager" data-component="large-table" data-lager-table="true" data-lager-table-key="' . auth_escape_html($tableKey) . '" role="table" aria-label="' . auth_escape_html($title) . '">';
|
||||||
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">' . auth_escape_html($title) . '</div></div>';
|
$html[] = '<div class="sg-card-segment sg-card-segment--header sg-card-segment--darkblue sg-large-table__title-segment" data-component-part="large-table-header"><div class="sg-strong">' . auth_escape_html($title) . '</div></div>';
|
||||||
@@ -209,40 +233,31 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
|
|||||||
$value = $tableKey === 'products'
|
$value = $tableKey === 'products'
|
||||||
? (string) max(0, (int) round((float) ($row['available_qty'] ?? 0)))
|
? (string) max(0, (int) round((float) ($row['available_qty'] ?? 0)))
|
||||||
: (string) ($row['item_code'] ?? '');
|
: (string) ($row['item_code'] ?? '');
|
||||||
$forecastText = '';
|
$currentForecast = ['text' => '', 'warning' => false, 'mhdWarning' => false];
|
||||||
$forecastWarning = false;
|
$nextForecast = ['text' => '-', 'warning' => false, 'mhdWarning' => false];
|
||||||
$forecastMhdWarning = false;
|
|
||||||
if ($tableKey === 'products') {
|
if ($tableKey === 'products') {
|
||||||
$forecastStatus = (string) ($row['sellout_forecast_status'] ?? 'no_demand');
|
$currentForecast = $renderForecast(
|
||||||
$forecastDate = $row['sellout_date'] ?? null;
|
$row['sellout_date'] ?? null,
|
||||||
if ($forecastDate !== null && $forecastDate !== '') {
|
(string) ($row['sellout_forecast_status'] ?? 'no_demand'),
|
||||||
$forecastDateObject = new DateTimeImmutable((string) $forecastDate);
|
$row['expiry_date'] ?? null,
|
||||||
$forecastText = $forecastDateObject->format('d.m.Y');
|
true
|
||||||
if ($forecastStatus === 'depleted') {
|
);
|
||||||
$forecastWarning = true;
|
$nextForecast = $renderForecast(
|
||||||
$forecastText .= ' (BESTAND ENDE)';
|
$row['next_sellout_date'] ?? null,
|
||||||
} elseif (!empty($row['expiry_date'])) {
|
(string) ($row['next_sellout_forecast_status'] ?? 'no_demand'),
|
||||||
$expiryDate = new DateTimeImmutable((string) $row['expiry_date']);
|
$row['next_expiry_date'] ?? null,
|
||||||
$forecastWarning = $forecastDateObject >= $expiryDate->modify('-3 months')
|
$row['next_lot_id'] !== null
|
||||||
&& $forecastDateObject <= $expiryDate;
|
);
|
||||||
$forecastMhdWarning = $forecastDateObject > $expiryDate;
|
|
||||||
if ($forecastWarning || $forecastMhdWarning) {
|
|
||||||
$forecastText .= ' (MHD: ' . $expiryDate->format('d.m.Y') . ')';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$forecastText = 'Keine Prognose';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$nameData = ' data-lager-drawer-name="' . auth_escape_html($name) . '"';
|
$nameData = ' data-lager-drawer-name="' . auth_escape_html($name) . '"';
|
||||||
$valueData = ' data-lager-drawer-value="' . auth_escape_html($value) . '"';
|
$valueData = ' data-lager-drawer-value="' . auth_escape_html($value) . '"';
|
||||||
$typeData = ' data-lager-drawer-type="' . auth_escape_html($tableKey === 'products' ? 'Produkt' : 'Artikel') . '"';
|
$typeData = ' data-lager-drawer-type="' . auth_escape_html($tableKey === 'products' ? 'Produkt' : 'Artikel') . '"';
|
||||||
$detailData = '';
|
$detailData = '';
|
||||||
if ($tableKey === 'products') {
|
if ($tableKey === 'products') {
|
||||||
$detailData = ' data-lager-drawer-forecast="' . auth_escape_html($forecastText) . '"'
|
$detailData = ' data-lager-drawer-forecast="' . auth_escape_html($currentForecast['text']) . '"'
|
||||||
. ' data-lager-drawer-forecast-status="' . auth_escape_html($forecastStatus) . '"'
|
. ' data-lager-drawer-forecast-status="' . auth_escape_html((string) ($row['sellout_forecast_status'] ?? 'no_demand')) . '"'
|
||||||
. ' data-lager-drawer-forecast-warning="' . ($forecastWarning ? 'true' : 'false') . '"'
|
. ' data-lager-drawer-forecast-warning="' . ($currentForecast['warning'] ? 'true' : 'false') . '"'
|
||||||
. ' data-lager-drawer-forecast-mhd-warning="' . ($forecastMhdWarning ? 'true' : 'false') . '"'
|
. ' data-lager-drawer-forecast-mhd-warning="' . ($currentForecast['mhdWarning'] ? 'true' : 'false') . '"'
|
||||||
. ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['current_qty'] ?? 0))) . '"'
|
. ' data-lager-drawer-current-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['current_qty'] ?? 0))) . '"'
|
||||||
. ' data-lager-drawer-qty-in="' . auth_escape_html(auth_format_lager_quantity((float) ($row['qty_in'] ?? 0))) . '"'
|
. ' data-lager-drawer-qty-in="' . auth_escape_html(auth_format_lager_quantity((float) ($row['qty_in'] ?? 0))) . '"'
|
||||||
. ' data-lager-drawer-virtual-shortage-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['virtual_shortage_qty'] ?? 0))) . '"'
|
. ' data-lager-drawer-virtual-shortage-qty="' . auth_escape_html(auth_format_lager_quantity((float) ($row['virtual_shortage_qty'] ?? 0))) . '"'
|
||||||
@@ -269,10 +284,15 @@ function auth_render_lager_table(string $title, array $rows, array $columns, str
|
|||||||
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($name) . '</a></div>';
|
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($name) . '</a></div>';
|
||||||
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($value) . '</a></div>';
|
$html[] = '<div class="sg-large-table__cell" role="cell"><a class="sg-hyperlink" href="#" aria-haspopup="dialog" aria-expanded="false" data-lager-drawer-open="true"' . $nameData . $valueData . $typeData . $detailData . '>' . auth_escape_html($value) . '</a></div>';
|
||||||
if ($tableKey === 'products') {
|
if ($tableKey === 'products') {
|
||||||
$forecastClass = $forecastWarning
|
$forecastClass = $currentForecast['warning']
|
||||||
? ' sg-strong lager-forecast-warning'
|
? ' sg-strong lager-forecast-warning'
|
||||||
: ($forecastMhdWarning ? ' sg-strong lager-forecast-mhd-warning' : '');
|
: ($currentForecast['mhdWarning'] ? ' sg-strong lager-forecast-mhd-warning' : '');
|
||||||
$html[] = '<div class="sg-large-table__cell' . $forecastClass . '" role="cell">' . auth_escape_html($forecastText) . '</div>';
|
$nextForecastClass = $nextForecast['warning']
|
||||||
|
? ' sg-strong lager-forecast-warning'
|
||||||
|
: ($nextForecast['mhdWarning'] ? ' sg-strong lager-forecast-mhd-warning' : '');
|
||||||
|
$html[] = '<div class="sg-large-table__cell' . $forecastClass . '" role="cell">' . auth_escape_html($currentForecast['text']) . '</div>';
|
||||||
|
$html[] = '<div class="sg-large-table__cell" role="cell">' . auth_escape_html((string) max(0, (int) round((float) ($row['next_qty'] ?? 0)))) . '</div>';
|
||||||
|
$html[] = '<div class="sg-large-table__cell' . $nextForecastClass . '" role="cell">' . auth_escape_html($nextForecast['text']) . '</div>';
|
||||||
}
|
}
|
||||||
$html[] = '</div>';
|
$html[] = '</div>';
|
||||||
}
|
}
|
||||||
@@ -382,7 +402,7 @@ function auth_render_lager_content(array $lagerProducts, array $lagerArtikel, ar
|
|||||||
}
|
}
|
||||||
|
|
||||||
$tables = $sectionTitle === 'Lager'
|
$tables = $sectionTitle === 'Lager'
|
||||||
? auth_render_lager_table('Produkte', $lagerProducts, ['Produktname', 'Anzahl im Lager', 'Verbraucht bis'], 'products')
|
? auth_render_lager_table('Produkte', $lagerProducts, ['Produktname', 'Lager aktuell', 'Verkauf bis aktuell', 'Lager neu', 'Verkauf bis neu'], 'products')
|
||||||
: auth_render_lager_table('Artikel', $lagerArtikel, ['Artikel-Name', 'Artikelnummer'], 'articles');
|
: auth_render_lager_table('Artikel', $lagerArtikel, ['Artikel-Name', 'Artikelnummer'], 'articles');
|
||||||
$openLotCardTemplate = auth_render_lager_open_lots([[
|
$openLotCardTemplate = auth_render_lager_open_lots([[
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ body.sg-lager-mobile-detail-open > :not([data-lager-mobile-detail-host]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__row {
|
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__row {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__cell:last-child {
|
.sg-large-table--lager[data-lager-table-key="products"] .sg-large-table__cell:last-child {
|
||||||
|
|||||||
Reference in new issue
Block a user