From ea360678318b4aee527dc4e4afc35b46087d6698 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Mon, 6 Jul 2026 11:42:55 +0000 Subject: [PATCH] Warenwirtschaft: AP-20 Rohstoffbestand-Filter + Kein-Rohstoffbestand-Flag - ingredients.exclude_from_stock (Checkbox im INCI-Formular, z. B. Allergene): erscheint nie im Rohstoffbestand und nicht im Kritisch-Badge - Rohstoffbestand-Uebersicht: Umschalter "Alle" / "Nur aus Herstellerrezepturen" (?scope=recipe, nutzt scopeUsedInActiveRecipes aus AP-21) - Fix: AP-21-Migrationen hatten identischen Timestamp (add_storage lief vor create_storage und scheiterte auf MySQL am FK) - umbenannt auf 120753, Teilreste bereinigt, Migrationen sauber ausgefuehrt - Hinweise-Doku fortgeschrieben Tests: RawMaterialStockTest +2 (volle Suite 172 gruen, 624 Assertions) Co-Authored-By: Claude Fable 5 --- .../Inventory/RawMaterialStockController.php | 9 +++- app/Http/Requests/StoreIngredientRequest.php | 2 + app/Models/Ingredient.php | 2 + app/Services/InventoryService.php | 1 + ...age_and_alt_name_to_ingredients_table.php} | 0 ...xclude_from_stock_to_ingredients_table.php | 26 ++++++++++ ...ntwicklungsplan-aktualisiert-06-07-2026.md | 8 +-- resources/docs/hinweise.md | 5 ++ .../views/admin/ingredient/form.blade.php | 10 ++++ .../raw-material-stock/index.blade.php | 6 +++ tests/Feature/RawMaterialStockTest.php | 49 +++++++++++++++++++ 11 files changed, 113 insertions(+), 5 deletions(-) rename database/migrations/{2026_07_06_120752_add_storage_and_alt_name_to_ingredients_table.php => 2026_07_06_120753_add_storage_and_alt_name_to_ingredients_table.php} (100%) create mode 100644 database/migrations/2026_07_06_133714_add_exclude_from_stock_to_ingredients_table.php diff --git a/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php b/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php index 47fa42a..eb2104c 100644 --- a/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php +++ b/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php @@ -10,6 +10,7 @@ use App\Models\TaxRate; use App\Services\InventoryService; use App\Services\ProductionService; use App\Services\ProductStockService; +use Illuminate\Http\Request; use Illuminate\View\View; class RawMaterialStockController extends Controller @@ -20,10 +21,15 @@ class RawMaterialStockController extends Controller protected ProductStockService $productStockService, ) {} - public function index(): View + public function index(Request $request): View { + // AP-20: Modus „alle" / „nur aus Herstellerrezepturen (aktiver Produkte)" + $scope = $request->query('scope') === 'recipe' ? 'recipe' : 'all'; + $ingredients = Ingredient::query() ->where('active', true) + ->where('exclude_from_stock', false) + ->when($scope === 'recipe', fn ($q) => $q->usedInActiveRecipes()) ->with('materialQuality') ->orderBy('pos') ->orderBy('name') @@ -57,6 +63,7 @@ class RawMaterialStockController extends Controller 'criticalCount' => $rows->where('status', 'critical')->count(), 'horizonOptions' => $this->horizonOptions(), 'defaultHorizon' => 90, + 'scope' => $scope, ]); } diff --git a/app/Http/Requests/StoreIngredientRequest.php b/app/Http/Requests/StoreIngredientRequest.php index 724fe8d..9b859ab 100644 --- a/app/Http/Requests/StoreIngredientRequest.php +++ b/app/Http/Requests/StoreIngredientRequest.php @@ -35,6 +35,7 @@ class StoreIngredientRequest extends FormRequest 'storage_shelf_id' => ['nullable', 'integer', 'exists:storage_shelves,id'], 'storage_slot_id' => ['nullable', 'integer', 'exists:storage_slots,id'], 'active' => ['sometimes', 'boolean'], + 'exclude_from_stock' => ['sometimes', 'boolean'], ]; } @@ -42,6 +43,7 @@ class StoreIngredientRequest extends FormRequest { $this->merge([ 'active' => $this->boolean('active'), + 'exclude_from_stock' => $this->boolean('exclude_from_stock'), 'default_factor' => $this->normalizeDecimal($this->input('default_factor')), 'min_stock_alert' => $this->normalizeDecimal($this->input('min_stock_alert')), 'material_quality_id' => $this->nullIfEmpty($this->input('material_quality_id')), diff --git a/app/Models/Ingredient.php b/app/Models/Ingredient.php index 88f771f..5ff16ec 100644 --- a/app/Models/Ingredient.php +++ b/app/Models/Ingredient.php @@ -55,6 +55,7 @@ class Ingredient extends Model protected $casts = [ 'active' => 'bool', + 'exclude_from_stock' => 'bool', 'pos' => 'int', 'default_factor' => 'decimal:2', 'min_stock_alert' => 'decimal:2', @@ -73,6 +74,7 @@ class Ingredient extends Model 'pos', 'default_factor', 'min_stock_alert', + 'exclude_from_stock', 'material_quality_id', 'tax_rate_id', 'delivery_time', diff --git a/app/Services/InventoryService.php b/app/Services/InventoryService.php index 813c684..670bea6 100644 --- a/app/Services/InventoryService.php +++ b/app/Services/InventoryService.php @@ -284,6 +284,7 @@ class InventoryService { $ingredients = Ingredient::query() ->where('active', true) + ->where('exclude_from_stock', false) ->whereNotNull('min_stock_alert') ->where('min_stock_alert', '>', 0) ->get(['id', 'min_stock_alert']); diff --git a/database/migrations/2026_07_06_120752_add_storage_and_alt_name_to_ingredients_table.php b/database/migrations/2026_07_06_120753_add_storage_and_alt_name_to_ingredients_table.php similarity index 100% rename from database/migrations/2026_07_06_120752_add_storage_and_alt_name_to_ingredients_table.php rename to database/migrations/2026_07_06_120753_add_storage_and_alt_name_to_ingredients_table.php diff --git a/database/migrations/2026_07_06_133714_add_exclude_from_stock_to_ingredients_table.php b/database/migrations/2026_07_06_133714_add_exclude_from_stock_to_ingredients_table.php new file mode 100644 index 0000000..a7f7be0 --- /dev/null +++ b/database/migrations/2026_07_06_133714_add_exclude_from_stock_to_ingredients_table.php @@ -0,0 +1,26 @@ +boolean('exclude_from_stock')->default(false)->after('min_stock_alert'); + }); + } + + public function down(): void + { + Schema::table('ingredients', function (Blueprint $table) { + $table->dropColumn('exclude_from_stock'); + }); + } +}; diff --git a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md index 40ce70b..8512439 100644 --- a/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md +++ b/dev/product management /entwicklungsplan-aktualisiert-06-07-2026.md @@ -100,8 +100,8 @@ Alle Punkte dieser älteren Runde sind in V4.0 eingeflossen und umgesetzt (URL-F | 1 | ✅ OPT-1 | Quick Wins (N+1, Indizes, ExampleTest) — **erledigt 06.07.2026** | – | 0,5 Tage | | 2 | ✅ OPT-2 | Settings-CRUD-Basisklasse — **erledigt 06.07.2026** | – | 1 Tag | | 3 | ✅ AP-21 | INCI-Erweiterungen (Lieferant+URL-Repeater, „nur aktive", Alternativname, Lagerort + 3 Stammdaten-CRUDs) — **erledigt 06.07.2026** | OPT-2 | 2–3,5 Tage | -| 4 | ➡️ AP-20 | **NÄCHSTER SCHRITT:** Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag | AP-21 | 1–2 Tage | -| 5 | AP-23 | Rezeptur-Phasen (Hersteller-Rezeptur) | – | 3–5 Tage | +| 4 | ✅ AP-20 | Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag — **erledigt 06.07.2026** | AP-21 | 1–2 Tage | +| 5 | ➡️ AP-23 | **NÄCHSTER SCHRITT:** Rezeptur-Phasen (Hersteller-Rezeptur) | – | 3–5 Tage | | 6 | AP-24 | Chargen-Nr.-Präfix | – | 1 Tag | | 7 | OPT-3 + OPT-4 | Pagination + show()-Refactoring | – | 1,5–2,5 Tage | | 8 | AP-27 | Wareneingang: Chargen-Merge | OPT-4 | 1–2 Tage | @@ -150,7 +150,7 @@ Die fachlichen Detailspezifikationen zu **AP-20, AP-21, AP-23, AP-24, AP-27, AP- | 06.07.2026 | **OPT-1 (Quick Wins)** | **N+1-Fix Ausschussliste:** `StockDisposalController@index` lädt jetzt `user.account` eager (vorher `user` → bis zu 500 Lazy-Loads durch `getFullName()`). **Composite-Indizes:** Migration `2026_07_06_113731_add_aggregation_indexes_to_stock_entries_table` — `stock_entries(entry_type, status, ingredient_id)` + `(entry_type, status, packaging_item_id)` für die Aggregations-Queries im `InventoryService`; auf DB ausgeführt. **Befund-Korrektur:** Der Index `production_ingredients(production_id, ingredient_id)` ist **nicht** redundant — er bedient den FK auf `production_id` mit (kein separater FK-Index vorhanden, per `SHOW INDEX` verifiziert) → bleibt bestehen. **Ungenutzter Eager-Load** `producedByUser` aus `ProductionRepository::listForIndex()` entfernt (wird nur in `show` gebraucht, dort bereits via `producedByUser.account` geladen). **`ExampleTest`** auf `assertRedirect()` umgestellt (Domain-Redirect auf `/`) — Suite dauerhaft 100 % grün. | Neuer Query-Count-Test `ausschuss-liste laedt benutzerkonten ohne n+1` in `StockDisposalTest` (verifiziert: schlägt ohne Fix fehl). Volle Suite: **155 grün, 538 Assertions** | | 06.07.2026 | **OPT-2 (Settings-CRUD-Basisklasse)** | Neue abstrakte Basisklasse `app/Http/Controllers/Admin/Inventory/SettingCrudController.php`: generisches index/create/store/edit/update/destroy; Kind-Controller definieren nur noch `modelClass()`, `viewPrefix()`, `redirectRoute()`, `storeRequestClass()` (+ optional `newModelAttributes()`, `indexOrder()`, `updateRequestClass()`). Validierung per Container-Auflösung des FormRequests (Verhalten wie Method-Injection), Model-Auflösung aus dem Routen-Parameter via `findOrFail` (404-Verhalten unverändert). Umgestellt: `TaxRate`, `DeliveryTime`, `DisposalReason`, `MaterialQuality`, `Location`, `SupplierCategory`, `PackagingMaterial` (je ~50 → ~25 Zeilen reine Konfiguration). Die 7 `Update*Request`-Klassen waren mit den Store-Requests identisch und wurden entfernt (Update nutzt den Store-Request; bei künftiger Abweichung `updateRequestClass()` überschreiben). Form-Views bleiben bewusst je Entität (unterschiedliche Felder). **Bilanz: −441/+73 Zeilen.** Neue Settings-CRUDs (z. B. Raum/Regal/Fach in AP-21) = 1 Model + 1 Store-Request + ~25-Zeilen-Controller. | Volle Suite **155 grün, 538 Assertions** (inkl. `TaxRateSettingsTest`, `DeliveryTimeSettingsTest`, `DisposalReasonSettingsTest`, `InventoryPhase2Test`, `InventoryPhase3Test`) | | 06.07.2026 | **AP-21 (INCI-Erweiterungen)** | **DB:** Migrationen `2026_07_06_120752_create_storage_segment_tables` (`storage_rooms`/`storage_shelves`/`storage_slots`, je `label`/`active`/`pos`) + `2026_07_06_120752_add_storage_and_alt_name_to_ingredients_table` (`ingredients.alt_name` + 3 FKs `storage_room/shelf/slot_id`, `nullOnDelete`); auf DB ausgeführt. **Models:** abstrakte Basis `StorageSegment` + `StorageRoom`/`StorageShelf`/`StorageSlot` (+ Factories); `Ingredient` um `alt_name`, Lagerort-FKs, Relationen, `storageLabel()` („Raum 1 \| Regal 3 \| A") und `scopeUsedInActiveRecipes()` (Hersteller-Rezeptur ∧ aktives Produkt) erweitert. **Settings:** 3 CRUDs als `SettingCrudController`-Kinder (OPT-2, je ~25 Zeilen) mit **einem** gemeinsamen `StoreStorageSegmentRequest`; 3 Karten auf Einstellungen → Allgemein über neues generisches Partial `setting-list-card`; gemeinsames Form-Partial `setting-segment-form`; Routen-Resources in der `superadmin`-Gruppe. **INCI-Formular:** Lieferanten-**Repeater** (Zeile = Lieferant-Select + URL-Feld, „+ weiteren Lieferanten anlegen", Entfernen-Button; jQuery-Template) statt Multi-Select — speichert `ingredient_supplier.url` per `sync()` (URL leer ⇒ null ⇒ Mailbestellung; `preferred`/`supplier_sku` bleiben erhalten); Feld „Alternativer Name"; 3 Lagerort-Dropdowns (nur aktive Bausteine). **INCI-Liste:** Checkbox „nur aktive (in Rezeptur aktiver Produkte)" (`?only_active=1`, serverseitig via Scope) + Spalte „Alternativer Name". **Rohstoffbestand-Detail:** Bestellweg jetzt **pro INCI-Lieferant** — Pivot-URL gesetzt ⇒ „Zum Shop", leer ⇒ „Per Mail" (ersetzt `order_method`-Fallback); Kopf zeigt Alternativname + Lagerort. **Hinweise-Doku (AP-18)** fortgeschrieben inkl. Kundenentscheid Chargen-Format `ttmmjj` (für AP-24). | `StorageSettingsTest` (7), `IngredientActiveRecipeFilterTest` (3), `IngredientOrderFieldsTest` (9, umgestellt auf Repeater-Format + URL/Lagerort), `RawMaterialStockTest` +2 (Bestellweg, Alternativname/Lagerort). Volle Suite: **170 grün, 613 Assertions** | -| _offen_ | AP-20 | Rohstoffbestand-Filter + Flag | – | +| 06.07.2026 | **AP-20 (Rohstoffbestand-Filter + „Kein Rohstoffbestand")** | Migration `2026_07_06_133714_add_exclude_from_stock_to_ingredients_table` (`ingredients.exclude_from_stock` bool default 0); `Ingredient` fillable+cast; Checkbox „Kein Rohstoffbestand" (Hinweistext Allergen-Angaben) im INCI-Formular. `RawMaterialStockController@index`: grundsätzlich `exclude_from_stock=false`, neuer Umschalt-Modus `?scope=all\|recipe` (Default `all`; `recipe` nutzt `scopeUsedInActiveRecipes()` aus AP-21); Button-Gruppe „Alle / Nur aus Herstellerrezepturen" in der Toolbar. `InventoryService::criticalIngredientCount()` (Sidenav-Badge) respektiert das Flag. Hinweise-Doku fortgeschrieben. **Nebenbei behoben:** die beiden AP-21-Migrationen hatten denselben Timestamp, wodurch `add_storage…` alphabetisch **vor** `create_storage…` lief und auf MySQL am FK scheiterte (Teilreste `alt_name`/`storage_room_id`) — Migration auf `…120753` umbenannt, Teilreste bereinigt, alle drei Migrationen sauber ausgeführt. | `RawMaterialStockTest` +2 (Flag blendet aus Übersicht + Kritisch-Zähler aus; Modus `recipe` filtert, Default zeigt alle, Pills rendern). Volle Suite: **172 grün, 624 Assertions** | | _offen_ | AP-23 | Rezeptur-Phasen | – | | _offen_ | AP-24 | Chargen-Präfix | – | | _offen_ | OPT-3/OPT-4 | Pagination + show()-Refactoring | – | @@ -159,4 +159,4 @@ Die fachlichen Detailspezifikationen zu **AP-20, AP-21, AP-23, AP-24, AP-27, AP- | _offen_ | AP-13 | Shop-Bestandsabzug | – | | _offen_ | AP-14/15/16/17 | Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen | – | -> **➡️ HIER GEHT ES WEITER: AP-20 (Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag)** gemäß V5.0-Spezifikation. Grundlagen aus AP-21 liegen vor: `Ingredient::scopeUsedInActiveRecipes()` für den Modus „nur aus Herstellerrezepturen" und das INCI-Formular für das neue Flag `exclude_from_stock`. Danach AP-23 (Rezeptur-Phasen) → AP-24 (Chargen-Präfix, Format `ttmmjj` bereits in Hinweisen dokumentiert) → OPT-3/OPT-4 → AP-27 → AP-28. +> **➡️ HIER GEHT ES WEITER: AP-23 (Rezeptur-Phasen auf der Hersteller-Rezeptur)** gemäß V5.0-Spezifikation (§4, Screen `2026-12-06-Rezeptur-phase.jpeg`): Tabelle `recipe_phases` + `product_ingredients.recipe_phase_id` (nullable, rückwärtskompatibel), Phasen-Blöcke im Produktformular (Rohstoffe + Anteil % + Notiz je Phase, Drag&Drop, „+ Neue Phase"), 100-%-Validierung über **alle** Phasen, `Product::copy()` kopiert Phasen mit. Danach AP-24 (Chargen-Präfix, Format `ttmmjj` bereits in Hinweisen dokumentiert) → OPT-3/OPT-4 → AP-27 → AP-28. diff --git a/resources/docs/hinweise.md b/resources/docs/hinweise.md index a371fa1..7191a4c 100644 --- a/resources/docs/hinweise.md +++ b/resources/docs/hinweise.md @@ -120,6 +120,11 @@ - **INCI-Liste „nur aktive":** Der Filter zeigt nur Inhaltsstoffe, die in der **Hersteller-Rezeptur mindestens eines aktiven Produkts** vorkommen – nicht zu verwechseln mit dem Aktiv-Häkchen am INCI selbst. +- **Rohstoffbestand-Filter:** Die Übersicht kennt zwei Modi – **„Alle"** (auch + Rohstoffe ohne Rezeptur-Bezug, z. B. für Neuentwicklungen eingekauft) und + **„Nur aus Herstellerrezepturen"** (aktiver Produkte). INCI mit dem Häkchen + **„Kein Rohstoffbestand"** (z. B. Allergen-Angaben) erscheinen nie im + Rohstoffbestand und zählen nicht im Kritisch-Badge. --- diff --git a/resources/views/admin/ingredient/form.blade.php b/resources/views/admin/ingredient/form.blade.php index e00b03b..34d2a75 100755 --- a/resources/views/admin/ingredient/form.blade.php +++ b/resources/views/admin/ingredient/form.blade.php @@ -184,5 +184,15 @@ +
+
+ + {{ __('Für Einträge ohne Bevorratung (z. B. Allergen-Angaben) — erscheint nie im Rohstoffbestand.') }} +
+
+ \ No newline at end of file diff --git a/resources/views/admin/inventory/raw-material-stock/index.blade.php b/resources/views/admin/inventory/raw-material-stock/index.blade.php index 22f8697..c326fa8 100644 --- a/resources/views/admin/inventory/raw-material-stock/index.blade.php +++ b/resources/views/admin/inventory/raw-material-stock/index.blade.php @@ -65,6 +65,12 @@ +