diff --git a/app/Http/Controllers/Admin/Inventory/DisposalReasonController.php b/app/Http/Controllers/Admin/Inventory/DisposalReasonController.php new file mode 100644 index 0000000..43ff27b --- /dev/null +++ b/app/Http/Controllers/Admin/Inventory/DisposalReasonController.php @@ -0,0 +1,54 @@ + new DisposalReason(['active' => true, 'pos' => 0]), + ]); + } + + public function store(StoreDisposalReasonRequest $request): RedirectResponse + { + DisposalReason::create($request->validated()); + + \Session::flash('alert-save', '1'); + + return redirect()->route('admin.inventory.general'); + } + + public function edit(DisposalReason $disposalReason): View + { + return view('admin.inventory.disposal-reasons.form', [ + 'model' => $disposalReason, + ]); + } + + public function update(UpdateDisposalReasonRequest $request, DisposalReason $disposalReason): RedirectResponse + { + $disposalReason->update($request->validated()); + + \Session::flash('alert-save', '1'); + + return redirect()->route('admin.inventory.general'); + } + + public function destroy(DisposalReason $disposalReason): RedirectResponse + { + $disposalReason->delete(); + + \Session::flash('alert-success', __('Eintrag gelöscht')); + + return redirect()->route('admin.inventory.general'); + } +} diff --git a/app/Http/Controllers/Admin/Inventory/GeneralSettingController.php b/app/Http/Controllers/Admin/Inventory/GeneralSettingController.php index 4fbb773..8a1dc92 100644 --- a/app/Http/Controllers/Admin/Inventory/GeneralSettingController.php +++ b/app/Http/Controllers/Admin/Inventory/GeneralSettingController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin\Inventory; use App\Http\Controllers\Controller; use App\Models\DeliveryTime; +use App\Models\DisposalReason; use App\Models\TaxRate; use Illuminate\Contracts\View\View; @@ -14,6 +15,7 @@ class GeneralSettingController extends Controller return view('admin.inventory.general.index', [ 'taxRates' => TaxRate::query()->orderBy('pos')->orderBy('percent')->get(), 'deliveryTimes' => DeliveryTime::query()->orderBy('pos')->orderBy('label')->get(), + 'disposalReasons' => DisposalReason::query()->orderBy('pos')->orderBy('label')->get(), ]); } } diff --git a/app/Http/Controllers/Admin/Inventory/ProductStockController.php b/app/Http/Controllers/Admin/Inventory/ProductStockController.php index 220b8d9..27f44fb 100644 --- a/app/Http/Controllers/Admin/Inventory/ProductStockController.php +++ b/app/Http/Controllers/Admin/Inventory/ProductStockController.php @@ -22,27 +22,40 @@ class ProductStockController extends Controller $products = Product::query() ->where('active', true) ->where('is_set', false) + ->where('show_in_product_stock', true) ->whereNull('main_product_id') ->with('images') ->orderBy('pos') ->orderBy('name') ->get(); - $stock = $this->productStockService->currentStockByProduct($products->pluck('id')->all()); + $ids = $products->pluck('id')->all(); + $stock = $this->productStockService->currentStockByProduct($ids); + $consumption = $this->productStockService->monthlyConsumptionByProduct($ids); - $rows = $products->map(function (Product $product) use ($stock) { + $statusRank = ['critical' => 0, 'warning' => 1, 'ok' => 2]; + + $rows = $products->map(function (Product $product) use ($stock, $consumption) { $current = $stock[$product->id] ?? 0; return [ 'product' => $product, 'stock' => $current, + 'monthly_consumption' => $consumption[$product->id] ?? 0.0, 'status' => $this->productStockService->productStatus( $current, $product->min_product_stock, $product->critical_product_stock, ), ]; - }); + }) + // AP-22: Default-Sortierung nach Dringlichkeit (kritisch → niedrig → ok), innerhalb nach Bestand/Name. + ->sortBy([ + fn (array $a, array $b) => ($statusRank[$a['status']] <=> $statusRank[$b['status']]) + ?: ($a['stock'] <=> $b['stock']) + ?: strcasecmp($a['product']->name, $b['product']->name), + ]) + ->values(); return view('admin.inventory.product-stock.index', [ 'rows' => $rows, diff --git a/app/Http/Controllers/Admin/Inventory/StockDisposalController.php b/app/Http/Controllers/Admin/Inventory/StockDisposalController.php index 63a670d..d69db2f 100644 --- a/app/Http/Controllers/Admin/Inventory/StockDisposalController.php +++ b/app/Http/Controllers/Admin/Inventory/StockDisposalController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin\Inventory; use App\Http\Controllers\Controller; use App\Http\Requests\Inventory\StoreStockDisposalRequest; +use App\Models\DisposalReason; use App\Models\Ingredient; use App\Models\Location; use App\Models\StockDisposal; @@ -109,17 +110,17 @@ class StockDisposalController extends Controller } /** + * Aktive Ausschuss-Gründe aus den Einstellungen (Warenwirtschaft → Allgemein). + * * @return array */ protected function reasons(): array { - return [ - __('Bruch / Beschädigung'), - __('Verfall / MHD überschritten'), - __('Qualitätsmangel'), - __('Schwund / Inventurdifferenz'), - __('Muster / Testverbrauch'), - __('Sonstiges'), - ]; + return DisposalReason::query() + ->active() + ->orderBy('pos') + ->orderBy('label') + ->pluck('label') + ->all(); } } diff --git a/app/Http/Controllers/User/OrderController.php b/app/Http/Controllers/User/OrderController.php index 272f3f3..0a758e7 100755 --- a/app/Http/Controllers/User/OrderController.php +++ b/app/Http/Controllers/User/OrderController.php @@ -319,21 +319,23 @@ class OrderController extends Controller $qty = isset($cartItem->qty) ? $cartItem->qty : 0; $rowId = isset($cartItem->rowId) ? $cartItem->rowId : ''; + // AP-25: „Nicht vorrätig" ist nur ein Hinweis — der VP entscheidet selbst, + // ob er die Ware später bekommt. Die Mengen-Buttons bleiben immer aktiv. + $controls = ''; if ($product->isOutOfStock()) { - $controls = '
'.e($product->outOfStockNotice()).'
'; - } else { - $controls = '
-
- - - - - - - -
-
'; + $controls .= '
'.e($product->outOfStockNotice()).'
'; } + $controls .= '
+
+ + + + + + + +
+
'; return ''.$product->name.'
'.$controls; }) diff --git a/app/Http/Requests/Inventory/StoreDisposalReasonRequest.php b/app/Http/Requests/Inventory/StoreDisposalReasonRequest.php new file mode 100644 index 0000000..e83dbe1 --- /dev/null +++ b/app/Http/Requests/Inventory/StoreDisposalReasonRequest.php @@ -0,0 +1,33 @@ +> + */ + public function rules(): array + { + return [ + 'label' => ['required', 'string', 'max:100'], + 'active' => ['sometimes', 'boolean'], + 'pos' => ['nullable', 'integer', 'min:0', 'max:255'], + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'active' => $this->boolean('active'), + 'pos' => $this->input('pos', '') === '' ? 0 : $this->input('pos'), + ]); + } +} diff --git a/app/Http/Requests/Inventory/UpdateDisposalReasonRequest.php b/app/Http/Requests/Inventory/UpdateDisposalReasonRequest.php new file mode 100644 index 0000000..9740373 --- /dev/null +++ b/app/Http/Requests/Inventory/UpdateDisposalReasonRequest.php @@ -0,0 +1,5 @@ + */ + use HasFactory; + + protected $fillable = [ + 'label', + 'active', + 'pos', + ]; + + /** + * @return array + */ + protected function casts(): array + { + return [ + 'active' => 'boolean', + ]; + } + + /** + * @param Builder $query + * @return Builder + */ + public function scopeActive(Builder $query): Builder + { + return $query->where('active', true); + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php index 4260490..8d1d82d 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -210,6 +210,7 @@ class Product extends Model 'out_of_stock_indefinite' => 'bool', 'min_product_stock' => 'int', 'critical_product_stock' => 'int', + 'show_in_product_stock' => 'bool', ]; use Sluggable; @@ -265,6 +266,7 @@ class Product extends Model 'out_of_stock_indefinite', 'min_product_stock', 'critical_product_stock', + 'show_in_product_stock', ]; diff --git a/app/Repositories/ProductRepository.php b/app/Repositories/ProductRepository.php index c1152d2..2597ec5 100644 --- a/app/Repositories/ProductRepository.php +++ b/app/Repositories/ProductRepository.php @@ -11,6 +11,7 @@ use App\Models\ProductCategory; use App\Models\ProductImage; use App\Models\ProductIngredient; use App\Services\Slim; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Collection; class ProductRepository extends BaseRepository @@ -46,6 +47,9 @@ class ProductRepository extends BaseRepository ? (int) $data['main_product_id'] : null; + // AP-22: Sichtbarkeit im Produktbestand (Checkbox, Default sichtbar). + $data['show_in_product_stock'] = isset($data['show_in_product_stock']) ? 1 : 0; + // AP-11: Produktbestand-Schwellwerte (leer => null). $data['min_product_stock'] = isset($data['min_product_stock']) && $data['min_product_stock'] !== '' ? max(0, (int) $data['min_product_stock']) @@ -54,14 +58,18 @@ class ProductRepository extends BaseRepository ? max(0, (int) $data['critical_product_stock']) : null; - // AP-03: „Nicht vorrätig"-Status. „Unbestimmt" hat Vorrang vor der Tagesangabe. + // AP-03/AP-25: „Nicht vorrätig"-Status. „Unbestimmt" hat Vorrang vor der Datumsangabe. + // Eingabe als festes Datum („Wieder lieferbar ab", dd.mm.yyyy); Resttage werden daraus berechnet. $data['out_of_stock_indefinite'] = isset($data['out_of_stock_indefinite']) ? 1 : 0; if ($data['out_of_stock_indefinite']) { $data['out_of_stock_until'] = null; - } elseif (isset($data['out_of_stock_active']) && isset($data['out_of_stock_days']) && $data['out_of_stock_days'] !== '') { - $days = max(0, (int) $data['out_of_stock_days']); - $data['out_of_stock_until'] = now()->addDays($days)->startOfDay(); + } elseif (isset($data['out_of_stock_active']) && isset($data['out_of_stock_date']) && $data['out_of_stock_date'] !== '') { + try { + $data['out_of_stock_until'] = Carbon::createFromFormat('d.m.Y', trim($data['out_of_stock_date']))->startOfDay(); + } catch (\Throwable) { + $data['out_of_stock_until'] = null; + } } else { $data['out_of_stock_until'] = null; } diff --git a/app/Services/ProductStockService.php b/app/Services/ProductStockService.php index 4b67db0..0477110 100644 --- a/app/Services/ProductStockService.php +++ b/app/Services/ProductStockService.php @@ -106,6 +106,37 @@ class ProductStockService ); } + /** + * Durchschnittlicher Verbrauch pro Monat je Produkt (Ø der letzten X Monate, Default 6). + * + * „Verbrauch" = alle Abgänge (direction=out) außer Produktions-Gegenbuchungen + * (source=production sind Korrekturen der Eingangs-Buchung, kein echter Abgang). + * Mit AP-13 fließen Verkaufs-Abgänge (source=sale) automatisch mit ein. + * + * @param array|null $productIds + * @return array [product_id => Stück/Monat] + */ + public function monthlyConsumptionByProduct(?array $productIds = null, int $months = 6): array + { + $months = max(1, $months); + + $rows = ProductStockMovement::query() + ->when($productIds !== null, fn ($q) => $q->whereIn('product_id', $productIds)) + ->where('direction', 'out') + ->where('source', '!=', 'production') + ->where('created_at', '>=', now()->subMonths($months)) + ->selectRaw('product_id, SUM(quantity) as consumed') + ->groupBy('product_id') + ->pluck('consumed', 'product_id'); + + $result = []; + foreach ($rows as $id => $consumed) { + $result[(int) $id] = round(((int) $consumed) / $months, 1); + } + + return $result; + } + /** * Bestands-Status: "critical" (rot) ≤ kritischer Schwellwert, "warning" (gelb) ≤ Meldebestand, sonst "ok". */ @@ -129,6 +160,7 @@ class ProductStockService $products = Product::query() ->where('active', true) ->where('is_set', false) + ->where('show_in_product_stock', true) ->whereNull('main_product_id') ->whereNotNull('critical_product_stock') ->get(['id', 'critical_product_stock']); diff --git a/database/factories/DisposalReasonFactory.php b/database/factories/DisposalReasonFactory.php new file mode 100644 index 0000000..fde8513 --- /dev/null +++ b/database/factories/DisposalReasonFactory.php @@ -0,0 +1,33 @@ + + */ +class DisposalReasonFactory extends Factory +{ + protected $model = DisposalReason::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'label' => $this->faker->unique()->randomElement([ + 'Bruch / Beschädigung', + 'Verfall / MHD überschritten', + 'Qualitätsmangel', + 'Schwund / Inventurdifferenz', + 'Muster / Testverbrauch', + 'Sonstiges', + ]), + 'active' => true, + 'pos' => 0, + ]; + } +} diff --git a/database/migrations/2026_06_12_170531_create_disposal_reasons_table.php b/database/migrations/2026_06_12_170531_create_disposal_reasons_table.php new file mode 100644 index 0000000..8be3433 --- /dev/null +++ b/database/migrations/2026_06_12_170531_create_disposal_reasons_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('label'); + $table->boolean('active')->default(true); + $table->unsignedTinyInteger('pos')->default(0); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('disposal_reasons'); + } +}; diff --git a/database/migrations/2026_06_12_175615_add_show_in_product_stock_to_products_table.php b/database/migrations/2026_06_12_175615_add_show_in_product_stock_to_products_table.php new file mode 100644 index 0000000..fdaa0d8 --- /dev/null +++ b/database/migrations/2026_06_12_175615_add_show_in_product_stock_to_products_table.php @@ -0,0 +1,29 @@ +boolean('show_in_product_stock')->default(true)->after('critical_product_stock'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('show_in_product_stock'); + }); + } +}; diff --git a/database/seeders/InventoryStammdatenSeeder.php b/database/seeders/InventoryStammdatenSeeder.php index 4392c73..7872268 100644 --- a/database/seeders/InventoryStammdatenSeeder.php +++ b/database/seeders/InventoryStammdatenSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Models\DeliveryTime; +use App\Models\DisposalReason; use App\Models\Location; use App\Models\MaterialQuality; use App\Models\PackagingMaterial; @@ -71,5 +72,20 @@ class InventoryStammdatenSeeder extends Seeder ['days' => $deliveryTime['days'], 'active' => true, 'pos' => $pos] ); } + + $disposalReasons = [ + 'Bruch / Beschädigung', + 'Verfall / MHD überschritten', + 'Qualitätsmangel', + 'Schwund / Inventurdifferenz', + 'Muster / Testverbrauch', + 'Sonstiges', + ]; + foreach ($disposalReasons as $pos => $label) { + DisposalReason::query()->firstOrCreate( + ['label' => $label], + ['active' => true, 'pos' => $pos] + ); + } } } diff --git a/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md b/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md index 42ec1f0..3d03ee3 100644 --- a/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md +++ b/dev/product management /entwicklungsplan-aktualisiert-02-06-2026.md @@ -1,5 +1,7 @@ # 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.md` diff --git a/dev/product management /entwicklungsplan-aktualisiert-12-06-2026.md b/dev/product management /entwicklungsplan-aktualisiert-12-06-2026.md new file mode 100644 index 0000000..27d1bf0 --- /dev/null +++ b/dev/product management /entwicklungsplan-aktualisiert-12-06-2026.md @@ -0,0 +1,367 @@ +# Aktualisierter Entwicklungsplan: Warenwirtschaft, Produktion & Produktbestand + +> **Version:** 5.0 - Stand 12.06.2026 +> **Ersetzt:** `entwicklungsplan-aktualisiert-02-06-2026.md` (V4.0) als operative Arbeitsgrundlage +> **Neue Anforderungsquelle:** `docs/nächsten Anforderungen an die WaWi.md` (Kunde, 12.06.2026) + Screens `screens/2026-12-06-Rezeptur-phase.jpeg`, `screens/2026-12-06-neue-produktion.jpeg` +> **Referenzen:** V4.0 (AP-00 … AP-19), `briefing-anpassungen-27-04-2026.md`, `feedback.md`, `konzept-final.md`, `docs/Todos.md` +> **Methodik:** Backlog aus kleinen, sequenziell abarbeitbaren Arbeitspaketen (AP). Jedes AP hat Ziel, konkrete Schritte mit Dateipfaden, DB-Änderungen, Akzeptanzkriterien und Tests. Reihenfolge so gewählt, dass jedes AP einzeln deploybar ist. + +--- + +## 0. Was dieses Dokument neu macht + +V4.0 hat **AP-00 bis AP-19 abgeschlossen** (Bugfixes, Einstellungen, Lieferanten/INCI/Einkauf erweitert, Produktion auf Hersteller-Rezeptur, Sets, „Nicht vorrätig", Rohstoffbestand, Produktbestand + Historie, Ausgang/Ausschuss, Hinweise-Doku, UI-Vereinheitlichung). **AP-13 (Shop-Bestandsabzug) liegt als Konzept vor, ist aber noch nicht umgesetzt.** + +V5.0 nimmt die **neue Anforderungsrunde vom 12.06.2026** auf. Diese ist überwiegend **Feinschliff und Ergänzung an bereits gebauten Bausteinen** (Rohstoffbestand, INCI, Produktbestand, Ausschuss, Wareneingang) plus **zwei größere fachliche Erweiterungen**: + +1. **Rezeptur-Phasen** auf der Hersteller-Rezeptur (Screen 1). +2. **Neue Produktions-Maske mit Produktionsergebnis** — der Produktbestand wird **nicht** mehr aus der geplanten Stückzahl gebucht, sondern aus dem real eingetragenen Ergebnis (mehrere Outputs möglich). Der Rohstoffverbrauch bleibt wie gehabt chargenbasiert (Screen 2). + +Der **reale Code-Stand wurde erneut verifiziert** (12.06.2026); die daraus folgenden Abgrenzungen stehen je AP unter „Ist-Stand". + +Geprüfte Dateien u. a.: `RawMaterialStockController`, `ProductStockController` + `ProductStockService`, `IngredientController` + `app/Models/Ingredient.php` + `resources/views/admin/ingredient/*`, `StockDisposalController`, `StockEntryController` + `app/Models/StockEntry.php`, `ProductionController` + `app/Services/ProductionService.php` + `app/Models/Production*.php`, `app/Models/Product.php`, `GeneralSettingController`, `app/Models/Location.php`, Migrationen unter `database/migrations/`. + +--- + +## 0a. Umsetzungsprotokoll V5.0 (laufend) + +> Jede abgeschlossene Teil-Lieferung wird hier mit Datum, betroffenen Dateien und Test-Status protokolliert. + +| Datum | AP | Kurzbeschreibung | Tests | +|---|---|---|---| +| 12.06.2026 | **AP-22 (Produktbestand-Erweiterungen)** | Migration `2026_06_12_175615_add_show_in_product_stock_to_products_table` (`products.show_in_product_stock` bool default 1). `Product` fillable+cast; Checkbox „Im Produktbestand anzeigen" in der Warenwirtschaft-Card (`ProductRepository` normalisiert; Hinweistext „Abrechnung Druckkosten / Logo-Etiketten"). `ProductStockController@index` + `ProductStockService::criticalProductCount()` filtern auf das Flag (ausgeblendete Produkte zählen nicht im Badge). **Dringlichkeits-Sortierung:** Übersicht serverseitig default nach Status-Rang (kritisch → niedrig → ok), innerhalb nach Bestand aufsteigend, dann Name; Klick auf den „Status"-Spaltenkopf toggelt die Reihenfolge (JS über `data-rank`). **Kacheln:** alle vier Kacheln (`Produkte`/`Bestand OK`/`Niedrig`/`Kritisch`) klickbar — „Produkte" hebt den Filter auf, die anderen filtern exakt ihren Status (Checkbox „nur kritische" bleibt erhalten). **Neue Spalte „Verbrauch/Monat":** `ProductStockService::monthlyConsumptionByProduct()` = Ø der `out`-Bewegungen der letzten 6 Monate (rollierendes Fenster), **ohne** `source=production` (Produktions-Gegenbuchungen sind Korrekturen, kein Verbrauch); mit AP-13 fließen Verkäufe (`source=sale`) automatisch ein. Migration auf DB ausgeführt. | `ProductStockTest` erweitert (13 grün, 37 Assertions): Flag blendet aus Übersicht + Kritisch-Zähler aus, Verbrauch/Monat-Mittelung (Fenstergrenze, production-Ausschluss, Eingänge zählen nie), Default-Sortierung kritisch vor ok, Render Verbrauch-Spalte + 4 Kachel-Filter. Regression Produkt-Suite (32 grün) | +| 12.06.2026 | **AP-25 (Lieferbestand: Datum statt Tage)** | „Nicht vorrätig" wird jetzt über ein festes **Datum** („Wieder lieferbar ab", Datepicker `datepicker-base`, `dd.mm.yyyy`) statt über eine Tagesangabe gepflegt; Datenmodell unverändert (`out_of_stock_until` DATE passte bereits). `ProductRepository::update()` parst `out_of_stock_date` via `Carbon::createFromFormat('d.m.Y')` (ungültige Eingabe ⇒ null statt Crash); „unbestimmt" behält Vorrang, Deaktivierung leert beides. Resttage-Helper (`outOfStockRemainingDays()`/`outOfStockNotice()`) unverändert — zählen täglich automatisch herunter. **Interne Bestellliste wieder kaufbar:** `User/OrderController::datatable` zeigt den roten Hinweis jetzt **zusätzlich** zu den Mengen-Buttons (vorher ersetzte er sie) — der VP entscheidet selbst, ob er später beliefert wird; Produkt-Detail-Modal zeigte den Hinweis bereits nur zusätzlich. Formular-Card „Verfügbarkeit" + JS-Toggle (`js-out-of-stock-date`) angepasst. | `tests/Feature/ProductOutOfStockTest.php` umgebaut (8 grün, 29 Assertions): Datum→`out_of_stock_until`, Unbestimmt-Vorrang, Deaktivierung leert, ungültiges Datum ⇒ null, Vergangenheit gilt nicht, Resttage zählen mit `travel()` herunter, HTTP-Store mit Datum, **Bestellliste zeigt Hinweis + Mengen-Buttons**. Regression `ProductSetTest`/`ProductPhase51Test`/`ProductStockTest` (24 grün) | +| 12.06.2026 | **AP-26 (Ausschuss-Gründe konfigurierbar)** | Neue Stammdaten-Tabelle `disposal_reasons` (`label`, `active`, `pos`) via Migration `2026_06_12_170531_create_disposal_reasons_table`; Model `DisposalReason` (cast `active`, `scopeActive`), `DisposalReasonFactory`, Seeder-Erweiterung `InventoryStammdatenSeeder` (bisherige 6 Gründe idempotent per `firstOrCreate` übernommen). CRUD: `DisposalReasonController` (create/store/edit/update/destroy → Redirect `general`), `Store/UpdateDisposalReasonRequest` (`label` Pflicht max 100, passend zur `stock_disposals.reason`-Spalte). Dritte Karte „Ausschuss-Gründe" auf **Einstellungen → Allgemein** (`general/index.blade.php` + `GeneralSettingController`), View `disposal-reasons/form.blade.php`. Route Resource `disposal-reasons` (ohne index/show) in der `superadmin`-Gruppe. **`StockDisposalController::reasons()` liest jetzt aktive `disposal_reasons`** (Reihenfolge `pos`, dann `label`) statt der hartkodierten Liste; `StoreStockDisposalRequest` bleibt bewusst ohne `in:`-Zwang (bestehende Disposals behalten ihren String-Grund). Migration + Seed auf DB ausgeführt. | `tests/Feature/DisposalReasonSettingsTest.php` (8 grün, 24 Assertions): Render Allgemein-Karte, CRUD, Pflicht-Label, `active`-Scope, **Ausschuss-Formular zeigt nur aktive Gründe in Sortierung**, Zugriffsschutz Nicht-SuperAdmin. Regression `StockDisposalTest`/`TaxRateSettingsTest`/`DeliveryTimeSettingsTest` (24 grün) | +| _offen_ | AP-20 | Rohstoffbestand-Filter „alle / nur aus Herstellerrezepturen" + INCI-Flag „Kein Rohstoffbestand" | – | +| _offen_ | AP-21 | INCI: Lieferant **+ URL** je Zeile (Repeater, „+ weiterer Lieferant"), „nur aktive"-Filter, „Alternativer Name", Lagerort (Raum/Regal/Buchstabe) inkl. zentraler Einstellungen | – | +| _offen_ | AP-23 | Rezeptur-**Phasen** auf der Hersteller-Rezeptur (Phase A/B …, je Phase Rohstoffe + Notiz, Drag&Drop, Gesamt 100 %) | – | +| _offen_ | AP-24 | Chargen-Nr.-**Präfix** auf Produktebene + Auto-Generierung (editierbar) in der Produktion | – | +| _offen_ | AP-27 | Wareneingang: gleiche **Charge addieren** (Bestand zusammenführen), Einkaufsvorgang bleibt separat erfasst | – | +| _offen_ | AP-28 | **Neue Produktions-Maske + Produktionsergebnis** (mehrere Outputs, entkoppelt von der Planung; Produktbestand aus Ergebnis, Rohstoffbestand aus Chargen, Phasen-Anzeige, Regal-Spalte) | – | + +**Status Roadmap V5.0 (12.06.2026):** **AP-26, AP-25 und AP-22 sind erledigt** (Details siehe Protokoll oben). Offen aus V5.0: AP-21, AP-20, AP-23, AP-24, AP-27, AP-28. + +**Übernommen aus V4.0 / weiterhin offen:** **AP-13** (Shop-Bestandsabzug bei Versand inkl. Sets, Konzept liegt vor), **AP-14** (Audit-Trail), **AP-15** (Blockbasierte Rechte), **AP-16** (2FA), **AP-17** (WaWi-Einstellungen). + +> **➡️ HIER GEHT ES WEITER: AP-21 (INCI-Erweiterungen).** Das ist das nächste Paket in der Reihenfolge und zugleich die Grundlage für AP-20 (liefert `scopeUsedInActiveRecipes()` + „Kein Rohstoffbestand"-Flag-Formularfeld) und für die neue Produktions-Maske AP-28 (liefert Alternativname + Lagerort/„Regal"-Spalte). Vor dem Start die Lagerort-Modell-Frage final entscheiden (§5 Punkt 1 — Empfehlung: drei getrennte Stammdaten-Tabellen Raum/Regal/Fach). Danach: AP-20 → AP-23 → AP-24 → AP-27+AP-28 gemeinsam. + +--- + +## 1. Verifizierter Ist-Stand (12.06.2026) — relevant für die neue Runde + +| Bereich | Ist im Code | Neue Anforderung verlangt | +|---|---|---| +| **Rohstoffbestand** (`RawMaterialStockController@index`) | Listet **alle aktiven** Rohstoffe (`active=true`), unabhängig von Rezeptur-Zugehörigkeit. Filter: Suche, „nur kritische", Status-Kacheln. | Umschalt-Filter **„alle" / „nur aus Herstellerrezepturen"**; Rohstoffe ohne Bestandsführung (Allergene etc.) per Flag ausblenden. | +| **INCI / `ingredients`** (`app/Models/Ingredient.php`, `IngredientController`, `ingredient/form.blade.php`) | Felder: `name`, `inci`, `effect`, `active`, `pos`, `default_factor`, `min_stock_alert`, `material_quality_id`, `tax_rate_id`, `delivery_time(_days)`. Lieferanten über Pivot `ingredient_supplier` (Spalten `preferred`, `supplier_sku`, **`url`** vorhanden), Form macht aber **nur Multi-Select** der Lieferanten — **keine** Per-Lieferant-URL-Eingabe. **Kein** `alt_name`, **kein** „Kein Rohstoffbestand"-Flag, **kein** Lagerort. | `alt_name` (Alternativer Name), Per-Lieferant-URL-Repeater mit „+", „nur aktive"-Filter, Lagerort (Raum/Regal/Buchstabe), „Kein Rohstoffbestand"-Flag. | +| **Produktbestand** (`ProductStockController@index`, `ProductStockService`) | Sortiert nach `pos`,`name`. Kacheln: nur **warning/critical** klickbar (`all`/`ok` nicht). Filtert `active=true`, `is_set=false`, `main_product_id IS NULL`. **Kein** Sichtbarkeits-Flag, **keine** „Verbrauch/Monat"-Spalte. | Default-Sortierung nach **Dringlichkeit**, **alle** Kacheln klickbar, Produkt-Flag „im Produktbestand anzeigen", Spalte „Verbrauch/Monat" (Ø 6 Mon.). | +| **Rezeptur / `product_ingredients`** | Spalten: `product_id`, `ingredient_id`, `pos`, `gram`, `factor`, `recipe_type` (`product`/`manufacturer`). **Kein** Phasen-Begriff. | Phasen auf der **Hersteller-Rezeptur** (Phase A/B …, je Phase Rohstoffe + Notiz, Gesamt 100 %). | +| **Produkt / `products`** | Hat `out_of_stock_until`, `out_of_stock_indefinite`, `min/critical_product_stock`, Set-Felder, `no_recipe_required`. **Kein** Chargen-Präfix, **kein** Produktbestand-Sichtbarkeits-Flag. | `batch_prefix`, `show_in_product_stock`, Lieferbestand auf Datum umstellen. | +| **Ausschuss** (`StockDisposalController::reasons()`) | Gründe **hartkodiert** im Controller (6 Werte). | Gründe als **konfigurierbare Stammdaten** (Einstellungen). | +| **Wareneingang / `stock_entries`** | `batch_number` (string) vorhanden, **keine** Erkennung/Zusammenführung gleicher Chargen. Restbestand wird ohnehin chargenweise über `production_ingredients` gerechnet. | Beim Erfassen prüfen, ob `batch_number` für denselben Rohstoff schon existiert → Mengen zusammenführen, Einkaufsvorgang trotzdem separat erfassen. | +| **Produktion / `productions`** | Ein `quantity` (Output), `produced_at`, `location_id`, `notes`. `production_ingredients` (`stock_entry_id`, `quantity_used`). **Produktbestand wird heute aus `quantity` gebucht** (`ProductStockService::recordProductionStock`). **Kein** Ergebnis-/Mehrfach-Output-Begriff, **keine** Phasen-Anzeige, **keine** Regal-Spalte. | Neue Maske: Planung (Produkt/Größe/Datum/geplante Stückzahl) nur informativ; **Produktionsergebnis** mit **mehreren** Outputs (Produkt, produzierte Stückzahl, Chargen-Nr.) bucht den Produktbestand; Rohstoffverbrauch chargenbasiert wie bisher; Phasen + Regal-Spalte. | +| **Locations** | `locations`: `name`, `active`. Keine Raum/Regal-Struktur. | Lagerort-Bausteine (Raum/Regal/Buchstabe) zentral pflegbar, am INCI referenziert. | + +--- + +## 2. Interpretation der neuen Anforderungen (Mapping auf APs) + +| # | Anforderung (12.06.) | AP | +|---|---|---| +| Rohstoffbestand 1a/1b | Filter „alle Rohstoffe" / „nur aus Herstellerrezepturen" | **AP-20** | +| Rohstoffbestand (INCI-Flag) | „Kein Rohstoffbestand"-Kontrollkästchen (Allergene etc. ausblenden) | **AP-20** | +| INCI 1 | Lieferant **+ URL** je Zeile, URL leer ⇒ Mailbestellung; „+ weiteren Lieferanten" | **AP-21** | +| INCI 2 | INCI-Liste „nur aktive" (≥ 1 aktives Produkt) | **AP-21** | +| INCI 3 | Feld „Alternativer Name" (z. B. LECITHIN → Phospholipon 80 H) | **AP-21** | +| INCI 4 | Lagerort RAUM \| REGAL-NR. \| BUCHSTABE, zentral unter Einstellungen pflegbar | **AP-21** | +| Produktbestand 1 | Sortierung nach Dringlichkeit (default), Kacheln „Produkte"/„Bestand ok" klickbar | **AP-22** | +| Produktbestand 2 | Produkt-Flag „im Produktbestand anzeigen" | **AP-22** | +| Produktbestand 3 | Spalte „Verbrauch pro Monat" (Ø letzte 6 Monate) | **AP-22** | +| Produktebene 1 | Rezeptur-**Phasen** unter der Hersteller-Rezeptur (Screen 1) | **AP-23** | +| Produktebene 2 | Chargen-Nr.-**Präfix** (Kategorie-Kürzel + Produkt-Nr. + Produktionsdatum) | **AP-24** | +| Lieferbestand | **Datum** statt Tage, Produkt bleibt kaufbar, Resttage für VP zählen herunter | **AP-25** | +| Ausschuss | Gründe unter Einstellungen selbst anlegen | **AP-26** | +| Wareneingang | Gleiche Charge addieren, Einkaufsvorgang trotzdem erfassen | **AP-27** | +| Produktion | Neue Maske + **Produktionsergebnis** (mehrere Outputs), Phasen, Regal (Screen 2) | **AP-28** | + +--- + +## 3. Priorisierte Roadmap V5.0 + +> **Leitidee der Reihenfolge:** Erst die kleinen, isolierten Ergänzungen an bestehenden Seiten (schnell sichtbarer Nutzen, geringes Risiko), dann die Datenmodell-Grundlagen (Phasen, Chargen-Präfix), zuletzt die große Produktions-Maske, die auf Phasen + Präfix + Lagerort aufsetzt. + +| Reihenfolge | AP | Titel | Abhängigkeit | Aufwand | +|---|---|---|---|---| +| 1 | ✅ AP-26 | Ausschuss-Gründe konfigurierbar — **erledigt 12.06.2026** | – | 0,5–1 Tag | +| 2 | ✅ AP-25 | Lieferbestand: Datum statt Tage (Revision AP-03) — **erledigt 12.06.2026** | – | 0,5–1 Tag | +| 3 | ✅ AP-22 | Produktbestand-Erweiterungen (Sortierung, Kacheln, Flag, Verbrauch/Monat) — **erledigt 12.06.2026** | – | 1,5–2,5 Tage | +| 4 | ➡️ AP-21 | **NÄCHSTER SCHRITT:** INCI-Erweiterungen (Lieferant+URL, nur aktive, Alternativname, Lagerort + Einstellungen) | AP-05-Muster (Stammdaten) | 2,5–4 Tage | +| 5 | AP-20 | Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag | AP-21 (Flag/Lagerort am INCI) | 1–2 Tage | +| 6 | AP-23 | Rezeptur-Phasen (Hersteller-Rezeptur) | – | 3–5 Tage | +| 7 | AP-24 | Chargen-Nr.-Präfix (Produktebene) | – | 1 Tag | +| 8 | AP-28 | Neue Produktions-Maske + Produktionsergebnis | AP-21 (Lagerort), AP-23 (Phasen), AP-24 (Präfix) | 5–8 Tage | + +> **Querschnitt:** Alle neuen/angepassten Seiten verwenden das Design-System aus AP-19 (`resources/views/admin/inventory/partials/wawi-ui.blade.php`) und die Datumsfeld-Konvention (`datepicker-base`, `dd.mm.yyyy`). Jede DB-Änderung wird per Migration nachgezogen, jedes AP mit `vendor/bin/pint --dirty` und Pest-Tests abgeschlossen. + +--- + +## 4. Arbeitspakete im Detail + +### AP-26 — Ausschuss-Gründe konfigurierbar +**Anforderung:** „Ich möchte die Gründe für den Ausschluss unter Einstellungen selber anlegen können." + +**Ist-Stand:** Gründe sind in `StockDisposalController::reasons()` (≈ Z. 114–124) hartkodiert. + +**DB** +- Neue Tabelle `disposal_reasons`: `label` VARCHAR, `active` bool, `pos` int. Seeder mit den bisherigen 6 Werten (idempotent per `firstOrCreate`), damit Bestandsdaten unverändert bleiben. + +**Code** +- Model `DisposalReason` (`scopeActive`, casts), Factory, `InventoryStammdatenSeeder`-Erweiterung. +- CRUD `DisposalReasonController` (create/store/edit/update/destroy → Redirect `general`) + `Store/UpdateDisposalReasonRequest` — analog zu `TaxRateController`/`DeliveryTimeController`. +- Dritte Karte „Ausschuss-Gründe" auf der Seite **Einstellungen → Allgemein** (`general/index.blade.php`) + `GeneralSettingController` um `disposalReasons` erweitern. +- `StockDisposalController`: `reasons()` liest jetzt aktive `disposal_reasons` (Reihenfolge `pos`); `create`/Formular nutzt sie. `StoreStockDisposalRequest` validiert weiterhin nur „nicht leer" (kein harter `in:`-Zwang, da frei pflegbar). +- Routen Resource `disposal-reasons` (ohne index/show) in der `superadmin`-Gruppe; Sidenav unverändert (liegt unter „Allgemein"). + +**Akzeptanz:** SuperAdmin legt/ändert/deaktiviert Ausschuss-Gründe; das Ausschuss-Formular zeigt nur aktive Gründe in gepflegter Reihenfolge; bestehende Disposals behalten ihren (als String gespeicherten) Grund. + +**Tests:** `DisposalReasonSettingsTest` (Render, CRUD, Validierung, `active`-Scope, Zugriffsschutz) + Regression `StockDisposalTest`. + +> **Status:** Erledigt (12.06.2026). Umgesetzt wie geplant — Tabelle `disposal_reasons`, CRUD analog TaxRate/DeliveryTime, dritte Karte unter Einstellungen → Allgemein, `StockDisposalController::reasons()` liest aktive DB-Gründe. Details siehe Umsetzungsprotokoll (§0a). Tests: `tests/Feature/DisposalReasonSettingsTest.php` (8 grün). + +--- + +### AP-25 — Lieferbestand: Datum statt Tage (Revision AP-03) +**Anforderung:** „Bei ‚erst in 12 Tagen wieder lieferbar' machen wir das Produkt doch wieder kaufbar — der VP entscheidet selbst. Ich präferiere, dass ich auf Produktebene ein **Datum** einstelle und für den VP dann die **Anzahl der Tage** erscheint, die sich täglich aktualisiert." + +**Ist-Stand:** AP-03 hat `out_of_stock_until` (DATE) + `out_of_stock_indefinite` (bool). Eingabe erfolgt heute über ein **Tagefeld** (`now()->addDays($tage)`); Kauf bleibt im Shop bereits möglich. **Interne Bestellliste** (`OrderController::datatable` + `admin/modal/show_product`) ersetzt die Mengen-Buttons aktuell durch einen Hinweis → laut neuer Anforderung soll **kaufbar bleiben**. + +**Kern der Änderung:** Eingabe-Logik von „Tage" auf **„Datum"** umstellen (Datenmodell `out_of_stock_until` bleibt — passt bereits), Resttage werden für den VP weiterhin tagesgenau berechnet und gezählt. Kauf **überall** möglich (auch interne Bestellliste). + +**Code** +- **Produktformular** (Card „Verfügbarkeit", `resources/views/admin/product/…` + JS `toggleOutOfStock` in `edit.blade.php`): Tagefeld → **Datepicker** „Wieder lieferbar ab" (`datepicker-base`, `dd.mm.yyyy`), vorbefüllt aus `out_of_stock_until->format('d.m.Y')`. Checkbox „unbestimmt" bleibt. +- **`ProductRepository::update()`**: statt `addDays($tage)` jetzt `Carbon::parse($datum)` aus dem Datepicker; „unbestimmt" hat weiter Vorrang (Datum=null), Deaktivierung leert beides. Helfer `outOfStockRemainingDays()`/`outOfStockNotice()` bleiben unverändert (zählen schon tagesgenau herunter). +- **Interne Bestellliste kaufbar machen:** in `OrderController::datatable` (Produkt-Spalte) und `admin/modal/show_product` die Mengen-Buttons **nicht mehr** unterdrücken; stattdessen Hinweis „In ca. X Tag(en) wieder da!" **zusätzlich** anzeigen (Kauf bleibt). Shop zeigt den Hinweis bereits zusätzlich. + +**Akzeptanz:** Auf Produktebene wird ein Datum gesetzt; VP sieht im Shop und in der internen Bestellliste „In ca. X Tagen wieder da!", die Tage zählen täglich herunter, das Produkt bleibt überall kaufbar; nach Ablauf verschwindet der Hinweis automatisch. + +**Tests:** `ProductOutOfStockTest` anpassen (Datum→Resttage statt Tage→Datum; kaufbar in interner Liste; Vergangenheit gilt nicht; „unbestimmt"-Vorrang). Hinweise-Doku (AP-18) aktualisieren (Kauf-Sperre weiterhin nur optionale Zukunftsoption). + +> **Status:** Erledigt (12.06.2026). Datepicker „Wieder lieferbar ab" statt Tagefeld; `ProductRepository` parst `d.m.Y` (ungültig ⇒ null); interne Bestellliste zeigt den Hinweis jetzt **zusätzlich** zu den Mengen-Buttons (Kauf überall möglich). Hinweise-Doku aktualisiert. Details siehe Umsetzungsprotokoll (§0a). Tests: `tests/Feature/ProductOutOfStockTest.php` (8 grün). + +--- + +### AP-22 — Produktbestand-Erweiterungen +**Anforderungen:** +1. Sortierung nach **Dringlichkeit** (Klick auf „Status" sortiert dringlichste oben; darf default so sein). Kacheln „Produkte" und „Bestand ok" sollen ebenfalls **klickbar** sein. +2. Produkt-Flag: „im Produktbestand anzeigen ja/nein" (z. B. Abrechnung Druckkosten / Logo-Etiketten gehören da nicht rein). +3. Spalte **„Verbrauch pro Monat"** (Ø der letzten 6 Monate). + +**Ist-Stand:** Sortierung `pos`,`name`; nur warning/critical-Kacheln klickbar; Filter `active`, `is_set=false`, `main_product_id IS NULL`; keine Verbrauchsspalte; kein Sichtbarkeits-Flag. + +**DB (`products`)** +- `show_in_product_stock` bool default 1 (Migration). Bestehende Produkte bleiben sichtbar; gezielt abwählbar. + +**Code** +- **Flag:** `Product` fillable + cast; Checkbox „Im Produktbestand anzeigen" in der Warenwirtschaft-Card des Produktformulars; `ProductRepository::update()` normalisiert (`isset ? 1 : 0`). `ProductStockController@index` filtert zusätzlich `where('show_in_product_stock', true)`. Auch `criticalProductCount()` (`ProductStockService`) respektiert das Flag. +- **Dringlichkeits-Sortierung:** `ProductStockService::productStatus()` liefert bereits `critical`/`warning`/`ok`. Default-Reihenfolge serverseitig nach Status-Rang (critical → warning → ok), innerhalb gleich nach Reichweite/Name; DataTables-`order` auf die Status-Spalte (mit `data-order`-Rang) setzen, sodass ein Klick auf „Status" die dringlichsten oben hält. +- **Kacheln klickbar:** in `product-stock/index.blade.php` allen vier `wawi-stat`-Kacheln (`all`/`ok`/`warning`/`critical`) `is-clickable` + `data-filter` geben; JS-Filter (analog Rohstoffbestand) auf alle vier Werte erweitern. „Produkte" (=all) hebt Filter auf, „Bestand ok" filtert auf `ok`. +- **Verbrauch/Monat:** neue Methode `ProductStockService::monthlyConsumptionByProduct()` = Ø der `out`-Bewegungen mit `source IN ('sale','production_out',…)` bzw. der relevanten Abgänge der **letzten 6 Kalendermonate** (Summe der Abgänge / 6). Spalte „Verbrauch/Monat" in der Übersicht (rechtsbündig, Einheit Stück). + > **Klärung (vermerkt, nicht blockierend):** „Verbrauch" = abgehende Bewegungen. Solange AP-13 (Verkaufsabzug) nicht live ist, basiert der Wert auf manuellen `out`-Bewegungen; mit AP-13 fließen Verkäufe automatisch ein. Default-Fenster 6 Monate. + +**Akzeptanz:** Übersicht ist standardmäßig nach Dringlichkeit sortiert; alle vier Kacheln filtern; ausgeblendete Produkte (Flag aus) erscheinen nicht und zählen nicht in den Kritisch-Badge; „Verbrauch/Monat" zeigt den 6-Monats-Durchschnitt. + +**Tests:** `ProductStockTest` erweitern (Flag blendet aus + aus Kritisch-Zähler, Verbrauch/Monat-Mittelung über 6 Monate, Status-Sortierrang, Render der vier Kachel-Filter). + +> **Status:** Erledigt (12.06.2026). Abweichung zur Planung: Sortier-Toggle nicht über DataTables-`order`, sondern leichtgewichtig per eigenem JS (`data-rank` an der Zeile, Klick auf den „Status"-Kopf sortiert um) — die Seite nutzt kein DataTables. „Verbrauch/Monat" als **rollierendes** 6-Monats-Fenster umgesetzt (statt Kalendermonate; Klärungspunkt §5.3 bleibt zur Bestätigung offen), Produktions-Gegenbuchungen (`source=production`) ausgenommen. Details siehe Umsetzungsprotokoll (§0a). Tests: `ProductStockTest` (13 grün). + +--- + +### AP-21 — INCI-Erweiterungen (Lieferant+URL, nur aktive, Alternativname, Lagerort) +**Anforderungen (INCI-Ebene 1–4):** +1. Pro INCI mehrere **Lieferanten mit eigener URL** (URL frei ⇒ Bestellung per Mail), „+ weiteren Lieferanten anlegen". +2. INCI-Liste „**nur aktive**" filtern (aktiv = in mind. einer Hersteller-Rezeptur eines **aktiven** Produkts). +3. Feld **„Alternativer Name"** (Handelsname; findbar für Mitarbeiter). +4. Feld **Lagerort** im Schema RAUM \| REGAL-NR. \| BUCHSTABE; die einzelnen Bausteine zentral unter **Einstellungen** pflegbar. + +**Ist-Stand:** `ingredient_supplier.url` existiert im Schema, wird im Formular aber nicht ausgespielt (nur Multi-Select). Kein `alt_name`, kein Lagerort, keine „nur aktive"-Liste. + +**DB** +- `ingredients.alt_name` VARCHAR nullable (Migration). +- Lagerort am INCI: `ingredients.location_room_id`, `location_shelf_id`, `location_slot_id` (nullable FKs) **oder** kompakt drei Strings — **Entscheidung: drei FK auf neue Stammdaten** (Punkt 4 verlangt zentrale Pflege). +- Zentrale Stammdaten unter Einstellungen — drei kleine Tabellen `storage_rooms` (Raum), `storage_shelves` (Regal-Nr.), `storage_slots` (Buchstabe), je `label`, `active`, `pos`. (Alternativ generische `storage_segments` mit `type`-Enum `room|shelf|slot`; bei der Umsetzung die einfachere von beiden wählen — Default: **drei Tabellen** für klare Dropdowns.) +- **Lieferanten-URL:** Pivot `ingredient_supplier.url` ist vorhanden → **keine** Migration nötig; nur das Formular muss die Spalte bespielen. + +**Code** +- `Ingredient`: fillable `alt_name` + Lagerort-FKs; Relationen `room()/shelf()/slot()`; Helper `storageLabel()` = `"Raum 1 | Regal 3 | O"` (für die Anzeige in Rohstoffbestand-Detail **und** in der Produktions-Maske, Spalte „Regal"). +- **Lieferanten-Repeater** in `ingredient/form.blade.php`: pro Zeile `Lieferant`-Select + `URL`-Textfeld (`type="text"`, max 2048) + Entfernen; „+ weiterer Lieferant"-Button (JS klont eine Zeile). Speichern via `suppliers()->sync([$id => ['url' => …, 'preferred' => …]])` (Pivot-Daten). `StoreIngredientRequest` validiert paarweise (`suppliers.*.id` exists, `suppliers.*.url` nullable string max 2048). **Ableitung Bestellweg:** je INCI-Lieferant gilt `url` leer ⇒ **Mailbestellung**, sonst Shop-Link — diese Ableitung ersetzt im Rohstoffbestand-Detail (AP-10) den bisherigen `supplier.order_method`-Fallback (INCI-spezifisch sticht). +- **„Alternativer Name":** Eingabefeld neben „Name"/„INCI"; Anzeige als eigene Spalte in der INCI-Verwaltungsliste und in der Produktions-INCI-Liste (Screen 2 zeigt Spalte „Alternativer Name"). +- **Lagerort:** drei abhängige Dropdowns (Raum/Regal/Buchstabe) aus den Stammdaten; Anzeige als „Regal"-Spalte (Screen 2) und im Rohstoffbestand-Detail. +- **Einstellungen → Allgemein:** je eine Karte „Räume", „Regale", „Fächer/Buchstaben" mit CRUD (Muster AP-05); `GeneralSettingController` um die drei Listen erweitern; Routen-Resources (`superadmin`). +- **„nur aktive"-Filter der INCI-Liste:** Scope `Ingredient::scopeUsedInActiveRecipes()` = `whereHas('products', fn($q) => $q->where('recipe_type','manufacturer')->whereHas('product', active))` (Pivot-Pfad über `product_ingredients` mit `recipe_type='manufacturer'` zu aktiven Produkten). In der INCI-Übersicht (`IngredientController@index` bzw. die Verwaltungsliste) eine Checkbox „nur aktive" (server- oder DataTables-clientseitig). **Achtung:** „aktiv" hier ≙ in Herstellerrezeptur eines aktiven Produkts — **nicht** das Feld `ingredients.active`. Begriffe in der UI sauber trennen („nur aktive (in Rezeptur)"). + +**Akzeptanz:** Pro INCI können mehrere Lieferanten mit je eigener URL gepflegt werden (leer = Mail); INCI hat einen Alternativnamen und einen zentral gepflegten Lagerort; die INCI-Liste lässt sich auf „nur in aktiven Herstellerrezepturen verwendete" filtern; Lagerort und Alternativname erscinen in Rohstoffbestand-Detail und Produktions-Maske. + +**Tests:** `IngredientOrderFieldsTest` erweitern (Per-Lieferant-URL speichern/sync, `alt_name`, Lagerort-FKs); neuer `IngredientActiveRecipeFilterTest` (Scope liefert nur INCI aus aktiven Herstellerrezepturen); `StorageSettingsTest` (CRUD Räume/Regale/Fächer, Zugriffsschutz). + +--- + +### AP-20 — Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag +**Anforderungen (Rohstoffbestand 1):** +- Filter (a) **alle** Rohstoffe (auch nicht in aktiven Produkten — z. B. für Neuentwicklung eingekauft) und (b) **nur Rohstoffe aus Herstellerrezepturen**. +- INCI-Kontrollkästchen „**Kein Rohstoffbestand**" — Rohstoffe/Einträge (Allergene etc.), die gar nicht im Rohstoffbestand auftauchen sollen. + +**Ist-Stand:** `RawMaterialStockController@index` listet **alle aktiven** Rohstoffe ohne Rezeptur-Bezug; kein Ausschluss-Flag. + +**DB (`ingredients`)** +- `exclude_from_stock` bool default 0 (Migration). (Name bewusst „Kein Rohstoffbestand".) + +**Code** +- `Ingredient`: fillable + cast `exclude_from_stock`; Checkbox „Kein Rohstoffbestand (z. B. Allergen-Angabe, nicht bevorratet)" im INCI-Formular. +- `RawMaterialStockController@index`: + - grundsätzlich `where('exclude_from_stock', false)`. + - Umschalt-Filter **Modus** (`?scope=all|recipe`, Default `all`): bei `recipe` zusätzlich `scopeUsedInActiveRecipes()` (aus AP-21) anwenden. UI: zwei Pills/Tabs „Alle" / „Nur aus Herstellerrezepturen" im `wawi-toolbar`. + - `criticalIngredientCount()` (für den Badge) respektiert ebenfalls `exclude_from_stock`. +- Optional: gleiche Ausschlusslogik in der Ausschuss-/Disposal-Auswahl, damit Allergen-INCIs dort nicht auftauchen (nice-to-have, nicht zwingend). + +**Akzeptanz:** Rohstoffbestand lässt sich zwischen „alle" und „nur aus Herstellerrezepturen" umschalten; mit „Kein Rohstoffbestand" markierte INCI erscheinen nie im Rohstoffbestand (auch nicht im Kritisch-Badge). + +**Tests:** `RawMaterialStockTest` erweitern (Modus „recipe" zeigt nur Rezeptur-INCI; `exclude_from_stock` blendet aus + aus Kritisch-Zähler; Default „alle" zeigt auch nicht-verwendete). + +**Abhängigkeit:** AP-21 (liefert `scopeUsedInActiveRecipes()` + INCI-Formularfeld). + +--- + +### AP-23 — Rezeptur-Phasen (Hersteller-Rezeptur) +**Anforderung (Produktebene 1):** „Auf Produktebene müssen unter der Hersteller-Rezeptur noch die Phasen angelegt werden können." (Screen `2026-12-06-Rezeptur-phase.jpeg`.) + +**Screen-Analyse:** Mehrere Phasen (Phase A, Phase B …), je Phase eine Liste aus Rohstoff-Auswahl + Anteil %, ein **Notiz-Textfeld pro Phase**, „+" zum Hinzufügen einer Rohstoffzeile, Drag-Handle (≡) zum Sortieren, „**+ Neue Phase**". **Gesamt 100,000 %** über alle Phasen. + +**Ist-Stand:** `product_ingredients` hat `recipe_type` (`product`/`manufacturer`), `gram`, `factor`, `pos` — **keinen** Phasen-Begriff. + +**DB** +- Phasen als eigene Tabelle `recipe_phases`: `product_id` FK, `recipe_type` (vorerst nur `manufacturer`, Feld für spätere Ausweitung), `name` VARCHAR (z. B. „Phase A"), `note` TEXT nullable, `pos` int. Unique (`product_id`,`recipe_type`,`name`). +- `product_ingredients.recipe_phase_id` nullable FK (nur Hersteller-Zeilen tragen eine Phase; Produkt-Rezeptur bleibt phasenlos). Bestehende Hersteller-Zeilen ⇒ `recipe_phase_id = NULL` (= „ohne Phase"), Migration bricht nichts. + +**Code** +- Models `RecipePhase` (+ `Product::recipePhases()`, `phaseIngredients()`); `ProductIngredient` um `recipe_phase_id` + `phase()`-Relation. +- **Produktformular** (Card „Hersteller-Rezeptur", `resources/views/admin/product/…`): Umbau analog Screen 1 — Phasen-Blöcke (jede mit Rohstoffzeilen, Anteil %, Notizfeld), Drag&Drop (Rohstoffzeilen innerhalb der Phase + Phasen untereinander), „+ Rohstoff" je Phase, „+ Neue Phase". **Gesamtsumme 100 % über alle Phasen** (vorhandene 100%-Validierung auf die Summe **aller** Phasen umstellen). +- **Repository:** Speichern persistiert Phasen (`recipe_phases`) und ordnet Hersteller-Rezepturzeilen ihrer Phase zu (`recipe_phase_id`); `Product::copy()` kopiert Phasen + Zuordnung mit. +- **Rückwärtskompatibilität:** Produkte ohne Phasen funktionieren weiter (eine implizite „Phase ohne Namen"). Die Produkt-Rezeptur (`recipe_type='product'`) bleibt unverändert ohne Phasen. +- **Konsumenten:** `ProductionService::requiredGramsByIngredient()`/`buildRecipePayload()` rechnen weiterhin über alle Hersteller-Zeilen (Summe ist phasenunabhängig) — Phasen sind primär Darstellung/Reihenfolge; die Produktions-Maske (AP-28) gruppiert die Anzeige nach Phase. + +**Akzeptanz:** Unter der Hersteller-Rezeptur lassen sich beliebig viele Phasen mit Rohstoffen, Anteilen und einer Phasen-Notiz anlegen, sortieren und löschen; die Gesamtsumme über alle Phasen muss 100 % ergeben; Kopieren übernimmt die Phasenstruktur. + +**Tests:** `RecipePhaseTest` (Phasen + Zeilen speichern/sortieren, 100%-Summe über Phasen, Hersteller-Zeile trägt `recipe_phase_id`, Kopie inkl. Phasen, Produktion rechnet unverändert über alle Zeilen). Regression `ProductionManufacturerRecipeTest`. + +--- + +### AP-24 — Chargen-Nr.-Präfix (Produktebene) +**Anforderung (Produktebene 2):** Präfix aus Kategorie-Kürzel (TP – Tattoopflege) + Produkt-Nr. (1) + Produktionsdatum → Beispiel `TP1150626`. Das Präfix „TP1" wird auf Produktebene angelegt; in der Produktions-Maske erzeugt sich die Chargen-Nr. von selbst im Textfeld, bleibt aber **editierbar** (alte Etiketten). + +**Ist-Stand:** Kein `batch_prefix` am Produkt; Produktion hat keine Chargen-Nr.-Logik. Screen 2 zeigt im „Produktionsergebnis" das Feld „Chargen-Nr." vorbefüllt mit `KP1100626`. + +**DB (`products`)** +- `batch_prefix` VARCHAR nullable (z. B. „TP1"). + +**Code** +- `Product`: fillable `batch_prefix`; Eingabefeld „Chargen-Nr.-Präfix" in der Warenwirtschaft-Card des Produktformulars (Hilfetext: „Kürzel + Produkt-Nr., z. B. TP1 → Charge TP1150626"). +- Helper `Product::suggestedBatchNumber(?Carbon $date)` = `batch_prefix . $date->format('dmy')` (Beispiel `TP1` + `150626`). Wird in AP-28 (Produktionsergebnis) als Vorbelegung des editierbaren Chargen-Nr.-Felds genutzt. + > **Format-Klärung (vermerkt):** Beispiel „TP1150626" = `TP1` + `150626` (Produktionsdatum `ddmmyy`, hier 15.06.26). Bestätigung Datumsformat (`ddmmyy`) beim Kunden, falls abweichend (z. B. `dmmyy`). + +**Akzeptanz:** Pro Produkt ist ein Chargen-Präfix hinterlegbar; in der Produktion wird die Chargen-Nr. daraus + Produktionsdatum vorgeschlagen und bleibt frei editierbar. + +**Tests:** `BatchPrefixTest` (Präfix speichern; `suggestedBatchNumber()` setzt Präfix + Datum korrekt zusammen). + +--- + +### AP-28 — Neue Produktions-Maske + Produktionsergebnis +**Anforderung (Produktion):** Neue Maske (Screen `2026-12-06-neue-produktion.jpeg`). Phasen fließen ein. **Wichtig:** Der **Produktbestand** errechnet sich **nicht** aus der oben gewählten Auswahl Produkt + Stückzahl (nur Planung). Ganz unten („Produktionsergebnis") trägt man ein, was real entstanden ist — **mehrere Outputs** möglich (Beispiel: Plan 50×50 ml → Ergebnis 40×50 ml + 50×5 ml). Der **Rohstoffbestand** berechnet sich wie gehabt aus den Werten hinter jeder Charge. + +**Screen-Analyse (Aufbau der Maske):** +1. **Kopf/Planung:** Produkt, Größe, Produktionsdatum, **Planung Stückzahl** (rein informativ). +2. **INCI-Liste (Hersteller-Rezeptur):** Tabelle mit Name, **Alternativer Name** (AP-21), Qualität, Anteil %, Gramm, **Regal** (Lagerort, AP-21). Summenzeile 100 % / Gesamt-Gramm. +3. **Verpackung (Vorschau):** Artikel + Stück gesamt (aus BOM). +4. **Notizen** + „Speichern"/„Drucken". +5. **Phasen-Erfassung (Phase A/B …, AP-23):** je Rohstoffzeile Soll-Gramm + Status-Ampel (grün/orange) + **Charge wählen** + Ist-Gramm-Eingabe; je Phase ein Notiz-Textfeld. Gesamt-Gramm. **→ treibt den Rohstoffbestand** (chargenbasiert, wie bisher `production_ingredients`). +6. **Produktionsergebnis:** Zeilen mit Produkt, **Produzierte Stückzahl**, **Chargen-Nr.** (vorbefüllt aus AP-24, editierbar), „+" für weitere Ergebniszeile. **→ treibt den Produktbestand.** + +**Ist-Stand:** `productions.quantity` ist der einzige Output und bucht heute den Produktbestand (`ProductStockService::recordProductionStock`). Keine Mehrfach-Outputs, keine Phasen-Anzeige, keine Regal-Spalte, kein Chargen-Nr.-Feld. + +**DB** +- Neue Tabelle `production_outputs`: `production_id` FK cascade, `product_id` FK, `quantity` UINT, `batch_number` VARCHAR nullable, `pos` int. (Ein Production-Lauf ⇒ n Ergebniszeilen.) +- `productions.quantity` bleibt als **geplante** Stückzahl erhalten (umdeuten zu „Planung"; Spaltenname unverändert, um Bestandsdaten/Tests stabil zu halten) — optional Kommentar/Accessor `planned_quantity`. +- **Migration der Buchungslogik (wichtig):** Produktbestand wird künftig aus `production_outputs` gebucht, **nicht** mehr aus `productions.quantity`. + +**Code** +- Model `ProductionOutput` (+ `Production::outputs()`). +- **`ProductStockService::recordProductionStock(Production)` umstellen:** idempotenter Differenz-Abgleich jetzt **pro Output-Produkt** (Summe der `production_outputs.quantity` je `product_id`), append-only — statt der bisherigen Einzel-`quantity`. Beim Bearbeiten einer Produktion wird die Differenz je betroffenem Produkt nachgebucht (vorhandenes idempotentes Muster beibehalten). +- **`ProductionService::store()/updateProduction()`:** nimmt zusätzlich `outputs[]` (Produkt + Stückzahl + Chargen-Nr.) entgegen, persistiert sie, ruft danach `recordProductionStock`. Rohstoffverbrauch (`production_ingredients`) bleibt **unverändert** chargenbasiert. `StoreProductionRequest`: `outputs` Pflicht (≥ 1 Zeile), je Zeile `product_id` exists + `quantity` ≥ 1; `batch_number` nullable string. +- **Phasen-Anzeige:** Hersteller-Rezeptur nach Phase gruppieren (`RecipePhase`, AP-23); Soll-Gramm/Ampel/Charge wie heute, nur nach Phase gegliedert + Phasen-Notiz anzeigen. +- **Views:** `productions/_form_fields.blade.php` + `_scripts.blade.php` (von create/edit/copy genutzt) erweitern: + - INCI-Tabelle um Spalten „Alternativer Name" + „Regal" (`Ingredient::storageLabel()`). + - Phasen-Gruppierung der Charge-Erfassung. + - **Produktionsergebnis-Block** unten: Repeater (Produkt-Select default = geplantes Produkt, Stückzahl, Chargen-Nr. vorbefüllt aus `Product::suggestedBatchNumber(produced_at)`, editierbar; „+" für weitere Zeile). Standard: eine Ergebniszeile mit geplantem Produkt + geplanter Stückzahl als Vorschlag. +- **Klarer Hinweis-Text** in der Maske: „Planung oben ist unverbindlich. Der Produktbestand entsteht aus dem Produktionsergebnis unten." (Anforderung explizit.) +- **Produktentwicklung** (Platzhalter aus AP-09) bleibt unberührt. + +**Akzeptanz:** +- Die geplante Stückzahl oben bucht **nichts**; nur das Produktionsergebnis unten bucht den Produktbestand (auch mehrere Output-Produkte, z. B. 40×50 ml + 50×5 ml). +- Rohstoffbestand wird weiterhin aus den je Charge eingetragenen Ist-Mengen reduziert. +- Chargen-Nr. je Ergebniszeile ist vorbefüllt (Präfix + Datum) und editierbar. +- INCI-Liste zeigt Alternativname + Regal; die Charge-Erfassung ist nach Phasen gegliedert; Bearbeiten einer Produktion korrigiert den Produktbestand sauber per Differenz. + +**Tests:** `ProductionOutputTest` (mehrere Outputs buchen Produktbestand je Produkt; Plan-Stückzahl bucht nicht; Bearbeiten korrigiert per Differenz; Rohstoffverbrauch unverändert chargenbasiert; Chargen-Nr.-Vorbelegung; Validierung ≥ 1 Output). Regression `ProductStockTest` + `ProductionManufacturerRecipeTest` (Produktionsbuchung jetzt über Outputs). + +> **Migrationshinweis Bestandsdaten:** Für bereits erfasste Produktionen ohne `production_outputs` einmalig je Produktion eine Output-Zeile (`product_id`=Produktionsprodukt, `quantity`=`productions.quantity`) backfillen, damit der gebuchte Produktbestand konsistent bleibt. Backfill als Teil der Migration (idempotent). + +--- + +## 5. Offene Klärungspunkte (nicht blockierend, parallel mit Kunde abstimmen) + +1. **Lagerort-Modell (AP-21):** drei getrennte Stammdaten (Raum/Regal/Fach) vs. eine generische Tabelle. Empfehlung: drei Tabellen (klare Dropdowns). → bei Umsetzungsstart final entscheiden. +2. **Chargen-Nr.-Datumsformat (AP-24):** Beispiel „TP1150626" als `TP1`+`ddmmyy` interpretiert (15.06.26). Bestätigen. +3. **„Verbrauch/Monat" (AP-22):** Definition „abgehende Bewegungen, Ø 6 Monate". Vor AP-13 nur manuelle/Produktions-Abgänge; mit AP-13 fließen Verkäufe ein. Bestätigen, ob 6 **Kalendermonate** gewünscht. +4. **Produktionsergebnis-Produkte (AP-28):** Dürfen Output-Produkte **andere** Produkte als das geplante sein (z. B. anderes Gebinde 5 ml als eigenes Produkt)? Screen + Beispiel legen **ja** nahe (40×50 ml + 50×5 ml ⇒ zwei Produkte/Varianten). → bestätigen; ggf. Varianten als eigene Produkte/`main_product_id` führen. +5. **Wareneingang-Charge (AP-27):** „addiert sich dazu" — Bestand zusammenführen, aber **jeder** Einkaufsvorgang bleibt als eigener `stock_entry` erhalten (nur Anzeige/Restbestand summiert je `batch_number`). Bestätigt durch die Formulierung „den Einkaufsvorgang erfassen wir trotzdem" → so umgesetzt. + +--- + +## 6. AP-27 — Wareneingang: gleiche Charge zusammenführen +**Anforderung:** Gleiche Charge (z. B. Sonnenblumenöl `XY-123`) kommt erneut → soll sich beim weiteren Einkauf **dazuaddieren**. In der Maske erst prüfen, ob die Charge schon im Haus ist. Die Charge wird **nicht** einfach erweitert — der **Einkaufsvorgang** wird trotzdem (separat) erfasst. + +**Ist-Stand:** `stock_entries.batch_number` vorhanden; keine Erkennung. Restbestand wird ohnehin pro `batch_number`/`stock_entry` über `production_ingredients` gerechnet. + +**Interpretation:** Jeder Einkauf bleibt ein **eigener** `stock_entry` (Audit/Preis/Datum je Vorgang). „Dazuaddieren" = **Restbestand und Anzeige werden je `batch_number` (+ Rohstoff) summiert**. In der Erfassungsmaske ein **Hinweis/Autofill**, wenn die Charge für denselben Rohstoff bereits existiert (MHD/Lagerort vorbelegen, Bestätigung „zu bestehender Charge hinzufügen"). + +**Code** +- `StockEntryController` (`create`/`store`): beim Eingeben von `batch_number` + `ingredient_id` per kleinem JSON-Endpoint prüfen, ob eine Charge existiert; UI-Hinweis „Charge bereits im Haus — Menge wird dieser Charge zugerechnet" + Vorbelegung `best_before`/`location_id` aus dem bestehenden Eintrag (überschreibbar). +- **`InventoryService`:** Restbestand-/Detailansicht (AP-10) so anpassen, dass Chargen **je `batch_number`** über mehrere `stock_entries` summiert dargestellt werden (Eingang = Summe `received_quantity` aller Einträge dieser Charge; Verbrauch wie gehabt). Falls `remainingByLocationForIngredient()`/Charge-Liste bereits pro `stock_entry` gruppiert: auf Gruppierung nach `batch_number` umstellen. +- **Produktion (Charge-Auswahl):** Charge-Dropdown nach `batch_number` gruppieren (nicht je Einzel-`stock_entry`), Restbestand = Summe der Charge; Verbrauch bucht FEFO auf die Einträge der Charge. + +**Akzeptanz:** Erfasst man eine bereits vorhandene Charge erneut, weist die Maske darauf hin und führt die Mengen in Bestand/Anzeige zusammen; jeder Einkauf bleibt als eigener Vorgang (Preis/Datum) erhalten; Produktion sieht die Charge mit summiertem Restbestand. + +**Tests:** `StockEntryChargeMergeTest` (zwei Einkäufe gleicher Charge ⇒ summierter Restbestand, zwei separate `stock_entry`-Zeilen; Existenz-Check-Endpoint; Produktion sieht Charge einmal mit Summen-Rest). Regression `RawMaterialStockTest`, `StockEntryPriceTest`. + +> **Einordnung:** AP-27 ist in der Roadmap (Abschnitt 3) bewusst **nicht** terminiert vorgereiht, da es Anzeige-Logik des Rohstoffbestands (AP-10) und der Produktion (AP-28) berührt. Empfehlung: **zusammen mit AP-28** umsetzen (gemeinsame Chargen-Gruppierung), spätestens davor. In der Reihenfolge daher zwischen AP-24 und AP-28 einzuplanen. + +--- + +## 7. Empfohlene Sofort-Reihenfolge (nächste Schritte) + +✅ **Erledigt (12.06.2026):** AP-26 (Ausschuss-Gründe konfigurierbar), AP-25 (Lieferbestand: Datum statt Tage, überall kaufbar), AP-22 (Produktbestand: Dringlichkeits-Sortierung / 4 klickbare Kacheln / Sichtbarkeits-Flag / Verbrauch-Monat). Hinweise-Doku (AP-18) jeweils fortgeschrieben. + +**➡️ Hier geht es weiter:** +1. **AP-21** (INCI: Lieferant+URL-Repeater, „nur aktive", Alternativname, Lagerort + Einstellungen) — Grundlage für AP-20 und für die Produktions-Maske. **Vorab final entscheiden:** Lagerort-Modell (§5 Punkt 1, Empfehlung: drei getrennte Stammdaten-Tabellen Raum/Regal/Fach). +2. **AP-20** (Rohstoffbestand-Filter + „Kein Rohstoffbestand"-Flag) — direkt nach AP-21, nutzt dessen Scope + Formularfeld. +3. **AP-23** (Rezeptur-Phasen) → **AP-24** (Chargen-Präfix; Datumsformat `ddmmyy` bestätigen, §5 Punkt 2) → **AP-27 + AP-28** (Wareneingang-Charge-Merge gemeinsam mit der neuen Produktions-Maske inkl. Produktionsergebnis; Output-Produkte-Frage §5 Punkt 4 bestätigen). +4. Danach offene V4.0-Pakete: **AP-13** (Shop-Bestandsabzug, Konzept liegt vor), **AP-14** (Audit-Trail), **AP-15** (Blockrechte), **AP-16** (2FA), **AP-17** (WaWi-Einstellungen). +5. **AP-18 (Hinweise-Doku)** mit jedem AP fortschreiben. + +--- + +## 8. Pflege dieses Dokuments + +- Jedes abgeschlossene AP im Umsetzungsprotokoll (0a) mit Datum + Kurzbeschreibung + Test-Status protokollieren. +- Bei DB-Änderungen Migration-Dateinamen referenzieren; Casts in `casts()` pflegen (L11-Konvention). +- Vor jedem Commit: `vendor/bin/pint --dirty` und betroffene Tests (`php artisan test --filter=...`). +- **UI-Konventionen** (aus V4.0 weiter gültig): Design-System `partials/wawi-ui.blade.php` (AP-19); Datumsfelder als `datepicker-base` (`dd.mm.yyyy`, kein natives `type="date"`); Status über `wawi-pill` (ok/warning/danger). +- Neue Anforderungsquelle dieser Runde: `docs/nächsten Anforderungen an die WaWi.md` (12.06.2026) + die beiden 12.06.-Screens. diff --git a/dev/product management /screens/2026-12-06-Rezeptur-phase.jpeg b/dev/product management /screens/2026-12-06-Rezeptur-phase.jpeg new file mode 100644 index 0000000..19bd979 Binary files /dev/null and b/dev/product management /screens/2026-12-06-Rezeptur-phase.jpeg differ diff --git a/dev/product management /screens/2026-12-06-neue-produktion.jpeg b/dev/product management /screens/2026-12-06-neue-produktion.jpeg new file mode 100644 index 0000000..f9ceca4 Binary files /dev/null and b/dev/product management /screens/2026-12-06-neue-produktion.jpeg differ diff --git a/docs/Todos.md b/docs/Todos.md new file mode 100644 index 0000000..b48e106 --- /dev/null +++ b/docs/Todos.md @@ -0,0 +1,144 @@ +Könntest Du das Feld so anlegen, dass der so eine URL mit Parametern annimmt? Dann könnten man gleich in den Konfigurator springen bei solchen Anbietern (Versandverpackungen): +solceh URLS https://www.kartonsaufmass.de/bestellen?bom_configuration=%7B%2522length%2522:125,%2522width%2522:125,%2522height%2522:30,%2522amount%2522:100%7D + +Wird entsprechend validiert und mit einem Fehler ausgegeben + +Lege ich einen neuen Lieferanten an, dann sagt er URL eingeben, obwohl eine drinsteht. Wenn ich die rausmache, dann geht das Abspeichern. Also genau umgekeht wie es eigentlich soll. + +danke. Könntest Du bei „Neue Produktion“ bitte die Incis aus der Herstellerrezeptur nehmen? + +Dort auch die Wörter / Zeile mit **Charge** und **Menge (g)** bei dem jeweiligen INCI aus den Tabellen nehmen. Das ist zu viel fürs Auge. Bei Menge steht im Feld als Platzhalter dann 0 g, dann weiß man, dass man Gramm eintragen muss. + + + +Wenn ich „Weitere Charge“ anklicke, reicht es, wenn ein weiteres Dropdown aufgeht. + + +** +Neuer Einkauf** + +A) Felder für Preise wie folgt: + +- Dropdown UST (Auswahl 19%, 7% etc.) -> Muss ich vorab in den Einstellungen definieren können + +- Brutto-Preis pro kg + +- Netto-Preis pro kg + +-> Manche Shops zeigen mir den Netto-Preis an, manche den Brutto-Preis. Ich will da nicht immer rumrechnen. Ich trage entweder den Netto- oder Bruttobetrag ein und das jeweilige leere Feld berechnet sich dann von selbst. + + + +B) Bitte einen ausgefüllten Einkauf duplizierbar machen. Hintergrund: Manchmal kommen Rohstoffe in 2,3 Kanistern mit unterschiedlichen Chargen, dann muss ich das nicht alles nochmal ausfüllen. + + + +c) Bitte die Icons Auge, Stift und Mülleimber in der Übersicht „Einkauf & Wareneingang“ weiter auseinander und ein Stück größer - ist schwer anzuklicken auf dem ipad - bitte bei allen Tabellen in den Menüpunkten + + + +……………. + + + +**Lieferanten** + +A) Hier muss ich ein Kästchen anklicken können, ob ich per Mail oder online in einem Shop bestelle. Hintergrund: Bei manchen Lieferanten muss ich per Mail bestellen, da soll dann in der Liste für den Rohstoffbestand ein Link entweder zu einem Onlineshop oder ins Mailformular mit der richtigen Email-Adresse bereits hinterlegt erscheinen. + + + +B) Ich möchte bei jedem Lieferanten eine Lieferzeit eintragen können, die ich vorher definiere (z. B. 3-5 Werktage). Aber bitte als Textfeld. Zuätzlich benötige ich bei den INCIs auch ein Feld, wo ich eine Lieferzeit eintragen kann (auch als Textfeld). Wenn da was drinsteht, dann überschreibt der Inhalt den vom Feld beim Lieferanten. Hintergrund: Dragonspice z.B. liefert immer prompt nach 3 Tagen. Manche (wenn auch wenige) INCI von denen sind aber nicht immer vorrätig oder müssen vorbestellt werden. + + + +……………. + + + +**INCIs** + +A) Bei jedem INCI muss ich eine Mehrfachauswahl als Dropdown haben, bei welchen Lieferanten ich dieses eine INCI kaufen kann (beziehe nicht immer beim gleichen). Das brauchen wir dann später in der Übersicht Rohstoffbestand. + + + +B) Pro INIC möchte die Höhe der UST hinterlegen über ein Dropdown (7%, 19% etc. -> anzulegen in Einstellungen). + + + +……………. + + + +**Neue Produktion** + +A) Bitte weniger Linien - Überschriften Charge und Menge bei den einzelnen Rohstoffen weg - steht ja im Dropdown / Feld (hier noch g hinter die Zahl) + + + +B) Im Dropdown für die Charge steht zusätzlich noch der Lieferant, also „**Manske GmbH - DE-170722 - 30.09.2028**“ (Datum bitte so in dieser Schreibweise und die Buchstaben MHD raus). Die Charge darf auch nur angezeigt werden, wenn noch was von ihr da ist. + + + +C) Das mit den Megenangaben bei einer neuen Produktion müssen wir eleganter lösen. Sonst muss ich ja ständig in die Excel schauen und manuell übertragen. Vorschlag: Ich trage oben die Stückzahl ein (z.B. 50), unten errechnet sich in den Felden „Menge Soll (g)“ die vollständige Zahl, die ich brauche - **auf Basis der Herstellerrezeptur!.** Daneben steht ein weiteres Feld „Menge Ist (g)“. Wenn ich Sonnenblumenöl Kanister Charge A dann 2000 von benötigten 5000 g leere, trage ich ins Ist-Feld 2.000 g ein für Charge A und mache dann eine weitere Charge (zweite Zeile) auf, wo ich ins Ist-Feld dann 3000 g eintrage. Habe ich ausreichend von einer Charge, also sind da die vollständigen 5.000 g im Kanister, trage ich im Ist-Feld nichts ein, da reicht ja dann die Zahl im Soll-Feld. Bei Speichern übernimmt der dann die Felder, die ausgefüllt sind. Mann kann **nicht** in beide Felder Zahlen schreiben! + + + +D) Schau Dir mal bitte „Neue Produktion“ auf dem ipad an. Die Felder Produktionsdatum und Produzierte Stückzahl überlappen grafisch. + + + +E) Wir haben die Produktentwicklung noch nicht bedacht, da gehen ja auch Rohstoffe für drauf. Unter dem Menüpunkt „Produktion“ brauchen wir noch einen Menüpunkt Produktentwicklung. Konzept dafür folgt, aber bitte schon mal einfügen, damit wir das nicht vergessen. + + + +……………. + + + +**Übersicht Rohstoffeinkauf** + +Neuer Menüpunkt: Rohstoffbestand + + + +Das ist unsere Übersicht, in der ich sehen kann, welche Rohstoffe im Lager sind. Alles weitere in den Screens anbei. + + + +……………. + + + +**Produktbestand** + +A) Wir haben einen Hauptmenüpunkt: Produktbestand (sh. Screen anbei - hier trage ich Ein und Ausgänge ein - ganz simpel) + +Darunter einen weiteren Menüpunkt: Historie (Hier machen wir ein umfangreiches Archiv vor allem fürs Finanzamt, bei dem man jedes einzelne Produkt nachverfolgen kann, wenn nötig. D.h., JEDER Ein- und Ausgang wird hier dokumentiert - sei es durch einen Verkauf über die Shops oder die manuelle Eingabe z. B. durch Verschenken von Testern. Hier gibt es zwei Screens für. + + + +B) Bei dem Produktbestand dürfen nur Hauptprodukte angezeigt werden. Also die „Bio Tattoocreme 15 ml (einzeln)“ ist das Hauptprodukt, das Produkt Bio Tattoocreme 50 Stück als Set ist das „Child-Produkt“ davon. Das muss ich also in den Produkten noch vermerken. Musst Du Dir was überlegen. Aber beim Child-Produkt könnte man das unten im Bereich Verpackung & Material noch mit angeben, dass wir hier 50 x das Hauptprodukt haben. + + + +……………. + + + +**2FA - Google Authenticator für die Admins** + +Wie besprochen + + + +Rechtevergabe - Wir vergeben pro Block ein Zugriffsrecht. Am Ende sind Mitarbeiter Admins, aber ich kann als SuperAdmin anklicken, welchen Block die sehen und bearbeiten können. Also ein Mitarbeitern kann z. B. die Produktliste einsehen, aber nicht bearbeiten oder einsehen UND bearbeiten. + + + +……………… + + + +**Nicht vorrätig** + +Brauche die Funktion „Nicht vorrätig“ inkl. Zeitangabe. Ich hake also ein Kästchen im Produkt an „Nicht vorrätig“ und trage dahinter eine Zahl in einem Feld ein (Anzahl Tage). Von da an zählt das System runter und die Zahl aktualisiert sich dann in der Bestellansicht. Ein zweites Kästchen mit „Auf unbestimmte Zeit vergriffen“ - da dann natürlich kein Feld für Tage. \ No newline at end of file diff --git a/docs/nächsten Anforderungen an die WaWi.md b/docs/nächsten Anforderungen an die WaWi.md new file mode 100644 index 0000000..d582b4e --- /dev/null +++ b/docs/nächsten Anforderungen an die WaWi.md @@ -0,0 +1,111 @@ +Datum 12-06-2026 + + +**Rohstoffbestand:** + +1. Es ist gut, alle Rohstoffe aufzunehmen und einen Überblick zu haben, was alles im Haus ist. Aber in der Ansicht Rohstobestand muss ich die Filtermöglichkeit haben: + +a) Alle Rohstoffe anzeigen (also auch solche, die nicht in aktiven Produkten drin sind, die ich aber z. B. für Neuentwicklung eingekauft habe) + +b) Nur Rohstoffe aus Herstellerrezepturen + + +Allergene etc. dürfen da gar nicht auftauchen. Hier müsste ich bei den INCI also die Möglichkeit eines Kontrollkästchens haben „Kein Rohstoffbestand“. + + + +… + + + +**INCI-Ebene:** + +1. Wenn ich bei Rohstoffbestand auf einen Rostoff klicke (nimm mal ZinClearXP), dann sehe ich ganz rechts „per Mail“. Aber wo definiere ich das? Da ich bei Dragonspice sowohl im Onlineshop als auch per Mail bestellen muss (z.B. größere Mengen), dann muss ich also im INCI selbst eintragen, ob online oder per Mail. Daher muss ich auf INCI-Ebene einstellen können a) Lieferant und dahinter b) URL (bleibt die URL frei, muss ich per Mail bestellen). Dann ein Plus für „Weiteren Lieferanten anlegen“. + + + +2. Ich möchte die INCI-Liste filtern können. Also oben ein Kästchen „nur aktive“ anklicken. „Aktive“ = Alle INCI aus Herstellerrezepturen (mindestens 1 Produkt aktiv). + + + +3. Bitte bei INCI noch ein Feld rein, wo ich einen Alternativen Namen eintragen kann, damit ein Mitarbeiter das INCI auch findet (Beispiel: In der Herstellerrezeptur steht LECITHIN, der Handelsname auf der Packung lautet aber Phospholipon 80 H). + + + +4. Außerdem ein Feld, wo ich eintragen kann, wo das INCI / die Verpackung etc. liegt. Aktuell plane ich die Phrase RAUM | REGAL-NR. | BUCHSTABE (Raum 1 | Regal 3 | A). Am besten ich lege diese Felder auch unter Einstellungen zentral an, falls ich da mal was ändern muss, kann ich das gleich übergeordnet für alle machen. + + + +… + + + +**Produktbestand:** + +1. Ich möchte sortieren können in der Tabelle nach Dringlichkeit. Also Klick auf Status und dann von oben nach unten erscheinen die dringlichsten. Kann defaultmäßig auch schon so eingestellt sein (falls es nicht schon so ist). Die Filter oben sind gut, aber „Produkte“ und „Bestand ok“ kann man nicht anklicken. + + + +2. Auf Produktebene muss ich anklicken können, ob das Produkt im Produktbestand erscheinen soll oder nicht (z.B. Abrechnung Druckkosten für Logo-Etiketten haben da nix zu suchen). + + + +3. Bitte hier noch Spalte „Verbrauch pro Monat“ reinbauen (Durchschnitt der letzten 6 Monate). + + + +… + + + +**Produktebene:** + +1. Auf Produktebene müssen unter der Herstellerrezeptur noch die Phasen angelegt werden können. (siehe Screen dev/product management /screens/2026-12-06-Rezeptur-phase.jpeg). + + + +2. Ich brauche noch ein Feld für ein Präfix für die Chargen-Nr. der Produkte. Es setzt sich zusammen aus einem Kürzel für die Kategorie (TP - Tattoopflege) + Produkt-Nr. (1) + Produktionsdatum. Beispiel Tattoocreme: TP1150626. Diese Chargen-Nr. schreiben wir immer in eins durch. Ich lege also das Präfix „TP1“ auf Produktebene an. In der Maske für die neue Produktion erstellt sich dann in dem Textfeld also die Chargen-Nr. von selbst, aber editierbar, da wir oft alte Chargen-Nr. Etiketten verwenden. + + + +… + + + +**Lieferbestand:** + +Bei der Funktion „erst in 12 Tagen wieder lieferbar“ - hier machen wir das Produkt doch wieder kaufbar. Soll der VP selber entscheiden, ob er die Ware später bekommt. Ich würde hier außerdem präferieren, dass ich auf Produktebene eine Datum einstelle und für den VP dann die Anzahl der Tage erscheint, die sich dann täglich natürlich aktualisieren. + + + +…. + + + +**Ausschuss:** + +Ich möchte die Gründe für den Ausschluss unter Einstellungen selber anlegen können. + + + +…. + + + +**Wareneingang:** + +Ich bestelle heute Sonnenblumenöl mit Charge XY-123. In 3 Wochen kommt wieder Sonnenblumenöl mit der gleichen Chargen-Nr. XY-123. Hier brauche ich eine Lösung, dass sich das bei weiteren Einkauf dazuaddiert. Man müsste in der Maske also erstmal schauen, ob die Charge schon im Hause ist. Wir erweitern aber nicht einfach die Charge - den Einkaufsvorgang erfassen wir trotzdem. + + + +…. + + + +**Produktion:** + +Ich habe eine neue Maske konzipiert für die Produktion (siehe Screen dev/product management /screens/2026-12-06-neue-produktion.jpeg). Da fließen natürlich auch die neuen Rezeptur-Phasen mit ein. Wichtig: Der Produktbestand (!) errechnet sich nicht aus der oben definierten Auswahl Produkt und Anzahl. Das ist nur zur Planung. Ganz unten trägt man hinterher ein, was genau draus entstanden ist. Beispiel: Ich plane 50 x 50 ml HAELD Deobalm. Aus dieser Masse entstehen dann 40 x 50 ml und 50 x 5 ml. Das muss ich also extra eintragen. + + + +Der Rohstoffbestand berechnet sich dann wie gehabt aus den Werten, die ich hinter jeder Charge eintrage. \ No newline at end of file diff --git a/resources/docs/hinweise.md b/resources/docs/hinweise.md index 7deb185..c053641 100644 --- a/resources/docs/hinweise.md +++ b/resources/docs/hinweise.md @@ -4,7 +4,7 @@ > wichtige Hinweise für die Nutzung sowie festgehaltene Entscheidungen, die > später noch ausgebaut werden können. > -> **Stand:** 03.06.2026 +> **Stand:** 12.06.2026 --- @@ -12,8 +12,9 @@ ### Bereits nutzbar -- **Einstellungen → Allgemein:** Umsatzsteuersätze und Lieferzeit-Vorlagen - (inkl. Tageswert) pflegbar. +- **Einstellungen → Allgemein:** Umsatzsteuersätze, Lieferzeit-Vorlagen + (inkl. Tageswert) und **Ausschuss-Gründe** pflegbar. Die Ausschuss-Gründe + erscheinen im Ausschuss-Formular (nur aktive, in gepflegter Reihenfolge). - **Stammdaten:** Lagerorte, Rohstoffqualität, Verpackungsmaterial, Lieferanten-Kategorien. - **Lieferanten:** Bestellweg (E-Mail / Online-Shop), Bestell-Adresse, @@ -28,11 +29,12 @@ - **Produkt-Klassen:** Einzelprodukt vs. **Set** (Bündel mehrerer Einzelprodukte mit Menge). Sets werden nicht produziert; optional kann ein Einzelprodukt einem Hauptprodukt zugeordnet werden. -- **„Nicht vorrätig":** Produkt zeitlich begrenzt (mit Tagesangabe → - „In ca. X Tagen wieder da!") oder auf unbestimmte Zeit als nicht vorrätig - markierbar. Im öffentlichen Shop erscheint nur ein Hinweis, der Kauf bleibt - möglich. In der **internen Bestellliste** ersetzt der rote Hinweis die - Mengen-Buttons – dort ist das Produkt also vorübergehend nicht bestellbar. +- **„Nicht vorrätig":** Produkt zeitlich begrenzt (mit **Datum** „Wieder + lieferbar ab" → der Hinweis „In ca. X Tagen wieder da!" zählt täglich + automatisch herunter) oder auf unbestimmte Zeit als nicht vorrätig + markierbar. Es erscheint **überall nur ein Hinweis** – im öffentlichen Shop + **und** in der internen Bestellliste bleibt der Kauf möglich; der + Vertriebspartner entscheidet selbst, ob er die Ware später bekommt. - **Rohstoffbestand:** Übersicht aller aktiven Rohstoffe mit echtem Restbestand (Wareneingang abzüglich Produktionsverbrauch), durchschnittlichem Verbrauch pro Tag, voraussichtlichem „auf Null"-Datum und Hochrechnung für 1/3/6/12 @@ -53,14 +55,21 @@ ausgewähltem Produkt. Suche und Filter „nur kritische anzeigen"; bei unterschrittenem kritischem Bestand ist die Zeile rot, beim Meldebestand gelb – ein Badge in der Navigation zeigt die Anzahl. Schwellwerte werden je Produkt im - Produktformular (Warenwirtschaft) gepflegt. + Produktformular (Warenwirtschaft) gepflegt. Die Übersicht ist standardmäßig + nach **Dringlichkeit** sortiert (kritische Produkte oben, Klick auf „Status" + dreht die Reihenfolge), alle vier Status-Kacheln oben sind als Filter + anklickbar, und die Spalte **„Verbrauch/Monat"** zeigt den Durchschnitt der + Abgänge der letzten 6 Monate. Produkte ohne Lagerführung (z. B. Abrechnung + Druckkosten, Logo-Etiketten) lassen sich über die Produkt-Option + **„Im Produktbestand anzeigen"** aus der Übersicht ausblenden. - **Produktbestand-Historie:** Revisionssichere Liste aller Bewegungen (Eingang/Ausgang, Stückzahl, Datum, Grund, Hinweis, Mitarbeiter), filterbar nach Produkt, Richtung, Grund und Zeitraum (Monat/Jahr). - **Ausgang / Ausschuss:** Erfassung von Rohstoff- und Verpackungs-Abgängen - (z. B. Bruch, Verfall/MHD, Qualitätsmangel, Schwund, Testverbrauch). Pflichtfeld - **Grund**, optionale **Charge** (setzt den Lagerort automatisch), Menge in + (z. B. Bruch, Verfall/MHD, Qualitätsmangel, Schwund, Testverbrauch). Die + Gründe sind selbst pflegbar unter **Einstellungen → Allgemein → + Ausschuss-Gründe**. Pflichtfeld **Grund**, optionale **Charge** (setzt den Lagerort automatisch), Menge in Gramm (Rohstoff) bzw. Stück (Verpackung) und Datum. Jeder Ausgang reduziert sofort den Bestand – beim Rohstoffbestand also auch die „auf Null"-Prognose und den Kritisch-Status. diff --git a/resources/views/admin/inventory/disposal-reasons/form.blade.php b/resources/views/admin/inventory/disposal-reasons/form.blade.php new file mode 100644 index 0000000..6c67a2c --- /dev/null +++ b/resources/views/admin/inventory/disposal-reasons/form.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.layout-2') + +@section('content') +
+
{{ $model->exists ? __('Ausschuss-Grund bearbeiten') : __('Ausschuss-Grund anlegen') }}
+
+
+ @csrf + @if($model->exists) + @method('PUT') + @endif + +
+ + + @error('label') +
{{ $message }}
+ @enderror +
+ +
+ + + @error('pos') +
{{ $message }}
+ @enderror +
+ +
+ +
+ + + {{ __('Zurück') }} +
+
+
+@endsection diff --git a/resources/views/admin/inventory/general/index.blade.php b/resources/views/admin/inventory/general/index.blade.php index ec10dc8..c4b0ff4 100644 --- a/resources/views/admin/inventory/general/index.blade.php +++ b/resources/views/admin/inventory/general/index.blade.php @@ -7,7 +7,7 @@

{{ __('Einstellungen') }}

-

{{ __('Umsatzsteuersätze und Lieferzeit-Vorlagen') }}

+

{{ __('Umsatzsteuersätze, Lieferzeit-Vorlagen und Ausschuss-Gründe') }}

@@ -119,5 +119,58 @@ + +
+
+ {{ __('Ausschuss-Gründe') }} + {{ __('Neu anlegen') }} +
+
+ + + + + + + + + + + @forelse($disposalReasons as $disposalReason) + + + + + + + @empty + + + + @endforelse + +
 {{ __('Bezeichnung') }}{{ __('Status') }}
+ + + + {{ $disposalReason->label }} + @if ($disposalReason->active) + {{ __('Aktiv') }} + @else + {{ __('Inaktiv') }} + @endif + +
+ @csrf + @method('DELETE') + +
+
{{ __('Noch keine Ausschuss-Gründe angelegt.') }}
+
+
@endsection diff --git a/resources/views/admin/inventory/partials/table-actions-style.blade.php b/resources/views/admin/inventory/partials/table-actions-style.blade.php index 11856b4..3fd2399 100644 --- a/resources/views/admin/inventory/partials/table-actions-style.blade.php +++ b/resources/views/admin/inventory/partials/table-actions-style.blade.php @@ -1,35 +1,6 @@ @once