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

73 KiB
Raw Blame History

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:500string|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 (ProductionServicerecordProductionStock 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-BasisAusschließlich Hersteller-Rezeptur. Kein Fallback. Ist keine angelegt, erscheint direkt eine Warnung. → eingearbeitet in AP-09.

  2. Shop-BestandsabzugBeim 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-GeltungNur Warenwirtschaft und Produktmanagement. Dokumentieren, dass die Rechte bei Bedarf später ausgebaut werden können. → eingearbeitet in AP-15 + Hinweis in AP-18.

  5. ProduktentwicklungPlatzhalter-Seite mit Hinweis, dass ein genaues Briefing noch aussteht. → eingearbeitet in AP-09.

  6. Child-Produkt / SetsEchte 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.