# Aktualisierter Entwicklungsplan: Warenwirtschaft, Produktion & Produktbestand > ⚠️ **ABGELÖST (12.06.2026):** Operative Arbeitsgrundlage ist jetzt `entwicklungsplan-aktualisiert-12-06-2026.md` (V5.0). Dieses Dokument (V4.0) bleibt als Referenz/Historie für AP-00 … AP-19 bestehen. > > **Version:** 4.0 - Stand 02.06.2026 > **Ersetzt:** `entwicklungsplan-aktualisiert-27-04-2026.md` (V3.0) als operative Arbeitsgrundlage > **Referenzen:** `entwicklungsplan.md` (V2.0), `briefing-anpassungen-27-04-2026.md`, `feedback.md`, `konzept-final.md`, `docs/Todos.md` > **Methodik:** Backlog aus kleinen, sequenziell abarbeitbaren Arbeitspaketen (AP). Jedes AP hat Ziel, konkrete Schritte mit Dateipfaden, DB-Änderungen, Akzeptanzkriterien und Tests. Reihenfolge ist so gewählt, dass jedes AP einzeln deploybar ist. --- ## 0. Was dieses Dokument neu macht Gegenüber V3.0 wurde der **reale Code-Stand verifiziert** (nicht nur die Protokolltabelle übernommen). Daraus ergeben sich Korrekturen, neu entdeckte Bugs und eine feinere Zerlegung in einzeln umsetzbare Schritte. Geprüfte Dateien u. a.: `routes/web.php`, `app/Http/Controllers/Admin/Inventory/*`, `app/Services/ProductionService.php`, `app/Http/Requests/Inventory/*`, `resources/views/admin/inventory/*`, Migrationen unter `database/migrations/`. --- ## 0a. Umsetzungsprotokoll V4.0 (laufend) > Jede abgeschlossene Teil-Lieferung wird hier mit Datum, betroffenen Dateien und Test-Status protokolliert. | Datum | AP | Kurzbeschreibung | Tests | |---|---|---|---| | 03.06.2026 | **AP-12 (Ausgang / Ausschuss)** | Neue Übersicht **Warenwirtschaft → Ausgang / Ausschuss** (CopyReader, nach Einkauf). Migration `…_create_stock_disposals_table` (`disposal_type`, `ingredient_id`/`packaging_item_id`/`stock_entry_id` nullable FKs, `location_id`, `quantity`, `unit`, `reason` Pflicht, `note`, `user_id`, `disposed_at`). Modell `StockDisposal`. `InventoryService` zieht Ausschuss in `remainingByIngredient()`/`remainingByLocationForIngredient()` ab (wirkt auf Kritisch-Badge) + neue `remainingByPackagingItem()`/`disposed*`-Methoden. `StockDisposalController` (`index`/`create`/`store`/`ingredientCharges`-JSON), FormRequest `StoreStockDisposalRequest` (Grund Pflicht, dt. Zahl/Datum). Erfassungsformular mit Typ-Umschaltung, Select2-Suche, optionaler Charge (setzt Lagerort), Grund-Auswahl, Datepicker. Sidenav-Eintrag + „Ausschuss erfassen"-Button (vorbelegt) auf der Rohstoff-Detailseite. | `tests/Feature/StockDisposalTest.php` (8 grün): Summierung, Restbestand-Abzug Rohstoff + je Lager + Verpackung, HTTP-Buchung, Grund-Pflicht, Admin-Schutz, Render, Chargen-Endpoint. Regression Rohstoff-/Produktbestand grün | | 03.06.2026 | **AP-11 (Produktbestand + Historie)** | Neues Menü **Warenwirtschaft → Produktbestand** (CopyReader) mit Untermenü „Übersicht"/„Historie" und Kritisch-Badge. Migrationen `…_create_product_stock_movements_table` (`product_id`, `direction` ENUM(`in`,`out`), `quantity`, `reason`, `source`, `note`, `user_id`, `nullableMorphs('reference')`) + `…_add_stock_thresholds_to_products_table` (`min_product_stock`/`critical_product_stock`). Modell `ProductStockMovement` + `Product::stockMovements()`. `app/Services/ProductStockService.php`: `currentStockByProduct()` (=`SUM(in)`−`SUM(out)`), `recordMovement()`, `recordProductionStock()` (idempotenter Differenz-Abgleich, append-only), `productStatus()`, `criticalProductCount()`. `ProductStockController` (`index`/`storeMovement`/`history`): Übersicht (nur Haupt-/Einzelprodukte, Bild, Bestand rot/gelb, Suche + „nur kritische", `+`/`−`-Buchungsmodal nur für Admin, `Produzieren`-Link mit Produktvorwahl), Historie (Filter Produkt/Eingang-Ausgang/Grund/Monat+Jahr). Produktion bucht Produktbestand automatisch (`ProductionService`). Schwellwert-Felder im Produktformular (Warenwirtschaft-Card) + `ProductRepository`. AP-10 „Enthalten in" um Produktbestand-Spalte ergänzt. | `tests/Feature/ProductStockTest.php` (9 grün): Bestand=Eingang−Ausgang, Status, Kritisch-Zähler, Produktionsbuchung + Update-Korrektur, Index/Historie-Render, HTTP-Buchung + Admin-Schutz. Regression Produktion + `RawMaterialStockTest` (26) grün | | 03.06.2026 | **AP-10 (Rohstoffbestand)** | Neue Übersicht **Warenwirtschaft → Rohstoffbestand** (CopyReader, erster Menüpunkt). Neuer `app/Services/InventoryService.php` als zentrale Bestandslogik: `remainingByIngredient()` (= `SUM(received_quantity)` der eingegangenen Rohstoff-Chargen − `SUM(production_ingredients.quantity_used)`), `remainingByLocationForIngredient()`, `dailyConsumptionByIngredient()` (Ø Gramm/Tag aus Produktionshistorie der letzten 90 Tage), `daysUntilEmpty()`/`expectedEmptyDate()`, `stockStatus()` (critical = Meldebestand unterschritten, warning = Reichweite ≤ Lieferzeit) und `criticalIngredientCount()`. `RawMaterialStockController@index` (Tabelle: Name, Qualität, Bestand, Verbrauch/Tag, Voraussichtlich auf Null + Resttage, Hochrechnungs-Spalte mit Horizont-Dropdown 1/3/6/12 Monate; Suche + „nur kritische anzeigen"-Filter; kritisch=`table-danger`, warning=`table-warning`, Zeile klickbar) und `@show` (Bestell-Detailseite: Kennzahlen, „Enthalten in" = aktive Rezepturen mit g/Stück, Lieferanten mit Lieferzeit/letztem Netto-kg-Preis + Bestellaktion `Zum Shop`/`Per Mail` je `order_method`, verfügbare Chargen mit Restbestand + Bestand je Lagerort). Routen `raw-material-stock.index`/`.show` in der `copyreader`-Gruppe. Sidenav-Eintrag „Rohstoffbestand" inkl. rotem Kritisch-Badge über `View::composer` (AppServiceProvider, nur für CopyReader). **Hinweis:** Produktbestand/Verkauf-pro-Tag je Produkt im „Enthalten in"-Block folgt mit AP-11 (Produktbestand existiert noch nicht). | `tests/Feature/RawMaterialStockTest.php` (8 grün): Restbestand=Eingang−Verbrauch, Verbrauch/Tag-Mittelung, Fenstergrenze, Status kritisch/warnung, Kritisch-Zähler, Index- und Detail-Render. Regression `ProductionManufacturerRecipeTest`/`ProductSetTest` (19) grün | | 02.06.2026 | **AP-01** | URL-Bugfixes B1/B2 umgesetzt: `suppliers/form.blade.php` und `packaging-items/form.blade.php` von `type="url"` auf `type="text"` (placeholder `https://`); `Store/UpdatePackagingItemRequest` URL-Regel `url\|max:500` → `string\|max:2048`; Migration `2026_06_02_145358_widen_url_columns_in_inventory_tables` (suppliers.url + packaging_items.url → varchar(2048)). | `tests/Feature/InventoryUrlFieldsTest.php` (3 grün); Regression Phase 2+3 grün (17) | | 02.06.2026 | **AP-04** | iPad-taugliche Tabellen-Aktionen (B5): neue Partial `resources/views/admin/inventory/partials/table-actions-style.blade.php` (`@once`-Style für `.wawi-table td .btn`, min. 42px Touch-Target, mehr Abstand); Klasse `wawi-table` + Partial-Include in allen 8 Index-Views (locations, material-qualities, packaging-materials, supplier-categories, suppliers, packaging-items, stock-entries, productions). | Render-Regression Phase 2+3+5 grün (21) | | 02.06.2026 | **AP-00** | Regressionsbasis für umgesetzte 5.1-Features als Pest-Tests nachgezogen: INCI-Rohstoffqualität-Relation, Hersteller-Rezeptur getrennt von Produkt-Rezeptur, Produkt-Kopie inkl. beider Rezepturen, „nur aktive Produkte" im Produktions-Formular, Produktion edit/copy rendern. | `tests/Feature/ProductPhase51Test.php` (5 grün) | | 02.06.2026 | **AP-04.1** | Aktionsspalten vereinheitlicht (Kunden-Feedback): einheitliches Schema **Spalte 1 = Ansicht + Bearbeiten** (+ Kopieren bei Produktion), **letzte Spalte = Löschen**. Umgebaut: `stock-entries/index` (Ansicht/Bearbeiten nach vorn, Löschen ans Ende, DataTables `order`/`columnDefs` an verschobene Spalten angepasst) und `productions/index` (Ansicht/Bearbeiten/Kopieren nach vorn). Stammdaten-Tabellen waren bereits konform (kein `show`-Route → keine Ansicht). CSS-Feinjustierung der Button-Abstände durch Kunden in `partials/table-actions-style.blade.php` übernommen. | Produktions-Index-Render + Aktionslinks geprüft; stock-entries Index-Render grün (13) | | 02.06.2026 | **AP-05 (Teil 1: UST)** | Neuer Unterpunkt **Warenwirtschaft → Einstellungen → „Allgemein"** als erweiterbarer Container für kleinteilige Einstellungen (erstes Modul = Umsatzsteuersätze). Tabelle `tax_rates` (`name`, `percent` DECIMAL(5,2), `active`, `pos`) via Migration `2026_06_02_152721_create_tax_rates_table`; Model `TaxRate` (casts `percent`/`active`, `scopeActive`), `TaxRateFactory`, Seeder-Erweiterung `InventoryStammdatenSeeder` (19/7/0, idempotent per `firstOrCreate`). CRUD: `GeneralSettingController@index` (Allgemein-Seite), `TaxRateController` (create/store/edit/update/destroy, Redirect zurück auf `general`), `Store/UpdateTaxRateRequest`. Views `admin/inventory/general/index.blade.php` (Karte „Umsatzsteuersätze" mit Tabelle + Neu/Bearbeiten/Löschen) und `admin/inventory/tax-rates/form.blade.php`. Routen in `superadmin`-Gruppe (`admin.inventory.general`, Resource `tax-rates` ohne index/show). Sidenav-Eintrag „Allgemein" inkl. `open`/`active`-Logik. Migration + Seed auf DB ausgeführt (3 Default-Sätze vorhanden). | `tests/Feature/TaxRateSettingsTest.php` (7 grün, 24 Assertions): Render, CRUD, Validierung Pflicht/Bereich, `active`-Scope, Zugriffsschutz Nicht-SuperAdmin | | 02.06.2026 | **AP-05 (Teil 2: Lieferzeiten)** | Zweite Karte „Lieferzeit-Vorlagen" auf der Allgemein-Seite. Tabelle `delivery_times` (`label`, `active`, `pos`) via Migration `2026_06_02_153243_create_delivery_times_table`; Model `DeliveryTime` (cast `active`, `scopeActive`), `DeliveryTimeFactory`, Seeder-Erweiterung (1–3 / 3–5 Werktage / 1–2 Wochen, idempotent). CRUD: `DeliveryTimeController` (create/store/edit/update/destroy → Redirect `general`), `Store/UpdateDeliveryTimeRequest`. `GeneralSettingController` um `deliveryTimes` erweitert. View `admin/inventory/delivery-times/form.blade.php` + zweite Karte in `general/index`. Route Resource `delivery-times` (ohne index/show) in `superadmin`-Gruppe. Sidenav `open`/`active` um `delivery-times` ergänzt. Migration + Seed auf DB ausgeführt. | `tests/Feature/DeliveryTimeSettingsTest.php` (7 grün, 21 Assertions): Render, CRUD, Validierung, `active`-Scope, Zugriffsschutz | | 02.06.2026 | **AP-06 (Lieferanten erweitern)** | Felder `order_method` ENUM(`email`,`online_shop`), `order_email`, `order_url`, `delivery_time` (Freitext) an `suppliers` via Migration `2026_06_02_154755_add_order_fields_to_suppliers_table`. `Supplier` fillable erweitert; `Store/UpdateSupplierRequest` Regeln (`order_method` in:email,online_shop; `order_email` email; `order_url` string max 2048; `delivery_time` string). `SupplierRepository::extractSupplierAttributes` erweitert. `SupplierController` create/edit übergeben aktive `deliveryTimes` als Vorlagen. `suppliers/form.blade.php`: Bestellweg-Select + bedingte Felder (Bestell-E-Mail / Bestell-URL via JS-Toggle) + Lieferzeit-Textfeld mit `` aus aktiven Lieferzeit-Vorlagen. Migration auf DB ausgeführt. | `tests/Feature/SupplierOrderFieldsTest.php` (6 grün): Formular zeigt nur aktive Vorlagen, Speichern E-Mail-/Shop-Bestellweg, Update, Validierung Bestellweg/Bestell-E-Mail. Regression `InventoryPhase2Test` (9 grün) | | 02.06.2026 | **AP-06 (Nachtrag: Lieferzeit in Tagen)** | Lieferzeit-Vorlagen erhalten festes Feld `days` (ganze Tage bis Wareneingang, Basis für spätere „rechtzeitig bestellen"-Ableitung). Migration `2026_06_02_160411_add_days_to_delivery_times_table` (`delivery_times.days` unsignedSmallInt nullable) + `2026_06_02_160411_add_delivery_time_days_to_suppliers_table` (`suppliers.delivery_time_days`). `DeliveryTime` (fillable+cast `days`), Factory/Seeder (3/5/14 Tage, Bestandsdaten nachgepflegt). `Store/UpdateDeliveryTimeRequest` + `Store/UpdateSupplierRequest` um `days`/`delivery_time_days` (nullable int) erweitert; `SupplierRepository` + `Supplier` cast. Views: Tage-Feld in `delivery-times/form`, Spalte „Tage" in `general/index`, Tage-Feld im `suppliers/form` + JS-Autofill (`data-days` an Datalist-Optionen setzt Tage bei Vorlagenauswahl, manuell überschreibbar). Migrationen auf DB ausgeführt, Default-Vorlagen mit Tagen befüllt. | `DeliveryTimeSettingsTest` (10 grün): days speichern/optional/Integer-Validierung; `SupplierOrderFieldsTest` (9 grün): `delivery_time_days` speichern, Integer-Validierung, `data-days`-Ausgabe | | 02.06.2026 | **AP-08 (Einkauf erweitern)** | Einkauf um UST-Satz + Netto/Brutto-Automatik + Duplizieren erweitert. Migration `2026_06_02_181548_add_price_fields_to_stock_entries_table` (`price_per_kg_gross` DECIMAL(10,4), `tax_rate_id` FK→`tax_rates` nullOnDelete, `tax_rate_percent` DECIMAL(5,2) als Snapshot). `price_per_kg` bleibt das bestehende **Netto**-Feld (kein Rename → keine Migration der Bestandsdaten/Tests). `StockEntry`: fillable + casts (`price_per_kg_gross`/`tax_rate_percent`) + `taxRate()` belongsTo. `Store/UpdateStockEntryRequest`: Regeln `tax_rate_id` (exists) + `price_per_kg_gross` (numeric), Reformat dt. Zahl, neue Regel „bei Rohstoff genau eines von Netto/Brutto verpflichtend". **Berechnung zentral im `StockEntryRepository::resolvePrices()`:** UST-Prozent als Snapshot, fehlender Netto-/Brutto-Wert wird aus dem Faktor `(1+%/100)` berechnet (Netto↔Brutto), bei Verpackung Preisfelder/UST genullt (Netto-Gesamt bleibt). View `_form`: UST-Dropdown (aktive `tax_rates`, `data-percent`) + Netto-/Brutto-Felder nebeneinander; `_scripts`: JS rechnet live Netto↔Brutto bei Eingabe und UST-Wechsel (dt. Zahlenformat). `show`: Anzeige Netto/Brutto/USt. **Duplizieren:** Route `stock-entries/{stock_entry}/copy` + `StockEntryController@copy` legt direkt eine `pending`-Kopie der Stufe-1-Felder an (Charge/MHD/Eingangsdaten leer, `ordered_at`=heute, `ordered_by`=aktueller User) und leitet zur Bearbeitung; Kopieren-Button in `index` (Aktionsspalte) + `show`-Header. Migration auf DB ausgeführt. | `tests/Feature/StockEntryPriceTest.php` (6 grün): Netto→Brutto, Brutto→Netto, ohne UST Netto=Brutto, Netto/Brutto-Pflicht, Duplizieren erzeugt pending-Kopie ohne Chargendaten, Copy-Zugriffsschutz. Regression `InventoryPhase3Test` (8 grün) | | 02.06.2026 | **AP-07.1 (Lieferanten-Detailansicht/Modal)** | Zwischenschritt (Kunde): Lieferanten-Zuordnungen auch von der Lieferantenseite aus sichtbar/pflegbar. `Supplier::ingredients()` belongsToMany (Gegenstück zu `Ingredient::suppliers()`). Resource `suppliers` `show` reaktiviert + neue Routen `suppliers.ingredients.attach/detach` und `suppliers.packaging-items.attach/detach` (admin-Gruppe). `SupplierController`: `show()` + `attach/detachIngredient()` + `attach/detachPackagingItem()` rendern gemeinsames Partial `suppliers/_details.blade.php` (Stammdaten + zwei kleine Listen „Zugeordnete INCIs" / „Zugeordnete Verpackungsartikel" mit Entfernen-Button und Hinzufügen-Auswahl der noch nicht zugeordneten Einträge). Index: Augen-Button (Spalte 1) öffnet Bootstrap-Modal, lädt Details per AJAX; Hinzufügen/Entfernen via delegiertem jQuery-AJAX (X-CSRF-TOKEN-Header) und ersetzt den Modal-Body mit dem neu gerenderten Partial. Verpackungsartikel-Zuordnung = `packaging_items.supplier_id` setzen/leeren. | `tests/Feature/SupplierDetailsTest.php` (7 grün): show zeigt zugeordnete INCIs/Verpackung, INCI attach/detach, Verpackung attach/detach, Validierung, Zugriffsschutz Nicht-Admin | | 03.06.2026 | **AP-03 („Nicht vorrätig" mit Zeitangabe)** | Produkt zeitlich begrenzt oder unbefristet als nicht vorrätig markierbar (vorerst **nur Hinweis**, Kauf bleibt möglich). Migration `2026_06_03_111226_add_out_of_stock_fields_to_products_table` (`products.out_of_stock_until` DATE nullable, `out_of_stock_indefinite` bool default 0). `Product`: fillable + casts (`date`/`bool`), Helper `isOutOfStock()`, `outOfStockRemainingDays()` (Differenz tagesgenau, ≥0), `outOfStockNotice()` (Singular/Plural „In ca. X Tag(en) wieder da!" bzw. „Zur Zeit nicht vorrätig"). **Produktformular:** neue Card „Verfügbarkeit" (Section-Nav-Eintrag nach „Details") mit Checkbox „Vorübergehend nicht vorrätig (mit Zeitangabe)" + Tagefeld und zweiter Checkbox „Auf unbestimmte Zeit nicht vorrätig"; JS (`toggleOutOfStock` in `edit.blade.php`) blendet das Tagefeld nur bei aktiver Zeitangabe ein und deaktiviert sie bei „unbestimmt". **Repository:** `update()` normalisiert die Felder — „unbestimmt" hat Vorrang (Datum=null), sonst `out_of_stock_until = now()->addDays($tage)`, ohne Aktivierung beides geleert. **Shop:** Hinweis im Produktraster (`web/shop/_shop_products_inner`) und in der Detailansicht (`web/shop/show_product`). Hinweise-Doku (AP-18) aktualisiert. Migration auf DB ausgeführt. | `tests/Feature/ProductOutOfStockTest.php` (6 grün): Tage→Datum, Unbestimmt-Vorrang+Datum-Nullung, Deaktivierung leert Felder, Vergangenheit gilt nicht, Hinweis-Resttage, HTTP-Store. Regression `ProductSetTest`/`ProductPhase51Test` grün | | 03.06.2026 | **AP-02 (Produkt-Klassen: Einzelprodukt vs. Set + Hauptprodukt)** | Echte Sets via Pivot. Migrationen `2026_06_03_105204_add_set_fields_to_products_table` (`products.is_set` bool, `main_product_id` FK→products nullOnDelete, `main_product_quantity` uint) + `…_create_product_set_items_table` (`set_product_id`/`component_product_id` FK cascade, `quantity`, `pos`, unique-Paar). `Product`: fillable + casts (`is_set` bool, `main_product_id`/`main_product_quantity` int), Relationen `setItems()`/`partOfSets()` (belongsToMany self), `mainProduct()`/`variants()`, Scopes `singleProducts()`/`sets()`/`mainProducts()`. **Produktformular:** neue Card „Set / Produktart" mit Checkbox „Ist Set" + Set-Bestandteile-Tabelle (Modal-Auswahl nur aktiver Einzelprodukte, Menge, Drag&Drop) analog Verpackung; Hauptprodukt-Zuordnung (`main_product_id` Dropdown + `main_product_quantity`) in der Warenwirtschaft-Card; Section-Nav-Eintrag „Set". **JS (`edit.blade.php`):** `toggleSetMode` blendet bei aktivem Set die Cards Rezeptur/Hersteller-Rezeptur/Verpackung/Warenwirtschaft (+ `.js-nav-recipe`-Sprungmarken) aus und die Set-Felder ein; Set-Item-Modal/Sortable. **Repository:** `update()` persistiert `is_set`/`main_product_*` (Set ⇒ `main_product_id`=null), bei Set werden Rezeptur (beide Typen) + Verpackung geleert und Set-Items gesynct, sonst Set-Items detached; `copy()` übernimmt Set-Bestandteile. **Validierung (`ProductController::validateSetItems`):** Set braucht ≥1 Bestandteil, Bestandteile müssen existieren, dürfen selbst keine Sets sein, nicht das Produkt selbst. **Produktion:** Sets aus den Produkt-Dropdowns (create/edit/copy) ausgeschlossen; neuer `ProductionService::assertNotASet()` blockiert das Produzieren von Sets. Migrationen auf DB ausgeführt. | `tests/Feature/ProductSetTest.php` (10 grün): Set+Mengen speichern, Set leert Rezeptur/Verpackung, Hauptprodukt-Zuordnung/Set-Nullung, Scopes, Set nicht produzierbar, Dropdown ohne Sets, Validierung (leer/Set-Bestandteil), HTTP-Store gültiges Set, Kopie inkl. Bestandteile. Regression `ProductPhase51Test`/`ProductionPhase5Test`/`ProductionManufacturerRecipeTest` grün | | 03.06.2026 | **AP-18 (Hinweise-/Doku-Seite)** | MD-basierte Hinweise-Seite unter **Warenwirtschaft → Einstellungen → „Hinweise"** (SuperAdmin). Pflege-Quelle `resources/docs/hinweise.md` (Entwicklungsstand, Nutzungshinweise, festgehaltene Entscheidungen §5.2/§5.3/§5.4, offene Briefings). `NoticeController@index` liest die MD-Datei und rendert sie via `Str::markdown()` (`league/commonmark` vorhanden) zu HTML; View `admin/inventory/notices/index.blade.php` (Card + dezentes `.wawi-notices`-Styling über `@section('styles')`). Route `admin.inventory.notices` in der `superadmin`-Gruppe; Sidenav-Eintrag „Hinweise" als letzter Punkt unter „Einstellungen" inkl. `open`/`active`-Logik. Früh als laufend gepflegter Platzhalter angelegt – mit jedem weiteren AP fortzuschreiben. | `tests/Feature/InventoryNoticesTest.php` (2 grün): Render (MD→HTML, `

