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>
75 KiB
Aktualisierter Entwicklungsplan: Warenwirtschaft, Produktion & Produktbestand
⚠️ ABGELÖST (12.06.2026): Operative Arbeitsgrundlage ist jetzt
entwicklungsplan-aktualisiert-12-06-2026.md(V5.0). Dieses Dokument (V4.0) bleibt als Referenz/Historie für AP-00 … AP-19 bestehen.Version: 4.0 - Stand 02.06.2026 Ersetzt:
entwicklungsplan-aktualisiert-27-04-2026.md(V3.0) als operative Arbeitsgrundlage Referenzen:entwicklungsplan.md(V2.0),briefing-anpassungen-27-04-2026.md,feedback.md,konzept-final.md,docs/Todos.mdMethodik: Backlog aus kleinen, sequenziell abarbeitbaren Arbeitspaketen (AP). Jedes AP hat Ziel, konkrete Schritte mit Dateipfaden, DB-Änderungen, Akzeptanzkriterien und Tests. Reihenfolge ist so gewählt, dass jedes AP einzeln deploybar ist.
0. Was dieses Dokument neu macht
Gegenüber V3.0 wurde der reale Code-Stand verifiziert (nicht nur die Protokolltabelle übernommen). Daraus ergeben sich Korrekturen, neu entdeckte Bugs und eine feinere Zerlegung in einzeln umsetzbare Schritte.
Geprüfte Dateien u. a.: routes/web.php, app/Http/Controllers/Admin/Inventory/*, app/Services/ProductionService.php, app/Http/Requests/Inventory/*, resources/views/admin/inventory/*, Migrationen unter database/migrations/.
0a. Umsetzungsprotokoll V4.0 (laufend)
Jede abgeschlossene Teil-Lieferung wird hier mit Datum, betroffenen Dateien und Test-Status protokolliert.
| Datum | AP | Kurzbeschreibung | Tests |
|---|---|---|---|
| 03.06.2026 | AP-12 (Ausgang / Ausschuss) | Neue Übersicht Warenwirtschaft → Ausgang / Ausschuss (CopyReader, nach Einkauf). Migration …_create_stock_disposals_table (disposal_type, ingredient_id/packaging_item_id/stock_entry_id nullable FKs, location_id, quantity, unit, reason Pflicht, note, user_id, disposed_at). Modell StockDisposal. InventoryService zieht Ausschuss in remainingByIngredient()/remainingByLocationForIngredient() ab (wirkt auf Kritisch-Badge) + neue remainingByPackagingItem()/disposed*-Methoden. StockDisposalController (index/create/store/ingredientCharges-JSON), FormRequest StoreStockDisposalRequest (Grund Pflicht, dt. Zahl/Datum). Erfassungsformular mit Typ-Umschaltung, Select2-Suche, optionaler Charge (setzt Lagerort), Grund-Auswahl, Datepicker. Sidenav-Eintrag + „Ausschuss erfassen"-Button (vorbelegt) auf der Rohstoff-Detailseite. |
tests/Feature/StockDisposalTest.php (8 grün): Summierung, Restbestand-Abzug Rohstoff + je Lager + Verpackung, HTTP-Buchung, Grund-Pflicht, Admin-Schutz, Render, Chargen-Endpoint. Regression Rohstoff-/Produktbestand grün |
| 03.06.2026 | AP-11 (Produktbestand + Historie) | Neues Menü Warenwirtschaft → Produktbestand (CopyReader) mit Untermenü „Übersicht"/„Historie" und Kritisch-Badge. Migrationen …_create_product_stock_movements_table (product_id, direction ENUM(in,out), quantity, reason, source, note, user_id, nullableMorphs('reference')) + …_add_stock_thresholds_to_products_table (min_product_stock/critical_product_stock). Modell ProductStockMovement + Product::stockMovements(). app/Services/ProductStockService.php: currentStockByProduct() (=SUM(in)−SUM(out)), recordMovement(), recordProductionStock() (idempotenter Differenz-Abgleich, append-only), productStatus(), criticalProductCount(). ProductStockController (index/storeMovement/history): Übersicht (nur Haupt-/Einzelprodukte, Bild, Bestand rot/gelb, Suche + „nur kritische", +/−-Buchungsmodal nur für Admin, Produzieren-Link mit Produktvorwahl), Historie (Filter Produkt/Eingang-Ausgang/Grund/Monat+Jahr). Produktion bucht Produktbestand automatisch (ProductionService). Schwellwert-Felder im Produktformular (Warenwirtschaft-Card) + ProductRepository. AP-10 „Enthalten in" um Produktbestand-Spalte ergänzt. |
tests/Feature/ProductStockTest.php (9 grün): Bestand=Eingang−Ausgang, Status, Kritisch-Zähler, Produktionsbuchung + Update-Korrektur, Index/Historie-Render, HTTP-Buchung + Admin-Schutz. Regression Produktion + RawMaterialStockTest (26) grün |
| 03.06.2026 | AP-10 (Rohstoffbestand) | Neue Übersicht Warenwirtschaft → Rohstoffbestand (CopyReader, erster Menüpunkt). Neuer app/Services/InventoryService.php als zentrale Bestandslogik: remainingByIngredient() (= SUM(received_quantity) der eingegangenen Rohstoff-Chargen − SUM(production_ingredients.quantity_used)), remainingByLocationForIngredient(), dailyConsumptionByIngredient() (Ø Gramm/Tag aus Produktionshistorie der letzten 90 Tage), daysUntilEmpty()/expectedEmptyDate(), stockStatus() (critical = Meldebestand unterschritten, warning = Reichweite ≤ Lieferzeit) und criticalIngredientCount(). RawMaterialStockController@index (Tabelle: Name, Qualität, Bestand, Verbrauch/Tag, Voraussichtlich auf Null + Resttage, Hochrechnungs-Spalte mit Horizont-Dropdown 1/3/6/12 Monate; Suche + „nur kritische anzeigen"-Filter; kritisch=table-danger, warning=table-warning, Zeile klickbar) und @show (Bestell-Detailseite: Kennzahlen, „Enthalten in" = aktive Rezepturen mit g/Stück, Lieferanten mit Lieferzeit/letztem Netto-kg-Preis + Bestellaktion Zum Shop/Per Mail je order_method, verfügbare Chargen mit Restbestand + Bestand je Lagerort). Routen raw-material-stock.index/.show in der copyreader-Gruppe. Sidenav-Eintrag „Rohstoffbestand" inkl. rotem Kritisch-Badge über View::composer (AppServiceProvider, nur für CopyReader). Hinweis: Produktbestand/Verkauf-pro-Tag je Produkt im „Enthalten in"-Block folgt mit AP-11 (Produktbestand existiert noch nicht). |
tests/Feature/RawMaterialStockTest.php (8 grün): Restbestand=Eingang−Verbrauch, Verbrauch/Tag-Mittelung, Fenstergrenze, Status kritisch/warnung, Kritisch-Zähler, Index- und Detail-Render. Regression ProductionManufacturerRecipeTest/ProductSetTest (19) grün |
| 02.06.2026 | AP-01 | URL-Bugfixes B1/B2 umgesetzt: suppliers/form.blade.php und packaging-items/form.blade.php von type="url" auf type="text" (placeholder https://); Store/UpdatePackagingItemRequest URL-Regel url|max:500 → string|max:2048; Migration 2026_06_02_145358_widen_url_columns_in_inventory_tables (suppliers.url + packaging_items.url → varchar(2048)). |
tests/Feature/InventoryUrlFieldsTest.php (3 grün); Regression Phase 2+3 grün (17) |
| 02.06.2026 | AP-04 | iPad-taugliche Tabellen-Aktionen (B5): neue Partial resources/views/admin/inventory/partials/table-actions-style.blade.php (@once-Style für .wawi-table td .btn, min. 42px Touch-Target, mehr Abstand); Klasse wawi-table + Partial-Include in allen 8 Index-Views (locations, material-qualities, packaging-materials, supplier-categories, suppliers, packaging-items, stock-entries, productions). |
Render-Regression Phase 2+3+5 grün (21) |
| 02.06.2026 | AP-00 | Regressionsbasis für umgesetzte 5.1-Features als Pest-Tests nachgezogen: INCI-Rohstoffqualität-Relation, Hersteller-Rezeptur getrennt von Produkt-Rezeptur, Produkt-Kopie inkl. beider Rezepturen, „nur aktive Produkte" im Produktions-Formular, Produktion edit/copy rendern. | tests/Feature/ProductPhase51Test.php (5 grün) |
| 02.06.2026 | AP-04.1 | Aktionsspalten vereinheitlicht (Kunden-Feedback): einheitliches Schema Spalte 1 = Ansicht + Bearbeiten (+ Kopieren bei Produktion), letzte Spalte = Löschen. Umgebaut: stock-entries/index (Ansicht/Bearbeiten nach vorn, Löschen ans Ende, DataTables order/columnDefs an verschobene Spalten angepasst) und productions/index (Ansicht/Bearbeiten/Kopieren nach vorn). Stammdaten-Tabellen waren bereits konform (kein show-Route → keine Ansicht). CSS-Feinjustierung der Button-Abstände durch Kunden in partials/table-actions-style.blade.php übernommen. |
Produktions-Index-Render + Aktionslinks geprüft; stock-entries Index-Render grün (13) |
| 02.06.2026 | AP-05 (Teil 1: UST) | Neuer Unterpunkt Warenwirtschaft → Einstellungen → „Allgemein" als erweiterbarer Container für kleinteilige Einstellungen (erstes Modul = Umsatzsteuersätze). Tabelle tax_rates (name, percent DECIMAL(5,2), active, pos) via Migration 2026_06_02_152721_create_tax_rates_table; Model TaxRate (casts percent/active, scopeActive), TaxRateFactory, Seeder-Erweiterung InventoryStammdatenSeeder (19/7/0, idempotent per firstOrCreate). CRUD: GeneralSettingController@index (Allgemein-Seite), TaxRateController (create/store/edit/update/destroy, Redirect zurück auf general), Store/UpdateTaxRateRequest. Views admin/inventory/general/index.blade.php (Karte „Umsatzsteuersätze" mit Tabelle + Neu/Bearbeiten/Löschen) und admin/inventory/tax-rates/form.blade.php. Routen in superadmin-Gruppe (admin.inventory.general, Resource tax-rates ohne index/show). Sidenav-Eintrag „Allgemein" inkl. open/active-Logik. Migration + Seed auf DB ausgeführt (3 Default-Sätze vorhanden). |
tests/Feature/TaxRateSettingsTest.php (7 grün, 24 Assertions): Render, CRUD, Validierung Pflicht/Bereich, active-Scope, Zugriffsschutz Nicht-SuperAdmin |
| 02.06.2026 | AP-05 (Teil 2: Lieferzeiten) | Zweite Karte „Lieferzeit-Vorlagen" auf der Allgemein-Seite. Tabelle delivery_times (label, active, pos) via Migration 2026_06_02_153243_create_delivery_times_table; Model DeliveryTime (cast active, scopeActive), DeliveryTimeFactory, Seeder-Erweiterung (1–3 / 3–5 Werktage / 1–2 Wochen, idempotent). CRUD: DeliveryTimeController (create/store/edit/update/destroy → Redirect general), Store/UpdateDeliveryTimeRequest. GeneralSettingController um deliveryTimes erweitert. View admin/inventory/delivery-times/form.blade.php + zweite Karte in general/index. Route Resource delivery-times (ohne index/show) in superadmin-Gruppe. Sidenav open/active um delivery-times ergänzt. Migration + Seed auf DB ausgeführt. |
tests/Feature/DeliveryTimeSettingsTest.php (7 grün, 21 Assertions): Render, CRUD, Validierung, active-Scope, Zugriffsschutz |
| 02.06.2026 | AP-06 (Lieferanten erweitern) | Felder order_method ENUM(email,online_shop), order_email, order_url, delivery_time (Freitext) an suppliers via Migration 2026_06_02_154755_add_order_fields_to_suppliers_table. Supplier fillable erweitert; Store/UpdateSupplierRequest Regeln (order_method in:email,online_shop; order_email email; order_url string max 2048; delivery_time string). SupplierRepository::extractSupplierAttributes erweitert. SupplierController create/edit übergeben aktive deliveryTimes als Vorlagen. suppliers/form.blade.php: Bestellweg-Select + bedingte Felder (Bestell-E-Mail / Bestell-URL via JS-Toggle) + Lieferzeit-Textfeld mit <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=Eingang−Ausgang, Schwellwert-Status, manuelle +/−-Bewegungen, automatische Produktionsbuchung, Übersicht + revisionssichere Historie, Kritisch-Badge); AP-12 erledigt (Ausgang/Ausschuss: stock_disposals, Erfassung mit Pflicht-Grund + optionaler Charge, reduziert Rohstoff-/Verpackungsbestand inkl. Kritisch-Badge).
Alle Klärungspunkte aus §5 sind beantwortet (Kunde, 02.06.2026) und in die jeweiligen APs eingearbeitet — keine Blocker mehr offen.
➡️ NÄCHSTER SCHRITT: AP-18 (Hinweise-Doku) und/oder AP-02 (Produkt-Klassen: Einzelprodukt vs. Set). AP-18 kann jederzeit als Platzhalter angelegt und laufend gepflegt werden; danach Datenmodell AP-02 (Sets via Pivot) / AP-03 („Nicht vorrätig", nur Hinweis), dann die großen Übersichten AP-10/AP-11.
1. Verifizierter Ist-Stand (02.06.2026)
Umgesetzt und im Code vorhanden
| Bereich | Status | Belegt durch |
|---|---|---|
| Produktmanagement (Produkte, INCIs, Kategorien, Attribute) | vorhanden | app/Http/Controllers/ProductController.php, IngredientController.php |
| Rezeptur + Hersteller-Rezeptur (Prozent, Faktor, 100%-Summe) | vorhanden | product_ingredients.recipe_type, Product::p_ingredients() / manufacturer_ingredients() |
| Haltbarkeit am Produkt (PAO / festes MHD) | vorhanden | products.shelf_life_type, shelf_life_months |
| Stammdaten (Lagerorte, Lieferanten, Kategorien, Rohstoffqualität, Verpackungsmaterial, Produkt-/Versandverpackung) | vorhanden | app/Http/Controllers/Admin/Inventory/*, Migrationen 2026_03_27_* |
| INCI mit Rohstoffqualität | vorhanden | ingredients.material_quality_id |
| Verpackung & Material am Produkt (BOM) | vorhanden | product_packagings, Product::packagings() |
| Einkauf & Wareneingang (zweistufig pending → received, Charge, MHD) | vorhanden | stock_entries, StockEntryController, ReceiveStockEntryRequest |
| Produktion (Chargen-Zuordnung, Soll-Verbrauch, MHD-Warnung, Packaging-Snapshot, edit/copy) | vorhanden | ProductionService, ProductionController, production_*-Tabellen |
| Tests Phase 0–5 | vorhanden | tests/Feature/ProductPhase0/1/4Test.php, InventoryPhase2/3Test.php, ProductionPhase5Test.php |
Noch NICHT im Code vorhanden (entgegen Eindruck aus V3.0-Lesart)
- Phase 5.2 ist vollständig offen — keine der dort beschriebenen DB-Strukturen existiert:
- kein
tax_rates/tax_rate_id/tax_rate_percent - kein
is_set,main_product_id,product_set_items - kein
order_method/order_email/order_url/delivery_timeansuppliers - kein
ingredient_supplier-Pivot, keintax_rate/delivery_timeaningredients - kein
price_per_kg_net/price_per_kg_grossanstock_entries - kein
out_of_stock_untilanproducts - kein
product_stock_movements, keinInventoryService, keine Bestandsseiten
- kein
- Produktion basiert noch auf
p_ingredients(Produkt-Rezeptur), nicht auf der Hersteller-Rezeptur (ProductionService::store()undbuildRecipePayload()ladenp_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.phpZeile 56 nutzt<input type="url">. Die native Browser-Validierung lehnt eine ausgefüllte, aber nicht streng schema-konforme URL (z. B. ohnehttps://oder mit kodierten Parametern) ab; ein leeres Feld ist gültig → exakt das gemeldete „umgekehrte" Verhalten. Die Server-Validierung ist bereits korrekt (nullable|string|max:2048). - Fix:
type="url"→type="text"(Server validiert ohnehin als String). Damit werden auch Konfigurator-URLs mit Parametern akzeptiert (siehe B2). - Aufwand: ~15 Min.
B2 — URL-Felder müssen Konfigurator-URLs mit Parametern akzeptieren
- Anforderung (Todos Z. 1–2): URLs wie
https://www.kartonsaufmass.de/bestellen?bom_configuration=%7B%2522length%2522:125,...%7Dmüssen gespeichert werden können (Versandverpackungs-Konfiguratoren). - Status (verifiziert): Lieferanten-URL ist bereits
nullable|string|max:2048(ok, mit B1-Fix vollständig).PackagingItemist NICHT ok:Store/UpdatePackagingItemRequesthaben['nullable','url','max:500']— dieurl-Regel lehnt kodierte Konfigurator-URLs ab undmax:500ist für lange Konfigurator-Links zu kurz. → auf['nullable','string','max:2048']ändern und Blade-Input auftype="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.phpreparieren. → 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-actionsmit größeren Touch-Targets + Abstand) einführen und in allenindex.blade.phpanwenden. → AP-04 (Querschnitt, früh, weil überall sichtbar).
Optimierungen (Konsistenz/Sauberkeit)
- O1:
IngredientControllernutzt nochRequest::all()statt FormRequest → bei INCI-Erweiterung (AP-07) aufStoreIngredientRequest/UpdateIngredientRequestumstellen. - O2: Offene Tests aus Phase 5.1 (Menü-Labels, INCI-Qualität, Prozent-Rezeptur, 100%-Summe, Hersteller-Rezeptur, Produktion edit/copy, nur aktive Produkte) sind im Plan als
[ ]markiert, aber Features sind umgesetzt → AP-00 schreibt diese Tests nach, um eine grüne Regressionsbasis zu haben, bevor 5.2 beginnt. - O3: Steuerart als Enum, später änderbar gewünscht (feedback/Briefing INCI B). Lösung: konfigurierbare
tax_rates-Stammdaten statt Hardcode-Enum (AP-05).
3. Priorisierte Roadmap (Phasenüberblick)
| Reihenfolge | AP | Titel | Abhängigkeit | Aufwand |
|---|---|---|---|---|
| 1 | AP-00 | Regressionsbasis: offene 5.1-Tests nachziehen | – | 1 Tag |
| 2 | AP-01 | Quick-Fixes B1/B2 (URL-Felder) | – | 0,5 Tag |
| 3 | AP-04 | Querschnitt: iPad-taugliche Tabellen-Aktionen (B5) | – | 0,5–1 Tag |
| 4 | AP-05 | Einstellungen: UST-Sätze & Lieferzeiten (Stammdaten) | – | 1–2 Tage |
| 5 | AP-06 | Lieferanten erweitern (Bestellweg, Lieferzeit) | AP-05 | 1–2 Tage |
| 6 | AP-07 | INCI erweitern (Lieferanten-Mehrfachwahl, UST, Lieferzeit) | AP-05, AP-06 | 2–3 Tage |
| 7 | AP-08 | Einkauf erweitern (UST, Netto/Brutto, Duplizieren) | AP-05 | 2–3 Tage |
| 8 | AP-09 | Produktion korrigieren (Hersteller-Rezeptur, Charge-JS, iPad, Produktentwicklung-Platzhalter) | – | 2–4 Tage |
| 9 | AP-02 | Produkt-Klassen: Einzelprodukt vs. Set + Hauptprodukt | – | 3–5 Tage |
| 10 | AP-03 | „Nicht vorrätig" mit Zeitangabe | – | 1–2 Tage |
| 11 | AP-10 | Rohstoffbestand (InventoryService + Übersicht) | AP-06, AP-07, AP-09 | 4–6 Tage |
| 12 | AP-11 | Produktbestand + Historie + manuelle Bewegungen | AP-02, AP-10 | 5–8 Tage |
| 13 | AP-12 | Ausgang/Ausschuss (Rohstoffe/Verpackung) | AP-10 | 2–3 Tage |
| 14 | AP-13 | Shop-Anbindung: Bestand bei Verkauf reduzieren (inkl. Sets) | AP-02, AP-11 | 3–5 Tage |
| 15 | AP-14 | Audit-Trail (inventory_logs) | AP-10–13 | 2–3 Tage |
| 16 | AP-15 | Blockbasierte Rechte | AP-05+ | 5–8 Tage |
| 17 | AP-16 | 2FA Google Authenticator für Admins | – | 3–5 Tage |
| 18 | AP-17 | Warenwirtschafts-Einstellungen (Alarm-Mail, Default-Lager, Schwellwerte) | AP-10/11 | 1–2 Tage |
| 19 | AP-18 | Hinweise-/Doku-Seite (Einstellungen → Hinweise, MD-basiert) | – | 0,5 Tag |
Leitplanke: AP-00 bis AP-09 sind „Korrektur & Datenmodell-Vorbereitung". Erst danach werden die großen Übersichten (Rohstoff-/Produktbestand) gebaut, weil sie auf den neuen Stammdatenfeldern aufsetzen.
4. Arbeitspakete im Detail
AP-00 — Regressionsbasis: offene 5.1-Tests nachziehen
Ziel: Grüne Test-Suite als Sicherheitsnetz, bevor 5.2 beginnt.
Schritte
- Pest-Feature-Tests ergänzen (
php artisan make:test --pest <Name>):- Menü-Labels (Rohstoffqualität, Verpackungsmaterial, Produkt-/Versandverpackung).
- INCI mit
material_quality_idspeichern + 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/updateundcopy. - 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: fallsurl-Regel → auf['nullable','string','max:2048']ändern; zugehöriges Blade-Input auftype="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; danachnpm run dev/prod). - In allen
resources/views/admin/inventory/**/index.blade.phpdie 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"),percentDECIMAL(5,2),activebool,pos. Seeder: 19,00 / 7,00 / 0,00.delivery_times:labelVARCHAR (Freitext, z. B. „3–5 Werktage"),days(ganze Tage bis Wareneingang, optional – Basis für „rechtzeitig bestellen"-Ableitung),active,pos.
Code
- Models
TaxRate,DeliveryTime(make:model -mf), CRUD-Controller unterAdmin/Inventory/, FormRequests, Viewsindex+form, Routen unteradmin/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_methodENUM(email,online_shop) nullable.order_emailnullable (falls abweichend vonemail).order_urlnullable (falls abweichend vonurl).delivery_timeVARCHAR nullable (Freitext; optional Verknüpfung mitdelivery_timesals Vorlage, aber Freitext bleibt führend).
Code
- Migration +
Supplierfillable/casts;Store/UpdateSupplierRequesterweitern;suppliers/form.blade.php: Radio/Select Bestellweg + bedingte Felder + Lieferzeit-Textfeld (mit Vorlagen-Datalist ausdelivery_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 aktivendelivery_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 hatdelivery_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, optionalpreferredbool,supplier_sku,url. ingredients:tax_rate_idnullable FK,delivery_timeVARCHAR nullable.
Code
Ingredient:suppliers()belongsToMany,taxRate()belongsTo; fillable/casts.- O1:
IngredientControlleraufStoreIngredientRequest/UpdateIngredientRequestumstellen (Ersatz fürRequest::all()). admin/ingredient/form.blade.php: Select2-Mehrfachauswahl Lieferanten, UST-Dropdown (aktivetax_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_idFK,delivery_time,delivery_time_days) +2026_06_02_161237_create_ingredient_supplier_table(Pivot mitpreferred/supplier_sku/url).Ingredient:taxRate()belongsTo,suppliers()belongsToMany (mit Pivot-Feldern), castdelivery_time_days. O1 umgesetzt:IngredientControllernutzt jetztStoreIngredientRequest(stattRequest::all()) und synct Lieferanten viasuppliers()->sync(). Formular: UST-Dropdown, Select2-Lieferanten-Mehrfachwahl, Lieferzeit-Textfeld mitdata-days-Datalist + Tage-Feld inkl. JS-Autofill (manuell überschreibbar). Der bestehende Single-Endpoint (admin_product_ingredient_storefü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_idnullable FK + Snapshottax_rate_percentDECIMAL(5,2) (für historische Korrektheit).price_per_kg_netDECIMAL(10,4) nullable,price_per_kg_grossDECIMAL(10,4) nullable.
Code
- Migration +
StockEntryfillable/casts;Store/UpdateStockEntryRequesterweitern (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, setztstatus=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 vorhandeneprice_per_kgdient als Netto-Feld (price_per_kg_net), ergänzt umprice_per_kg_gross; bewusst kein Rename, um Bestandsdaten/Factory/Tests stabil zu halten. Netto/Brutto-Umrechnung zentral inStockEntryRepository::resolvePrices()(UST-Prozent-Snapshot, fehlender Wert wird berechnet), live im Formular via JS. Duplizieren überstock-entries/{id}/copylegt direkt einepending-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()vonp_ingredientsaufmanufacturer_ingredientsumstellen (Pivotgram/factoranalog).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 inproduction_ingredientsverbrauchte Menge dieser Charge). Erfordert Verbrauchsabfrage jestock_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;
ghinter 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).
ProductionServiceaufmanufacturer_ingredientsumgestellt (kein Fallback),assertManufacturerRecipe()+has_recipe-Flag inbuildRecipePayload. Restbestand überconsumedByStockEntry()/availableStockEntriesForIngredient()(FEFO, nur Rest > 0, beim Bearbeiten viaexclude_production). Chargen-LabelLieferant - Charge - dd.mm.yyyy(stockEntryLabel()). Views in gemeinsame Partials_form_fields/_scriptsrefactored; 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-Gridcol-sm-6. Produktentwicklung-Platzhalter (ProductDevelopmentController, Routeadmin.inventory.product-development, Sidenav-Untermenü). Tests:tests/Feature/ProductionManufacturerRecipeTest.php(6 grün) + angepasste RegressionProductionPhase5Test/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_setbool default 0.main_product_idnullable FK aufproducts(Child→Hauptprodukt).main_product_quantityUINT nullable (z. B. 50 für „50 × 15 ml").- Pivot
product_set_items:set_product_id,component_product_id,quantity.
Code
Product:setItems(),mainProduct(), ScopesmainProducts()/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(), ScopessingleProducts()/sets()/mainProducts(). Produktformular: Card „Set / Produktart" (Checkbox + Bestandteile-Modal nur für aktive Einzelprodukte, Menge, Drag&Drop), Hauptprodukt-Felder in der Warenwirtschaft-Card, JStoggleSetModeblendet Rezeptur/Verpackung/Warenwirtschaft bei Set aus.ProductRepository: bei Set werden Rezeptur/Verpackung geleert,main_product_idgenullt, Set-Items gesynct;copy()übernimmt Bestandteile. Validierung inProductController::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_untilDATE nullable (Empfehlung: aus Tagen berechnet, sauber für Resttage).out_of_stock_indefinitebool 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_untilin der Zukunft Hinweis „In ca. X Tagen wieder da!" (Resttage dynamisch); beiindefiniteentsprechender 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_untilDATE nullable,out_of_stock_indefinitebool).Product-HelperisOutOfStock()/outOfStockRemainingDays()/outOfStockNotice(). Produktformular-Card „Verfügbarkeit" (Checkbox + Tagefeld + Checkbox „unbestimmt", JS-Toggle);ProductRepository::update()rechnetout_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::datatableProdukt-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 jeorder_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.phpzentralisiert 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 + BestellaktionZum Shop/Per Mail, Chargen mit Restbestand + Bestand je Lagerort). Sidenav-Eintrag „Rohstoffbestand" mit Kritisch-Badge (View::composer). Offene Bestellungen (status=pending) werden überInventoryService::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 Statuscritical_orderedentschärft (nicht im Badge gezählt). Über „Einkauf erfassen" auf der Detailseite wird der Einkauf mit Art=Rohstoff + vorausgewähltem Inhaltsstoff geöffnet (StockEntryController@createliestingredient_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,directionENUM(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_idFK→productscascade,directionENUM(in,out),quantity,reason,sourcedefaultmanual,note,user_idFK→usersnullOnDelete,nullableMorphs('reference')) +…_add_stock_thresholds_to_products_table(min_product_stock,critical_product_stock). ModellProductStockMovement(+Product::stockMovements(), Schwellwerte in fillable/casts).app/Services/ProductStockService.phpzentralisiert 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(FormRequestStoreProductStockMovementRequest, nurisAdmin),history(Filter Produkt/Eingang-Ausgang/Grund/Monat+Jahr, revisionssicher). Produktion bucht Produktbestand automatisch (ProductionService→recordProductionStockbeistore/update). Produktformular: Schwellwert-Felder in der Warenwirtschaft-Card (+ProductRepository). Sidenav „Produktbestand" mit Untermenü „Übersicht"/„Historie" und Kritisch-Badge (View::composererweitert umcriticalProductCount). 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 inInventoryService.- Akzeptanz: Ausgang reduziert Rohstoff-/Verpackungsbestand; Grund ist Pflicht.
Status: Erledigt (03.06.2026). Migration
2026_06_03_124056_create_stock_disposals_table(disposal_typeENUM(ingredient,packaging),ingredient_id/packaging_item_id/stock_entry_idnullable FKs,location_id,quantitydecimal,unit,reasonPflicht,note,user_id,disposed_at). ModellStockDisposal.InventoryServiceerweitert:disposedByIngredient()+ Abzug inremainingByIngredient()undremainingByLocationForIngredient()(somit auch im Kritisch-Zähler/Badge),remainingByPackagingItem()(Wareneingang − Produktionsverbrauch − Ausschuss) +disposedByPackagingItem().StockDisposalController(copyreader-Gruppe;create/storenurisAdmin):index(Liste mit Art-Filter),create/store(FormRequestStoreStockDisposalRequest, 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 perproduct()auf das Produkt. - Produkt-Mapping zu WooCommerce besteht: über
products.wp_number(sieheapp/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, Actionstore_shipped(~Z. 421–454). Dort wirdshippedgesetzt und – bereits idempotent überif (! $shopping_order->shipped_at)– einmaligshipped_atbeim Ü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_movementsmitdirection,source,reason,nullableMorphs('reference')undProductStockService::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_movementswiederverwenden:direction='out', neuer Quell-Wertsource='sale',reference=ShoppingOrder(polymorph),reasonz. 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 dieseShoppingOrder-Referenz mitsource='sale'bereits eine Bewegung existiert.
4. Set-Auflösung (Abhängigkeit AP-02 ✅)
Pro shopping_order_item Produkt laden:
- Set (
is_set): übersetItems()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 → Gegenbuchungdirection='in',source='sale_reversal', gleichereference,reason='Storno #…';stock_booked_atwieder aufnull. - Kein Löschen, nur Gegenbuchung (konsistent zur AP-11-Historie). Anknüpfpunkte:
ShoppingUserController::cancel()/open()(WP-Seite) undSalesController@store(Backend).
6. Zentrale Logik in einem Service
- Neuer schlanker
app/Services/SaleStockService.php(oder Methodenpaar inProductStockService):bookShipment(ShoppingOrder $order)undreverseShipment(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, dershipped=2setzt und denselben Service ruft. Alternativ periodischer WooCommerce-REST-Pull (GET wc/v3/orders?status=completed), Abgleich überwp_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_numberbzw. 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_shippedbucht nicht doppelt. - Storno bucht „in" als Gegenbuchung;
stock_booked_atzurückgesetzt. - Produkt ohne
wp_numberwird übersprungen (kein Fehler). - Historie zeigt Quelle „Verkauf".
11. Abhängigkeiten & Aufwand
- Abhängig von AP-02 (Sets) ✅ und AP-11 (Produktbestand) ✅ — beide erledigt.
- Aufwand: Szenario A ~2–3 Tage; Szenario B (Webhook/Pull) ~3–5 Tage.
Akzeptanz: Versand reduziert den Produktbestand; Set-Versand reduziert die enthaltenen Einzelprodukte; Storno bucht zurück; jede Buchung steht revisionssicher in der Historie.
AP-14 — Audit-Trail
inventory_logs(polymorph) + Observer aufStockEntry/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(Guarduser), 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.mdoderresources/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;NoticeControllerrendert sie mitStr::markdown()(CommonMark) zu HTML. Viewadmin/inventory/notices/index.blade.php, Routeadmin.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.
-
Produktion-Basis → Ausschließlich Hersteller-Rezeptur. Kein Fallback. Ist keine angelegt, erscheint direkt eine Warnung. → eingearbeitet in AP-09.
-
Shop-Bestandsabzug → Beim Versand (erst mit gebuchtem Versand ist das Produkt real „aus dem Regal"). Als Hinweis dokumentieren. → eingearbeitet in AP-13 + Hinweis in AP-18.
-
„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.
-
Blockrechte-Geltung → Nur Warenwirtschaft und Produktmanagement. Dokumentieren, dass die Rechte bei Bedarf später ausgebaut werden können. → eingearbeitet in AP-15 + Hinweis in AP-18.
-
Produktentwicklung → Platzhalter-Seite mit Hinweis, dass ein genaues Briefing noch aussteht. → eingearbeitet in AP-09.
-
Child-Produkt / Sets → Echte Sets via Pivot (
product_set_items). → eingearbeitet in AP-02. -
Hinweise-Doku (neu): MD-basierte Doku-Seite unter Einstellungen → Hinweise mit Entwicklungsstand, wichtigen Hinweisen und noch nötigen Schritten, einsehbar auch für den Kunden. → neues AP-18.
6. Empfohlene Sofort-Reihenfolge (nächste Schritte)
✅ Erledigt: AP-00, AP-01, AP-04 (+ AP-04.1), AP-05, AP-06 (+ Nachtrag), AP-07 (+ AP-07.1), AP-08, AP-09 (+ AP-09.1), AP-02 (Sets via Pivot), AP-03 („Nicht vorrätig"), AP-10 (Rohstoffbestand), AP-11 (Produktbestand + Historie), AP-12 (Ausgang / Ausschuss), AP-18 (Platzhalter, laufend zu pflegen), AP-19 (UI-Vereinheitlichung Warenwirtschaft).
➡️ Hier geht es weiter:
- 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 überproducts.wp_number), Versandstatus wird zentral inSalesController@storegesetzt → Abzug kann ohne neue Tabelle auf der AP-11-Infrastruktur aufsetzen. Vorab nur noch zu bestätigen: Szenario A (Versand wird im Backend gebucht – empfohlen, kein neuer Woo-Eingriff) vs. Szenario B (WooCommerce meldet Versand per Webhook/REST-Pull zurück) sowie die Varianten-/Hauptprodukt-Detailfrage. Danach Folge-APs (AP-14–AP-17). - AP-18 mit jedem weiteren AP fortschreiben (Hinweise-Seite aktuell halten).
AP-19 — UI-Vereinheitlichung Warenwirtschaft (Darstellung)
Status: Erledigt (03.06.2026). Rein gestalterisch, keine fachliche Logik geändert.
Ein einheitliches, modernes Erscheinungsbild für alle Warenwirtschafts-Seiten. Zentrale, wiederverwendbare Design-Partial resources/views/admin/inventory/partials/wawi-ui.blade.php (per @once eingebundenes, auf .wawi-page gescoptes Inline-CSS – kein SCSS-Build/npm run nötig).
Bausteine: Seitenkopf (wawi-page-head, Titel + Untertitel + Aktionsbereich), Kennzahlen-Kacheln (wawi-stats/wawi-stat, teils klickbar als Filter), Karten (wawi-card + wawi-card__header/__footer), Toolbar mit Such-Feld (wawi-toolbar/wawi-search), aufgeräumte Tabellen (wawi-table mit Uppercase-Köpfen, Hover, schmaler Status-Akzentleiste links), Status-Pills (wawi-pill--ok/--warning/--danger), Datenblatt-Definitionsliste (wawi-deflist/wawi-deflist__item), Name-Zelle mit fester Icon-Spalte (wawi-name-cell), einheitlicher Leer-Zustand (wawi-empty).
Responsive: Detail-Datenblätter brechen unter 768 px auf einspaltig um – Label klein oben, Wert linksbündig darunter. Die Name-Zelle hält das Icon in eigener Spalte, damit umgebrochener Text nicht unter das Icon rutscht.
Umgestellt: alle Übersicht-/Listen- und Detailseiten unter admin/inventory (Produktbestand + Historie, Rohstoffbestand inkl. Detail, Ausgang/Ausschuss, Einkauf & Wareneingang inkl. Detail, Produktion inkl. Detail, Hinweise, Produktentwicklung, Einstellungen sowie die Stammdaten-Listen Lieferanten, Lieferanten-Kategorien, Verpackungsmaterial/-artikel, Materialqualität, Lagerorte).
Offen (bewusst): Die reinen Formularseiten (Anlegen/Bearbeiten) nutzen noch den alten h4-Kopf; Angleichung an wawi-page-head als Folgeaufgabe vorgemerkt.
7. Pflege dieses Dokuments
- Jedes abgeschlossene AP hier mit Datum + Kurzbeschreibung + Test-Status protokollieren (analog Umsetzungsprotokoll in
entwicklungsplan.md). - Bei DB-Änderungen: Migration-Dateinamen referenzieren; bei Modellen Casts in
casts()-Methode pflegen (L11-Konvention). - Vor jedem Commit:
vendor/bin/pint --dirtyund 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 nativestype="date"). Der Datepicker wird global überpublic/js/custom.jsauf.datepicker-basegebunden (Formatdd.mm.yyyy, deutsche Locale). Modellwerte mit->format('d.m.Y')ausgeben; Backend parstd.m.YüberCarbon::parsebzw. diedate-Validierungsregel. - UI-Konvention Warenwirtschaft (AP-19): Neue Seiten unter
admin/inventoryverwenden das Design-System ausresources/views/admin/inventory/partials/wawi-ui.blade.php(@include('admin.inventory.partials.wawi-ui')+ Inhalt in<div class="wawi-page">). Bausteine:wawi-page-head,wawi-stats/wawi-stat,wawi-card,wawi-toolbar/wawi-search,wawi-table,wawi-pill,wawi-deflist,wawi-name-cell,wawi-empty. Status immer überwawi-pill(ok/warning/danger) statt Bootstrap-badge-pill.