gruene-seele/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md
Kevin Adametz a1f94ea427 Warenwirtschaft: Logistik-Mail regeneriert auch fehlende Rechnungs-PDF
Nachtrag zum Lieferschein-Fix (7d77314): Fehlte die Rechnungs-PDF auf der
Platte (Altdaten), brach MailLogistic::build() still ab — die Mail ging ohne
Anhänge bzw. gar nicht raus, obwohl das UI Erfolg meldete. Jetzt wird die
Rechnung wie der Lieferschein mit bestehender Rechnungsnummer regeneriert.
Hinweise-Doku und Plan-Protokoll fortgeschrieben.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:19:12 +00:00

56 KiB
Raw Blame History

Aktualisierter Entwicklungsplan: Warenwirtschaft, Produktion & Produktbestand

Version: 6.0 - Stand 06.07.2026 Ersetzt: entwicklungsplan-aktualisiert-12-06-2026.md (V5.0) als operative Arbeitsgrundlage Anforderungsquellen: docs/nächsten Anforderungen an die WaWi.md (12.06.2026), docs/Todos.md, Screens screens/2026-12-06-Rezeptur-phase.jpeg + screens/2026-12-06-neue-produktion.jpeg Referenzen: V5.0 (vollständige AP-Details zu AP-20 … AP-28), V4.0 (AP-00 … AP-19), briefing-anpassungen-27-04-2026.md, konzept-final.md Methodik: unverändert — kleine, sequenziell abarbeitbare Arbeitspakete (AP), jedes einzeln deploybar; jedes AP endet mit vendor/bin/pint --dirty + Pest-Tests.


0. Was dieses Dokument neu macht

V6.0 basiert auf einer vollständigen Code-Verifikation vom 06.07.2026 (alle AP-Status per Grep/Migrations-/Test-Abgleich geprüft, komplette Testsuite ausgeführt) plus einem Code-Qualitäts-Review des gesamten WaWi-Moduls. Es gibt zwei Neuerungen gegenüber V5.0:

  1. Verifizierter Stand: Seit dem 12.06.2026 wurde nicht weiterentwickelt (letzter Commit e53201f, Arbeitsverzeichnis sauber). Der V5.0-Status stimmt exakt: AP-22, AP-25, AP-26 sind fertig, alles andere offen.
  2. Neuer Optimierungs-Block AP-29 (OPT-1 … OPT-5): Ergebnisse des Code-Reviews als eigene, kleine Arbeitspakete. Zwei davon (OPT-1, OPT-2) werden vor AP-21 eingeschoben, weil AP-21 drei weitere Settings-CRUDs erzeugt und von der Entduplizierung direkt profitiert.

1. Verifizierter Ist-Stand (06.07.2026)

1.1 Testsuite

php artisan test: 153 Tests grün, 533 Assertions, Dauer ~2,3 s. Einziger Fail: ExampleTest (erwartet 200 auf /, bekommt den Domain-Redirect 302) — kein WaWi-Problem, Test sollte auf assertRedirect() angepasst oder entfernt werden (in OPT-1 miterledigt).

Stand nach Blockabschluss 06.07.2026 abends: 201 Tests grün, 731 Assertions — Suite dauerhaft 100 % grün.

1.2 AP-Status (per Code verifiziert; Status nach Blockabschluss 06.07.2026 aktualisiert)

AP Titel Status Evidenz
AP-00…AP-12, AP-18, AP-19 V4.0-Pakete (Bugfixes, Stammdaten, Einkauf, Produktion, Rohstoff-/Produktbestand, Ausschuss, Doku, UI) ✅ fertig Migrationen bis 2026_06_03_*, 20+ Feature-Tests grün
AP-22 Produktbestand-Erweiterungen (Flag, Dringlichkeits-Sortierung, 4 Kacheln, Verbrauch/Monat) ✅ fertig 2026_06_12_175615_add_show_in_product_stock_to_products_table, ProductStockTest (13 grün)
AP-25 Lieferbestand: Datum statt Tage, überall kaufbar ✅ fertig out_of_stock_until-Datepicker in product/form.blade.php, ProductOutOfStockTest (8 grün)
AP-26 Ausschuss-Gründe konfigurierbar ✅ fertig 2026_06_12_170531_create_disposal_reasons_table, DisposalReasonController, DisposalReasonSettingsTest (8 grün)
AP-21 INCI-Erweiterungen ✅ fertig 06.07.2026 (d05df49) alt_name + 3 Storage-Tabellen (Migrationen 2026_07_06_1207**), Lieferanten-Repeater mit URL, scopeUsedInActiveRecipes(), StorageSettingsTest/IngredientOrderFieldsTest grün
AP-20 Rohstoffbestand-Filter + „Kein Rohstoffbestand" ✅ fertig 06.07.2026 (ea36067) exclude_from_stock (Migration 2026_07_06_133714), ?scope=all|recipe-Umschalter, Kritisch-Badge respektiert Flag
AP-23 Rezeptur-Phasen ✅ fertig 06.07.2026 (97d420c) recipe_phases + product_ingredients.recipe_phase_id (Migration 2026_07_06_134518), Phasen-Blöcke mit Drag&Drop im Produktformular, RecipePhaseTest (6 grün)
AP-24 Chargen-Nr.-Präfix ✅ fertig 06.07.2026 (209ea46) products.batch_prefix (Migration 2026_07_06_140324), Product::suggestedBatchNumber() (TP1 + ttmmjj), BatchPrefixTest (6 grün)
AP-27 Wareneingang: Charge zusammenführen ✅ fertig 06.07.2026 (7a5b5e0) Batch-Check-Endpoint + Vorbelegung, Chargen-Gruppierung Detail/Produktion, FEFO-expandBatchLines(), StockEntryChargeMergeTest (7 grün)
AP-28 Neue Produktions-Maske + Produktionsergebnis ✅ fertig 06.07.2026 (99a6c92) production_outputs + Backfill (Migration 2026_07_06_153907), Buchung je Output-Produkt, Phasen-Maske + INCI-Liste + Ergebnis-Repeater, ProductionOutputTest (8 grün)
AP-13 Shop-Bestandsabzug bei Versand (inkl. Sets) + API-Bestellungen-Protokoll ✅ fertig 16.07.2026 (211aeed, 71d5591) ProductStockService::syncSaleStock(), api_order_logs, Menüpunkt „API-Bestellungen"; SaleStockDeductionTest (10), ApiOrderLogTest (8)
AP-32 Versand im System: DHL-Etiketten per API ⏸ nachgelagert (Kundenentscheid 16.07.2026: erst Testphase) Umsetzungsansatz in §4; Andockpunkt = Versand-Statuswechsel (wie AP-13)
AP-33a Bestell-Abwicklung: Nav „Bestellungen" + Fulfillment-Board ✅ fertig 16.07.2026 (a367d85) OrderFulfillmentController, OrderStatusService, shipped_status_locked; OrderFulfillmentTest (10)
AP-33b Sammel-Dokumente (Lieferscheine/Rechnungen als ein PDF) ✅ fertig 16.07.2026 (1252c30) Checkbox-Auswahl im Board, batchDocuments() via PDFMerger; OrderFulfillmentTest (14)
AP-33c Etiketten-Stapel ⏸ nachgelagert (mit AP-32) Ansatz in §4
AP-14 Audit-Trail ⏸ nachgelagert kein Package, keine Spur
AP-15 Blockbasierte Rechte ⏸ nachgelagert nur isAdmin()-Stufen
AP-16 2FA (Google Authenticator) ⏸ nachgelagert kein TOTP-Package in composer.json
AP-17 WaWi-Einstellungsseite ⏸ nachgelagert wawi-ui.blade.php ist nur das CSS-Partial

1.3 docs/Todos.md (03.06.2026) — Abgleich

Alle Punkte dieser älteren Runde sind in V4.0 eingeflossen und umgesetzt (URL-Felder inkl. Parameter-Validierung, Brutto/Netto-Rechner + UST-Dropdown im Einkauf, Einkauf duplizierbar, Lieferzeiten Lieferant/INCI, INCI-Lieferanten-Mehrfachauswahl, Produktions-Maske-Feinschliff, Produktbestand + Historie, Set-/Child-Produkte). Offen daraus nur noch: 2FA (= AP-16) und blockweise Rechtevergabe (= AP-15). Die Datei kann als „historisch/eingearbeitet" markiert werden.

1.4 Code-Qualitäts-Review (06.07.2026) — Kernaussagen

Positiv: Bestands- und Verbrauchsrechnungen laufen über aggregierte SQL-Queries (kein Zeilen-Looping), keine Queries in Blade-Views, Produktionsbuchung korrekt in DB::transaction() mit idempotentem Differenz-Abgleich. Die Architektur (Controller → Repository/Service) ist tragfähig.