`/„Entwicklungsstand" sichtbar), Zugriffsschutz Nicht-SuperAdmin | | 03.06.2026 | **AP-09.1 (Eigenprodukte ohne Rezeptur)** | Kunden-Feedback beim Testen: Eigenprodukte (Broschüren, Etiketten etc.) haben keine Rezeptur. Neue Spalte `products.no_recipe_required` (bool, Migration `2026_06_03_102214_add_no_recipe_required_to_products_table`), `Product` fillable + cast `bool`, `ProductRepository::update()` normalisiert die Checkbox (`isset ? 1 : 0`). **Produktformular:** Checkbox „Dieses Produkt benötigt keine Rezeptur (Eigenprodukt …)" oben in der Card „Inhaltsstoffe/Rezeptur"; bei aktiver Option blendet JS (`toggleRecipeFields` in `edit.blade.php`) die Rezeptur-Felder **beider** Cards (Produkt- + Hersteller-Rezeptur, Wrapper `.js-recipe-fields`) aus. **Produktion:** `ProductionService::store/updateProduction` überspringt bei `no_recipe_required` die Hersteller-Rezeptur-Prüfung und Chargen (keine `production_ingredients`); `buildRecipePayload` liefert `recipe_required=false`; `StoreProductionRequest` macht `ingredient_lines` dann optional; Produktions-JS zeigt Hinweis „benötigt keine Rezeptur" statt Warnung. | `ProductionManufacturerRecipeTest` erweitert (9 grün): Produktion ohne Chargen, `recipe_required=false`, HTTP-Store eines Eigenprodukts ohne Chargen | | 03.06.2026 | **AP-09 (Produktion korrigieren)** | Produktion vollständig auf **Hersteller-Rezeptur** umgestellt (kein Fallback). `ProductionService` (`store`/`updateProduction`/`requiredGramsByIngredient`/`buildRecipePayload`) lädt jetzt `manufacturer_ingredients`; neue `assertManufacturerRecipe()` wirft deutliche Warnung, wenn keine Hersteller-Rezeptur gepflegt ist; `buildRecipePayload` liefert Flag `has_recipe`. **Restbestand-Logik:** neue `consumedByStockEntry()` + `availableStockEntriesForIngredient()` (FEFO nach MHD, nur Chargen mit Restbestand > 0; `remaining_quantity` als verbraucht-abzüglich-Berechnung, beim Bearbeiten via `exclude_production` ohne die eigene Produktion). **Chargen-Label** `stockEntryLabel()` = `Lieferant - Chargennr. - dd.mm.yyyy` (kein „MHD"-Text). `recipeJson` nimmt `exclude_production` entgegen. **Views refactored:** gemeinsame Partials `productions/_form_fields.blade.php` + `productions/_scripts.blade.php` (create/edit/copy nutzen beide). JS: **B3-Fix** „Weitere Charge" fügt genau **eine** Zeile hinzu; **stabile Soll-Neuberechnung** (Stückzahländerung berechnet nur Soll neu via `data-recipe-ing`, ohne bereits eingetragene Chargen/Mengen zu überschreiben — Refetch nur bei Produkt-/Lagerortwechsel); Hersteller-Rezeptur-Warnung blockiert Submit. **UI vereinfacht:** Charge+Menge je Zeile als `input-group` mit `g`-Suffix, keine Pro-Zeile-Spaltenüberschriften. **B4 iPad-Fix:** Kopfdaten-Grid auf `col-12 col-sm-6`/`col-md-6` (keine Überlappung). **Produktentwicklung-Platzhalter:** `ProductDevelopmentController`, Route `admin.inventory.product-development`, View mit „Briefing ausstehend"-Hinweis; Sidenav „Produktion" zu Untermenü („Produktionen" + „Produktentwicklung") umgebaut. **Datumsfelder-Konvention (Kunde):** alle Datumsfelder im Modul von nativem `type="date"` auf `` (Format `dd.mm.yyyy`, global initialisiert in `public/js/custom.js`) umgestellt — Produktion `produced_at`, Einkauf `ordered_at`, Wareneingang `received_at`/`best_before`. Backend bleibt format-agnostisch (`Carbon::parse`/`date`-Regel verarbeiten `d.m.Y`). | `tests/Feature/ProductionManufacturerRecipeTest.php` (6 grün): Soll aus Hersteller-Rezeptur, Block ohne Hersteller-Rezeptur trotz Produkt-Rezeptur, `has_recipe=false`, Charge ohne Restbestand fehlt, Label-Format ohne MHD, Produktentwicklung-Render. Regression `ProductionPhase5Test` (4) + `ProductPhase51Test` (5) auf Hersteller-Rezeptur angepasst, grün | | 02.06.2026 | **AP-07 (INCI erweitern)** | INCI/Rohstoffe um Lieferanten-Mehrfachwahl, UST-Satz und eigene Lieferzeit (inkl. Tage-Autofill) erweitert. Migration `2026_06_02_161237_add_order_fields_to_ingredients_table` (`ingredients.tax_rate_id` FK→`tax_rates` nullOnDelete, `delivery_time` VARCHAR, `delivery_time_days` unsignedSmallInt) + `2026_06_02_161237_create_ingredient_supplier_table` (Pivot `ingredient_id` (unsignedInt, passend zu altem `increments`) / `supplier_id`, `preferred` bool, `supplier_sku`, `url`(2048), unique-Paar, cascadeOnDelete). `Ingredient`: fillable + cast `delivery_time_days`, Relationen `taxRate()` belongsTo + `suppliers()` belongsToMany (Pivot `preferred`/`supplier_sku`/`url`). **O1 erledigt:** `IngredientController::store()` von `Request::all()` auf neuen `App\Http\Requests\StoreIngredientRequest` umgestellt (validiert + normalisiert deutsche Dezimalzahlen `default_factor`/`min_stock_alert`, leere FKs→null). `edit()` lädt aktive `taxRates`, aktive `deliveryTimes`, aktive `suppliers` + eager-load `suppliers`; nach Speichern `suppliers()->sync()`. Single-Endpoint-Schema (`admin_product_ingredient_store` für Neu+Update) beibehalten → ein FormRequest genügt. View `admin/ingredient/form.blade.php`: UST-Dropdown (aktive `tax_rates`), Select2-Mehrfachwahl Lieferanten, Lieferzeit-Textfeld mit `` (`data-days`) + Tage-Feld; `edit.blade.php` `@section('scripts')` mit Select2-Init + Tage-Autofill (manuell überschreibbar). Lieferzeit-Logik: INCI-Lieferzeit hat Vorrang vor Lieferanten-Lieferzeit (Auswertung erst in AP-10). Migrationen auf DB ausgeführt. | `tests/Feature/IngredientOrderFieldsTest.php` (6 grün): Formular zeigt Lieferanten/UST/aktive Vorlagen+`data-days`, Speichern mit UST/Lieferzeit/Tagen/Lieferanten, Lieferanten-Sync bei Update, Validierung Tage-Integer/UST-Existenz/Name-Pflicht. Regression `SupplierOrderFieldsTest` (8) + `ProductPhase51Test` (5) grün | **Status Roadmap:** AP-00, AP-01, AP-04, AP-05, AP-06 (inkl. Nachtrag) erledigt; **AP-07 erledigt** (INCI: Lieferanten-Mehrfachwahl, UST-Satz, eigene Lieferzeit inkl. Tage-Autofill, `ingredient_supplier`-Pivot; O1 `IngredientController` auf FormRequest umgestellt) inkl. **AP-07.1** (Lieferanten-Detailansicht im Modal mit pflegbaren INCI-/Verpackungs-Listen); **AP-08 erledigt** (Einkauf: UST-Snapshot, Netto/Brutto-Automatik, Duplizieren); **AP-09 erledigt** (Produktion: ausschließlich Hersteller-Rezeptur + Warnung, Restbestandsfilter + Chargen-Label, B3-Fix, stabile Soll-Neuberechnung, B4 iPad, Produktentwicklung-Platzhalter) inkl. **AP-09.1** (Eigenprodukte ohne Rezeptur); **AP-18 erledigt** (MD-basierte Hinweise-Seite unter Einstellungen, laufend zu pflegen); **AP-02 erledigt** (Produkt-Klassen: echte Sets via Pivot `product_set_items`, Hauptprodukt-Zuordnung, Set-UI im Produktformular, Sets nicht produzierbar); **AP-03 erledigt** („Nicht vorrätig" mit Zeitangabe/unbefristet, nur Hinweis im Shop, Resttage-Countdown, Kauf bleibt möglich); **AP-10 erledigt** (Rohstoffbestand: `InventoryService` mit Restbestand/Verbrauch-pro-Tag/Reichweite/Status, Übersicht + Bestell-Detailseite, Kritisch-Badge in der Navigation); **AP-11 erledigt** (Produktbestand: `ProductStockService` mit Bestand=Eingang−Ausgang, Schwellwert-Status, manuelle `+`/`−`-Bewegungen, automatische Produktionsbuchung, Übersicht + revisionssichere Historie, Kritisch-Badge); **AP-12 erledigt** (Ausgang/Ausschuss: `stock_disposals`, Erfassung mit Pflicht-Grund + optionaler Charge, reduziert Rohstoff-/Verpackungsbestand inkl. Kritisch-Badge). > **Alle Klärungspunkte aus §5 sind beantwortet** (Kunde, 02.06.2026) und in die jeweiligen APs eingearbeitet — keine Blocker mehr offen. > > **➡️ NÄCHSTER SCHRITT: AP-18 (Hinweise-Doku) und/oder AP-02 (Produkt-Klassen: Einzelprodukt vs. Set).** AP-18 kann jederzeit als Platzhalter angelegt und laufend gepflegt werden; danach Datenmodell AP-02 (Sets via Pivot) / AP-03 („Nicht vorrätig", nur Hinweis), dann die großen Übersichten AP-10/AP-11. --- ## 1. Verifizierter Ist-Stand (02.06.2026) ### Umgesetzt und im Code vorhanden | Bereich | Status | Belegt durch | |---|---|---| | Produktmanagement (Produkte, INCIs, Kategorien, Attribute) | vorhanden | `app/Http/Controllers/ProductController.php`, `IngredientController.php` | | Rezeptur + Hersteller-Rezeptur (Prozent, Faktor, 100%-Summe) | vorhanden | `product_ingredients.recipe_type`, `Product::p_ingredients()` / `manufacturer_ingredients()` | | Haltbarkeit am Produkt (PAO / festes MHD) | vorhanden | `products.shelf_life_type`, `shelf_life_months` | | Stammdaten (Lagerorte, Lieferanten, Kategorien, Rohstoffqualität, Verpackungsmaterial, Produkt-/Versandverpackung) | vorhanden | `app/Http/Controllers/Admin/Inventory/*`, Migrationen `2026_03_27_*` | | INCI mit Rohstoffqualität | vorhanden | `ingredients.material_quality_id` | | Verpackung & Material am Produkt (BOM) | vorhanden | `product_packagings`, `Product::packagings()` | | Einkauf & Wareneingang (zweistufig pending → received, Charge, MHD) | vorhanden | `stock_entries`, `StockEntryController`, `ReceiveStockEntryRequest` | | Produktion (Chargen-Zuordnung, Soll-Verbrauch, MHD-Warnung, Packaging-Snapshot, edit/copy) | vorhanden | `ProductionService`, `ProductionController`, `production_*`-Tabellen | | Tests Phase 0–5 | vorhanden | `tests/Feature/ProductPhase0/1/4Test.php`, `InventoryPhase2/3Test.php`, `ProductionPhase5Test.php` | ### Noch NICHT im Code vorhanden (entgegen Eindruck aus V3.0-Lesart) - **Phase 5.2 ist vollständig offen** — keine der dort beschriebenen DB-Strukturen existiert: - kein `tax_rates` / `tax_rate_id` / `tax_rate_percent` - kein `is_set`, `main_product_id`, `product_set_items` - kein `order_method` / `order_email` / `order_url` / `delivery_time` an `suppliers` - kein `ingredient_supplier`-Pivot, kein `tax_rate`/`delivery_time` an `ingredients` - kein `price_per_kg_net` / `price_per_kg_gross` an `stock_entries` - kein `out_of_stock_until` an `products` - kein `product_stock_movements`, kein `InventoryService`, keine Bestandsseiten - **Produktion basiert noch auf `p_ingredients` (Produkt-Rezeptur), nicht auf der Hersteller-Rezeptur** (`ProductionService::store()` und `buildRecipePayload()` laden `p_ingredients`). Briefing fordert Hersteller-Rezeptur als Basis → offen. - **Kein Rohstoffbestand / Produktbestand / Historie**, kein Ausgang/Ausschuss, kein Audit-Trail, kein 2FA, keine blockbasierten Rechte, keine Warenwirtschafts-Einstellungen. --- ## 2. Gefundene Bugs & Optimierungen (sofort vor Feature-Arbeit) Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie werden als **Phase 5.1.x Nachzügler** vorgezogen. ### B1 — Lieferanten-URL: Speichern schlägt fehl, wenn URL ausgefüllt ist (umgekehrtes Verhalten) - **Symptom (Kunde):** „Neuer Lieferant: sagt ‚URL eingeben‘, obwohl eine drinsteht. Nehme ich sie raus, geht das Abspeichern." - **Ursache:** `resources/views/admin/inventory/suppliers/form.blade.php` Zeile 56 nutzt ``. Die native Browser-Validierung lehnt eine ausgefüllte, aber nicht streng schema-konforme URL (z. B. ohne `https://` oder mit kodierten Parametern) ab; ein **leeres** Feld ist gültig → exakt das gemeldete „umgekehrte" Verhalten. Die Server-Validierung ist bereits korrekt (`nullable|string|max:2048`). - **Fix:** `type="url"` → `type="text"` (Server validiert ohnehin als String). Damit werden auch Konfigurator-URLs mit Parametern akzeptiert (siehe B2). - **Aufwand:** ~15 Min. ### B2 — URL-Felder müssen Konfigurator-URLs mit Parametern akzeptieren - **Anforderung (Todos Z. 1–2):** URLs wie `https://www.kartonsaufmass.de/bestellen?bom_configuration=%7B%2522length%2522:125,...%7D` müssen gespeichert werden können (Versandverpackungs-Konfiguratoren). - **Status (verifiziert):** Lieferanten-URL ist bereits `nullable|string|max:2048` (ok, mit B1-Fix vollständig). **`PackagingItem` ist NICHT ok:** `Store/UpdatePackagingItemRequest` haben `['nullable','url','max:500']` — die `url`-Regel lehnt kodierte Konfigurator-URLs ab und `max:500` ist für lange Konfigurator-Links zu kurz. → auf `['nullable','string','max:2048']` ändern und Blade-Input auf `type="text"`. Konsistent für alle URL-Felder im Warenwirtschaftsmodul. - **Aufwand:** ~30 Min. ### B3 — „Weitere Charge": es erscheinen zwei Felder statt einem - **Anforderung (Todos Z. 14, Briefing 5.2.6):** Klick auf „Weitere Charge" soll genau **eine** neue Zeile/Dropdown hinzufügen. - **Status:** JS-Fehler in der Produktions-Create/Edit-View (Chargen-Splitting). Wird in **AP-09** (Produktionskorrekturen) sauber behoben, da es mit der Soll-Neuberechnung zusammenhängt. ### B4 — iPad: Produktionsdatum und Stückzahl überlappen grafisch - **Anforderung (Todos Z. 86, Briefing 5.2.6 D):** Responsive Grid der Kopfdaten in `productions/create.blade.php` / `edit.blade.php` reparieren. → Teil von **AP-09**. ### B5 — Tabellen-Aktionsicons (Auge/Stift/Mülleimer) zu klein/zu eng (iPad) - **Anforderung (Todos Z. 36, Briefing 5.2.3 C):** Betrifft **alle** Tabellen im Modul. Eine gemeinsame CSS-Utility-Klasse (z. B. `.wawi-actions` mit größeren Touch-Targets + Abstand) einführen und in allen `index.blade.php` anwenden. → **AP-04** (Querschnitt, früh, weil überall sichtbar). ### Optimierungen (Konsistenz/Sauberkeit) - **O1:** `IngredientController` nutzt noch `Request::all()` statt FormRequest → bei INCI-Erweiterung (AP-07) auf `StoreIngredientRequest`/`UpdateIngredientRequest` umstellen. - **O2:** Offene Tests aus Phase 5.1 (Menü-Labels, INCI-Qualität, Prozent-Rezeptur, 100%-Summe, Hersteller-Rezeptur, Produktion edit/copy, nur aktive Produkte) sind im Plan als `[ ]` markiert, aber Features sind umgesetzt → **AP-00** schreibt diese Tests nach, um eine grüne Regressionsbasis zu haben, bevor 5.2 beginnt. - **O3:** Steuerart als **Enum, später änderbar** gewünscht (feedback/Briefing INCI B). Lösung: konfigurierbare `tax_rates`-Stammdaten statt Hardcode-Enum (AP-05). --- ## 3. Priorisierte Roadmap (Phasenüberblick) | Reihenfolge | AP | Titel | Abhängigkeit | Aufwand | |---|---|---|---|---| | 1 | AP-00 | Regressionsbasis: offene 5.1-Tests nachziehen | – | 1 Tag | | 2 | AP-01 | Quick-Fixes B1/B2 (URL-Felder) | – | 0,5 Tag | | 3 | AP-04 | Querschnitt: iPad-taugliche Tabellen-Aktionen (B5) | – | 0,5–1 Tag | | 4 | AP-05 | Einstellungen: UST-Sätze & Lieferzeiten (Stammdaten) | – | 1–2 Tage | | 5 | AP-06 | Lieferanten erweitern (Bestellweg, Lieferzeit) | AP-05 | 1–2 Tage | | 6 | AP-07 | INCI erweitern (Lieferanten-Mehrfachwahl, UST, Lieferzeit) | AP-05, AP-06 | 2–3 Tage | | 7 | AP-08 | Einkauf erweitern (UST, Netto/Brutto, Duplizieren) | AP-05 | 2–3 Tage | | 8 | AP-09 | Produktion korrigieren (Hersteller-Rezeptur, Charge-JS, iPad, Produktentwicklung-Platzhalter) | – | 2–4 Tage | | 9 | AP-02 | Produkt-Klassen: Einzelprodukt vs. Set + Hauptprodukt | – | 3–5 Tage | | 10 | AP-03 | „Nicht vorrätig" mit Zeitangabe | – | 1–2 Tage | | 11 | AP-10 | Rohstoffbestand (InventoryService + Übersicht) | AP-06, AP-07, AP-09 | 4–6 Tage | | 12 | AP-11 | Produktbestand + Historie + manuelle Bewegungen | AP-02, AP-10 | 5–8 Tage | | 13 | AP-12 | Ausgang/Ausschuss (Rohstoffe/Verpackung) | AP-10 | 2–3 Tage | | 14 | AP-13 | Shop-Anbindung: Bestand bei Verkauf reduzieren (inkl. Sets) | AP-02, AP-11 | 3–5 Tage | | 15 | AP-14 | Audit-Trail (inventory_logs) | AP-10–13 | 2–3 Tage | | 16 | AP-15 | Blockbasierte Rechte | AP-05+ | 5–8 Tage | | 17 | AP-16 | 2FA Google Authenticator für Admins | – | 3–5 Tage | | 18 | AP-17 | Warenwirtschafts-Einstellungen (Alarm-Mail, Default-Lager, Schwellwerte) | AP-10/11 | 1–2 Tage | | 19 | AP-18 | Hinweise-/Doku-Seite (Einstellungen → Hinweise, MD-basiert) | – | 0,5 Tag | > **Leitplanke:** AP-00 bis AP-09 sind „Korrektur & Datenmodell-Vorbereitung". Erst danach werden die großen Übersichten (Rohstoff-/Produktbestand) gebaut, weil sie auf den neuen Stammdatenfeldern aufsetzen. --- ## 4. Arbeitspakete im Detail ### AP-00 — Regressionsbasis: offene 5.1-Tests nachziehen **Ziel:** Grüne Test-Suite als Sicherheitsnetz, bevor 5.2 beginnt. **Schritte** - Pest-Feature-Tests ergänzen (`php artisan make:test --pest `): - Menü-Labels (Rohstoffqualität, Verpackungsmaterial, Produkt-/Versandverpackung). - INCI mit `material_quality_id` speichern + Anzeige im Produktformular-Katalog. - Rezeptur in Prozent (3 Nachkomma) speichern; 100%-Summen-Validierung (grün/rot). - Hersteller-Rezeptur getrennt speichern (`recipe_type=manufacturer`). - Produktion `edit`/`update` und `copy`. - Nur aktive Produkte im Produktions-Dropdown. **Akzeptanz:** `php artisan test` läuft vollständig grün; neue Tests decken die genannten Features ab. --- ### AP-01 — Quick-Fixes URL-Felder (B1 + B2) **Schritte** - `resources/views/admin/inventory/suppliers/form.blade.php`: `type="url"` → `type="text"` (Zeile ~56). - URL-Validierung aller Warenwirtschafts-FormRequests prüfen (Supplier ist ok). `Store/UpdatePackagingItemRequest`: falls `url`-Regel → auf `['nullable','string','max:2048']` ändern; zugehöriges Blade-Input auf `type="text"`. - `vendor/bin/pint --dirty`. **Akzeptanz** - Lieferant mit ausgefüllter URL (auch ohne `https://` und mit kodierten Parametern) speichert ohne Fehler. - Konfigurator-URL aus Todos Z. 2 wird unverändert gespeichert. **Tests:** Feature-Test „Supplier mit Parameter-URL speichern", „PackagingItem mit Parameter-URL speichern". --- ### AP-04 — Querschnitt: iPad-taugliche Tabellen-Aktionen (B5) **Schritte** - Gemeinsame CSS-Klasse `.wawi-actions` (größere Buttons, mehr Abstand, Touch-Target ≥ 44px) in vorhandenes Admin-CSS aufnehmen (Laravel Mix; danach `npm run dev`/`prod`). - In allen `resources/views/admin/inventory/**/index.blade.php` die Aktionsspalte (Auge/Stift/Mülleimer) auf die Klasse umstellen. **Akzeptanz:** Aktionen sind auf dem iPad gut und einzeln klickbar; Optik in allen Modul-Tabellen konsistent. --- ### AP-05 — Einstellungen: UST-Sätze & Lieferzeiten **Ziel:** Konfigurierbare Steuersätze und Lieferzeit-Vorlagen als Stammdaten (Basis für AP-06/07/08). **DB** - `tax_rates`: `name` (z. B. „Standard"), `percent` DECIMAL(5,2), `active` bool, `pos`. Seeder: 19,00 / 7,00 / 0,00. - `delivery_times`: `label` VARCHAR (Freitext, z. B. „3–5 Werktage"), `days` (ganze Tage bis Wareneingang, optional – Basis für „rechtzeitig bestellen"-Ableitung), `active`, `pos`. **Code** - Models `TaxRate`, `DeliveryTime` (`make:model -mf`), CRUD-Controller unter `Admin/Inventory/`, FormRequests, Views `index`+`form`, Routen unter `admin/inventory` (`superadmin`), Sidenav-Einträge. **Akzeptanz:** SuperAdmin pflegt UST-Sätze und Lieferzeiten; nur aktive Sätze sind in Dropdowns wählbar; historische (deaktivierte) Sätze bleiben referenzierbar. > **Entscheidung (O3):** UST als Stammdaten-Tabelle statt PHP-Enum, weil „später änderbar" gefordert ist und historische Werte erhalten bleiben müssen. > **Umgesetzte Struktur (Kunde, 02.06.2026):** Unter **Warenwirtschaft → Einstellungen** neuer Unterpunkt **„Allgemein"** als Sammelseite für kleinteilige Einstellungen. Sektion 1 = Umsatzsteuersätze, Sektion 2 = Lieferzeit-Vorlagen (jeweils Tabelle, neue Einträge jederzeit ergänzbar). Weitere kleinteilige Einstellungen (Default-Werte etc.) werden später als zusätzliche Karten auf derselben „Allgemein"-Seite ergänzt. > > **Status:** Erledigt — Teil 1 (`tax_rates`) und Teil 2 (`delivery_times`) als CRUD unter „Allgemein". --- ### AP-06 — Lieferanten erweitern **DB (`suppliers`)** - `order_method` ENUM(`email`,`online_shop`) nullable. - `order_email` nullable (falls abweichend von `email`). - `order_url` nullable (falls abweichend von `url`). - `delivery_time` VARCHAR nullable (Freitext; optional Verknüpfung mit `delivery_times` als Vorlage, aber Freitext bleibt führend). **Code** - Migration + `Supplier` fillable/casts; `Store/UpdateSupplierRequest` erweitern; `suppliers/form.blade.php`: Radio/Select Bestellweg + bedingte Felder + Lieferzeit-Textfeld (mit Vorlagen-Datalist aus `delivery_times`). **Akzeptanz:** Pro Lieferant ist Bestellweg + Ziel (Mail/Shop) + Lieferzeit hinterlegt und editierbar; Daten stehen später dem Rohstoffbestand für Bestell-Links zur Verfügung. > **Status:** Erledigt (02.06.2026). Migration `2026_06_02_154755_add_order_fields_to_suppliers_table` (`order_method`, `order_email`, `order_url`, `delivery_time`). Formular mit Bestellweg-Select, JS-gesteuerten bedingten Feldern (E-Mail vs. URL) und Lieferzeit-Datalist aus aktiven `delivery_times`. Freitext bleibt führend, Vorlagen sind nur Eingabehilfe. Tests: `tests/Feature/SupplierOrderFieldsTest.php`. > > **Nachtrag (02.06.2026, Kunde):** Lieferzeit ist jetzt zusätzlich als fester Tageswert auswertbar. Lieferzeit-Vorlagen haben Feld `days` (ganze Tage). Lieferant hat `delivery_time_days` (`2026_06_02_160411_*`). Beim Auswählen einer Vorlage im Lieferzeit-Feld setzt JS automatisch den Tageswert (manuell überschreibbar). Dieser Tageswert ist die Grundlage, um später Rohstoffe rechtzeitig vor MHD/Bedarf zu bestellen. Gleiche Auto-Befüllung wird in AP-07 (INCI) übernommen. --- ### AP-07 — INCI erweitern **DB** - Pivot `ingredient_supplier`: `ingredient_id`, `supplier_id`, optional `preferred` bool, `supplier_sku`, `url`. - `ingredients`: `tax_rate_id` nullable FK, `delivery_time` VARCHAR nullable. **Code** - `Ingredient`: `suppliers()` belongsToMany, `taxRate()` belongsTo; fillable/casts. - **O1:** `IngredientController` auf `StoreIngredientRequest`/`UpdateIngredientRequest` umstellen (Ersatz für `Request::all()`). - `admin/ingredient/form.blade.php`: Select2-Mehrfachauswahl Lieferanten, UST-Dropdown (aktive `tax_rates`), Lieferzeit-Textfeld. **Lieferzeit-Logik:** INCI-Lieferzeit überschreibt Lieferanten-Lieferzeit (Auswertung erst im Rohstoffbestand AP-10). **Akzeptanz:** INCI kann mehrere Lieferanten, einen UST-Satz und eine eigene Lieferzeit haben; alles wird gespeichert und angezeigt. > **Status:** Erledigt (02.06.2026). Migrationen `2026_06_02_161237_add_order_fields_to_ingredients_table` (`tax_rate_id` FK, `delivery_time`, `delivery_time_days`) + `2026_06_02_161237_create_ingredient_supplier_table` (Pivot mit `preferred`/`supplier_sku`/`url`). `Ingredient`: `taxRate()` belongsTo, `suppliers()` belongsToMany (mit Pivot-Feldern), cast `delivery_time_days`. **O1 umgesetzt:** `IngredientController` nutzt jetzt `StoreIngredientRequest` (statt `Request::all()`) und synct Lieferanten via `suppliers()->sync()`. Formular: UST-Dropdown, Select2-Lieferanten-Mehrfachwahl, Lieferzeit-Textfeld mit `data-days`-Datalist + Tage-Feld inkl. JS-Autofill (manuell überschreibbar). Der bestehende Single-Endpoint (`admin_product_ingredient_store` für Neu+Update) wurde beibehalten, daher genügt ein FormRequest. Pivot-Zusatzfelder (`preferred`/`supplier_sku`/`url`) sind im Schema vorbereitet, das Formular synct vorerst nur die Lieferanten-Zuordnung. Tests: `tests/Feature/IngredientOrderFieldsTest.php` (6 grün). --- ### AP-08 — Einkauf erweitern **DB (`stock_entries`)** - `tax_rate_id` nullable FK + Snapshot `tax_rate_percent` DECIMAL(5,2) (für historische Korrektheit). - `price_per_kg_net` DECIMAL(10,4) nullable, `price_per_kg_gross` DECIMAL(10,4) nullable. **Code** - Migration + `StockEntry` fillable/casts; `Store/UpdateStockEntryRequest` erweitern (genau eines von Netto/Brutto verpflichtend bei Rohstoff). - `stock-entries/_form.blade.php` + `_scripts.blade.php`: UST-Dropdown; JS berechnet Netto↔Brutto gegenseitig beim Eintragen/UST-Wechsel (einheitliche Rundung). - **Duplizieren:** Route `GET stock-entries/{stock_entry}/copy` + `StockEntryController@copy`: dupliziert Stufe-1-Felder, setzt `status=pending`, lässt Charge/MHD/Eingangsdaten leer. **Akzeptanz** - Einkauf mit Netto **oder** Brutto anlegbar; Gegenfeld wird automatisch korrekt berechnet. - UST-Wechsel aktualisiert das Gegenfeld. - Ausgefüllter Einkauf für weitere Kanister/Chargen mit einem Klick duplizierbar. **Tests:** Netto→Brutto-Berechnung, Brutto→Netto, Duplizieren erzeugt `pending`-Kopie ohne Chargendaten. > **Status:** Erledigt (02.06.2026). Migration `2026_06_02_181548_add_price_fields_to_stock_entries_table` (`price_per_kg_gross`, `tax_rate_id`, `tax_rate_percent`). Das bereits vorhandene `price_per_kg` dient als Netto-Feld (`price_per_kg_net`), ergänzt um `price_per_kg_gross`; bewusst kein Rename, um Bestandsdaten/Factory/Tests stabil zu halten. Netto/Brutto-Umrechnung zentral in `StockEntryRepository::resolvePrices()` (UST-Prozent-Snapshot, fehlender Wert wird berechnet), live im Formular via JS. Duplizieren über `stock-entries/{id}/copy` legt direkt eine `pending`-Kopie der Stufe-1-Felder an. Verpackungspreis bleibt Netto-Gesamt ohne UST/Brutto (außerhalb des Plan-Scopes, kann später ergänzt werden). Tests: `tests/Feature/StockEntryPriceTest.php` (6 grün). --- ### AP-09 — Produktion korrigieren **Ziel:** Produktion auf Hersteller-Rezeptur stellen, JS-/iPad-Fehler beheben, Platzhalter Produktentwicklung. **Code** - **Basis Hersteller-Rezeptur:** `ProductionService::store()`, `updateProduction()`, `requiredGramsByIngredient()`, `buildRecipePayload()` von `p_ingredients` auf `manufacturer_ingredients` umstellen (Pivot `gram`/`factor` analog). `ProductionController::recipeJson()` entsprechend. > **Entscheidung (§5.1, geklärt):** Produktion nutzt **ausschließlich** die Hersteller-Rezeptur. **Kein Fallback** auf die Produkt-Rezeptur. Ist für das gewählte Produkt **keine Hersteller-Rezeptur** gepflegt, muss im Produktions-Formular direkt eine **deutliche Warnung** erscheinen (kein stilles Laden der Produkt-Rezeptur, Produktion ohne Hersteller-Rezeptur blockieren bzw. unmissverständlich warnen). - **Chargen-Dropdown-Label:** `Lieferant - Chargennr. - dd.mm.yyyy` (kein „MHD"-Text). Nur Chargen mit **Restbestand > 0** anzeigen (Restbestand = `received_quantity` − bereits in `production_ingredients` verbrauchte Menge dieser Charge). Erfordert Verbrauchsabfrage je `stock_entry_id`. - **B3 JS-Fix:** „Weitere Charge" fügt genau **eine** Zeile/ein Dropdown hinzu. - **Soll-Neuberechnung stabil:** Ändert sich oben die Stückzahl, bleiben bereits eingetragene Chargen/Ist-Mengen erhalten; nur Soll-Gramm werden neu berechnet (keine Überschreibung manueller Eingaben). - **UI vereinfachen:** Spaltenüberschriften „Charge"/„Menge" pro Rohstoffzeile entfernen; `g` hinter Mengen; weniger Linien. - **B4 iPad-Fix:** Bootstrap-Grid der Kopfdaten (Produktionsdatum / Stückzahl) responsive ohne Überlappung. - **Produktentwicklung-Platzhalter (§5.5, geklärt):** Sidenav-Unterpunkt unter „Produktion"; Route + simple View mit Hinweistext, dass hier **noch ein genaues Briefing aussteht** (keine Bestandsbuchung, keine Logik). **Akzeptanz:** Produktion rechnet auf Basis Hersteller-Rezeptur; **fehlt diese, erscheint eine Warnung** (kein Fallback); Chargenliste zeigt nur verfügbare Chargen im geforderten Label; „Weitere Charge" erzeugt eine Zeile; Stückzahländerung zerstört keine Eingaben; iPad-Layout sauber; Menüpunkt Produktentwicklung mit „Briefing ausstehend"-Hinweis sichtbar. **Tests:** Soll-Verbrauch aus Hersteller-Rezeptur; **Warnung bei fehlender Hersteller-Rezeptur**; Charge ohne Restbestand erscheint nicht; Service-Berechnung bei Stückzahländerung. > **Status:** Erledigt (03.06.2026). `ProductionService` auf `manufacturer_ingredients` umgestellt (kein Fallback), `assertManufacturerRecipe()` + `has_recipe`-Flag in `buildRecipePayload`. Restbestand über `consumedByStockEntry()`/`availableStockEntriesForIngredient()` (FEFO, nur Rest > 0, beim Bearbeiten via `exclude_production`). Chargen-Label `Lieferant - Charge - dd.mm.yyyy` (`stockEntryLabel()`). Views in gemeinsame Partials `_form_fields`/`_scripts` refactored; JS-B3-Fix (genau eine Zeile), stabile Soll-Neuberechnung (kein Refetch/Überschreiben bei Stückzahländerung), Hersteller-Rezeptur-Warnung blockiert Submit, UI vereinfacht (`g`-Suffix, keine Pro-Zeile-Headers), B4 iPad-Grid `col-sm-6`. Produktentwicklung-Platzhalter (`ProductDevelopmentController`, Route `admin.inventory.product-development`, Sidenav-Untermenü). Tests: `tests/Feature/ProductionManufacturerRecipeTest.php` (6 grün) + angepasste Regression `ProductionPhase5Test`/`ProductPhase51Test`. --- ### AP-02 — Produkt-Klassen: Einzelprodukt vs. Set + Hauptprodukt > **Entscheidung (§5.6, geklärt):** Echte Sets via Pivot (`product_set_items`) — mehrere Einzelprodukte bündelbar, nicht nur „genau ein Hauptprodukt". **DB (`products`)** - `is_set` bool default 0. - `main_product_id` nullable FK auf `products` (Child→Hauptprodukt). - `main_product_quantity` UINT nullable (z. B. 50 für „50 × 15 ml"). - Pivot `product_set_items`: `set_product_id`, `component_product_id`, `quantity`. **Code** - `Product`: `setItems()`, `mainProduct()`, Scopes `mainProducts()` / `singleProducts()`. - Produktformular: Checkbox „Ist Set"; bei aktiv Karten Rezeptur/Verpackung/Warenwirtschaft ausblenden, Karte „Set-Bestandteile" einblenden (Modal wie Rezeptur, nur Einzelprodukte wählbar, mit Menge). - Validierung: Set enthält nur Einzelprodukte (keine Sets), mind. 1 Bestandteil; Einzelprodukt darf Rezeptur/Packaging/Warenwirtschaft pflegen. **Akzeptanz:** Sets bestehen aus Einzelprodukten mit Menge; Sets sind nicht produzierbar; Produktbestand (AP-11) zeigt nur Haupt-/Einzelprodukte; Set-Verkauf reduziert später die enthaltenen Einzelprodukte (AP-13). > **Status:** Erledigt (03.06.2026). Migrationen `2026_06_03_105204_add_set_fields_to_products_table` + `…_create_product_set_items_table`. `Product`: `setItems()`/`partOfSets()`/`mainProduct()`/`variants()`, Scopes `singleProducts()`/`sets()`/`mainProducts()`. Produktformular: Card „Set / Produktart" (Checkbox + Bestandteile-Modal nur für aktive Einzelprodukte, Menge, Drag&Drop), Hauptprodukt-Felder in der Warenwirtschaft-Card, JS `toggleSetMode` blendet Rezeptur/Verpackung/Warenwirtschaft bei Set aus. `ProductRepository`: bei Set werden Rezeptur/Verpackung geleert, `main_product_id` genullt, Set-Items gesynct; `copy()` übernimmt Bestandteile. Validierung in `ProductController::validateSetItems` (≥1 Einzelprodukt, kein Set als Bestandteil, nicht sich selbst). Produktion: Sets aus Dropdowns ausgeschlossen + `ProductionService::assertNotASet()`. Tests: `tests/Feature/ProductSetTest.php` (10 grün). **Hinweis:** Set-Bestandsabzug beim Verkauf folgt in AP-13, Produktbestand-Filter (nur Haupt-/Einzelprodukte) in AP-11. --- ### AP-03 — „Nicht vorrätig" mit Zeitangabe **DB (`products`)** - `out_of_stock_until` DATE nullable (Empfehlung: aus Tagen berechnet, sauber für Resttage). - `out_of_stock_indefinite` bool default 0 (zweites Kästchen „auf unbestimmte Zeit vergriffen", ohne Tagefeld). **Code** - Produktformular: Checkbox „Nicht vorrätig" + Tagefeld → `out_of_stock_until = now()->addDays($tage)`; zweite Checkbox „unbestimmt". - Shop-/Bestellansicht: bei `out_of_stock_until` in der Zukunft Hinweis „In ca. X Tagen wieder da!" (Resttage dynamisch); bei `indefinite` entsprechender Dauerhinweis. **Entscheidung (§5.3, geklärt):** Vorerst **nur Hinweis**, der Kauf bleibt möglich. In der Hinweise-Doku (AP-18) ist zu dokumentieren, dass **künftig optional eine Kauf-Sperre** ergänzt werden kann/muss. **Akzeptanz:** Produkt zeitweise/unbefristet als nicht vorrätig markierbar; Resttage zählen automatisch herunter; nach Ablauf verschwindet der Hinweis ohne manuelles Zutun. > **Status:** Erledigt (03.06.2026). Migration `2026_06_03_111226_add_out_of_stock_fields_to_products_table` (`out_of_stock_until` DATE nullable, `out_of_stock_indefinite` bool). `Product`-Helper `isOutOfStock()`/`outOfStockRemainingDays()`/`outOfStockNotice()`. Produktformular-Card „Verfügbarkeit" (Checkbox + Tagefeld + Checkbox „unbestimmt", JS-Toggle); `ProductRepository::update()` rechnet `out_of_stock_until = now()->addDays($tage)` bzw. nullt bei „unbestimmt"/Deaktivierung (Backend tagesbasiert → Resttage zählen automatisch herunter, Hinweis verschwindet nach Ablauf). Shop-Hinweis im Produktraster und in der Detailansicht; Kauf bleibt möglich (Kauf-Sperre als spätere Option in AP-18 dokumentiert). **Interne Bestellliste** (`OrderController::datatable` Produkt-Spalte + `admin/modal/show_product`): roter Hinweis ersetzt die Mengen-Buttons (dort vorübergehend nicht bestellbar), Detail-Modal zeigt zusätzlich einen Hinweis. Tests: `tests/Feature/ProductOutOfStockTest.php` (6 grün). --- ### AP-10 — Rohstoffbestand (InventoryService + Übersicht) **Code** - `app/Services/InventoryService.php`: Restbestand je Rohstoff/Charge/Lagerort = `SUM(received_quantity)` − `SUM(production_ingredients.quantity_used)` − `SUM(stock_disposals.quantity)` (Ausgang ab AP-12). - Controller + View „Rohstoffbestand" (Sidenav-Menüpunkt). Spalten: INCI/Rohstoff, Qualität, Gesamtbestand, Bestand je Lagerort (dynamisch aus `locations`), verbraucht/Produktion, Meldebestand/Bedarf, Status, Lieferanten, Lieferzeit (INCI vor Lieferant), Bestellaktion (`mailto:`/Shop-Link je `order_method`). - Nur Chargen mit Restbestand > 0 einbeziehen; kritische Rohstoffe visuell markieren. **Akzeptanz:** Reale Restbestände sichtbar; Bestellweg direkt aus der Übersicht erreichbar; kritische Rohstoffe hervorgehoben. > **Status:** Erledigt (03.06.2026). `app/Services/InventoryService.php` zentralisiert die Bestandslogik (Restbestand = eingegangene Mengen − Produktionsverbrauch, Verbrauch/Tag aus 90-Tage-Produktionshistorie, Reichweite/Status, Kritisch-Zähler). `RawMaterialStockController` (`raw-material-stock.index`/`.show`, `copyreader`-Gruppe): Übersicht (Name, Qualität, Bestand, Verbrauch/Tag, „auf Null"-Datum, Hochrechnungs-Dropdown 1/3/6/12 Monate, Suche + „nur kritische"-Filter, rot/gelbe Markierung, Zeile klickbar) und Bestell-Detailseite (Kennzahlen, „Enthalten in", Lieferanten + Bestellaktion `Zum Shop`/`Per Mail`, Chargen mit Restbestand + Bestand je Lagerort). Sidenav-Eintrag „Rohstoffbestand" mit Kritisch-Badge (`View::composer`). **Offene Bestellungen** (`status=pending`) werden über `InventoryService::openOrderQuantityByIngredient()` als „Offen bestellt"-Spalte (Übersicht) bzw. eigener Block (Detail) sichtbar gemacht, zählen aber nicht zum Bestand; ein kritischer Rohstoff mit offener Bestellung wird zu Status `critical_ordered` entschärft (nicht im Badge gezählt). Über „Einkauf erfassen" auf der Detailseite wird der Einkauf mit Art=Rohstoff + vorausgewähltem Inhaltsstoff geöffnet (`StockEntryController@create` liest `ingredient_id`). Pro-Lagerort-Spalten der Übersicht bewusst weggelassen (Mockup zeigt Gesamtbestand; Lagerort-Aufschlüsselung steht auf der Detailseite). **Offen für AP-11:** Produktbestand/Verkauf-pro-Tag je Produkt im „Enthalten in"-Block (Produktbestand existiert noch nicht); Bedarfsableitung „rechtzeitig bestellen" über Lieferzeit-Tage kann später verfeinert werden. Tests: `tests/Feature/RawMaterialStockTest.php` (8 grün). --- ### AP-11 — Produktbestand + Historie **DB** - `product_stock_movements`: `product_id`, `direction` ENUM(`in`,`out`), `quantity`, `reason`, `source` (produktion/verkauf/manuell/set), `user_id`, `created_at`, `reference_type`/`reference_id` (polymorph, nullable). - Schwellwerte: Felder an `products` (`min_product_stock`, `critical_product_stock`) oder eigene Tabelle. - **Initialisierung (Briefing):** Lagerbestand einmalig einpflegbar (Anfangsbestand als `in`-Bewegung mit Grund „Initialbestand"). **Code** - Bestand = `SUM(in)` − `SUM(out)`. Manuelle Bewegung: Menge + Grund + Richtung Pflicht. - Hauptmenü „Produktbestand" (nur Hauptprodukte, Suche, Checkbox „nur kritische", Buttons `+`/`−`/`Produzieren`, rot/gelb-Markierung) + Untermenü „Historie" (filterbar Produkt/Quelle/Zeitraum/User; revisionssicher, Korrektur nur per Gegenbuchung). **Akzeptanz:** Bestand schnell pflegbar; jede Bewegung in der Historie; nur Hauptprodukte sichtbar; Kritisch-Filter funktioniert. > **Status:** Erledigt (03.06.2026). Migrationen `2026_06_03_122635_create_product_stock_movements_table` (`product_id` FK→`products` cascade, `direction` ENUM(`in`,`out`), `quantity`, `reason`, `source` default `manual`, `note`, `user_id` FK→`users` nullOnDelete, `nullableMorphs('reference')`) + `…_add_stock_thresholds_to_products_table` (`min_product_stock`, `critical_product_stock`). Modell `ProductStockMovement` (+ `Product::stockMovements()`, Schwellwerte in fillable/casts). `app/Services/ProductStockService.php` zentralisiert die Logik: `currentStockByProduct()`/`currentStock()` (= `SUM(in)`−`SUM(out)`), `recordMovement()` (Menge immer positiv, Richtung steuert Vorzeichen), `recordProductionStock()` (idempotenter Soll-/Ist-Abgleich je Produktion → bucht nur die Differenz, append-only), `productStatus()` (critical ≤ kritischer Schwellwert, warning ≤ Meldebestand), `criticalProductCount()` (nur aktive Hauptprodukte mit gesetztem kritischen Schwellwert). `ProductStockController` (`copyreader`-Gruppe): `index` (nur Haupt-/Einzelprodukte, Bild, Bestand rot/gelb, Suche + „nur kritische"-Filter, `+`/`−`-Modal für Bewegung, `Produzieren`-Link mit Produktvorwahl), `storeMovement` (FormRequest `StoreProductStockMovementRequest`, nur `isAdmin`), `history` (Filter Produkt/Eingang-Ausgang/Grund/Monat+Jahr, revisionssicher). Produktion bucht Produktbestand automatisch (`ProductionService` → `recordProductionStock` bei `store`/`update`). Produktformular: Schwellwert-Felder in der Warenwirtschaft-Card (+ `ProductRepository`). Sidenav „Produktbestand" mit Untermenü „Übersicht"/„Historie" und Kritisch-Badge (`View::composer` erweitert um `criticalProductCount`). AP-10 „Enthalten in"-Block um Produktbestand-Spalte ergänzt. Tests: `tests/Feature/ProductStockTest.php` (9 grün), Produktions-Regression unverändert grün. **Hinweis:** Verkauf/Tag je Produkt sowie Bestandsabzug beim (Set-)Verkauf folgen mit AP-13. --- ### AP-12 — Ausgang / Ausschuss (Rohstoffe/Verpackung) - `stock_disposals` (Typ, Artikel, Charge optional, Lagerort, Menge, Einheit, Grund Pflicht, User, Datum) + Controller/Views; Integration in `InventoryService`. - **Akzeptanz:** Ausgang reduziert Rohstoff-/Verpackungsbestand; Grund ist Pflicht. > **Status:** Erledigt (03.06.2026). Migration `2026_06_03_124056_create_stock_disposals_table` (`disposal_type` ENUM(`ingredient`,`packaging`), `ingredient_id`/`packaging_item_id`/`stock_entry_id` nullable FKs, `location_id`, `quantity` decimal, `unit`, `reason` Pflicht, `note`, `user_id`, `disposed_at`). Modell `StockDisposal`. `InventoryService` erweitert: `disposedByIngredient()` + Abzug in `remainingByIngredient()` und `remainingByLocationForIngredient()` (somit auch im Kritisch-Zähler/Badge), `remainingByPackagingItem()` (Wareneingang − Produktionsverbrauch − Ausschuss) + `disposedByPackagingItem()`. `StockDisposalController` (`copyreader`-Gruppe; `create`/`store` nur `isAdmin`): `index` (Liste mit Art-Filter), `create`/`store` (FormRequest `StoreStockDisposalRequest`, deutsche Dezimal-/Datumsnormalisierung, Pflicht-Grund), `ingredientCharges` (JSON-Endpoint: eingegangene Chargen je Rohstoff + Lagerort für die Charge-Vorauswahl). Erfassungsformular mit Typ-Umschaltung (Rohstoff/Verpackung), Select2-Suche, optionaler Charge (setzt Lagerort automatisch), Grund-Auswahl, Datepicker. Sidenav-Eintrag „Ausgang / Ausschuss" (nach „Einkauf & Wareneingang"); „Ausschuss erfassen"-Button auf der Rohstoff-Detailseite (vorbelegt). Tests: `tests/Feature/StockDisposalTest.php` (8 grün), Regression Rohstoff-/Produktbestand grün. --- ### AP-13 — Shop-Anbindung: Bestand bei Verkauf/Versand reduzieren (Entwicklungskonzept) > **Stand:** Konzept (03.06.2026), noch nicht umgesetzt. Skizziert Trigger, Datenmodell, Set-Auflösung, Storno und offene Klärungspunkte, damit die Umsetzung ohne Rückfragen starten kann. #### 0. Ausgangslage (verifizierter Code-Stand) Wichtigste Erkenntnis aus der Code-Prüfung: **Die Verkaufsdaten liegen bereits im System** — AP-13 braucht primär einen sauberen Trigger an den vorhandenen Statuswechsel, keine zwingend neue WooCommerce-Schnittstelle für den Abzug selbst. - **Bestellungen vorhanden:** `shopping_orders` + `shopping_order_items` (`product_id`, `qty`, `price`). Jede Position zeigt per `product()` auf das Produkt. - **Produkt-Mapping zu WooCommerce besteht:** über `products.wp_number` (siehe `app/Http/Controllers/Api/ShoppingUserController.php::prepareOrder()` → `Product::whereWpNumber($order->article)`). Es ist **kein** neues Mapping nötig. - **Versandstatus steckt in `shopping_orders.shipped`** (0 = offen, 1 = in Bearbeitung, **2 = versendet**, 3 = abgeschlossen, 4 = Abholung, 5 = Wartestellung, 10 = storniert) + `shipped_at` (datetime). - **Zentraler Statuswechsel:** `app/Http/Controllers/SalesController.php@store`, Action `store_shipped` (~Z. 421–454). Dort wird `shipped` gesetzt und – bereits **idempotent** über `if (! $shopping_order->shipped_at)` – einmalig `shipped_at` beim Übergang auf „sent"/„close" gesetzt. **Idealer Aufhängepunkt.** - **WooCommerce-Anbindung (Ist):** WP ruft per Passport-API (`/api/wp/*`, `ShoppingUserController`) `store`/`order`/`update`/`status`/`cancel`/`open`. Status „versendet" wird derzeit **nicht** von WP gesetzt, sondern intern im Backend; WP **liest** den Status (`ShoppingOrder::getAPIShippedType()`). - **Produktbestand-Infrastruktur steht (AP-11):** `product_stock_movements` mit `direction`, `source`, `reason`, `nullableMorphs('reference')` und `ProductStockService::recordMovement()`. → Die „out"-Buchung setzt **ohne neue Tabellen** darauf auf. #### 1. Ablauf-Skizze ``` WooCommerce ──(push /api/wp/order)──▶ shopping_orders / shopping_order_items (Mapping via products.wp_number) │ Backend: Versand buchen (SalesController@store / store_shipped) ──▶ shipped = 2 + shipped_at │ (Trigger) ▼ SaleStockService::bookShipment(ShoppingOrder) ├─ je Position: Set? ──ja──▶ Komponenten (× Set-Menge × qty) je „out" │ └─nein─▶ Produkt selbst (× qty) „out" ├─ source = 'sale', reference = ShoppingOrder, reason = "Versand #" └─ idempotent (shopping_orders.stock_booked_at + Referenzprüfung) │ Storno/Rücknahme (shipped = 10 / zurück auf offen) ──▶ reverseShipment() = Gegenbuchung „in" (source = 'sale_reversal') ▼ AP-11 Historie (Quelle „Verkauf"/„Storno") ``` #### 2. Trigger-Zeitpunkt — Entscheidung (§5.2, geklärt) Bestandsabzug **beim Versand** (erst mit gebuchtem Versand ist das Produkt real „aus dem Regal"). Buchung beim Übergang auf `shipped ∈ {2 versendet, 3 abgeschlossen, 4 Abholung}`, am vorhandenen `shipped_at`-Guard in `SalesController@store`. Dieser Hinweis ist auch in der Hinweise-Doku (AP-18) hinterlegt. #### 3. Datenmodell — keine neue Tabelle - **`product_stock_movements` wiederverwenden:** `direction='out'`, neuer Quell-Wert `source='sale'`, `reference` = `ShoppingOrder` (polymorph), `reason` z. B. „Versand #", `user_id` = ausführender Admin (falls vorhanden). - **Idempotenz zweistufig:** (a) schneller Marker `shopping_orders.stock_booked_at` (nullable datetime, neue Mini-Migration) + (b) Sicherheitsnetz: vor Buchung prüfen, ob für diese `ShoppingOrder`-Referenz mit `source='sale'` bereits eine Bewegung existiert. #### 4. Set-Auflösung (Abhängigkeit AP-02 ✅) Pro `shopping_order_item` Produkt laden: - **Set** (`is_set`): über `setItems()` jede Komponente einzeln buchen, Menge = `pivot.quantity × item.qty`. - **Einzel-/Hauptprodukt:** Produkt selbst buchen, Menge = `item.qty`. - **Variante/Hauptprodukt (`main_product_id`/`main_product_quantity`):** Default — Variante bucht auf **sich selbst** (eigener Produktbestand). Abweichende Behandlung (Abzug vom Hauptprodukt × `main_product_quantity`) ist eine **offene Detailfrage** (siehe §9). #### 5. Storno / Retoure / Rücknahme (revisionssicher) - Wechsel auf `shipped=10` (storniert) **oder** Zurücksetzen auf „offen/in Bearbeitung" **nach** erfolgter Abzugsbuchung → Gegenbuchung `direction='in'`, `source='sale_reversal'`, gleiche `reference`, `reason='Storno #…'`; `stock_booked_at` wieder auf `null`. - **Kein Löschen**, nur Gegenbuchung (konsistent zur AP-11-Historie). Anknüpfpunkte: `ShoppingUserController::cancel()`/`open()` (WP-Seite) und `SalesController@store` (Backend). #### 6. Zentrale Logik in einem Service - Neuer schlanker `app/Services/SaleStockService.php` (oder Methodenpaar in `ProductStockService`): `bookShipment(ShoppingOrder $order)` und `reverseShipment(ShoppingOrder $order)`. Kapselt Set-Auflösung, Mengenberechnung, Idempotenz und Gegenbuchung. - Aufruf aus `SalesController@store` (Backend-Versand) und – falls Szenario B (siehe §7) – aus dem WP-Pfad. Ein Ort, eine Wahrheit. #### 7. Bestelldatenherkunft — zwei Szenarien (vorab zu bestätigen) - **Szenario A (empfohlen, kleinster Eingriff):** Versand wird im Backend gebucht (heutiger Stand: `SalesController`). Trigger dort, **keine neue WooCommerce-Integration nötig** für den Abzug. Bestellungen kommen weiterhin über die bestehende `/api/wp/order`-Push-Schnittstelle herein. - **Szenario B (Fulfillment in WooCommerce):** Versand passiert extern, WooCommerce meldet „versendet" zurück → neuer/erweiterter `/api/wp/*`-Endpoint (z. B. `ship`) **oder** Webhook, der `shipped=2` setzt und denselben Service ruft. Alternativ periodischer **WooCommerce-REST-Pull** (`GET wc/v3/orders?status=completed`), Abgleich über `wp_order_number`. - **Zu klären:** Löst in WooCommerce der Status „completed" oder „processing" den Abzug aus? Wer ist führend für den Versandstatus — Backend oder Woo? #### 8. UI / Sichtbarkeit - **Keine neue Seite zwingend:** Verkaufsbuchungen erscheinen automatisch in der AP-11-**Historie**; deren Quell-Filter um „Verkauf"/„Storno" erweitern. - **Optional (schließt AP-10/AP-11-Lücke):** Kennzahl „Verkauf/Tag" je Produkt (analog „Verbrauch/Tag" bei Rohstoffen) im Produktbestand und im „Enthalten in"-Block der Rohstoff-Detailseite. #### 9. Edge Cases & offene Detailfragen - Produkt ohne `wp_number` bzw. nicht auffindbar → Position überspringen + Logeintrag, **kein** Abbruch der Bestellbuchung. - Bestand darf negativ werden (Versand ist Fakt) → nur visuelle Warnung, **kein** Block. - Doppelte/wiederholte Statuswechsel → durch Idempotenz (§3) abgesichert. - Teilstorno einzelner Positionen (falls Woo das liefert) → vorerst **ganze Bestellung**, Position-Granularität später. - Varianten vs. Hauptprodukt-Abzug (§4) → Entscheidung mit Kunde. #### 10. Tests (Pest) - Versand bucht „out" je Produkt × `qty`. - Set-Versand bucht Komponenten × (Set-Menge × qty). - Idempotenz: zweiter `store_shipped` bucht **nicht** doppelt. - Storno bucht „in" als Gegenbuchung; `stock_booked_at` zurückgesetzt. - Produkt ohne `wp_number` wird übersprungen (kein Fehler). - Historie zeigt Quelle „Verkauf". #### 11. Abhängigkeiten & Aufwand - Abhängig von **AP-02 (Sets)** ✅ und **AP-11 (Produktbestand)** ✅ — beide erledigt. - **Aufwand:** Szenario A ~2–3 Tage; Szenario B (Webhook/Pull) ~3–5 Tage. **Akzeptanz:** Versand reduziert den Produktbestand; Set-Versand reduziert die enthaltenen Einzelprodukte; Storno bucht zurück; jede Buchung steht revisionssicher in der Historie. --- ### AP-14 — Audit-Trail - `inventory_logs` (polymorph) + Observer auf `StockEntry`/`Production`/`StockDisposal`/`ProductStockMovement`. - **Akzeptanz:** Jede Bestandsbewegung wird mit User/Zeit/Änderungen protokolliert. --- ### AP-15 — Blockbasierte Rechte - **Entscheidung (§5.4, geklärt):** Blockrechte gelten **nur für Warenwirtschaft und Produktmanagement**, nicht für alle Admin-Bereiche. In der Hinweise-Doku (AP-18) dokumentieren, dass die Rechte **bei Bedarf später ausgebaut** werden können/müssen, falls sie nicht ausreichen. - `admin_permission_blocks` + `admin_permission_user` (view/edit pro Block: Produkte, Einkauf, Rohstoffbestand, Produktbestand, Produktion, Lieferanten, Einstellungen, Historie); Middleware/Gates; Sidenav zeigt nur erlaubte Blöcke. - Bestehende Level (`copyreader`/`admin`/`superadmin`) bleiben als Grundschutz. - **Akzeptanz:** SuperAdmin vergibt pro Mitarbeiter view/edit je Block (Warenwirtschaft + Produktmanagement); Leserecht ohne Schreibrecht greift; gesperrte Blöcke unsichtbar. --- ### AP-16 — 2FA Google Authenticator (Admins) - TOTP-Secret am `App\User` (Guard `user`), Setup-Flow, Login-Zwischenschritt; Recovery-Codes. - **Akzeptanz:** Bei aktivem 2FA kein Zugriff auf geschützte Bereiche ohne Code. --- ### AP-17 — Warenwirtschafts-Einstellungen - Über bestehendes `Setting`-Model: `inventory_alert_email`, `inventory_alert_enabled`, `inventory_default_location`, optional Produktbestands-Schwellwerte, Standardtexte „Nicht vorrätig". SuperAdmin-only. --- ### AP-18 — Hinweise-/Doku-Seite (Einstellungen → Hinweise) > **Anforderung (§5, Kunde):** Eine als MD gepflegte Doku, die unter **Warenwirtschaft → Einstellungen → „Hinweise"** im Admin sichtbar ist, damit auch der Kunde Einsicht hat. **Code** - Markdown-Datei im Repo (z. B. `docs/hinweise.md` oder `resources/docs/hinweise.md`) als Pflege-Quelle. - Route + View unter `admin/inventory` (Einstellungen-Gruppe), die das MD gerendert anzeigt (Parsedown o. Ä.); Sidenav-Eintrag „Hinweise". **Inhalt (laufend zu pflegen):** - Kurzer **Entwicklungsstand / Überblick** (was fertig ist, was offen ist). - Wichtige Hinweise & **noch nötige Schritte** verständlich für den Kunden. - Festgehaltene **offene/spätere Entscheidungen**, u. a.: - „Nicht vorrätig" kann künftig optional zur **Kauf-Sperre** ausgebaut werden (§5.3). - **Blockrechte** ggf. später über Warenwirtschaft/Produktmanagement hinaus ausbauen (§5.4). - Shop-Bestandsabzug erfolgt **bei Versand** (§5.2). - **Akzeptanz:** Kunde sieht unter Einstellungen → Hinweise eine lesbare, gepflegte Statusseite. > **Empfehlung:** Früh als Platzhalter anlegen und mit jedem AP fortschreiben, damit der Kunde jederzeit den Stand sieht. > > **Status:** Erledigt (03.06.2026). Pflege-Quelle `resources/docs/hinweise.md`; `NoticeController` rendert sie mit `Str::markdown()` (CommonMark) zu HTML. View `admin/inventory/notices/index.blade.php`, Route `admin.inventory.notices` (`superadmin`), Sidenav-Eintrag „Hinweise" unter „Einstellungen". Inhalt deckt Entwicklungsstand, Nutzungshinweise und die festgehaltenen Entscheidungen (§5.2 Versand-Abzug, §5.3 Kauf-Sperre optional, §5.4 Blockrechte) sowie das offene Produktentwicklungs-Briefing ab. **Bei jedem weiteren AP fortzuschreiben.** Tests: `tests/Feature/InventoryNoticesTest.php` (2 grün). --- ## 5. Klärungspunkte — ALLE GEKLÄRT (Kunde, 02.06.2026) > Alle Punkte sind beantwortet und in die jeweiligen Arbeitspakete eingearbeitet. Keine Blocker mehr offen. 1. **Produktion-Basis** → **Ausschließlich Hersteller-Rezeptur.** Kein Fallback. Ist keine angelegt, erscheint direkt eine Warnung. → eingearbeitet in **AP-09**. 2. **Shop-Bestandsabzug** → **Beim Versand** (erst mit gebuchtem Versand ist das Produkt real „aus dem Regal"). Als Hinweis dokumentieren. → eingearbeitet in **AP-13** + Hinweis in **AP-18**. 3. **„Nicht vorrätig"** → Vorerst **nur Hinweis**. Dokumentieren, dass künftig optional eine **Kauf-Sperre** ergänzt werden kann. → eingearbeitet in **AP-03** + Hinweis in **AP-18**. 4. **Blockrechte-Geltung** → **Nur Warenwirtschaft und Produktmanagement.** Dokumentieren, dass die Rechte bei Bedarf später ausgebaut werden können. → eingearbeitet in **AP-15** + Hinweis in **AP-18**. 5. **Produktentwicklung** → **Platzhalter-Seite** mit Hinweis, dass ein genaues Briefing noch aussteht. → eingearbeitet in **AP-09**. 6. **Child-Produkt / Sets** → **Echte Sets via Pivot** (`product_set_items`). → eingearbeitet in **AP-02**. 7. **Hinweise-Doku (neu):** MD-basierte Doku-Seite unter **Einstellungen → Hinweise** mit Entwicklungsstand, wichtigen Hinweisen und noch nötigen Schritten, einsehbar auch für den Kunden. → neues **AP-18**. --- ## 6. Empfohlene Sofort-Reihenfolge (nächste Schritte) ✅ **Erledigt:** AP-00, AP-01, AP-04 (+ AP-04.1), AP-05, AP-06 (+ Nachtrag), AP-07 (+ AP-07.1), AP-08, AP-09 (+ AP-09.1), AP-02 (Sets via Pivot), AP-03 („Nicht vorrätig"), AP-10 (Rohstoffbestand), AP-11 (Produktbestand + Historie), AP-12 (Ausgang / Ausschuss), AP-18 (Platzhalter, laufend zu pflegen), AP-19 (UI-Vereinheitlichung Warenwirtschaft). **➡️ Hier geht es weiter:** 1. **AP-13** (Shop-Anbindung: Bestandsabzug beim Versand inkl. Sets) – **Entwicklungskonzept liegt vor** (siehe AP-13 in §4). Kernbefund: Bestellungen liegen bereits im System (`shopping_orders`/`shopping_order_items`, Mapping über `products.wp_number`), Versandstatus wird zentral in `SalesController@store` gesetzt → Abzug kann ohne neue Tabelle auf der AP-11-Infrastruktur aufsetzen. **Vorab nur noch zu bestätigen:** Szenario A (Versand wird im Backend gebucht – empfohlen, kein neuer Woo-Eingriff) vs. Szenario B (WooCommerce meldet Versand per Webhook/REST-Pull zurück) sowie die Varianten-/Hauptprodukt-Detailfrage. Danach Folge-APs (AP-14–AP-17). 2. **AP-18** mit jedem weiteren AP fortschreiben (Hinweise-Seite aktuell halten). --- ### AP-19 — UI-Vereinheitlichung Warenwirtschaft (Darstellung) > **Status:** Erledigt (03.06.2026). Rein gestalterisch, keine fachliche Logik geändert. Ein einheitliches, modernes Erscheinungsbild für alle Warenwirtschafts-Seiten. Zentrale, wiederverwendbare Design-Partial **`resources/views/admin/inventory/partials/wawi-ui.blade.php`** (per `@once` eingebundenes, auf `.wawi-page` gescoptes Inline-CSS – **kein** SCSS-Build/`npm run` nötig). **Bausteine:** Seitenkopf (`wawi-page-head`, Titel + Untertitel + Aktionsbereich), Kennzahlen-Kacheln (`wawi-stats`/`wawi-stat`, teils klickbar als Filter), Karten (`wawi-card` + `wawi-card__header`/`__footer`), Toolbar mit Such-Feld (`wawi-toolbar`/`wawi-search`), aufgeräumte Tabellen (`wawi-table` mit Uppercase-Köpfen, Hover, schmaler Status-Akzentleiste links), Status-Pills (`wawi-pill--ok/--warning/--danger`), Datenblatt-Definitionsliste (`wawi-deflist`/`wawi-deflist__item`), Name-Zelle mit fester Icon-Spalte (`wawi-name-cell`), einheitlicher Leer-Zustand (`wawi-empty`). **Responsive:** Detail-Datenblätter brechen unter 768 px auf einspaltig um – Label klein oben, Wert linksbündig darunter. Die Name-Zelle hält das Icon in eigener Spalte, damit umgebrochener Text nicht unter das Icon rutscht. **Umgestellt:** alle Übersicht-/Listen- und Detailseiten unter `admin/inventory` (Produktbestand + Historie, Rohstoffbestand inkl. Detail, Ausgang/Ausschuss, Einkauf & Wareneingang inkl. Detail, Produktion inkl. Detail, Hinweise, Produktentwicklung, Einstellungen sowie die Stammdaten-Listen Lieferanten, Lieferanten-Kategorien, Verpackungsmaterial/-artikel, Materialqualität, Lagerorte). **Offen (bewusst):** Die reinen **Formularseiten** (Anlegen/Bearbeiten) nutzen noch den alten `h4`-Kopf; Angleichung an `wawi-page-head` als Folgeaufgabe vorgemerkt. --- ## 7. Pflege dieses Dokuments - Jedes abgeschlossene AP hier mit Datum + Kurzbeschreibung + Test-Status protokollieren (analog Umsetzungsprotokoll in `entwicklungsplan.md`). - Bei DB-Änderungen: Migration-Dateinamen referenzieren; bei Modellen Casts in `casts()`-Methode pflegen (L11-Konvention). - Vor jedem Commit: `vendor/bin/pint --dirty` und betroffene Tests (`php artisan test --filter=...`). - **UI-Konvention Datumsfelder:** Datumsfelder in Formularen immer als `` (kein natives `type="date"`). Der Datepicker wird global über `public/js/custom.js` auf `.datepicker-base` gebunden (Format `dd.mm.yyyy`, deutsche Locale). Modellwerte mit `->format('d.m.Y')` ausgeben; Backend parst `d.m.Y` über `Carbon::parse` bzw. die `date`-Validierungsregel. - **UI-Konvention Warenwirtschaft (AP-19):** Neue Seiten unter `admin/inventory` verwenden das Design-System aus `resources/views/admin/inventory/partials/wawi-ui.blade.php` (`@include('admin.inventory.partials.wawi-ui')` + Inhalt in `
`). Bausteine: `wawi-page-head`, `wawi-stats`/`wawi-stat`, `wawi-card`, `wawi-toolbar`/`wawi-search`, `wawi-table`, `wawi-pill`, `wawi-deflist`, `wawi-name-cell`, `wawi-empty`. Status immer über `wawi-pill` (ok/warning/danger) statt Bootstrap-`badge-pill`.