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

@@ -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.
+46 -12
View File
@@ -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']++;
+59 -9
View File
@@ -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'],
],
]; ];
} }
+51 -31
View File
@@ -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,
+1 -1
View File
@@ -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 {