Gefundene Schwachstellen (nach Impact):

# Befund Ort Impact
1 N+1 in Ausschuss-Übersicht: eager load user, aber getFullName() greift auf user.account → bis zu 500 Lazy-Loads StockDisposalController.php:27 + stock-disposals/index.blade.php:72 hoch
2 7× Copy-Paste-Settings-CRUD (TaxRate, DeliveryTime, DisposalReason, MaterialQuality, Location, SupplierCategory, PackagingMaterial — je ~50 nahezu wortgleiche Zeilen) app/Http/Controllers/Admin/Inventory/* hoch (Wartbarkeit; AP-21 würde 3 weitere Kopien erzeugen)
3 Index-Listen ohne Limit/Pagination: Wareneingänge und Produktionen laden die komplette Tabelle StockEntryRepository::listForIndex():128, ProductionRepository:13 mittel-hoch (Skalierung)
4 Fehlende Composite-Indizes für den Aggregations-Hotpath (entry_type, status, ingredient_id/packaging_item_id) auf stock_entries InventoryService.php:33-191 vs. Migration create_stock_entries_table mittel
5 Query-Logik im Controller + In-Memory-Preisermittlung: lastNetPricePerSupplier() lädt alle Eingänge eines Rohstoffs RawMaterialStockController::show():63-159 mittel
6 Kleinigkeiten: Horizont-Filter im Rohstoffbestand wirkt nur clientseitig (Service ist bereits parametrisierbar); ProductionRepository lädt producedByUser ungenutzt RawMaterialStockController:58, ProductionRepository:13 niedrig

2. Neuer Block: AP-29 — Code-Optimierung (OPT-1 … OPT-5)

Kleine, risikoarme Pakete. OPT-1 und OPT-2 kommen vor AP-21 (Begründung: OPT-2 liefert die Basis für die drei neuen Lagerort-CRUDs aus AP-21). OPT-3/4/5 laufen als Lückenfüller zwischen den Fach-APs.

OPT-1 — Quick Wins: N+1-Fix, Indizes, Test-Hygiene (0,5 Tage)

  • StockDisposalController@index: eager load user.account statt user (Muster: ProductStockController::history).
  • Migration: Composite-Indizes stock_entries(entry_type, status, ingredient_id) + stock_entries(entry_type, status, packaging_item_id); redundanten Index production_ingredients(production_id, ingredient_id) prüfen/entfernen.
  • ExampleTest reparieren (assertRedirect statt 200) — Suite dauerhaft 100 % grün.
  • Unnötigen Eager-Load producedByUser in ProductionRepository::listForIndex() entfernen.
  • Tests: Regression StockDisposalTest, RawMaterialStockTest, ProductionPhase5Test; Query-Count-Assertion für die Ausschussliste.

OPT-2 — Settings-CRUD entduplizieren (1 Tag)

  • Abstrakte Basisklasse AbstractSettingCrudController (oder Trait): Template-Methoden für Model-Klasse, View-Prefix, Redirect-Route, FormRequests; Kind-Controller schrumpfen auf ~10 Zeilen Konfiguration.
  • Umstellen: TaxRateController, DeliveryTimeController, DisposalReasonController, MaterialQualityController, LocationController, SupplierCategoryController, PackagingMaterialController.
  • Gemeinsames Blade-Partial für das Settings-Formular prüfen (Karten auf „Einstellungen → Allgemein" sind bereits ähnlich).
  • Akzeptanz: Verhalten unverändert (alle bestehenden Settings-Tests grün), neue CRUDs in < 30 Minuten anlegbar.
  • Tests: bestehende TaxRateSettingsTest, DeliveryTimeSettingsTest, DisposalReasonSettingsTest + je ein Smoke-Test der umgestellten Controller.

OPT-3 — Pagination der Index-Listen (1–1,5 Tage)

  • StockEntryRepository::listForIndex() und ProductionRepository::listForIndex(): serverseitige Pagination bzw. hartes Limit + „ältere anzeigen"; DataTables-Konfiguration entsprechend anpassen. Ausschuss- und Produktbestands-Historie (limit(500)) vereinheitlichen.
  • Akzeptanz: Seiten bleiben bei 10.000+ Einträgen unter 1 s; UI-Verhalten (Suche/Sortierung) bleibt erhalten.

OPT-4 — RawMaterialStockController::show entschlacken (0,5–1 Tag)

  • entries-Ladeblock, openOrders-Query und lastNetPricePerSupplier() nach InventoryService/StockEntryRepository verschieben.
  • Letzten Netto-Preis je Lieferant per gruppierter SQL-Query statt In-Memory-Schleife.
  • Sinnvoll unmittelbar vor AP-27/AP-28, weil die Chargen-Gruppierung dieselben Stellen anfasst.

OPT-5 — Horizont-Filter serverseitig (entfällt, Kundenentscheid 06.07.2026)

  • Kunde: clientseitige Filterung reicht vorerst. Falls später doch gewünscht: dailyConsumptionByIngredient($ids, $windowDays) ist bereits parametrisierbar → nur ?horizon=-Parameter im RawMaterialStockController@index durchreichen.

3. Priorisierte Roadmap V6.0

# AP Titel Abhängigkeit Aufwand
1 ✅ OPT-1 Quick Wins (N+1, Indizes, ExampleTest) — erledigt 06.07.2026 – 0,5 Tage
2 ✅ OPT-2 Settings-CRUD-Basisklasse — erledigt 06.07.2026 – 1 Tag
3 ✅ AP-21 INCI-Erweiterungen (Lieferant+URL-Repeater, „nur aktive", Alternativname, Lagerort + 3 Stammdaten-CRUDs) — erledigt 06.07.2026 OPT-2 2–3,5 Tage
4 ✅ AP-20 Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag — erledigt 06.07.2026 AP-21 1–2 Tage
5 ✅ AP-23 Rezeptur-Phasen (Hersteller-Rezeptur) — erledigt 06.07.2026 – 3–5 Tage
6 ✅ AP-24 Chargen-Nr.-Präfix — erledigt 06.07.2026 – 1 Tag
7 ✅ OPT-3 + OPT-4 Pagination + show()-Refactoring — erledigt 06.07.2026 – 1,5–2,5 Tage
8 ✅ AP-27 Wareneingang: Chargen-Merge — erledigt 06.07.2026 OPT-4 1–2 Tage
9 ✅ AP-28 Neue Produktions-Maske + Produktionsergebnis — erledigt 06.07.2026 AP-21, AP-23, AP-24, AP-27 5–8 Tage
10 ✅ AP-13 Shop-Bestandsabzug bei Versand (inkl. Sets) + API-Bestellungen-Protokoll — erledigt 16.07.2026 AP-28 (saubere Buchungslogik) ✅ 3–5 Tage
11 ✅ AP-33a Bestell-Abwicklung: Nav „Bestellungen" + Fulfillment-Board — erledigt 16.07.2026 AP-13 ✅ 2–3 Tage
12 ⏸ AP-32 Versand im System: DHL-Etiketten per API — nachgelagert nach Testphase (Kundenentscheid 16.07.2026) Kundenantworten (DHL-Konto, Versandprodukte, Druck) 3–5 Tage
13 ✅ AP-33b Sammel-Dokumente (Lieferscheine/Rechnungen als ein PDF) — erledigt 16.07.2026 AP-33a ✅ 1,5–2 Tage
13b ⏸ AP-33c Etiketten-Stapel + Automatik-Option — nachgelagert (mit AP-32) AP-32 1 Tag
14 ⏸ AP-14 Audit-Trail — nachgelagert – 2–3 Tage
15 ⏸ AP-16 2FA für Admins (Google Authenticator) — nachgelagert – 2–3 Tage
16 ⏸ AP-15 Blockbasierte Rechte (sehen/bearbeiten je Block) — nachgelagert AP-16 sinnvoll davor 4–6 Tage
17 ⏸ AP-17 WaWi-Einstellungsseite (Schwellwerte, Fenster, Defaults) — nachgelagert – 1–2 Tage

Blockabschluss 06.07.2026: Positionen 1–9 (OPT-1/2/3/4 + AP-21/20/23/24/27/28) sind vollständig umgesetzt und einzeln committet. Die Positionen 10–14 sind auf Kundenwunsch nachgelagert — vor der Umsetzung prüft der Kunde weitere Anforderungen, die dort mit einfließen sollen.

Begründung der Reihenfolge:

  • OPT-1/OPT-2 zuerst: minimales Risiko, sofortiger Nutzen, und AP-21 baut direkt auf OPT-2 auf (drei neue Lagerort-CRUDs Raum/Regal/Fach).
  • Danach unverändert die V5.0-Logik: kleine fachliche Ergänzungen (AP-21/20) → Datenmodell-Grundlagen (AP-23/24) → große Produktions-Maske (AP-28), der OPT-4 und AP-27 vorgelagert sind (gemeinsame Chargen-Gruppierung).
  • AP-13 direkt nach AP-28: dann existiert die endgültige Buchungslogik über production_outputs, und monthlyConsumptionByProduct() (wartet auf source='sale') wird automatisch korrekt.
  • Sicherheits-/Governance-Pakete (AP-14/15/16/17) zum Schluss als eigener Block — fachlich unabhängig, gut geschlossen lieferbar.

Gesamtaufwand offen: grob 28–42 Personentage.


4. Arbeitspakete im Detail

Die fachlichen Detailspezifikationen zu AP-20, AP-21, AP-23, AP-24, AP-27, AP-28 stehen unverändert und weiterhin gültig in entwicklungsplan-aktualisiert-12-06-2026.md (§4 + §6) — dort sind DB-Schema, Dateipfade, Akzeptanzkriterien und Tests je AP vollständig beschrieben. Die OPT-Pakete sind in §2 dieses Dokuments spezifiziert. Für AP-13 gilt das Konzept aus V4.0 (entwicklungsplan-aktualisiert-02-06-2026.md), mit einer Änderung: Buchung gegen production_outputs-basierte Bestände (nach AP-28).

AP-13 — Umsetzungsansatz (ergänzt 15.07.2026 nach Sichtung des WooCommerce-Plugins)

Das in den Partner-Shops installierte Plugin „PARTNERCENTER API Connector" v1.3.3 ist als Referenz eingecheckt (dev/partnercenter-api/, Commit 592b03a). Die Sichtung ergibt: Für den Shop-Bestandsabzug ist kein Plugin-Update und keine neue API-Route nötig — alle benötigten Daten liegen zum Versandzeitpunkt bereits im Partnercenter.

So arbeitet das Plugin heute (verifiziert im Code):

Auslöser Richtung Endpoint Inhalt
Automatisch bei neuer Bestellung (woocommerce_new_order-Hook, auch Admin-Anlage) WP → PC wp/store Kunde + Rechnungs-/Lieferadresse (ohne Warenkorb)
Automatisch bei Adressänderung (Hash-Vergleich in woocommerce_update_order) WP → PC wp/update geänderte Adressen
Manuell per Button „Send to PARTNERCENTER for fulfillment" (Metabox in der WooCommerce-Bestellansicht) WP → PC wp/order Warenkorb (SKU + Menge + Preis) inkl. Rechnungs-Link; SKU-Matching hier über products.wp_number
Manuell per Button „Retrieve" (nur solange Status open) WP → PC wp/cancel Bestellung zurückziehen (serverseitig gegen versendete Bestellungen gesperrt)
Stündlicher WP-Cron WP → PC wp/status fragt Versandstatus ab und schreibt ihn in die WooCommerce-Bestellung

Der Warenkorb kommt also per Knopfdruck (Fulfillment-Übergabe), nicht automatisch — die Kundendaten dagegen automatisch. Das Versandereignis selbst findet im Partnercenter statt (SalesController::store, Aktion store_shipped, setzt shipped_at und Provisionsfreigaben); das Plugin holt den Status nur ab.

Daraus folgt das AP-13-Design (reiner Serverseiten-Hook):

  1. Abzug beim erstmaligen Setzen von shipped_at (Statuswechsel auf „versendet"/„abgeschlossen" in SalesController::store): je ShoppingOrderItem Abgang mit source='sale' ins bestehende Ledger product_stock_movements; Sets werden über die Rezeptur in Einzelprodukte aufgelöst. monthlyConsumptionByProduct() wertet source='sale' bereits aus.
  2. Gegenbuchung bei Storno (Status 10) einer bereits versendeten Bestellung — einziger Weg dafür ist der Admin, der API-Storno ist gegen Versendetes gesperrt.
  3. Gilt automatisch für alle Bestellquellen (WooCommerce-Fulfillment, Whitelabel-Shop, manuelle Anlage), da alle als ShoppingOrder durch dieselbe Versand-Logik laufen.
  4. Bewusste Lücke (fachlich korrekt): Self-Shipping-Bestellungen der Partner übertragen keinen Warenkorb und erzeugen keinen Abzug — der Partner versendet aus eigenem Bestand, das Partnercenter-Lager wird nicht berührt. ✅ Kundenentscheid 16.07.2026: bestätigt — nur was per Knopfdruck (Fulfillment-Übergabe) übertragen wird, zählt fürs Partnercenter-Lager.
  5. Mitzulösen: (a) unbekannte SKUs sichtbar machen — heute geht „article not found" nur in der API-Antwort unter, die Position fehlt dann still in Bestellung und Abzug; (b) wp_number-Konsistenzprüfung (aktive Produkte ohne/mit doppelter SKU).

✅ Kundenentscheid 15.07.2026: Ansatz bestätigt — so umsetzen.

✅ Umgesetzt 16.07.2026 (Protokoll §6): Punkte 1–4 wie beschrieben; Punkt 5a als eigener Menüpunkt „API-Bestellungen" (Kundenanforderung 16.07.: Protokoll aller API-Bestellungen, Fehlermarkierung, Modal mit Bestellinhalt), Punkt 5b als SKU-Konsistenzprüfung auf derselben Seite. Zusätzlich abgesichert: Gegenbuchung auch beim Auto-Storno der Zahlungserinnerung und beim Zurücksetzen des Versandstatus (Doppelabzug-Risiko behoben).

AP-32 — Versand im System: DHL-Etiketten per API (neu aufgenommen 15.07.2026)

Kundenanforderung 15.07.2026: Der Versand soll vollständig im Partnercenter abgewickelt werden — nicht nur der Statuswechsel per Button, sondern am Ende auch die Erstellung der Versandetiketten über die DHL-API.

Zielbild: Aus der Bestellansicht heraus wird das DHL-Label per API erzeugt (PDF zum Druck), die Sendungsnummer an der Bestellung gespeichert und der Status auf „versendet" gesetzt — dieser Statuswechsel ist derselbe Andockpunkt wie der AP-13-Bestandsabzug, beides greift dann in einem Schritt. Perspektivisch kann die Sendungsnummer über den bestehenden wp/status-Poll auch an WooCommerce zurückgegeben werden (Tracking-Link für den Endkunden; kleines Plugin-Update, optional).

Technische Eckpunkte:

  • DHL „Parcel DE Shipping" API (REST, v2) — Nachfolger der SOAP-Geschäftskundenversand-API; benötigt ein DHL-Geschäftskundenkonto (Geschäftskundenportal), API-Key über das DHL-Entwicklerportal, Sandbox vorhanden. Label anlegen + stornieren, PDF in gängigen Druckformaten.
  • Datenlage im System ist gut: Lieferadresse (ShoppingUser/ShoppingOrder), Gewicht je Bestellung (shopping_orders.weight, wird beim Checkout aus den Produktgewichten berechnet) und Länderkennung sind vorhanden.
  • Neue Felder an shopping_orders: Sendungsnummer, Label-Referenz/Storno-Referenz, Versandprodukt; Label-PDF als Download/Druck aus der Bestellansicht.
  • Länder: DE/FR/NL sind EU-Standard; CH ist Drittland → Zolldaten (Inhaltsangaben, Warenwert, ggf. CN23) müssen mit übergeben werden — eigener Fall in der Umsetzung.

Vor Umsetzung mit dem Kunden zu klären (nächste Anforderungsrunde):

  1. DHL-Geschäftskundenkonto vorhanden? (EKP-Nummer + Abrechnungsnummern je Versandprodukt)
  2. Welche Versandprodukte? (DHL Paket, Warenpost/Kleinpaket; national/international)
  3. Standard-Paketmaße/-gewichte oder je Bestellung manuell?
  4. Druck-Setup (Format A4 vs. Etikettendrucker 910-300-700)
  5. Retourenlabel mit erzeugen? (separates DHL-Produkt „Retoure")

Aufwand: 3–5 Tage (ohne Retourenlabel). Reihenfolge: direkt nach oder gemeinsam mit AP-13 — gleicher Andockpunkt am Versand-Statuswechsel, gemeinsamer Test-Aufbau.

Geklärte Punkte mit dem Kunden (beantwortet 06.07.2026)

  1. Lagerort-Modell (AP-21): ✅ drei getrennte Stammdaten-Tabellen (Raum/Regal/Fach) — wie empfohlen.
  2. Chargen-Nr.-Datumsformat (AP-24): ✅ TP1 + ddmmyy bestätigt (Beispiel TP1150626 = 15.06.26). Bei AP-24-Umsetzung in der Hinweise-Doku (AP-18) hinterlegen.
  3. „Verbrauch/Monat" (AP-22): ✅ rollierendes 6-Monats-Fenster bleibt wie umgesetzt.
  4. Produktionsergebnis (AP-28): ✅ Output-Produkte dürfen andere Produkte sein als das geplante (z. B. 40×50 ml + 50×5 ml).
  5. Horizont-Filter Rohstoffbestand (OPT-5): ✅ vorerst nur clientseitige Filterung → OPT-5 entfällt.

AP-33 — Bestell-Abwicklung: Nav-Punkt „Bestellungen" mit Fulfillment-Board (neu aufgenommen 16.07.2026)

Kundenanforderung 16.07.2026: Eigener Navigationspunkt „Bestellungen" in der Warenwirtschaft mit zwei Unterpunkten: „Abwicklung" (neu — kompakte Tagesübersicht für die Bestellabwicklung) und „API-Bestellungen" (bestehend, wandert als Unterpunkt hinein). Das Board dient dem Mitarbeiter zur täglichen Abarbeitung: sehen, was zu konfektionieren ist und wo Etiketten fehlen, Zugriff auf Rechnung/Lieferschein, Etikett erstellen, per Klick auf „versendet" setzen — kompakter als die bestehende Verkäufe-Seite. Perspektivisch: Etiketten für bestätigte Bestellungen gesammelt als PDF erzeugen/drucken, dazu Lieferscheine und Rechnungen.

Vorhandene Bausteine (verifiziert 16.07.2026):

  • Rechnung und Lieferschein entstehen heute schon gemeinsam in InvoiceRepository::create() (DomPDF-Templates resources/views/pdf/invoice.blade.php + delivery.blade.php, Nummernkreis im Invoice-Service). Download-Routen existieren im FileController (Disks invoice, delivery, invoice_delivery = kombiniert).
  • Statuskette deckt den Prozess ab: 0 offen (= zu konfektionieren) → 1 in Bearbeitung (= gepackt/versandfertig) → 2 versendet / 3 abgeschlossen; 4 Abholung, 5 Wartestellung, 10 storniert. Der Statuswechsel ist seit AP-13 gehärtet (Transaktion + Lock) und bucht den Bestandsabzug — das Board erbt das kostenlos.
  • setasign/fpdi ist installiert → Zusammenführen mehrerer PDFs (Sammel-Druck) ist ohne neues Package machbar.
  • AP-32 (DHL) hat denselben Andockpunkt; „Etikett erstellen → Sendungsnr. speichern → versendet setzen" wird eine Aktion.

Umsetzung in drei Teilpaketen:

AP-33a — Abwicklungs-Board (2–3 Tage, sofort nutzbar ohne DHL):

  • Sidenav: „Bestellungen" mit Unterpunkten „Abwicklung" + „API-Bestellungen" (bestehende Route zieht um, Redirect/aktiver Menüpfad beachten).
  • Neue Seite admin/inventory/orders (WaWi-Design AP-19): Standardfilter Status offen + in Bearbeitung, Umschalter Zeitraum (heute/7 Tage/alle offenen), Quelle (API/Whitelabel/manuell), Zahlstatus.
  • 4 Status-Kacheln als Filter: Heute eingegangen / Zu konfektionieren / Versandfertig / Heute versendet (Muster Produktbestand AP-22).
  • Kompakte Zeile je Bestellung: Nr. + Quelle-Badge, Datum, Kunde + Land, Positionen aufklappbar als Packliste (Menge × Produkt, Sets mit Komponenten), Gewicht, Zahlstatus-Pill, Dokumente (Rechnung/Lieferschein: vorhanden ⇒ Download-Icons inkl. Kombi-PDF, fehlt ⇒ Button „erstellen" über bestehendes InvoiceRepository::create()), Aktionen: „Konfektioniert" (→ Status 1), „Etikett" (Platzhalter bis AP-32), „Versendet" (→ Status 2, mit Confirm; löst AP-13-Abzug aus).
  • Rechte wie Verkäufe: admin ≥ 7 für Statuswechsel/Dokumente; Sichtbarkeit des Nav-Punkts entsprechend.
  • Tests: Board-Render + Filter, Statuswechsel über die bestehende gehärtete Route, Dokument-Erzeugung fehlender Rechnungen.

AP-33b — Sammel-Dokumente (1,5–2 Tage, nach AP-33a):

  • Checkbox-Auswahl im Board + Aktion „Dokumente drucken": ein zusammengeführtes PDF (FPDI) aus Lieferscheinen + optional Rechnungen der gewählten Bestellungen; fehlende Dokumente werden dabei erzeugt. Download als eine Datei zum Stapeldruck.
  • Gleiches Muster vorbereitet für Etiketten-Sammel-PDF (kommt mit AP-32).

AP-33c — Etiketten-Integration & Automatik (mit/nach AP-32):

  • Je Zeile „Etikett erstellen" (DHL-API aus AP-32): Label-PDF + Sendungsnr. an der Bestellung, danach Ein-Klick „Versendet" (oder kombiniert „Etikett + versendet").
  • Sammel-Aktion „Etiketten für Auswahl erstellen" ⇒ ein PDF mit allen Labels.
  • Empfehlung zur Vollautomatik („bei bestätigter Bestellung automatisch Etikett"): zunächst halbautomatisch lassen (Sammel-Aktion mit einem Klick für alle versandfertigen Bestellungen). Begründung: jedes fehlerhafte Label muss kostenpflichtig storniert werden, Gewicht/Versandprodukt und Adressqualität müssen je Bestellung stimmen, und „bestätigt" ist noch nicht definiert (bezahlt? txaction?). Auto-Modus später als Option auf der WaWi-Einstellungsseite (AP-17), sobald der DHL-Prozess stabil läuft.

Kundenentscheide 16.07.2026 (alle vier Klärungspunkte beantwortet):

  1. „Versandfertig" = bezahlte Bestellung. Zahlart Rechnung gilt sofort als bezahlt (Versand vor Zahlungseingang); Vorkasse erst bei Geldeingang; API-Bestellungen gelten immer als bezahlt (Zahlung liegt im externen Shop).
  2. Rechnungen halbautomatisch (per Klick/Sammel-Aktion, keine Vollautomatik). Rechnungen werden nur für direkt im Partnercenter gekaufte Bestellungen erstellt. API-Bestellungen bekommen keine Rechnung im System — die Rechnung existiert im externen Shop; der übertragene Link (wp_invoice_path) wird im Board verlinkt.
  3. Auto-Wechsel „offen → in Bearbeitung" beim Öffnen der Detailseite bleibt vorerst — aber: wurde ein Status über die Warenwirtschaft (Abwicklungs-Board) gesetzt, überschreibt die Detailseite ihn nicht mehr. Umsetzung in AP-33a über ein Sperr-Flag an der Bestellung (Detailseiten prüfen das Flag vor dem Auto-Wechsel). ⚠️ Dokumentationspflichtig (Kundenwunsch: „nicht vergessen") — auch in der Hinweise-Doku hinterlegen.
  4. Rechte: auch Versand-Mitarbeiter dürfen abwickeln → Board-Routen in der copyreader-Gruppe. Die Board-Statusaktionen sind bewusst auf Vorwärts-Schritte beschränkt (konfektioniert → versendet); Storno/Rücksetzung bleibt der Verkäufe-Seite (admin ≥ 7) vorbehalten.
  5. Etiketten-Vollautomatik: halbautomatische Lösung (Sammel-Aktion) bestätigt — „machen wir erst mal so".

Aufwand gesamt: 4–6 Tage (ohne AP-32). Reihenfolge: AP-33a sofort umsetzbar; AP-32 sobald DHL-Klärungen da sind; AP-33b/c dazwischen bzw. danach.


5. Pflege dieses Dokuments

  • Jedes abgeschlossene AP im Umsetzungsprotokoll (§6) mit Datum, Dateien und Test-Status protokollieren.
  • UI-Konventionen unverändert: Design-System resources/views/admin/inventory/partials/wawi-ui.blade.php (AP-19), Datumsfelder datepicker-base (dd.mm.yyyy), Status über wawi-pill.
  • Vor jedem Commit: vendor/bin/pint --dirty + betroffene Tests.
  • docs/Todos.md ist vollständig eingearbeitet (Rest: AP-15/AP-16) und kann archiviert werden.

6. Umsetzungsprotokoll V6.0 (laufend)

Datum AP Kurzbeschreibung Tests
06.07.2026 OPT-1 (Quick Wins) N+1-Fix Ausschussliste: StockDisposalController@index lädt jetzt user.account eager (vorher user → bis zu 500 Lazy-Loads durch getFullName()). Composite-Indizes: Migration 2026_07_06_113731_add_aggregation_indexes_to_stock_entries_table — stock_entries(entry_type, status, ingredient_id) + (entry_type, status, packaging_item_id) für die Aggregations-Queries im InventoryService; auf DB ausgeführt. Befund-Korrektur: Der Index production_ingredients(production_id, ingredient_id) ist nicht redundant — er bedient den FK auf production_id mit (kein separater FK-Index vorhanden, per SHOW INDEX verifiziert) → bleibt bestehen. Ungenutzter Eager-Load producedByUser aus ProductionRepository::listForIndex() entfernt (wird nur in show gebraucht, dort bereits via producedByUser.account geladen). ExampleTest auf assertRedirect() umgestellt (Domain-Redirect auf /) — Suite dauerhaft 100 % grün. Neuer Query-Count-Test ausschuss-liste laedt benutzerkonten ohne n+1 in StockDisposalTest (verifiziert: schlägt ohne Fix fehl). Volle Suite: 155 grün, 538 Assertions
06.07.2026 OPT-2 (Settings-CRUD-Basisklasse) Neue abstrakte Basisklasse app/Http/Controllers/Admin/Inventory/SettingCrudController.php: generisches index/create/store/edit/update/destroy; Kind-Controller definieren nur noch modelClass(), viewPrefix(), redirectRoute(), storeRequestClass() (+ optional newModelAttributes(), indexOrder(), updateRequestClass()). Validierung per Container-Auflösung des FormRequests (Verhalten wie Method-Injection), Model-Auflösung aus dem Routen-Parameter via findOrFail (404-Verhalten unverändert). Umgestellt: TaxRate, DeliveryTime, DisposalReason, MaterialQuality, Location, SupplierCategory, PackagingMaterial (je ~50 → ~25 Zeilen reine Konfiguration). Die 7 Update*Request-Klassen waren mit den Store-Requests identisch und wurden entfernt (Update nutzt den Store-Request; bei künftiger Abweichung updateRequestClass() überschreiben). Form-Views bleiben bewusst je Entität (unterschiedliche Felder). Bilanz: −441/+73 Zeilen. Neue Settings-CRUDs (z. B. Raum/Regal/Fach in AP-21) = 1 Model + 1 Store-Request + ~25-Zeilen-Controller. Volle Suite 155 grün, 538 Assertions (inkl. TaxRateSettingsTest, DeliveryTimeSettingsTest, DisposalReasonSettingsTest, InventoryPhase2Test, InventoryPhase3Test)
06.07.2026 AP-21 (INCI-Erweiterungen) DB: Migrationen 2026_07_06_120752_create_storage_segment_tables (storage_rooms/storage_shelves/storage_slots, je label/active/pos) + 2026_07_06_120752_add_storage_and_alt_name_to_ingredients_table (ingredients.alt_name + 3 FKs storage_room/shelf/slot_id, nullOnDelete); auf DB ausgeführt. Models: abstrakte Basis StorageSegment + StorageRoom/StorageShelf/StorageSlot (+ Factories); Ingredient um alt_name, Lagerort-FKs, Relationen, storageLabel() („Raum 1 | Regal 3 | A") und scopeUsedInActiveRecipes() (Hersteller-Rezeptur ∧ aktives Produkt) erweitert. Settings: 3 CRUDs als SettingCrudController-Kinder (OPT-2, je ~25 Zeilen) mit einem gemeinsamen StoreStorageSegmentRequest; 3 Karten auf Einstellungen → Allgemein über neues generisches Partial setting-list-card; gemeinsames Form-Partial setting-segment-form; Routen-Resources in der superadmin-Gruppe. INCI-Formular: Lieferanten-Repeater (Zeile = Lieferant-Select + URL-Feld, „+ weiteren Lieferanten anlegen", Entfernen-Button; jQuery-Template) statt Multi-Select — speichert ingredient_supplier.url per sync() (URL leer ⇒ null ⇒ Mailbestellung; preferred/supplier_sku bleiben erhalten); Feld „Alternativer Name"; 3 Lagerort-Dropdowns (nur aktive Bausteine). INCI-Liste: Checkbox „nur aktive (in Rezeptur aktiver Produkte)" (?only_active=1, serverseitig via Scope) + Spalte „Alternativer Name". Rohstoffbestand-Detail: Bestellweg jetzt pro INCI-Lieferant — Pivot-URL gesetzt ⇒ „Zum Shop", leer ⇒ „Per Mail" (ersetzt order_method-Fallback); Kopf zeigt Alternativname + Lagerort. Hinweise-Doku (AP-18) fortgeschrieben inkl. Kundenentscheid Chargen-Format ttmmjj (für AP-24). StorageSettingsTest (7), IngredientActiveRecipeFilterTest (3), IngredientOrderFieldsTest (9, umgestellt auf Repeater-Format + URL/Lagerort), RawMaterialStockTest +2 (Bestellweg, Alternativname/Lagerort). Volle Suite: 170 grün, 613 Assertions
06.07.2026 AP-20 (Rohstoffbestand-Filter + „Kein Rohstoffbestand") Migration 2026_07_06_133714_add_exclude_from_stock_to_ingredients_table (ingredients.exclude_from_stock bool default 0); Ingredient fillable+cast; Checkbox „Kein Rohstoffbestand" (Hinweistext Allergen-Angaben) im INCI-Formular. RawMaterialStockController@index: grundsätzlich exclude_from_stock=false, neuer Umschalt-Modus ?scope=all|recipe (Default all; recipe nutzt scopeUsedInActiveRecipes() aus AP-21); Button-Gruppe „Alle / Nur aus Herstellerrezepturen" in der Toolbar. InventoryService::criticalIngredientCount() (Sidenav-Badge) respektiert das Flag. Hinweise-Doku fortgeschrieben. Nebenbei behoben: die beiden AP-21-Migrationen hatten denselben Timestamp, wodurch add_storage… alphabetisch vor create_storage… lief und auf MySQL am FK scheiterte (Teilreste alt_name/storage_room_id) — Migration auf …120753 umbenannt, Teilreste bereinigt, alle drei Migrationen sauber ausgeführt. RawMaterialStockTest +2 (Flag blendet aus Übersicht + Kritisch-Zähler aus; Modus recipe filtert, Default zeigt alle, Pills rendern). Volle Suite: 172 grün, 624 Assertions
06.07.2026 AP-23 (Rezeptur-Phasen) DB: Migration 2026_07_06_134518_create_recipe_phases_table — Tabelle recipe_phases (product_id FK cascade als unsignedInteger — products.id ist historisch int, kein foreignId!; recipe_type default manufacturer, name, note nullable, pos) + product_ingredients.recipe_phase_id (nullable FK, nullOnDelete); auf DB ausgeführt. Abweichung zur V5.0-Spec: kein Unique auf (product_id,recipe_type,name) — frei editierbare Namen sollen beim Speichern nicht crashen; Duplikate sind fachlich unkritisch. Models: RecipePhase (+ Product::recipePhases(), ProductIngredient::phase(), Pivot um recipe_phase_id erweitert). Formular (admin/product/form.blade.php + JS in edit.blade.php): Hersteller-Rezeptur-Card in Phasen-Blöcke umgebaut — je Block editierbarer Name (Platzhalter Phase A/B … aktualisiert sich), Rohstoff-Tabelle (Anteil %/Faktor/Effektiv wie bisher), Phasen-Notiz, „+ Rohstoffe hinzufügen" je Phase (gemeinsames Modal mit Ziel-Phase), „+ Neue Phase" (JS-Template), Phase entfernen (mit Warnung bei Zeilen); Drag&Drop: Phasen untereinander + Zeilen innerhalb und zwischen Phasen (Sortable group); 100-%-Summe global über alle Phasen (Anzeige grün/rot wie bisher); mfg_row_phase[]-Sync bei Drag/Add/Remove/Submit. Repository: updateManufacturerIngredients() persistiert Phasen (persistRecipePhases(): leerer Name ⇒ Auto „Phase A/B…", einzelner unbenannter Block ohne Notiz ⇒ phasenlos wie bisher, recipe_phase_id=NULL); copy() kopiert Phasen inkl. Zeilen-Zuordnung (ID-Mapping). Produktion (requiredGramsByIngredient) rechnet unverändert über alle Zeilen. Blade-Falle behoben: inline @php(…) (AP-25-Verfügbarkeits-Card) + neuer @php…@endphp-Block im selben Template ließen den Compiler beide falsch paaren (Raw-Block von Z. 106 bis 771) — inline-Variante auf Block-Form umgestellt. Hinweise-Doku fortgeschrieben. Neuer tests/Feature/RecipePhaseTest.php (6 grün, 25 Assertions): Phasen+Zeilen+Notiz+Reihenfolge, Auto-Name, implizit phasenlos, Re-Save ersetzt vollständig, Kopie inkl. Zuordnung, Produktion phasenunabhängig. Volle Suite: 178 grün, 649 Assertions
06.07.2026 AP-24 (Chargen-Nr.-Präfix) Migration 2026_07_06_140324_add_batch_prefix_to_products_table (products.batch_prefix VARCHAR(20) nullable); auf DB ausgeführt. Product fillable + Helper suggestedBatchNumber(?Carbon $date) = Präfix + Datum ttmmjj (Beispiel TP1 + 15.06.26 ⇒ TP1150626; ohne Präfix ⇒ null; Default-Datum heute). Eingabefeld „Chargen-Nr.-Präfix" in der Warenwirtschaft-Card des Produktformulars (Hilfetext mit Beispiel); ProductRepository::update() trimmt (leer ⇒ null). Vorbelegung des editierbaren Chargen-Nr.-Felds folgt in AP-28 (Produktionsergebnis). Chargen-Format war bereits in der Hinweise-Doku dokumentiert (AP-21-Lieferung). Neuer tests/Feature/BatchPrefixTest.php (6 grün): Speichern/Trim, leer ⇒ null, ttmmjj-Zusammensetzung, Default heute, ohne Präfix null, Formular-Render. Volle Suite: 184 grün, 657 Assertions
06.07.2026 OPT-3 + OPT-4 OPT-3 (Index-Limits): StockEntryRepository::listForIndex(?int $receivedLimit = 500) — offene Bestellungen immer vollständig, Eingegangene auf 500 gedeckelt (neueste zuerst, stabile Sekundärsortierung nach id); ungenutzte Eager-Loads orderedByUser/receivedByUser entfernt (nur show braucht sie). ProductionRepository::listForIndex(?int $limit = 500) analog. Beide Controller unterstützen ?all=1; die Views zeigen bei erreichtem Limit einen Hinweis mit „Alle anzeigen"-Link. OPT-4 (show() entschlackt): Chargen-Restmengen (chargesWithRemainingForIngredient(), FEFO) und offene Bestellungen (openOrdersForIngredient()) in den InventoryService verschoben (Konstruktor-Injection ProductionService, kein Zyklus); lastNetPricePerSupplier() in StockEntryRepository als eine Window-Function-Query (ROW_NUMBER() OVER (PARTITION BY supplier_id …), läuft auf MySQL 8 + SQLite) statt In-Memory-Schleife über alle Einkäufe. RawMaterialStockController::show() ist nur noch Orchestrierung. RawMaterialStockTest +2 (letzter Netto-Preis je Lieferant aus neuestem Einkauf; Liste deckelt Eingegangene, Offene vollständig, null = alle). Volle Suite: 186 grün, 663 Assertions
06.07.2026 AP-27 (Wareneingang: gleiche Charge zusammenführen) Grundprinzip wie in §4 V5.0 bestätigt: jeder Einkauf bleibt ein eigener stock_entry (Preis/Datum/Audit) — zusammengeführt werden nur Anzeige und Restbestand je batch_number. Gruppierungsschlüssel: StockEntry::batchGroupKey() (b:<batch_number>, Einträge ohne Chargennummer bleiben einzeln als e:<id>). Existenz-Check: neuer JSON-Endpoint GET api/stock-entries/batch-check (StockEntryController::batchCheck, Route admin.inventory.api.stock-entries.batch-check; Parameter ingredient_id+batch_number+optional exclude) liefert exists/purchases/remaining/best_before/location/quality_id; die Buchen-Maske (Stufe 2, stock-entries/show) prüft beim Tippen (debounced) und zeigt „Charge bereits im Haus — Menge wird dieser Charge zugerechnet" inkl. Rest/Lagerort/MHD; MHD + Qualität werden vorbelegt (nur wenn leer, überschreibbar). Rohstoffbestand-Detail: InventoryService::chargeGroupsForIngredient() (baut auf chargesWithRemainingForIngredient() aus OPT-4, FEFO bleibt) — Chargen-Tabelle zeigt je Charge eine Zeile mit Summen-Rest, frühestem MHD und Badge „x Einkäufe"; Einzel-Einkäufe als eingerückte Unterzeilen („Einkauf vom …"). Produktion: Recipe-Payload je Rohstoff um batches erweitert (ProductionService::batchOptionsForEntries(), eine Option je Charge mit Summen-Rest; stock_entries bleibt für Audit/Kompatibilität); Charge-Dropdown im Formular listet Chargen statt Einzel-Einkäufe (ingredient_lines[i][batch_key]); StoreProductionRequest akzeptiert batch_key oder stock_entry_id (required_without); ProductionService::expandBatchLines() löst Chargen-Zeilen FEFO auf die Einzel-Einkäufe auf (Überhang auf letzten Eintrag — kein neuer Hardstop, konsistent zum Bestandsverhalten), Controller ruft sie vor store/updateProduction auf (bei Update mit exclude_production); Service-API unverändert (store()/updateProduction() weiter je stock_entry_id — bestehende Tests/Aufrufer unberührt). Edit/Copy fassen bestehende Zeilen je Charge zusammen (Summe je batch_key). Hinweise-Doku fortgeschrieben. Neuer tests/Feature/StockEntryChargeMergeTest.php (7 grün, 35 Assertions): Gruppierung + separate Einkäufe, Endpoint (Treffer/Fehlschlag), Detailansicht summiert + Unterzeilen, Payload-batches, FEFO-Split, Passthrough + unbekannte Charge, HTTP-Produktion bucht auf 2 Einkäufe. Volle Suite: 193 grün, 698 Assertions
06.07.2026 AP-28 (Neue Produktions-Maske + Produktionsergebnis) DB: Migration 2026_07_06_153907_create_production_outputs_table — Tabelle production_outputs (production_id FK cascade als foreignId — productions.id ist bigint; product_id als unsignedInteger+foreign — products.id historisch int!; quantity UINT, batch_number VARCHAR(100) nullable, pos; Index (production_id, pos)) inkl. idempotentem Backfill (Bestandsproduktionen ⇒ je eine Output-Zeile aus geplantem Produkt + Stückzahl, NOT EXISTS-Guard); auf DB ausgeführt (3 Produktionen backgefüllt). Buchung: ProductStockService::recordProductionStock() bucht jetzt je Output-Produkt (Soll = Summe production_outputs.quantity je product_id, idempotenter Differenz-Abgleich, append-only; entfernte Output-Produkte werden per Gegenbuchung auf null zurückgeführt) — die geplante Stückzahl bucht nichts mehr. Service: ProductionService::store()/updateProduction() um outputs[] erweitert (4. Parameter, Default [] ⇒ Fallback auf geplantes Produkt + Stückzahl mit suggestedBatchNumber(produced_at) — hält Alt-Aufrufer/Tests stabil und entspricht der Backfill-Semantik); persistOutputs() validiert (Produkt existiert, kein Set, Menge ≥ 1), Update ersetzt Outputs vollständig. HTTP: StoreProductionRequest verlangt outputs (≥ 1 Zeile; product_id exists, quantity ≥ 1, batch_number nullable). Payload: buildRecipePayload() liefert zusätzlich product.batch_prefix, phases[] (AP-23) und je Rohstoff alt_name/quality/storage_label/phase_id (Relationen eager, kein N+1). Maske (_form_fields + _scripts): Planungs-Stückzahl umbenannt („Geplante Stückzahl (Planung)" + Hinweis „bucht keinen Bestand"), INCI-Liste (Name, Alternativname, Qualität, Anteil %, Gramm gesamt, Regal, Summenzeile), Chargen-Zuordnung nach Phasen gruppiert (Phasen-Block mit Name + Notiz; Rohstoffe ohne Phase in eigenem Block), Status-Ampel je Rohstoff (grün = Summe ≈ Soll ± 0,02 g), Produktionsergebnis-Repeater (Produkt-Select, Stückzahl, Chargen-Nr. mit Auto-Vorbelegung Präfix+ttmmjj aus Produktionsdatum — folgt Produkt-/Datumswechsel bis zur manuellen Eingabe; Default-Zeile spiegelt die Planung, bis sie angefasst wird; old()/Edit/Copy-Vorbelegung). Blade-Falle erneut: ein Kommentar mit literalem @endphp im @php-Block beendete den Block vorzeitig — Kommentartext angepasst. Show: neue Karte „Produktionsergebnis"; Planung als solche gekennzeichnet. Index: Spalte „Ergebnis" (Outputs summiert, Fallback Planung). Hinweise-Doku fortgeschrieben. Neuer tests/Feature/ProductionOutputTest.php (8 grün, 32 Assertions): Mehrfach-Outputs buchen je Produkt / Planung bucht nichts, Service-Fallback inkl. Chargen-Nr., Differenz-Korrektur + Entfernen ⇒ Rückbuchung, Rohstoffverbrauch bleibt chargenbasiert, HTTP-Pflicht ≥ 1 Output, Set/Menge-Validierung, Payload-Felder, Masken-Render. Zwei bestehende HTTP-Tests um outputs ergänzt. Volle Suite: 201 grün, 731 Assertions
15.07.2026 Bugfix Layout Doppeltes @yield('scripts') in layout-2.blade.php entfernt (seit 9ce711d rendert das Eltern-Layout application.blade.php:145 die Section zusätzlich) — alle Seiten-Skripte liefen doppelt, sichtbar u. a. als „Weitere Charge" legt 2 Zeilen an (Kundenmeldung 15.07.). Commit d8d4a9b. Produktions-/Chargen-Tests grün
15.07.2026 AP-31 (Löschen: Soft-Delete + Abhängigkeitsprüfung) Kundenanforderung 15.07.: nahezu alle WaWi-Einträge löschbar. Migration 2026_07_15_100000 (deleted_at auf 14 Tabellen) + SoftDeletes-Trait in den Models. Neuer App\Services\DeletionGuard: zentrale Abhängigkeitsprüfung — referenzierte Einträge nicht löschbar, Fehlermeldung nennt die Verwendungen (Stammdaten: TaxRate/MaterialQuality/Location/SupplierCategory/PackagingMaterial/StorageSegmente; Lieferanten/Verpackungsartikel/INCI; Wareneingänge). Wareneingänge: auch gebuchte löschbar ohne Verbrauch/Ausschuss. Ausschuss: löschbar, Menge wieder verfügbar. Produktionen: löschbar (admin ≥ 7), Produktbestand per Gegenbuchung zurück (reverseProductionStock), Verbrauchs-Queries schließen gelöschte Produktionen aus. Bestätigungswarnung vor jeder Löschung (deutsche Confirm-Texte). INCI-Löschroute GET→DELETE. Nebenbei: Debug-Logs PackagingItemController entfernt, lastNetPricePerSupplier-Global-Scope-Fix. Behebt Audit-Befunde M-4, M-6 (teilw.), N-16 (teilw.). Commit 35b9076. Neuer InventoryDeletionTest (10 grün), StorageSettingsTest angepasst. Volle Suite: 214 grün, 783 Assertions
15.07.2026 OPT-6 (Bestandswahrheit) Audit H-1/M-5: Chargen-Restmengen ziehen Ausschuss ab (consumedByStockEntry inkl. chargen-bezogener Disposals) — Detailseite, Produktions-FEFO und Batch-Check konsistent zur Index-Aggregation; Kopfwert der Detailseite aus remainingByIngredient(); Aggregationen über Key-Vereinigung (negative Bestände sichtbar); Standort-Zurechnung über Chargen-Location. Commit 8445038. ChargeDisposalConsistencyTest (5 grün)
15.07.2026 OPT-7 (Buchungs-Härtung) Audit H-2/H-3/M-1/M-9: quantity_used > 0 erzwungen (Request + Service); expandBatchLines() dekrementiert Restmengen (kein Doppel-Verbrauch derselben Charge); Ausschuss validiert Chargen-Zugehörigkeit + Restbestand; lockForUpdate bei Produktions-Update/-Löschung. Commit c69721d. ProductionHardeningTest (4 grün)
15.07.2026 OPT-8 (Rezeptur-Robustheit) Audit H-4/H-5/M-8/N-11/12/14: Rezeptur-Neuaufbau transaktional + validiert (unbekannte INCI-IDs, Phasenname ≤ 100); Phasen ohne Zeilen bleiben erhalten; Duplikat-Rohstoffe summieren; Set-Umstellung löscht Phasen; Auto-Phasenname AA-Schema; Legacy-Listen-Sync erhält gram/factor. Commit fa44af0. RecipeRobustnessTest (6 grün)
15.07.2026 OPT-9 (AuthZ/CSRF) Audit M-2/M-3/M-6/M-7/M-10: Produktionen erfordern admin ≥ 7 (Request + Controller-Gates + UI); tote Formulare für Stufen 1–6 ausgeblendet; stock-entries/copy POST, Produkt-Löschung DELETE-Formulare; imageAttribute mit Spalten-Whitelist; URL-Felder url:http,https. Commit 9ed47c0. +2 Tests, 3 Tests an neue Regeln angepasst
15.07.2026 Audit-Niedrigbefunde N-2/3/4/5/7/8/9/15 (Preis nur aus Eingängen, exaktes Verbrauchsfenster, einheitliche Kritisch-Zählung, Limit-Hinweis Ausschussliste, konsistentes Zahlenparsing, Kopie ohne Alt-Chargen-Nr., withTrashed-Korrekturen, Markdown-html_input=strip). Bewusst offen: N-1/6/10/13 (Design-/Kundenentscheid). Commit d34f0a7. Volle Suite: 231 grün, 823 Assertions
15.07.2026 N-1/N-6/N-10 (Kundenentscheide) N-1: Chargen-Gruppierung je Lieferant (batchGroupKey = b:<supplier>:<lot>, Batch-Check prüft Lieferant) — Kundenentscheid: Lot-Nummern kommen von extern und können kollidieren (9b6c9de). N-6: WaWi-Eingaben lesen einen einzelnen Punkt mit 1–2 Nachkommastellen als Dezimaltrenner („5.1" = 5,1; „1.500" = 1500) — neuer Helper reFormatWawiNumber() + JS-Parser vereinheitlicht (016e8bb). N-10: Bewegungs-Modal zeigt aktuellen Bestand + Bestätigung bei Minus-Buchung (6af6760). +2 Tests. Volle Suite: 233 grün, 832 Assertions
16.07.2026 Vorbereitung AP-13: SalesController-Härtung SalesController::store() (Versand-Statuswechsel) validiert Eingaben (Status aus $shippedTypes, Action-Whitelist, txaction/payment_id) und läuft in DB::transaction mit lockForUpdate gegen Doppelklick/parallele Tabs. Verhalten unverändert. Commit 6b8fcad. Neuer SalesShippedStatusTest (6 grün). Suite: 239 grün, 847 Assertions
16.07.2026 AP-13 (Shop-Bestandsabzug bei Versand) ProductStockService::syncSaleStock(): idempotenter Differenz-Abgleich der Verkaufs-Abgänge (source='sale', Referenz = Bestellung) mit dem Versandstatus — Abzug je Bestellposition bei sent/close, Sets rekursiv über product_set_items in Einzelprodukte aufgelöst (Zyklus-Schutz, withTrashed wie N-9); Zurücksetzen/Storno bucht per Gegenbuchung zurück. Behebt das Doppelabzug-Risiko Versand→Zurücksetzen→Versand (Ledger-Abgleich statt shipped_at-Flag). Andockpunkte: SalesController::store (in der Transaktion) und PaymentReminderService (Auto-Storno per Cron setzt Status 10 auch bei versendeten Bestellungen). „Verbrauch/Monat" zählt Verkaufs-Abgänge automatisch mit. Self-Shipping bewusst ohne Abzug (kein Warenkorb übertragen). Commit 211aeed. Erkenntnis für künftige HTTP-Tests: mehrere Requests im selben Pest-Test müssen denselben actingAs-User verwenden, sonst endet der Folge-Request als stiller Login-Redirect. Neuer SaleStockDeductionTest (10 grün: Positionen, Sets/verschachtelt, Idempotenz, Rückbuchung offen/Storno, Wiederversand, Cron-Pfad, Monatsverbrauch). Suite: 249 grün, 903 Assertions
16.07.2026 AP-33b (Sammel-Dokumente) Checkbox-Auswahl im Abwicklungs-Board (Kopf-Checkbox = alle sichtbaren, Zähler in den Buttons) + Sammel-Aktionen „Lieferscheine drucken" / „+ Rechnungen": OrderFulfillmentController::batchDocuments() merged die PDFs der Auswahl (PDFMerger/FPDI) zu einem Druck-PDF (neuer Tab, max. 100). Fehlende Dokumente werden ohne Mailversand erzeugt (InvoiceRepository::create), auf der Platte fehlende Altdateien mit bestehender Nummer regeneriert (update). API-Bestellungen: Checkbox deaktiviert, in gemischter Auswahl übersprungen, reine API-Auswahl ⇒ Fehlermeldung. Nebenbei (Kundenfeedback iPad): Tabelle kompaktiert — 6 Spalten, Kundenname mit Ellipsis, Positionen als schmale letzte Spalte, Aktionen+Positionen sticky rechts, Icon-Buttons unter 1200 px (4c73e64, c21cc63). Commit 1252c30. OrderFulfillmentTest +4 (Sammel-PDF inkl. Dokument-Erzeugung, reine API-Auswahl abgelehnt, gemischte Auswahl überspringt API, Validierung). Volle Suite: 272 grün, 1001 Assertions
16.07.2026 AP-33a (Bestell-Abwicklung: Nav „Bestellungen" + Fulfillment-Board) Neuer Sidenav-Punkt „Bestellungen" (copyreader) mit Unterpunkten Abwicklung (neu) und API-Bestellungen (umgezogen). Board admin/inventory/orders (OrderFulfillmentController): 4 Filter-Kacheln (heute eingegangen / zu konfektionieren / versandfertig / heute versendet), je Bestellung Packliste aufklappbar (Sets mit Komponenten), Gewicht, Zahlstatus-Pill, Quelle-Badge (API/Shop/Promo), Suche. Versandfertig-Regel (Kundenentscheid): ShoppingOrder::isReadyToShip() — Rechnung (fnc) sofort, Vorkasse (vor) erst bei paid, API (extern) immer, non/failed nie. Dokumente: Erstellen (Rechnung + Lieferschein, ohne Mailversand) über bestehendes InvoiceRepository::create(); Downloads (einzeln/kombiniert via PDFMerger) über eigene copyreader-Routen, da storage_file admin ≥ 7 verlangt; API-Bestellungen bekommen keine Rechnung — externer Link (wp_invoice_path). Statuswechsel: Logik aus SalesController::store() in neuen OrderStatusService::setShipped() extrahiert (Transaktion, Lock, Margin-/Promotion-Folgen, AP-13-Abzug) — Verkäufe-Modal und Board nutzen dieselbe Implementierung; Board nur vorwärts (1/2), nur aus Status 0/1. Sperr-Flag shopping_orders.shipped_status_locked (Migration 2026_07_16_130000, Kundenentscheid Punkt 3): vom Board gesetzte Status werden vom Auto-Wechsel der Detailseiten (offen → in Bearbeitung, 3 Stellen) nicht mehr überschrieben. Commit a367d85. Neuer OrderFulfillmentTest (10 grün: Render/Packliste, Rechte, Versandfertig-Regel, Statuswechsel + Lock + Bestandsabzug, nur-vorwärts, Alt-Status unangetastet, Detailseiten-Sperre, Dokumente ohne Mail, API ohne Rechnung). Volle Suite: 268 grün, 987 Assertions
16.07.2026 AP-13-Nachtrag: Stichtags-Schutz für Alt-Bestellungen Beim Blick auf Bestellung 1687 (versendet 12/2025) entdeckt: erneutes Speichern des Versandstatus einer vor AP-13 versendeten Bestellung hätte rückwirkend vom heutigen Bestand abgezogen. syncSaleStock() überspringt jetzt Bestellungen mit shipped_at vor dem Stichtag ProductStockService::SALE_STOCK_SINCE (16.07.2026). Kundenentscheid dokumentiert: Self-Shipping ohne Abzug bestätigt. SaleStockDeductionTest +1 (11 grün). Suite: 258 grün, 944 Assertions
16.07.2026 AP-13 (API-Bestellungen — Protokoll + SKU-Fehlermarkierung, Kundenanforderung 16.07.) Neue Tabelle api_order_logs (Migration 2026_07_16_090000) + Model ApiOrderLog; ShoppingUserController protokolliert jede wp/order-Übertragung — erfolgreiche inkl. Zuordnungsergebnis je Position (SKU, Produkt, Menge, Preis, API-Meldungen), abgelehnte mit Grund (Kunde unbekannt, Bestellung existiert bereits, Speicherfehler). Positionen ohne Produktzuordnung ⇒ has_errors + Zusammenfassung („x von y Positionen ohne Produktzuordnung"). Neuer Menüpunkt „API-Bestellungen" (copyreader, admin/inventory/api-orders): Liste (Limit 500/?all=1, Filter „nur Fehler"), rote Markierung, Modal „Ansehen" mit Positionstabelle (Meldungen deutsch übersetzt, Roh-Payload bei abgelehnten). Dazu SKU-Konsistenz (Plan §4 „mitzulösen"): Warnkarte doppelte WP-Nummern aktiver Produkte, aufklappbare Liste aktive Produkte ohne WP-Nummer. Commit 71d5591. Neuer ApiOrderLogTest (8 grün, Ende-zu-Ende über wp/order mit Passport::actingAs). Suite: 257 grün, 940 Assertions
16.07.2026 Bugfix Logistik-Mail (Kundenmeldung) Fehlversand an die Rechnungsadresse: die Versand-Mail (MailLogistic) hängte den Lieferschein nur bei White-Label-Bestellungen an. Jetzt wird er immer mitgesendet (unabhängig von Logo/White-Label); fehlt die PDF-Datei auf der Platte, wird sie mit bestehender Rechnungsnummer regeneriert. Dazu tote Duplikat-Klasse app/Mail/MailLogitic.php entfernt (Tippfehler-Dateiname, definierte MailLogistic in einer Altversion ohne Lieferschein-Anhang — Kollisionsrisiko im Classmap-Autoload). Commit 7d77314. Nachtrag: gleiche Regeneration auch für die Rechnungs-PDF — vorher brach build() bei fehlender Rechnungsdatei still ab (Mail ohne Anhänge trotz Erfolgsmeldung im UI). Neuer LogisticMailTest (4 grün: Anhang ohne White-Label, Regeneration Lieferschein + Rechnung, Versand-Button). Volle Suite: 276 grün, 1016 Assertions
offen AP-32 Versand im System: DHL-Etiketten per API — neu aufgenommen + Ansatz dokumentiert (§4, 15.07.2026); Klärungspunkte (DHL-Konto, Versandprodukte, Druck-Setup) für nächste Anforderungsrunde notiert –
offen AP-14/15/16/17 Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen –

✅ BLOCK ABGESCHLOSSEN (06.07.2026). Alle fachlichen Pakete der Anforderungsrunde vom 12.06.2026 sind umgesetzt (OPT-1 … OPT-4, AP-20/21/23/24/27/28); die Quelldateien docs/nächsten Anforderungen an die WaWi.md und docs/Todos.md sind vollständig eingearbeitet. Suite: 201 Tests grün, 731 Assertions.

✅ BLOCK AP-13 + AP-33a/b ABGESCHLOSSEN (16.07.2026). Shop-Bestandsabzug bei Versand (inkl. Sets, Gegenbuchungen, Stichtags-Schutz für Alt-Bestellungen), Menüpunkt „API-Bestellungen" mit SKU-Fehlermarkierung, Abwicklungs-Board „Bestellungen → Abwicklung" (iPad-optimiert) und Sammel-Druck (Lieferscheine/Rechnungen als ein PDF). Alle Kundenentscheide in §4 dokumentiert. Suite: 272 grün, 1001 Assertions.

➡️ TESTPHASE (Kundenentscheid 16.07.2026): Der Block geht jetzt in den Kundentest — Leitfaden dafür: resources/docs/testleitfaden.md (in der App unter Einstellungen → Leitfaden), Abschnitte 9 (Bestandsabzug + API-Bestellungen) und 10 (Bestell-Abwicklung inkl. Sammel-Druck).

Nachgelagert (nicht automatisch beginnen): AP-32 (DHL-Etiketten — Kundenentscheid 16.07.2026: nach der Testphase; Klärungspunkte DHL-Konto/EKP, Versandprodukte, Druckformat, Retourenlabel in §4) → AP-33c (Etiketten-Stapel, optional Automatik als AP-17-Einstellung) → AP-14 (Audit-Trail) → AP-16 (2FA) → AP-15 (Blockrechte) → AP-17 (WaWi-Einstellungen). ✅ Self-Shipping ohne Bestandsabzug am 16.07.2026 vom Kunden bestätigt (§4 Punkt 4). Wiedereinstieg: Test-Feedback des Kunden mit diesem Plan abgleichen (§6 fortschreiben), danach AP-32 starten.