gruene-seele/dev/product management /entwicklungsplan-aktualisiert-12-06-2026.md
Kevin Adametz e53201f229 Warenwirtschaft: Anforderungsrunde 12.06. — Plan V5.0 + AP-26/AP-25/AP-22
Neue Anforderungen (docs/) interpretiert und als Entwicklungsplan V5.0
(AP-20 bis AP-28) aufgenommen; erste drei Pakete umgesetzt:

AP-26 Ausschuss-Gründe konfigurierbar:
- Stammdaten-Tabelle disposal_reasons + CRUD unter Einstellungen → Allgemein
- StockDisposalController liest aktive DB-Gründe statt hartkodierter Liste
- Seeder übernimmt die bisherigen 6 Gründe idempotent

AP-25 Lieferbestand — Datum statt Tage:
- "Nicht vorrätig" wird über Datepicker "Wieder lieferbar ab" gepflegt;
  Resttage-Hinweis zählt täglich automatisch herunter
- Interne Bestellliste wieder kaufbar: Hinweis erscheint zusätzlich zu
  den Mengen-Buttons (VP entscheidet selbst)

AP-22 Produktbestand-Erweiterungen:
- Default-Sortierung nach Dringlichkeit, Status-Kopf toggelt
- Alle vier Status-Kacheln als Filter klickbar
- Neue Spalte "Verbrauch/Monat" (Ø Abgänge der letzten 6 Monate)
- Produkt-Flag "Im Produktbestand anzeigen" (products.show_in_product_stock)

