gruene-seele/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md
Kevin Adametz 3ee2d756e9 Warenwirtschaft: AP-09 bis AP-13 (Produktbestand, Set-Produkte, Ausschuss, Konzepte)
- AP-09 Produktbestand inkl. Bewegungshistorie (product_stock_movements, ProductStockService)
- AP-10 Rohstoffbestand-Ansicht je Lager (RawMaterialStockController)
- AP-11 Bestandsschwellen / Out-of-Stock-Handling fuer Produkte und Shop
- AP-12 Ausgang/Ausschuss (stock_disposals, StockDisposalController, InventoryService)
- Set-Produkte (product_set_items) inkl. Aufloesung
- Produktentwicklung & Hinweise-Verwaltung (Notices)
- AP-13 Entwicklungskonzept Shop-Bestandsabzug im Plan dokumentiert
- Feature-Tests fuer neue Module + aktualisierter Entwicklungsplan

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 11:04:22 +00:00

524 lines
73 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Aktualisierter Entwicklungsplan: Warenwirtschaft, Produktion & Produktbestand
> **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=EingangAusgang, 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=EingangVerbrauch, 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 (13 / 35 Werktage / 12 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 `<datalist>` 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, `<h2>`/„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 `<input type="text" class="form-control datepicker-base">` (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 `<datalist>` (`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=EingangAusgang, 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 05 | 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 `<input type="url">`. 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. 12):** 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,51 Tag |
| 4 | AP-05 | Einstellungen: UST-Sätze & Lieferzeiten (Stammdaten) | | 12 Tage |
| 5 | AP-06 | Lieferanten erweitern (Bestellweg, Lieferzeit) | AP-05 | 12 Tage |
| 6 | AP-07 | INCI erweitern (Lieferanten-Mehrfachwahl, UST, Lieferzeit) | AP-05, AP-06 | 23 Tage |
| 7 | AP-08 | Einkauf erweitern (UST, Netto/Brutto, Duplizieren) | AP-05 | 23 Tage |
| 8 | AP-09 | Produktion korrigieren (Hersteller-Rezeptur, Charge-JS, iPad, Produktentwicklung-Platzhalter) | | 24 Tage |
| 9 | AP-02 | Produkt-Klassen: Einzelprodukt vs. Set + Hauptprodukt | | 35 Tage |
| 10 | AP-03 | „Nicht vorrätig" mit Zeitangabe | | 12 Tage |
| 11 | AP-10 | Rohstoffbestand (InventoryService + Übersicht) | AP-06, AP-07, AP-09 | 46 Tage |
| 12 | AP-11 | Produktbestand + Historie + manuelle Bewegungen | AP-02, AP-10 | 58 Tage |
| 13 | AP-12 | Ausgang/Ausschuss (Rohstoffe/Verpackung) | AP-10 | 23 Tage |
| 14 | AP-13 | Shop-Anbindung: Bestand bei Verkauf reduzieren (inkl. Sets) | AP-02, AP-11 | 35 Tage |
| 15 | AP-14 | Audit-Trail (inventory_logs) | AP-1013 | 23 Tage |
| 16 | AP-15 | Blockbasierte Rechte | AP-05+ | 58 Tage |
| 17 | AP-16 | 2FA Google Authenticator für Admins | | 35 Tage |
| 18 | AP-17 | Warenwirtschafts-Einstellungen (Alarm-Mail, Default-Lager, Schwellwerte) | AP-10/11 | 12 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 <Name>`):
- 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. „35 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. 421454). 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 #<wp_order_number>"
└─ 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 #<wp_order_number>", `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 ~23 Tage; Szenario B (Webhook/Pull) ~35 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).
**➡️ 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-14AP-17).
2. **AP-18** mit jedem weiteren AP fortschreiben (Hinweise-Seite aktuell halten).
---
## 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 `<input type="text" class="form-control datepicker-base" value="dd.mm.yyyy">` (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.