From ca5cf5a03fced970aefe6270263ef95e4446a39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sun, 6 Sep 2026 17:37:01 +0200 Subject: [PATCH] Erlaube mehrere offene Lagerchargen --- db/README.md | 2 ++ .../0019_allow_multiple_open_stock_lots.sql | 5 +++++ .../erp.lager.stock_lot_forecast_refresh.md | 8 ++++---- modules/erp/lager/forecast.php | 20 +++++++++++-------- 4 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 db/migrations/0019_allow_multiple_open_stock_lots.sql diff --git a/db/README.md b/db/README.md index 7073e8c..dba14e9 100644 --- a/db/README.md +++ b/db/README.md @@ -15,6 +15,8 @@ 11. `db/migrations/0015_sales_order_payments.sql` 12. `db/migrations/0016_stock_lot_forecast_history.sql` 13. `db/migrations/0017_manufacturer_lot_traceability.sql` +14. `db/migrations/0018_shopify_delta_sync_state.sql` +15. `db/migrations/0019_allow_multiple_open_stock_lots.sql` ## Beispielausfuehrung diff --git a/db/migrations/0019_allow_multiple_open_stock_lots.sql b/db/migrations/0019_allow_multiple_open_stock_lots.sql new file mode 100644 index 0000000..538f7d2 --- /dev/null +++ b/db/migrations/0019_allow_multiple_open_stock_lots.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP INDEX IF EXISTS uq_stock_lot_one_open_per_product; + +COMMIT; diff --git a/docs/modules/erp/lager/processes/main/erp.lager.stock_lot_forecast_refresh.md b/docs/modules/erp/lager/processes/main/erp.lager.stock_lot_forecast_refresh.md index fd0f1eb..e21356d 100644 --- a/docs/modules/erp/lager/processes/main/erp.lager.stock_lot_forecast_refresh.md +++ b/docs/modules/erp/lager/processes/main/erp.lager.stock_lot_forecast_refresh.md @@ -13,17 +13,17 @@ Direkt ausgeführter Lager-Prozess mit zwei Triggern: synchron nach jeder erfolg ## 3. Input - optional eine einzelne `product_id` bei synchronem Verkaufstrigger -- aktive Produkte, aktuelle Chargen, eingetragene Mengen der offenen Folgecharge, physischer Chargenbestand und gültige Verkaufsallokationen aus dem eigenen Datenbestand +- aktive Produkte, aktuelle Chargen, eingetragene Mengen aller offenen Folgechargen, physischer Chargenbestand und gültige Verkaufsallokationen aus dem eigenen Datenbestand ## 4. Exakter Prozessablauf 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 den Nettobestand einer bereits angelegten offenen Folgecharge aus `product`, `stock_lot` und `v_stock_lot_balance` laden. Die Menge der offenen Folgecharge wird nur berücksichtigt, wenn sie positiv ist. +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. 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. 5. Stornierte Auftrags- und Positionsmengen aus der Nachfrage herausrechnen. 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 Menge der offenen Folgecharge und der gewichteten Nachfrage 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. 8. Jede erfolgreiche Berechnung in `stock_lot_forecast_history` speichern und den aktuellen Snapshot in `stock_lot` aktualisieren. 9. Im Cron Fehler einzelner Produkte sammeln und mit den übrigen Produkten fortfahren. @@ -37,7 +37,7 @@ Ein normalisiertes JSON-Ergebnis mit Gesamtzahl, erfolgreich verarbeiteten Produ ## 7. Erfolgskriterien -- Jede aktuelle Charge erhält täglich einen aktuellen Prognosesnapshot, der eine eingetragene Menge der offenen Folgecharge berücksichtigt. +- Jede aktuelle Charge erhält täglich einen aktuellen Prognosesnapshot, der eingetragene Mengen aller offenen Folgechargen berücksichtigt. - Verkäufe ohne Bestand über virtuelle `reserved`-Allokationen beeinflussen die Nachfrage. - Die vollständige Berechnungshistorie bleibt erhalten. - Ein einzelner Produktfehler verhindert keine weiteren Berechnungen. diff --git a/modules/erp/lager/forecast.php b/modules/erp/lager/forecast.php index 911ca79..9e1606f 100644 --- a/modules/erp/lager/forecast.php +++ b/modules/erp/lager/forecast.php @@ -35,13 +35,15 @@ 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) + GREATEST(COALESCE(open_v.qty_net, 0), 0) AS physical_qty + 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 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 open_lot - ON open_lot.product_id = p.id AND open_lot.status = 'open' - LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id WHERE p.status = 'active' ORDER BY p.id" ); @@ -52,13 +54,15 @@ 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) + GREATEST(COALESCE(open_v.qty_net, 0), 0) AS physical_qty + 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 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 open_lot - ON open_lot.product_id = p.id AND open_lot.status = 'open' - LEFT JOIN v_stock_lot_balance open_v ON open_v.stock_lot_id = open_lot.id WHERE p.status = 'active' AND p.id = :product_id LIMIT 1" );