Tests: 77 grün (DisposalReasonSettings 8, ProductOutOfStock 8,
ProductStock 13 + Regression). Hinweise-Doku + Plan-Protokoll fortgeschrieben;
nächster Schritt laut Plan: AP-21 (INCI-Erweiterungen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:28:45 +00:00

367 lines
45 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:** 5.0 - Stand 12.06.2026
> **Ersetzt:** `entwicklungsplan-aktualisiert-02-06-2026.md` (V4.0) als operative Arbeitsgrundlage
> **Neue Anforderungsquelle:** `docs/nächsten Anforderungen an die WaWi.md` (Kunde, 12.06.2026) + Screens `screens/2026-12-06-Rezeptur-phase.jpeg`, `screens/2026-12-06-neue-produktion.jpeg`
> **Referenzen:** V4.0 (AP-00 … AP-19), `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 so gewählt, dass jedes AP einzeln deploybar ist.
---
## 0. Was dieses Dokument neu macht
V4.0 hat **AP-00 bis AP-19 abgeschlossen** (Bugfixes, Einstellungen, Lieferanten/INCI/Einkauf erweitert, Produktion auf Hersteller-Rezeptur, Sets, „Nicht vorrätig", Rohstoffbestand, Produktbestand + Historie, Ausgang/Ausschuss, Hinweise-Doku, UI-Vereinheitlichung). **AP-13 (Shop-Bestandsabzug) liegt als Konzept vor, ist aber noch nicht umgesetzt.**
V5.0 nimmt die **neue Anforderungsrunde vom 12.06.2026** auf. Diese ist überwiegend **Feinschliff und Ergänzung an bereits gebauten Bausteinen** (Rohstoffbestand, INCI, Produktbestand, Ausschuss, Wareneingang) plus **zwei größere fachliche Erweiterungen**:
1. **Rezeptur-Phasen** auf der Hersteller-Rezeptur (Screen 1).
2. **Neue Produktions-Maske mit Produktionsergebnis** — der Produktbestand wird **nicht** mehr aus der geplanten Stückzahl gebucht, sondern aus dem real eingetragenen Ergebnis (mehrere Outputs möglich). Der Rohstoffverbrauch bleibt wie gehabt chargenbasiert (Screen 2).
Der **reale Code-Stand wurde erneut verifiziert** (12.06.2026); die daraus folgenden Abgrenzungen stehen je AP unter „Ist-Stand".
Geprüfte Dateien u. a.: `RawMaterialStockController`, `ProductStockController` + `ProductStockService`, `IngredientController` + `app/Models/Ingredient.php` + `resources/views/admin/ingredient/*`, `StockDisposalController`, `StockEntryController` + `app/Models/StockEntry.php`, `ProductionController` + `app/Services/ProductionService.php` + `app/Models/Production*.php`, `app/Models/Product.php`, `GeneralSettingController`, `app/Models/Location.php`, Migrationen unter `database/migrations/`.
---
## 0a. Umsetzungsprotokoll V5.0 (laufend)
> Jede abgeschlossene Teil-Lieferung wird hier mit Datum, betroffenen Dateien und Test-Status protokolliert.
| Datum | AP | Kurzbeschreibung | Tests |
|---|---|---|---|
| 12.06.2026 | **AP-22 (Produktbestand-Erweiterungen)** | Migration `2026_06_12_175615_add_show_in_product_stock_to_products_table` (`products.show_in_product_stock` bool default 1). `Product` fillable+cast; Checkbox „Im Produktbestand anzeigen" in der Warenwirtschaft-Card (`ProductRepository` normalisiert; Hinweistext „Abrechnung Druckkosten / Logo-Etiketten"). `ProductStockController@index` + `ProductStockService::criticalProductCount()` filtern auf das Flag (ausgeblendete Produkte zählen nicht im Badge). **Dringlichkeits-Sortierung:** Übersicht serverseitig default nach Status-Rang (kritisch → niedrig → ok), innerhalb nach Bestand aufsteigend, dann Name; Klick auf den „Status"-Spaltenkopf toggelt die Reihenfolge (JS über `data-rank`). **Kacheln:** alle vier Kacheln (`Produkte`/`Bestand OK`/`Niedrig`/`Kritisch`) klickbar — „Produkte" hebt den Filter auf, die anderen filtern exakt ihren Status (Checkbox „nur kritische" bleibt erhalten). **Neue Spalte „Verbrauch/Monat":** `ProductStockService::monthlyConsumptionByProduct()` = Ø der `out`-Bewegungen der letzten 6 Monate (rollierendes Fenster), **ohne** `source=production` (Produktions-Gegenbuchungen sind Korrekturen, kein Verbrauch); mit AP-13 fließen Verkäufe (`source=sale`) automatisch ein. Migration auf DB ausgeführt. | `ProductStockTest` erweitert (13 grün, 37 Assertions): Flag blendet aus Übersicht + Kritisch-Zähler aus, Verbrauch/Monat-Mittelung (Fenstergrenze, production-Ausschluss, Eingänge zählen nie), Default-Sortierung kritisch vor ok, Render Verbrauch-Spalte + 4 Kachel-Filter. Regression Produkt-Suite (32 grün) |
| 12.06.2026 | **AP-25 (Lieferbestand: Datum statt Tage)** | „Nicht vorrätig" wird jetzt über ein festes **Datum** („Wieder lieferbar ab", Datepicker `datepicker-base`, `dd.mm.yyyy`) statt über eine Tagesangabe gepflegt; Datenmodell unverändert (`out_of_stock_until` DATE passte bereits). `ProductRepository::update()` parst `out_of_stock_date` via `Carbon::createFromFormat('d.m.Y')` (ungültige Eingabe ⇒ null statt Crash); „unbestimmt" behält Vorrang, Deaktivierung leert beides. Resttage-Helper (`outOfStockRemainingDays()`/`outOfStockNotice()`) unverändert — zählen täglich automatisch herunter. **Interne Bestellliste wieder kaufbar:** `User/OrderController::datatable` zeigt den roten Hinweis jetzt **zusätzlich** zu den Mengen-Buttons (vorher ersetzte er sie) — der VP entscheidet selbst, ob er später beliefert wird; Produkt-Detail-Modal zeigte den Hinweis bereits nur zusätzlich. Formular-Card „Verfügbarkeit" + JS-Toggle (`js-out-of-stock-date`) angepasst. | `tests/Feature/ProductOutOfStockTest.php` umgebaut (8 grün, 29 Assertions): Datum→`out_of_stock_until`, Unbestimmt-Vorrang, Deaktivierung leert, ungültiges Datum ⇒ null, Vergangenheit gilt nicht, Resttage zählen mit `travel()` herunter, HTTP-Store mit Datum, **Bestellliste zeigt Hinweis + Mengen-Buttons**. Regression `ProductSetTest`/`ProductPhase51Test`/`ProductStockTest` (24 grün) |
| 12.06.2026 | **AP-26 (Ausschuss-Gründe konfigurierbar)** | Neue Stammdaten-Tabelle `disposal_reasons` (`label`, `active`, `pos`) via Migration `2026_06_12_170531_create_disposal_reasons_table`; Model `DisposalReason` (cast `active`, `scopeActive`), `DisposalReasonFactory`, Seeder-Erweiterung `InventoryStammdatenSeeder` (bisherige 6 Gründe idempotent per `firstOrCreate` übernommen). CRUD: `DisposalReasonController` (create/store/edit/update/destroy → Redirect `general`), `Store/UpdateDisposalReasonRequest` (`label` Pflicht max 100, passend zur `stock_disposals.reason`-Spalte). Dritte Karte „Ausschuss-Gründe" auf **Einstellungen → Allgemein** (`general/index.blade.php` + `GeneralSettingController`), View `disposal-reasons/form.blade.php`. Route Resource `disposal-reasons` (ohne index/show) in der `superadmin`-Gruppe. **`StockDisposalController::reasons()` liest jetzt aktive `disposal_reasons`** (Reihenfolge `pos`, dann `label`) statt der hartkodierten Liste; `StoreStockDisposalRequest` bleibt bewusst ohne `in:`-Zwang (bestehende Disposals behalten ihren String-Grund). Migration + Seed auf DB ausgeführt. | `tests/Feature/DisposalReasonSettingsTest.php` (8 grün, 24 Assertions): Render Allgemein-Karte, CRUD, Pflicht-Label, `active`-Scope, **Ausschuss-Formular zeigt nur aktive Gründe in Sortierung**, Zugriffsschutz Nicht-SuperAdmin. Regression `StockDisposalTest`/`TaxRateSettingsTest`/`DeliveryTimeSettingsTest` (24 grün) |
| _offen_ | AP-20 | Rohstoffbestand-Filter „alle / nur aus Herstellerrezepturen" + INCI-Flag „Kein Rohstoffbestand" | |
| _offen_ | AP-21 | INCI: Lieferant **+ URL** je Zeile (Repeater, „+ weiterer Lieferant"), „nur aktive"-Filter, „Alternativer Name", Lagerort (Raum/Regal/Buchstabe) inkl. zentraler Einstellungen | |
| _offen_ | AP-23 | Rezeptur-**Phasen** auf der Hersteller-Rezeptur (Phase A/B …, je Phase Rohstoffe + Notiz, Drag&Drop, Gesamt 100 %) | |
| _offen_ | AP-24 | Chargen-Nr.-**Präfix** auf Produktebene + Auto-Generierung (editierbar) in der Produktion | |
| _offen_ | AP-27 | Wareneingang: gleiche **Charge addieren** (Bestand zusammenführen), Einkaufsvorgang bleibt separat erfasst | |
| _offen_ | AP-28 | **Neue Produktions-Maske + Produktionsergebnis** (mehrere Outputs, entkoppelt von der Planung; Produktbestand aus Ergebnis, Rohstoffbestand aus Chargen, Phasen-Anzeige, Regal-Spalte) | |
**Status Roadmap V5.0 (12.06.2026):** **AP-26, AP-25 und AP-22 sind erledigt** (Details siehe Protokoll oben). Offen aus V5.0: AP-21, AP-20, AP-23, AP-24, AP-27, AP-28.
**Übernommen aus V4.0 / weiterhin offen:** **AP-13** (Shop-Bestandsabzug bei Versand inkl. Sets, Konzept liegt vor), **AP-14** (Audit-Trail), **AP-15** (Blockbasierte Rechte), **AP-16** (2FA), **AP-17** (WaWi-Einstellungen).
> **➡️ HIER GEHT ES WEITER: AP-21 (INCI-Erweiterungen).** Das ist das nächste Paket in der Reihenfolge und zugleich die Grundlage für AP-20 (liefert `scopeUsedInActiveRecipes()` + „Kein Rohstoffbestand"-Flag-Formularfeld) und für die neue Produktions-Maske AP-28 (liefert Alternativname + Lagerort/„Regal"-Spalte). Vor dem Start die Lagerort-Modell-Frage final entscheiden (§5 Punkt 1 — Empfehlung: drei getrennte Stammdaten-Tabellen Raum/Regal/Fach). Danach: AP-20 → AP-23 → AP-24 → AP-27+AP-28 gemeinsam.
---
## 1. Verifizierter Ist-Stand (12.06.2026) — relevant für die neue Runde
| Bereich | Ist im Code | Neue Anforderung verlangt |
|---|---|---|
| **Rohstoffbestand** (`RawMaterialStockController@index`) | Listet **alle aktiven** Rohstoffe (`active=true`), unabhängig von Rezeptur-Zugehörigkeit. Filter: Suche, „nur kritische", Status-Kacheln. | Umschalt-Filter **„alle" / „nur aus Herstellerrezepturen"**; Rohstoffe ohne Bestandsführung (Allergene etc.) per Flag ausblenden. |
| **INCI / `ingredients`** (`app/Models/Ingredient.php`, `IngredientController`, `ingredient/form.blade.php`) | Felder: `name`, `inci`, `effect`, `active`, `pos`, `default_factor`, `min_stock_alert`, `material_quality_id`, `tax_rate_id`, `delivery_time(_days)`. Lieferanten über Pivot `ingredient_supplier` (Spalten `preferred`, `supplier_sku`, **`url`** vorhanden), Form macht aber **nur Multi-Select** der Lieferanten — **keine** Per-Lieferant-URL-Eingabe. **Kein** `alt_name`, **kein** „Kein Rohstoffbestand"-Flag, **kein** Lagerort. | `alt_name` (Alternativer Name), Per-Lieferant-URL-Repeater mit „+", „nur aktive"-Filter, Lagerort (Raum/Regal/Buchstabe), „Kein Rohstoffbestand"-Flag. |
| **Produktbestand** (`ProductStockController@index`, `ProductStockService`) | Sortiert nach `pos`,`name`. Kacheln: nur **warning/critical** klickbar (`all`/`ok` nicht). Filtert `active=true`, `is_set=false`, `main_product_id IS NULL`. **Kein** Sichtbarkeits-Flag, **keine** „Verbrauch/Monat"-Spalte. | Default-Sortierung nach **Dringlichkeit**, **alle** Kacheln klickbar, Produkt-Flag „im Produktbestand anzeigen", Spalte „Verbrauch/Monat" (Ø 6 Mon.). |
| **Rezeptur / `product_ingredients`** | Spalten: `product_id`, `ingredient_id`, `pos`, `gram`, `factor`, `recipe_type` (`product`/`manufacturer`). **Kein** Phasen-Begriff. | Phasen auf der **Hersteller-Rezeptur** (Phase A/B …, je Phase Rohstoffe + Notiz, Gesamt 100 %). |
| **Produkt / `products`** | Hat `out_of_stock_until`, `out_of_stock_indefinite`, `min/critical_product_stock`, Set-Felder, `no_recipe_required`. **Kein** Chargen-Präfix, **kein** Produktbestand-Sichtbarkeits-Flag. | `batch_prefix`, `show_in_product_stock`, Lieferbestand auf Datum umstellen. |
| **Ausschuss** (`StockDisposalController::reasons()`) | Gründe **hartkodiert** im Controller (6 Werte). | Gründe als **konfigurierbare Stammdaten** (Einstellungen). |
| **Wareneingang / `stock_entries`** | `batch_number` (string) vorhanden, **keine** Erkennung/Zusammenführung gleicher Chargen. Restbestand wird ohnehin chargenweise über `production_ingredients` gerechnet. | Beim Erfassen prüfen, ob `batch_number` für denselben Rohstoff schon existiert → Mengen zusammenführen, Einkaufsvorgang trotzdem separat erfassen. |
| **Produktion / `productions`** | Ein `quantity` (Output), `produced_at`, `location_id`, `notes`. `production_ingredients` (`stock_entry_id`, `quantity_used`). **Produktbestand wird heute aus `quantity` gebucht** (`ProductStockService::recordProductionStock`). **Kein** Ergebnis-/Mehrfach-Output-Begriff, **keine** Phasen-Anzeige, **keine** Regal-Spalte. | Neue Maske: Planung (Produkt/Größe/Datum/geplante Stückzahl) nur informativ; **Produktionsergebnis** mit **mehreren** Outputs (Produkt, produzierte Stückzahl, Chargen-Nr.) bucht den Produktbestand; Rohstoffverbrauch chargenbasiert wie bisher; Phasen + Regal-Spalte. |
| **Locations** | `locations`: `name`, `active`. Keine Raum/Regal-Struktur. | Lagerort-Bausteine (Raum/Regal/Buchstabe) zentral pflegbar, am INCI referenziert. |
---
## 2. Interpretation der neuen Anforderungen (Mapping auf APs)
| # | Anforderung (12.06.) | AP |
|---|---|---|
| Rohstoffbestand 1a/1b | Filter „alle Rohstoffe" / „nur aus Herstellerrezepturen" | **AP-20** |
| Rohstoffbestand (INCI-Flag) | „Kein Rohstoffbestand"-Kontrollkästchen (Allergene etc. ausblenden) | **AP-20** |
| INCI 1 | Lieferant **+ URL** je Zeile, URL leer ⇒ Mailbestellung; „+ weiteren Lieferanten" | **AP-21** |
| INCI 2 | INCI-Liste „nur aktive" (≥ 1 aktives Produkt) | **AP-21** |
| INCI 3 | Feld „Alternativer Name" (z. B. LECITHIN → Phospholipon 80 H) | **AP-21** |
| INCI 4 | Lagerort RAUM \| REGAL-NR. \| BUCHSTABE, zentral unter Einstellungen pflegbar | **AP-21** |
| Produktbestand 1 | Sortierung nach Dringlichkeit (default), Kacheln „Produkte"/„Bestand ok" klickbar | **AP-22** |
| Produktbestand 2 | Produkt-Flag „im Produktbestand anzeigen" | **AP-22** |
| Produktbestand 3 | Spalte „Verbrauch pro Monat" (Ø letzte 6 Monate) | **AP-22** |
| Produktebene 1 | Rezeptur-**Phasen** unter der Hersteller-Rezeptur (Screen 1) | **AP-23** |
| Produktebene 2 | Chargen-Nr.-**Präfix** (Kategorie-Kürzel + Produkt-Nr. + Produktionsdatum) | **AP-24** |
| Lieferbestand | **Datum** statt Tage, Produkt bleibt kaufbar, Resttage für VP zählen herunter | **AP-25** |
| Ausschuss | Gründe unter Einstellungen selbst anlegen | **AP-26** |
| Wareneingang | Gleiche Charge addieren, Einkaufsvorgang trotzdem erfassen | **AP-27** |
| Produktion | Neue Maske + **Produktionsergebnis** (mehrere Outputs), Phasen, Regal (Screen 2) | **AP-28** |
---
## 3. Priorisierte Roadmap V5.0
> **Leitidee der Reihenfolge:** Erst die kleinen, isolierten Ergänzungen an bestehenden Seiten (schnell sichtbarer Nutzen, geringes Risiko), dann die Datenmodell-Grundlagen (Phasen, Chargen-Präfix), zuletzt die große Produktions-Maske, die auf Phasen + Präfix + Lagerort aufsetzt.
| Reihenfolge | AP | Titel | Abhängigkeit | Aufwand |
|---|---|---|---|---|
| 1 | ✅ AP-26 | Ausschuss-Gründe konfigurierbar — **erledigt 12.06.2026** | | 0,51 Tag |
| 2 | ✅ AP-25 | Lieferbestand: Datum statt Tage (Revision AP-03) — **erledigt 12.06.2026** | | 0,51 Tag |
| 3 | ✅ AP-22 | Produktbestand-Erweiterungen (Sortierung, Kacheln, Flag, Verbrauch/Monat) — **erledigt 12.06.2026** | | 1,52,5 Tage |
| 4 | ➡️ AP-21 | **NÄCHSTER SCHRITT:** INCI-Erweiterungen (Lieferant+URL, nur aktive, Alternativname, Lagerort + Einstellungen) | AP-05-Muster (Stammdaten) | 2,54 Tage |
| 5 | AP-20 | Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag | AP-21 (Flag/Lagerort am INCI) | 12 Tage |
| 6 | AP-23 | Rezeptur-Phasen (Hersteller-Rezeptur) | | 35 Tage |
| 7 | AP-24 | Chargen-Nr.-Präfix (Produktebene) | | 1 Tag |
| 8 | AP-28 | Neue Produktions-Maske + Produktionsergebnis | AP-21 (Lagerort), AP-23 (Phasen), AP-24 (Präfix) | 58 Tage |
> **Querschnitt:** Alle neuen/angepassten Seiten verwenden das Design-System aus AP-19 (`resources/views/admin/inventory/partials/wawi-ui.blade.php`) und die Datumsfeld-Konvention (`datepicker-base`, `dd.mm.yyyy`). Jede DB-Änderung wird per Migration nachgezogen, jedes AP mit `vendor/bin/pint --dirty` und Pest-Tests abgeschlossen.
---
## 4. Arbeitspakete im Detail
### AP-26 — Ausschuss-Gründe konfigurierbar
**Anforderung:** „Ich möchte die Gründe für den Ausschluss unter Einstellungen selber anlegen können."
**Ist-Stand:** Gründe sind in `StockDisposalController::reasons()` (≈ Z. 114124) hartkodiert.
**DB**
- Neue Tabelle `disposal_reasons`: `label` VARCHAR, `active` bool, `pos` int. Seeder mit den bisherigen 6 Werten (idempotent per `firstOrCreate`), damit Bestandsdaten unverändert bleiben.
**Code**
- Model `DisposalReason` (`scopeActive`, casts), Factory, `InventoryStammdatenSeeder`-Erweiterung.
- CRUD `DisposalReasonController` (create/store/edit/update/destroy → Redirect `general`) + `Store/UpdateDisposalReasonRequest` — analog zu `TaxRateController`/`DeliveryTimeController`.
- Dritte Karte „Ausschuss-Gründe" auf der Seite **Einstellungen → Allgemein** (`general/index.blade.php`) + `GeneralSettingController` um `disposalReasons` erweitern.
- `StockDisposalController`: `reasons()` liest jetzt aktive `disposal_reasons` (Reihenfolge `pos`); `create`/Formular nutzt sie. `StoreStockDisposalRequest` validiert weiterhin nur „nicht leer" (kein harter `in:`-Zwang, da frei pflegbar).
- Routen Resource `disposal-reasons` (ohne index/show) in der `superadmin`-Gruppe; Sidenav unverändert (liegt unter „Allgemein").
**Akzeptanz:** SuperAdmin legt/ändert/deaktiviert Ausschuss-Gründe; das Ausschuss-Formular zeigt nur aktive Gründe in gepflegter Reihenfolge; bestehende Disposals behalten ihren (als String gespeicherten) Grund.
**Tests:** `DisposalReasonSettingsTest` (Render, CRUD, Validierung, `active`-Scope, Zugriffsschutz) + Regression `StockDisposalTest`.
> **Status:** Erledigt (12.06.2026). Umgesetzt wie geplant — Tabelle `disposal_reasons`, CRUD analog TaxRate/DeliveryTime, dritte Karte unter Einstellungen → Allgemein, `StockDisposalController::reasons()` liest aktive DB-Gründe. Details siehe Umsetzungsprotokoll (§0a). Tests: `tests/Feature/DisposalReasonSettingsTest.php` (8 grün).
---
### AP-25 — Lieferbestand: Datum statt Tage (Revision AP-03)
**Anforderung:** „Bei erst in 12 Tagen wieder lieferbar' machen wir das Produkt doch wieder kaufbar — der VP entscheidet selbst. Ich präferiere, dass ich auf Produktebene ein **Datum** einstelle und für den VP dann die **Anzahl der Tage** erscheint, die sich täglich aktualisiert."
**Ist-Stand:** AP-03 hat `out_of_stock_until` (DATE) + `out_of_stock_indefinite` (bool). Eingabe erfolgt heute über ein **Tagefeld** (`now()->addDays($tage)`); Kauf bleibt im Shop bereits möglich. **Interne Bestellliste** (`OrderController::datatable` + `admin/modal/show_product`) ersetzt die Mengen-Buttons aktuell durch einen Hinweis → laut neuer Anforderung soll **kaufbar bleiben**.
**Kern der Änderung:** Eingabe-Logik von „Tage" auf **„Datum"** umstellen (Datenmodell `out_of_stock_until` bleibt — passt bereits), Resttage werden für den VP weiterhin tagesgenau berechnet und gezählt. Kauf **überall** möglich (auch interne Bestellliste).
**Code**
- **Produktformular** (Card „Verfügbarkeit", `resources/views/admin/product/…` + JS `toggleOutOfStock` in `edit.blade.php`): Tagefeld → **Datepicker** „Wieder lieferbar ab" (`datepicker-base`, `dd.mm.yyyy`), vorbefüllt aus `out_of_stock_until->format('d.m.Y')`. Checkbox „unbestimmt" bleibt.
- **`ProductRepository::update()`**: statt `addDays($tage)` jetzt `Carbon::parse($datum)` aus dem Datepicker; „unbestimmt" hat weiter Vorrang (Datum=null), Deaktivierung leert beides. Helfer `outOfStockRemainingDays()`/`outOfStockNotice()` bleiben unverändert (zählen schon tagesgenau herunter).
- **Interne Bestellliste kaufbar machen:** in `OrderController::datatable` (Produkt-Spalte) und `admin/modal/show_product` die Mengen-Buttons **nicht mehr** unterdrücken; stattdessen Hinweis „In ca. X Tag(en) wieder da!" **zusätzlich** anzeigen (Kauf bleibt). Shop zeigt den Hinweis bereits zusätzlich.
**Akzeptanz:** Auf Produktebene wird ein Datum gesetzt; VP sieht im Shop und in der internen Bestellliste „In ca. X Tagen wieder da!", die Tage zählen täglich herunter, das Produkt bleibt überall kaufbar; nach Ablauf verschwindet der Hinweis automatisch.
**Tests:** `ProductOutOfStockTest` anpassen (Datum→Resttage statt Tage→Datum; kaufbar in interner Liste; Vergangenheit gilt nicht; „unbestimmt"-Vorrang). Hinweise-Doku (AP-18) aktualisieren (Kauf-Sperre weiterhin nur optionale Zukunftsoption).
> **Status:** Erledigt (12.06.2026). Datepicker „Wieder lieferbar ab" statt Tagefeld; `ProductRepository` parst `d.m.Y` (ungültig ⇒ null); interne Bestellliste zeigt den Hinweis jetzt **zusätzlich** zu den Mengen-Buttons (Kauf überall möglich). Hinweise-Doku aktualisiert. Details siehe Umsetzungsprotokoll (§0a). Tests: `tests/Feature/ProductOutOfStockTest.php` (8 grün).
---
### AP-22 — Produktbestand-Erweiterungen
**Anforderungen:**
1. Sortierung nach **Dringlichkeit** (Klick auf „Status" sortiert dringlichste oben; darf default so sein). Kacheln „Produkte" und „Bestand ok" sollen ebenfalls **klickbar** sein.
2. Produkt-Flag: „im Produktbestand anzeigen ja/nein" (z. B. Abrechnung Druckkosten / Logo-Etiketten gehören da nicht rein).
3. Spalte **„Verbrauch pro Monat"** (Ø der letzten 6 Monate).
**Ist-Stand:** Sortierung `pos`,`name`; nur warning/critical-Kacheln klickbar; Filter `active`, `is_set=false`, `main_product_id IS NULL`; keine Verbrauchsspalte; kein Sichtbarkeits-Flag.
**DB (`products`)**
- `show_in_product_stock` bool default 1 (Migration). Bestehende Produkte bleiben sichtbar; gezielt abwählbar.
**Code**
- **Flag:** `Product` fillable + cast; Checkbox „Im Produktbestand anzeigen" in der Warenwirtschaft-Card des Produktformulars; `ProductRepository::update()` normalisiert (`isset ? 1 : 0`). `ProductStockController@index` filtert zusätzlich `where('show_in_product_stock', true)`. Auch `criticalProductCount()` (`ProductStockService`) respektiert das Flag.
- **Dringlichkeits-Sortierung:** `ProductStockService::productStatus()` liefert bereits `critical`/`warning`/`ok`. Default-Reihenfolge serverseitig nach Status-Rang (critical → warning → ok), innerhalb gleich nach Reichweite/Name; DataTables-`order` auf die Status-Spalte (mit `data-order`-Rang) setzen, sodass ein Klick auf „Status" die dringlichsten oben hält.
- **Kacheln klickbar:** in `product-stock/index.blade.php` allen vier `wawi-stat`-Kacheln (`all`/`ok`/`warning`/`critical`) `is-clickable` + `data-filter` geben; JS-Filter (analog Rohstoffbestand) auf alle vier Werte erweitern. „Produkte" (=all) hebt Filter auf, „Bestand ok" filtert auf `ok`.
- **Verbrauch/Monat:** neue Methode `ProductStockService::monthlyConsumptionByProduct()` = Ø der `out`-Bewegungen mit `source IN ('sale','production_out',…)` bzw. der relevanten Abgänge der **letzten 6 Kalendermonate** (Summe der Abgänge / 6). Spalte „Verbrauch/Monat" in der Übersicht (rechtsbündig, Einheit Stück).
> **Klärung (vermerkt, nicht blockierend):** „Verbrauch" = abgehende Bewegungen. Solange AP-13 (Verkaufsabzug) nicht live ist, basiert der Wert auf manuellen `out`-Bewegungen; mit AP-13 fließen Verkäufe automatisch ein. Default-Fenster 6 Monate.
**Akzeptanz:** Übersicht ist standardmäßig nach Dringlichkeit sortiert; alle vier Kacheln filtern; ausgeblendete Produkte (Flag aus) erscheinen nicht und zählen nicht in den Kritisch-Badge; „Verbrauch/Monat" zeigt den 6-Monats-Durchschnitt.
**Tests:** `ProductStockTest` erweitern (Flag blendet aus + aus Kritisch-Zähler, Verbrauch/Monat-Mittelung über 6 Monate, Status-Sortierrang, Render der vier Kachel-Filter).
> **Status:** Erledigt (12.06.2026). Abweichung zur Planung: Sortier-Toggle nicht über DataTables-`order`, sondern leichtgewichtig per eigenem JS (`data-rank` an der Zeile, Klick auf den „Status"-Kopf sortiert um) — die Seite nutzt kein DataTables. „Verbrauch/Monat" als **rollierendes** 6-Monats-Fenster umgesetzt (statt Kalendermonate; Klärungspunkt §5.3 bleibt zur Bestätigung offen), Produktions-Gegenbuchungen (`source=production`) ausgenommen. Details siehe Umsetzungsprotokoll (§0a). Tests: `ProductStockTest` (13 grün).
---
### AP-21 — INCI-Erweiterungen (Lieferant+URL, nur aktive, Alternativname, Lagerort)
**Anforderungen (INCI-Ebene 14):**
1. Pro INCI mehrere **Lieferanten mit eigener URL** (URL frei ⇒ Bestellung per Mail), „+ weiteren Lieferanten anlegen".
2. INCI-Liste „**nur aktive**" filtern (aktiv = in mind. einer Hersteller-Rezeptur eines **aktiven** Produkts).
3. Feld **„Alternativer Name"** (Handelsname; findbar für Mitarbeiter).
4. Feld **Lagerort** im Schema RAUM \| REGAL-NR. \| BUCHSTABE; die einzelnen Bausteine zentral unter **Einstellungen** pflegbar.
**Ist-Stand:** `ingredient_supplier.url` existiert im Schema, wird im Formular aber nicht ausgespielt (nur Multi-Select). Kein `alt_name`, kein Lagerort, keine „nur aktive"-Liste.
**DB**
- `ingredients.alt_name` VARCHAR nullable (Migration).
- Lagerort am INCI: `ingredients.location_room_id`, `location_shelf_id`, `location_slot_id` (nullable FKs) **oder** kompakt drei Strings — **Entscheidung: drei FK auf neue Stammdaten** (Punkt 4 verlangt zentrale Pflege).
- Zentrale Stammdaten unter Einstellungen — drei kleine Tabellen `storage_rooms` (Raum), `storage_shelves` (Regal-Nr.), `storage_slots` (Buchstabe), je `label`, `active`, `pos`. (Alternativ generische `storage_segments` mit `type`-Enum `room|shelf|slot`; bei der Umsetzung die einfachere von beiden wählen — Default: **drei Tabellen** für klare Dropdowns.)
- **Lieferanten-URL:** Pivot `ingredient_supplier.url` ist vorhanden → **keine** Migration nötig; nur das Formular muss die Spalte bespielen.
**Code**
- `Ingredient`: fillable `alt_name` + Lagerort-FKs; Relationen `room()/shelf()/slot()`; Helper `storageLabel()` = `"Raum 1 | Regal 3 | O"` (für die Anzeige in Rohstoffbestand-Detail **und** in der Produktions-Maske, Spalte „Regal").
- **Lieferanten-Repeater** in `ingredient/form.blade.php`: pro Zeile `Lieferant`-Select + `URL`-Textfeld (`type="text"`, max 2048) + Entfernen; „+ weiterer Lieferant"-Button (JS klont eine Zeile). Speichern via `suppliers()->sync([$id => ['url' => …, 'preferred' => …]])` (Pivot-Daten). `StoreIngredientRequest` validiert paarweise (`suppliers.*.id` exists, `suppliers.*.url` nullable string max 2048). **Ableitung Bestellweg:** je INCI-Lieferant gilt `url` leer ⇒ **Mailbestellung**, sonst Shop-Link — diese Ableitung ersetzt im Rohstoffbestand-Detail (AP-10) den bisherigen `supplier.order_method`-Fallback (INCI-spezifisch sticht).
- **„Alternativer Name":** Eingabefeld neben „Name"/„INCI"; Anzeige als eigene Spalte in der INCI-Verwaltungsliste und in der Produktions-INCI-Liste (Screen 2 zeigt Spalte „Alternativer Name").
- **Lagerort:** drei abhängige Dropdowns (Raum/Regal/Buchstabe) aus den Stammdaten; Anzeige als „Regal"-Spalte (Screen 2) und im Rohstoffbestand-Detail.
- **Einstellungen → Allgemein:** je eine Karte „Räume", „Regale", „Fächer/Buchstaben" mit CRUD (Muster AP-05); `GeneralSettingController` um die drei Listen erweitern; Routen-Resources (`superadmin`).
- **„nur aktive"-Filter der INCI-Liste:** Scope `Ingredient::scopeUsedInActiveRecipes()` = `whereHas('products', fn($q) => $q->where('recipe_type','manufacturer')->whereHas('product', active))` (Pivot-Pfad über `product_ingredients` mit `recipe_type='manufacturer'` zu aktiven Produkten). In der INCI-Übersicht (`IngredientController@index` bzw. die Verwaltungsliste) eine Checkbox „nur aktive" (server- oder DataTables-clientseitig). **Achtung:** „aktiv" hier ≙ in Herstellerrezeptur eines aktiven Produkts — **nicht** das Feld `ingredients.active`. Begriffe in der UI sauber trennen („nur aktive (in Rezeptur)").
**Akzeptanz:** Pro INCI können mehrere Lieferanten mit je eigener URL gepflegt werden (leer = Mail); INCI hat einen Alternativnamen und einen zentral gepflegten Lagerort; die INCI-Liste lässt sich auf „nur in aktiven Herstellerrezepturen verwendete" filtern; Lagerort und Alternativname erscinen in Rohstoffbestand-Detail und Produktions-Maske.
**Tests:** `IngredientOrderFieldsTest` erweitern (Per-Lieferant-URL speichern/sync, `alt_name`, Lagerort-FKs); neuer `IngredientActiveRecipeFilterTest` (Scope liefert nur INCI aus aktiven Herstellerrezepturen); `StorageSettingsTest` (CRUD Räume/Regale/Fächer, Zugriffsschutz).
---
### AP-20 — Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag
**Anforderungen (Rohstoffbestand 1):**
- Filter (a) **alle** Rohstoffe (auch nicht in aktiven Produkten — z. B. für Neuentwicklung eingekauft) und (b) **nur Rohstoffe aus Herstellerrezepturen**.
- INCI-Kontrollkästchen „**Kein Rohstoffbestand**" — Rohstoffe/Einträge (Allergene etc.), die gar nicht im Rohstoffbestand auftauchen sollen.
**Ist-Stand:** `RawMaterialStockController@index` listet **alle aktiven** Rohstoffe ohne Rezeptur-Bezug; kein Ausschluss-Flag.
**DB (`ingredients`)**
- `exclude_from_stock` bool default 0 (Migration). (Name bewusst „Kein Rohstoffbestand".)
**Code**
- `Ingredient`: fillable + cast `exclude_from_stock`; Checkbox „Kein Rohstoffbestand (z. B. Allergen-Angabe, nicht bevorratet)" im INCI-Formular.
- `RawMaterialStockController@index`:
- grundsätzlich `where('exclude_from_stock', false)`.
- Umschalt-Filter **Modus** (`?scope=all|recipe`, Default `all`): bei `recipe` zusätzlich `scopeUsedInActiveRecipes()` (aus AP-21) anwenden. UI: zwei Pills/Tabs „Alle" / „Nur aus Herstellerrezepturen" im `wawi-toolbar`.
- `criticalIngredientCount()` (für den Badge) respektiert ebenfalls `exclude_from_stock`.
- Optional: gleiche Ausschlusslogik in der Ausschuss-/Disposal-Auswahl, damit Allergen-INCIs dort nicht auftauchen (nice-to-have, nicht zwingend).
**Akzeptanz:** Rohstoffbestand lässt sich zwischen „alle" und „nur aus Herstellerrezepturen" umschalten; mit „Kein Rohstoffbestand" markierte INCI erscheinen nie im Rohstoffbestand (auch nicht im Kritisch-Badge).
**Tests:** `RawMaterialStockTest` erweitern (Modus „recipe" zeigt nur Rezeptur-INCI; `exclude_from_stock` blendet aus + aus Kritisch-Zähler; Default „alle" zeigt auch nicht-verwendete).
**Abhängigkeit:** AP-21 (liefert `scopeUsedInActiveRecipes()` + INCI-Formularfeld).
---
### AP-23 — Rezeptur-Phasen (Hersteller-Rezeptur)
**Anforderung (Produktebene 1):** „Auf Produktebene müssen unter der Hersteller-Rezeptur noch die Phasen angelegt werden können." (Screen `2026-12-06-Rezeptur-phase.jpeg`.)
**Screen-Analyse:** Mehrere Phasen (Phase A, Phase B …), je Phase eine Liste aus Rohstoff-Auswahl + Anteil %, ein **Notiz-Textfeld pro Phase**, „+" zum Hinzufügen einer Rohstoffzeile, Drag-Handle (≡) zum Sortieren, „**+ Neue Phase**". **Gesamt 100,000 %** über alle Phasen.
**Ist-Stand:** `product_ingredients` hat `recipe_type` (`product`/`manufacturer`), `gram`, `factor`, `pos`**keinen** Phasen-Begriff.
**DB**
- Phasen als eigene Tabelle `recipe_phases`: `product_id` FK, `recipe_type` (vorerst nur `manufacturer`, Feld für spätere Ausweitung), `name` VARCHAR (z. B. „Phase A"), `note` TEXT nullable, `pos` int. Unique (`product_id`,`recipe_type`,`name`).
- `product_ingredients.recipe_phase_id` nullable FK (nur Hersteller-Zeilen tragen eine Phase; Produkt-Rezeptur bleibt phasenlos). Bestehende Hersteller-Zeilen ⇒ `recipe_phase_id = NULL` (= „ohne Phase"), Migration bricht nichts.
**Code**
- Models `RecipePhase` (+ `Product::recipePhases()`, `phaseIngredients()`); `ProductIngredient` um `recipe_phase_id` + `phase()`-Relation.
- **Produktformular** (Card „Hersteller-Rezeptur", `resources/views/admin/product/…`): Umbau analog Screen 1 — Phasen-Blöcke (jede mit Rohstoffzeilen, Anteil %, Notizfeld), Drag&Drop (Rohstoffzeilen innerhalb der Phase + Phasen untereinander), „+ Rohstoff" je Phase, „+ Neue Phase". **Gesamtsumme 100 % über alle Phasen** (vorhandene 100%-Validierung auf die Summe **aller** Phasen umstellen).
- **Repository:** Speichern persistiert Phasen (`recipe_phases`) und ordnet Hersteller-Rezepturzeilen ihrer Phase zu (`recipe_phase_id`); `Product::copy()` kopiert Phasen + Zuordnung mit.
- **Rückwärtskompatibilität:** Produkte ohne Phasen funktionieren weiter (eine implizite „Phase ohne Namen"). Die Produkt-Rezeptur (`recipe_type='product'`) bleibt unverändert ohne Phasen.
- **Konsumenten:** `ProductionService::requiredGramsByIngredient()`/`buildRecipePayload()` rechnen weiterhin über alle Hersteller-Zeilen (Summe ist phasenunabhängig) — Phasen sind primär Darstellung/Reihenfolge; die Produktions-Maske (AP-28) gruppiert die Anzeige nach Phase.
**Akzeptanz:** Unter der Hersteller-Rezeptur lassen sich beliebig viele Phasen mit Rohstoffen, Anteilen und einer Phasen-Notiz anlegen, sortieren und löschen; die Gesamtsumme über alle Phasen muss 100 % ergeben; Kopieren übernimmt die Phasenstruktur.
**Tests:** `RecipePhaseTest` (Phasen + Zeilen speichern/sortieren, 100%-Summe über Phasen, Hersteller-Zeile trägt `recipe_phase_id`, Kopie inkl. Phasen, Produktion rechnet unverändert über alle Zeilen). Regression `ProductionManufacturerRecipeTest`.
---
### AP-24 — Chargen-Nr.-Präfix (Produktebene)
**Anforderung (Produktebene 2):** Präfix aus Kategorie-Kürzel (TP Tattoopflege) + Produkt-Nr. (1) + Produktionsdatum → Beispiel `TP1150626`. Das Präfix „TP1" wird auf Produktebene angelegt; in der Produktions-Maske erzeugt sich die Chargen-Nr. von selbst im Textfeld, bleibt aber **editierbar** (alte Etiketten).
**Ist-Stand:** Kein `batch_prefix` am Produkt; Produktion hat keine Chargen-Nr.-Logik. Screen 2 zeigt im „Produktionsergebnis" das Feld „Chargen-Nr." vorbefüllt mit `KP1100626`.
**DB (`products`)**
- `batch_prefix` VARCHAR nullable (z. B. „TP1").
**Code**
- `Product`: fillable `batch_prefix`; Eingabefeld „Chargen-Nr.-Präfix" in der Warenwirtschaft-Card des Produktformulars (Hilfetext: „Kürzel + Produkt-Nr., z. B. TP1 → Charge TP1150626").
- Helper `Product::suggestedBatchNumber(?Carbon $date)` = `batch_prefix . $date->format('dmy')` (Beispiel `TP1` + `150626`). Wird in AP-28 (Produktionsergebnis) als Vorbelegung des editierbaren Chargen-Nr.-Felds genutzt.
> **Format-Klärung (vermerkt):** Beispiel „TP1150626" = `TP1` + `150626` (Produktionsdatum `ddmmyy`, hier 15.06.26). Bestätigung Datumsformat (`ddmmyy`) beim Kunden, falls abweichend (z. B. `dmmyy`).
**Akzeptanz:** Pro Produkt ist ein Chargen-Präfix hinterlegbar; in der Produktion wird die Chargen-Nr. daraus + Produktionsdatum vorgeschlagen und bleibt frei editierbar.
**Tests:** `BatchPrefixTest` (Präfix speichern; `suggestedBatchNumber()` setzt Präfix + Datum korrekt zusammen).
---
### AP-28 — Neue Produktions-Maske + Produktionsergebnis
**Anforderung (Produktion):** Neue Maske (Screen `2026-12-06-neue-produktion.jpeg`). Phasen fließen ein. **Wichtig:** Der **Produktbestand** errechnet sich **nicht** aus der oben gewählten Auswahl Produkt + Stückzahl (nur Planung). Ganz unten („Produktionsergebnis") trägt man ein, was real entstanden ist — **mehrere Outputs** möglich (Beispiel: Plan 50×50 ml → Ergebnis 40×50 ml + 50×5 ml). Der **Rohstoffbestand** berechnet sich wie gehabt aus den Werten hinter jeder Charge.
**Screen-Analyse (Aufbau der Maske):**
1. **Kopf/Planung:** Produkt, Größe, Produktionsdatum, **Planung Stückzahl** (rein informativ).
2. **INCI-Liste (Hersteller-Rezeptur):** Tabelle mit Name, **Alternativer Name** (AP-21), Qualität, Anteil %, Gramm, **Regal** (Lagerort, AP-21). Summenzeile 100 % / Gesamt-Gramm.
3. **Verpackung (Vorschau):** Artikel + Stück gesamt (aus BOM).
4. **Notizen** + „Speichern"/„Drucken".
5. **Phasen-Erfassung (Phase A/B …, AP-23):** je Rohstoffzeile Soll-Gramm + Status-Ampel (grün/orange) + **Charge wählen** + Ist-Gramm-Eingabe; je Phase ein Notiz-Textfeld. Gesamt-Gramm. **→ treibt den Rohstoffbestand** (chargenbasiert, wie bisher `production_ingredients`).
6. **Produktionsergebnis:** Zeilen mit Produkt, **Produzierte Stückzahl**, **Chargen-Nr.** (vorbefüllt aus AP-24, editierbar), „+" für weitere Ergebniszeile. **→ treibt den Produktbestand.**
**Ist-Stand:** `productions.quantity` ist der einzige Output und bucht heute den Produktbestand (`ProductStockService::recordProductionStock`). Keine Mehrfach-Outputs, keine Phasen-Anzeige, keine Regal-Spalte, kein Chargen-Nr.-Feld.
**DB**
- Neue Tabelle `production_outputs`: `production_id` FK cascade, `product_id` FK, `quantity` UINT, `batch_number` VARCHAR nullable, `pos` int. (Ein Production-Lauf ⇒ n Ergebniszeilen.)
- `productions.quantity` bleibt als **geplante** Stückzahl erhalten (umdeuten zu „Planung"; Spaltenname unverändert, um Bestandsdaten/Tests stabil zu halten) — optional Kommentar/Accessor `planned_quantity`.
- **Migration der Buchungslogik (wichtig):** Produktbestand wird künftig aus `production_outputs` gebucht, **nicht** mehr aus `productions.quantity`.
**Code**
- Model `ProductionOutput` (+ `Production::outputs()`).
- **`ProductStockService::recordProductionStock(Production)` umstellen:** idempotenter Differenz-Abgleich jetzt **pro Output-Produkt** (Summe der `production_outputs.quantity` je `product_id`), append-only — statt der bisherigen Einzel-`quantity`. Beim Bearbeiten einer Produktion wird die Differenz je betroffenem Produkt nachgebucht (vorhandenes idempotentes Muster beibehalten).
- **`ProductionService::store()/updateProduction()`:** nimmt zusätzlich `outputs[]` (Produkt + Stückzahl + Chargen-Nr.) entgegen, persistiert sie, ruft danach `recordProductionStock`. Rohstoffverbrauch (`production_ingredients`) bleibt **unverändert** chargenbasiert. `StoreProductionRequest`: `outputs` Pflicht (≥ 1 Zeile), je Zeile `product_id` exists + `quantity` ≥ 1; `batch_number` nullable string.
- **Phasen-Anzeige:** Hersteller-Rezeptur nach Phase gruppieren (`RecipePhase`, AP-23); Soll-Gramm/Ampel/Charge wie heute, nur nach Phase gegliedert + Phasen-Notiz anzeigen.
- **Views:** `productions/_form_fields.blade.php` + `_scripts.blade.php` (von create/edit/copy genutzt) erweitern:
- INCI-Tabelle um Spalten „Alternativer Name" + „Regal" (`Ingredient::storageLabel()`).
- Phasen-Gruppierung der Charge-Erfassung.
- **Produktionsergebnis-Block** unten: Repeater (Produkt-Select default = geplantes Produkt, Stückzahl, Chargen-Nr. vorbefüllt aus `Product::suggestedBatchNumber(produced_at)`, editierbar; „+" für weitere Zeile). Standard: eine Ergebniszeile mit geplantem Produkt + geplanter Stückzahl als Vorschlag.
- **Klarer Hinweis-Text** in der Maske: „Planung oben ist unverbindlich. Der Produktbestand entsteht aus dem Produktionsergebnis unten." (Anforderung explizit.)
- **Produktentwicklung** (Platzhalter aus AP-09) bleibt unberührt.
**Akzeptanz:**
- Die geplante Stückzahl oben bucht **nichts**; nur das Produktionsergebnis unten bucht den Produktbestand (auch mehrere Output-Produkte, z. B. 40×50 ml + 50×5 ml).
- Rohstoffbestand wird weiterhin aus den je Charge eingetragenen Ist-Mengen reduziert.
- Chargen-Nr. je Ergebniszeile ist vorbefüllt (Präfix + Datum) und editierbar.
- INCI-Liste zeigt Alternativname + Regal; die Charge-Erfassung ist nach Phasen gegliedert; Bearbeiten einer Produktion korrigiert den Produktbestand sauber per Differenz.
**Tests:** `ProductionOutputTest` (mehrere Outputs buchen Produktbestand je Produkt; Plan-Stückzahl bucht nicht; Bearbeiten korrigiert per Differenz; Rohstoffverbrauch unverändert chargenbasiert; Chargen-Nr.-Vorbelegung; Validierung ≥ 1 Output). Regression `ProductStockTest` + `ProductionManufacturerRecipeTest` (Produktionsbuchung jetzt über Outputs).
> **Migrationshinweis Bestandsdaten:** Für bereits erfasste Produktionen ohne `production_outputs` einmalig je Produktion eine Output-Zeile (`product_id`=Produktionsprodukt, `quantity`=`productions.quantity`) backfillen, damit der gebuchte Produktbestand konsistent bleibt. Backfill als Teil der Migration (idempotent).
---
## 5. Offene Klärungspunkte (nicht blockierend, parallel mit Kunde abstimmen)
1. **Lagerort-Modell (AP-21):** drei getrennte Stammdaten (Raum/Regal/Fach) vs. eine generische Tabelle. Empfehlung: drei Tabellen (klare Dropdowns). → bei Umsetzungsstart final entscheiden.
2. **Chargen-Nr.-Datumsformat (AP-24):** Beispiel „TP1150626" als `TP1`+`ddmmyy` interpretiert (15.06.26). Bestätigen.
3. **„Verbrauch/Monat" (AP-22):** Definition „abgehende Bewegungen, Ø 6 Monate". Vor AP-13 nur manuelle/Produktions-Abgänge; mit AP-13 fließen Verkäufe ein. Bestätigen, ob 6 **Kalendermonate** gewünscht.
4. **Produktionsergebnis-Produkte (AP-28):** Dürfen Output-Produkte **andere** Produkte als das geplante sein (z. B. anderes Gebinde 5 ml als eigenes Produkt)? Screen + Beispiel legen **ja** nahe (40×50 ml + 50×5 ml ⇒ zwei Produkte/Varianten). → bestätigen; ggf. Varianten als eigene Produkte/`main_product_id` führen.
5. **Wareneingang-Charge (AP-27):** „addiert sich dazu" — Bestand zusammenführen, aber **jeder** Einkaufsvorgang bleibt als eigener `stock_entry` erhalten (nur Anzeige/Restbestand summiert je `batch_number`). Bestätigt durch die Formulierung „den Einkaufsvorgang erfassen wir trotzdem" → so umgesetzt.
---
## 6. AP-27 — Wareneingang: gleiche Charge zusammenführen
**Anforderung:** Gleiche Charge (z. B. Sonnenblumenöl `XY-123`) kommt erneut → soll sich beim weiteren Einkauf **dazuaddieren**. In der Maske erst prüfen, ob die Charge schon im Haus ist. Die Charge wird **nicht** einfach erweitert — der **Einkaufsvorgang** wird trotzdem (separat) erfasst.
**Ist-Stand:** `stock_entries.batch_number` vorhanden; keine Erkennung. Restbestand wird ohnehin pro `batch_number`/`stock_entry` über `production_ingredients` gerechnet.
**Interpretation:** Jeder Einkauf bleibt ein **eigener** `stock_entry` (Audit/Preis/Datum je Vorgang). „Dazuaddieren" = **Restbestand und Anzeige werden je `batch_number` (+ Rohstoff) summiert**. In der Erfassungsmaske ein **Hinweis/Autofill**, wenn die Charge für denselben Rohstoff bereits existiert (MHD/Lagerort vorbelegen, Bestätigung „zu bestehender Charge hinzufügen").
**Code**
- `StockEntryController` (`create`/`store`): beim Eingeben von `batch_number` + `ingredient_id` per kleinem JSON-Endpoint prüfen, ob eine Charge existiert; UI-Hinweis „Charge bereits im Haus — Menge wird dieser Charge zugerechnet" + Vorbelegung `best_before`/`location_id` aus dem bestehenden Eintrag (überschreibbar).
- **`InventoryService`:** Restbestand-/Detailansicht (AP-10) so anpassen, dass Chargen **je `batch_number`** über mehrere `stock_entries` summiert dargestellt werden (Eingang = Summe `received_quantity` aller Einträge dieser Charge; Verbrauch wie gehabt). Falls `remainingByLocationForIngredient()`/Charge-Liste bereits pro `stock_entry` gruppiert: auf Gruppierung nach `batch_number` umstellen.
- **Produktion (Charge-Auswahl):** Charge-Dropdown nach `batch_number` gruppieren (nicht je Einzel-`stock_entry`), Restbestand = Summe der Charge; Verbrauch bucht FEFO auf die Einträge der Charge.
**Akzeptanz:** Erfasst man eine bereits vorhandene Charge erneut, weist die Maske darauf hin und führt die Mengen in Bestand/Anzeige zusammen; jeder Einkauf bleibt als eigener Vorgang (Preis/Datum) erhalten; Produktion sieht die Charge mit summiertem Restbestand.
**Tests:** `StockEntryChargeMergeTest` (zwei Einkäufe gleicher Charge ⇒ summierter Restbestand, zwei separate `stock_entry`-Zeilen; Existenz-Check-Endpoint; Produktion sieht Charge einmal mit Summen-Rest). Regression `RawMaterialStockTest`, `StockEntryPriceTest`.
> **Einordnung:** AP-27 ist in der Roadmap (Abschnitt 3) bewusst **nicht** terminiert vorgereiht, da es Anzeige-Logik des Rohstoffbestands (AP-10) und der Produktion (AP-28) berührt. Empfehlung: **zusammen mit AP-28** umsetzen (gemeinsame Chargen-Gruppierung), spätestens davor. In der Reihenfolge daher zwischen AP-24 und AP-28 einzuplanen.
---
## 7. Empfohlene Sofort-Reihenfolge (nächste Schritte)
**Erledigt (12.06.2026):** AP-26 (Ausschuss-Gründe konfigurierbar), AP-25 (Lieferbestand: Datum statt Tage, überall kaufbar), AP-22 (Produktbestand: Dringlichkeits-Sortierung / 4 klickbare Kacheln / Sichtbarkeits-Flag / Verbrauch-Monat). Hinweise-Doku (AP-18) jeweils fortgeschrieben.
**➡️ Hier geht es weiter:**
1. **AP-21** (INCI: Lieferant+URL-Repeater, „nur aktive", Alternativname, Lagerort + Einstellungen) — Grundlage für AP-20 und für die Produktions-Maske. **Vorab final entscheiden:** Lagerort-Modell (§5 Punkt 1, Empfehlung: drei getrennte Stammdaten-Tabellen Raum/Regal/Fach).
2. **AP-20** (Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag) — direkt nach AP-21, nutzt dessen Scope + Formularfeld.
3. **AP-23** (Rezeptur-Phasen) → **AP-24** (Chargen-Präfix; Datumsformat `ddmmyy` bestätigen, §5 Punkt 2) → **AP-27 + AP-28** (Wareneingang-Charge-Merge gemeinsam mit der neuen Produktions-Maske inkl. Produktionsergebnis; Output-Produkte-Frage §5 Punkt 4 bestätigen).
4. Danach offene V4.0-Pakete: **AP-13** (Shop-Bestandsabzug, Konzept liegt vor), **AP-14** (Audit-Trail), **AP-15** (Blockrechte), **AP-16** (2FA), **AP-17** (WaWi-Einstellungen).
5. **AP-18 (Hinweise-Doku)** mit jedem AP fortschreiben.
---
## 8. Pflege dieses Dokuments
- Jedes abgeschlossene AP im Umsetzungsprotokoll (0a) mit Datum + Kurzbeschreibung + Test-Status protokollieren.
- Bei DB-Änderungen Migration-Dateinamen referenzieren; Casts in `casts()` pflegen (L11-Konvention).
- Vor jedem Commit: `vendor/bin/pint --dirty` und betroffene Tests (`php artisan test --filter=...`).
- **UI-Konventionen** (aus V4.0 weiter gültig): Design-System `partials/wawi-ui.blade.php` (AP-19); Datumsfelder als `datepicker-base` (`dd.mm.yyyy`, kein natives `type="date"`); Status über `wawi-pill` (ok/warning/danger).
- Neue Anforderungsquelle dieser Runde: `docs/nächsten Anforderungen an die WaWi.md` (12.06.2026) + die beiden 12.06.-Screens.