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>
This commit is contained in:
parent
78679e0c55
commit
3ee2d756e9
63 changed files with 5968 additions and 901 deletions
|
|
@ -21,6 +21,9 @@ Geprüfte Dateien u. a.: `routes/web.php`, `app/Http/Controllers/Admin/Inventory
|
|||
|
||||
| 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) |
|
||||
|
|
@ -31,13 +34,18 @@ Geprüfte Dateien u. a.: `routes/web.php`, `app/Http/Controllers/Admin/Inventory
|
|||
| 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).
|
||||
**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-09 (Produktion korrigieren).** Konkret: (1) Produktion **ausschließlich** auf Hersteller-Rezeptur umstellen + **Warnung**, wenn keine gepflegt ist (kein Fallback); (2) Chargen-Dropdown-Label + nur Chargen mit Restbestand; (3) B3 „Weitere Charge"-JS-Fix (genau eine Zeile); (4) Soll-Neuberechnung ohne Überschreiben manueller Eingaben; (5) B4 iPad-Layout der Kopfdaten; (6) Produktentwicklung-Platzhalterseite („Briefing ausstehend"). Danach AP-02/AP-03, dann die großen Übersichten. **Neu:** AP-18 (Hinweise-Doku unter Einstellungen) kann jederzeit dazwischengezogen werden.
|
||||
> **➡️ 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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -269,6 +277,8 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
|
||||
**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
|
||||
|
|
@ -287,6 +297,8 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
|
||||
**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
|
||||
|
|
@ -302,6 +314,8 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
|
||||
**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)
|
||||
|
|
@ -312,6 +326,8 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
|
||||
**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
|
||||
|
|
@ -326,18 +342,100 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
|
||||
**Akzeptanz:** Bestand schnell pflegbar; jede Bewegung in der Historie; nur Hauptprodukte sichtbar; Kritisch-Filter funktioniert.
|
||||
|
||||
> **Status:** Erledigt (03.06.2026). Migrationen `2026_06_03_122635_create_product_stock_movements_table` (`product_id` FK→`products` cascade, `direction` ENUM(`in`,`out`), `quantity`, `reason`, `source` default `manual`, `note`, `user_id` FK→`users` nullOnDelete, `nullableMorphs('reference')`) + `…_add_stock_thresholds_to_products_table` (`min_product_stock`, `critical_product_stock`). Modell `ProductStockMovement` (+ `Product::stockMovements()`, Schwellwerte in fillable/casts). `app/Services/ProductStockService.php` zentralisiert die Logik: `currentStockByProduct()`/`currentStock()` (= `SUM(in)`−`SUM(out)`), `recordMovement()` (Menge immer positiv, Richtung steuert Vorzeichen), `recordProductionStock()` (idempotenter Soll-/Ist-Abgleich je Produktion → bucht nur die Differenz, append-only), `productStatus()` (critical ≤ kritischer Schwellwert, warning ≤ Meldebestand), `criticalProductCount()` (nur aktive Hauptprodukte mit gesetztem kritischen Schwellwert). `ProductStockController` (`copyreader`-Gruppe): `index` (nur Haupt-/Einzelprodukte, Bild, Bestand rot/gelb, Suche + „nur kritische"-Filter, `+`/`−`-Modal für Bewegung, `Produzieren`-Link mit Produktvorwahl), `storeMovement` (FormRequest `StoreProductStockMovementRequest`, nur `isAdmin`), `history` (Filter Produkt/Eingang-Ausgang/Grund/Monat+Jahr, revisionssicher). Produktion bucht Produktbestand automatisch (`ProductionService` → `recordProductionStock` bei `store`/`update`). Produktformular: Schwellwert-Felder in der Warenwirtschaft-Card (+ `ProductRepository`). Sidenav „Produktbestand" mit Untermenü „Übersicht"/„Historie" und Kritisch-Badge (`View::composer` erweitert um `criticalProductCount`). AP-10 „Enthalten in"-Block um Produktbestand-Spalte ergänzt. Tests: `tests/Feature/ProductStockTest.php` (9 grün), Produktions-Regression unverändert grün. **Hinweis:** Verkauf/Tag je Produkt sowie Bestandsabzug beim (Set-)Verkauf folgen mit AP-13.
|
||||
|
||||
---
|
||||
|
||||
### AP-12 — Ausgang / Ausschuss (Rohstoffe/Verpackung)
|
||||
- `stock_disposals` (Typ, Artikel, Charge optional, Lagerort, Menge, Einheit, Grund Pflicht, User, Datum) + Controller/Views; Integration in `InventoryService`.
|
||||
- **Akzeptanz:** Ausgang reduziert Rohstoff-/Verpackungsbestand; Grund ist Pflicht.
|
||||
|
||||
> **Status:** Erledigt (03.06.2026). Migration `2026_06_03_124056_create_stock_disposals_table` (`disposal_type` ENUM(`ingredient`,`packaging`), `ingredient_id`/`packaging_item_id`/`stock_entry_id` nullable FKs, `location_id`, `quantity` decimal, `unit`, `reason` Pflicht, `note`, `user_id`, `disposed_at`). Modell `StockDisposal`. `InventoryService` erweitert: `disposedByIngredient()` + Abzug in `remainingByIngredient()` und `remainingByLocationForIngredient()` (somit auch im Kritisch-Zähler/Badge), `remainingByPackagingItem()` (Wareneingang − Produktionsverbrauch − Ausschuss) + `disposedByPackagingItem()`. `StockDisposalController` (`copyreader`-Gruppe; `create`/`store` nur `isAdmin`): `index` (Liste mit Art-Filter), `create`/`store` (FormRequest `StoreStockDisposalRequest`, deutsche Dezimal-/Datumsnormalisierung, Pflicht-Grund), `ingredientCharges` (JSON-Endpoint: eingegangene Chargen je Rohstoff + Lagerort für die Charge-Vorauswahl). Erfassungsformular mit Typ-Umschaltung (Rohstoff/Verpackung), Select2-Suche, optionaler Charge (setzt Lagerort automatisch), Grund-Auswahl, Datepicker. Sidenav-Eintrag „Ausgang / Ausschuss" (nach „Einkauf & Wareneingang"); „Ausschuss erfassen"-Button auf der Rohstoff-Detailseite (vorbelegt). Tests: `tests/Feature/StockDisposalTest.php` (8 grün), Regression Rohstoff-/Produktbestand grün.
|
||||
|
||||
---
|
||||
|
||||
### AP-13 — Shop-Anbindung: Bestand bei Verkauf reduzieren
|
||||
- **Entscheidung (§5.2, geklärt):** Bestandsabzug erfolgt **beim Versand** (erst wenn der Versand gebucht ist, wurde das Produkt real „aus dem Regal" genommen). Dieser Hinweis ist auch in der Hinweise-Doku (AP-18) zu hinterlegen.
|
||||
- Beim Statuswechsel auf **versendet** `product_stock_movements`-`out`-Buchung; bei Sets die enthaltenen Einzelprodukte (× Menge) reduzieren. Stornos/Retouren als Gegenbuchung (Detailregel bei Umsetzung festzurren).
|
||||
- **Akzeptanz:** Versand reduziert Produktbestand; Set-Versand reduziert Einzelprodukte; jede Buchung in der Historie.
|
||||
### AP-13 — Shop-Anbindung: Bestand bei Verkauf/Versand reduzieren (Entwicklungskonzept)
|
||||
|
||||
> **Stand:** Konzept (03.06.2026), noch nicht umgesetzt. Skizziert Trigger, Datenmodell, Set-Auflösung, Storno und offene Klärungspunkte, damit die Umsetzung ohne Rückfragen starten kann.
|
||||
|
||||
#### 0. Ausgangslage (verifizierter Code-Stand)
|
||||
Wichtigste Erkenntnis aus der Code-Prüfung: **Die Verkaufsdaten liegen bereits im System** — AP-13 braucht primär einen sauberen Trigger an den vorhandenen Statuswechsel, keine zwingend neue WooCommerce-Schnittstelle für den Abzug selbst.
|
||||
|
||||
- **Bestellungen vorhanden:** `shopping_orders` + `shopping_order_items` (`product_id`, `qty`, `price`). Jede Position zeigt per `product()` auf das Produkt.
|
||||
- **Produkt-Mapping zu WooCommerce besteht:** über `products.wp_number` (siehe `app/Http/Controllers/Api/ShoppingUserController.php::prepareOrder()` → `Product::whereWpNumber($order->article)`). Es ist **kein** neues Mapping nötig.
|
||||
- **Versandstatus steckt in `shopping_orders.shipped`** (0 = offen, 1 = in Bearbeitung, **2 = versendet**, 3 = abgeschlossen, 4 = Abholung, 5 = Wartestellung, 10 = storniert) + `shipped_at` (datetime).
|
||||
- **Zentraler Statuswechsel:** `app/Http/Controllers/SalesController.php@store`, Action `store_shipped` (~Z. 421–454). Dort wird `shipped` gesetzt und – bereits **idempotent** über `if (! $shopping_order->shipped_at)` – einmalig `shipped_at` beim Übergang auf „sent"/„close" gesetzt. **Idealer Aufhängepunkt.**
|
||||
- **WooCommerce-Anbindung (Ist):** WP ruft per Passport-API (`/api/wp/*`, `ShoppingUserController`) `store`/`order`/`update`/`status`/`cancel`/`open`. Status „versendet" wird derzeit **nicht** von WP gesetzt, sondern intern im Backend; WP **liest** den Status (`ShoppingOrder::getAPIShippedType()`).
|
||||
- **Produktbestand-Infrastruktur steht (AP-11):** `product_stock_movements` mit `direction`, `source`, `reason`, `nullableMorphs('reference')` und `ProductStockService::recordMovement()`. → Die „out"-Buchung setzt **ohne neue Tabellen** darauf auf.
|
||||
|
||||
#### 1. Ablauf-Skizze
|
||||
```
|
||||
WooCommerce ──(push /api/wp/order)──▶ shopping_orders / shopping_order_items (Mapping via products.wp_number)
|
||||
│
|
||||
Backend: Versand buchen (SalesController@store / store_shipped) ──▶ shipped = 2 + shipped_at
|
||||
│ (Trigger)
|
||||
▼
|
||||
SaleStockService::bookShipment(ShoppingOrder)
|
||||
├─ je Position: Set? ──ja──▶ Komponenten (× Set-Menge × qty) je „out"
|
||||
│ └─nein─▶ Produkt selbst (× qty) „out"
|
||||
├─ source = 'sale', reference = ShoppingOrder, reason = "Versand #<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 ~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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -383,6 +481,8 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
- **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).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -408,13 +508,11 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
|
||||
## 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.
|
||||
✅ **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-09** Produktionskorrekturen: ausschließlich Hersteller-Rezeptur (+ Warnung bei fehlender Rezeptur, kein Fallback), Chargen-Label + Restbestandsfilter, B3 „Weitere Charge"-Fix, stabile Soll-Neuberechnung, B4 iPad-Layout, Produktentwicklung-Platzhalter.
|
||||
2. **AP-18** Hinweise-Doku (Einstellungen → Hinweise) — kann parallel/früh als Platzhalter angelegt und laufend gepflegt werden.
|
||||
3. Datenmodell **AP-02** (Sets via Pivot) / **AP-03** („Nicht vorrätig", nur Hinweis).
|
||||
4. Große Übersichten **AP-10/AP-11** und Folge-APs (AP-12–AP-17).
|
||||
1. **AP-13** (Shop-Anbindung: Bestandsabzug beim Versand inkl. Sets) – **Entwicklungskonzept liegt vor** (siehe AP-13 in §4). Kernbefund: Bestellungen liegen bereits im System (`shopping_orders`/`shopping_order_items`, Mapping über `products.wp_number`), Versandstatus wird zentral in `SalesController@store` gesetzt → Abzug kann ohne neue Tabelle auf der AP-11-Infrastruktur aufsetzen. **Vorab nur noch zu bestätigen:** Szenario A (Versand wird im Backend gebucht – empfohlen, kein neuer Woo-Eingriff) vs. Szenario B (WooCommerce meldet Versand per Webhook/REST-Pull zurück) sowie die Varianten-/Hauptprodukt-Detailfrage. Danach Folge-APs (AP-14–AP-17).
|
||||
2. **AP-18** mit jedem weiteren AP fortschreiben (Hinweise-Seite aktuell halten).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -423,3 +521,4 @@ Diese Punkte sind klein, konkret und blockieren teils die tägliche Nutzung. Sie
|
|||
- 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue