710 lines
29 KiB
Markdown
710 lines
29 KiB
Markdown
# Finales technisches Konzept: Warenwirtschaft & Produktion
|
|
|
|
> **Version:** 1.1 - Stand 12.03.2026
|
|
> **Grundlage:** Kunden-Briefing + initiales Konzept (`briefing.md`, `konzept.md`)
|
|
> **System:** Laravel 11 (L10-Struktur), PHP 8.4, Bootstrap 4, jQuery, Laravel Mix, MySQL
|
|
|
|
---
|
|
|
|
## Inhaltsverzeichnis
|
|
|
|
1. [Ist-Analyse & identifizierte Probleme](#1-ist-analyse)
|
|
2. [Architektur-Entscheidungen](#2-architektur-entscheidungen)
|
|
3. [Datenmodell](#3-datenmodell)
|
|
4. [Modul A: INCI-Management (Bugfix & Erweiterung)](#4-modul-a-inci-management)
|
|
5. [Modul B: Stammdaten](#5-modul-b-stammdaten)
|
|
6. [Modul C: Wareneingang (Zwei-Stufen-System)](#6-modul-c-wareneingang)
|
|
7. [Modul D: Rezeptur & BOM (Bill of Materials)](#7-modul-d-rezeptur--bom)
|
|
8. [Modul E: Produktion & Bestandsabzug](#8-modul-e-produktion)
|
|
9. [Modul F: Bestandsuebersicht & Alarme](#9-modul-f-bestandsuebersicht)
|
|
10. [Modul G: Ausgang / Ausschuss](#10-modul-g-ausgang)
|
|
11. [Modul H: Rollen, Berechtigungen & Audit-Trail](#11-modul-h-rollen)
|
|
12. [Modul I: Warenwirtschafts-Einstellungen](#12-modul-i-einstellungen)
|
|
13. [Menuestruktur](#13-menuestruktur)
|
|
14. [Abweichungen vom initialen Konzept](#14-abweichungen)
|
|
|
|
---
|
|
|
|
## 1. Ist-Analyse
|
|
|
|
### Bestehende Strukturen
|
|
|
|
| Bereich | Status | Details |
|
|
|---------|--------|---------|
|
|
| Produkte | vorhanden | `products`-Tabelle mit umfangreichen Feldern |
|
|
| INCI/Inhaltsstoffe | rudimentaer | `ingredients` + `product_ingredients` Pivot (ohne Sortierung, Gramm, Faktor) |
|
|
| Warenwirtschaft | nicht vorhanden | Keine Tabellen, keine Routen, kein Menuepunkt |
|
|
| Lieferanten | nicht vorhanden | - |
|
|
| Lagerorte | nicht vorhanden | - |
|
|
| Bestandsfuehrung | nicht vorhanden | - |
|
|
| Settings-System | vorhanden | `Setting`-Model mit Key-Value-Store (`getContentBySlug` / `setContentBySlug`) |
|
|
|
|
### Identifizierte Bugs im Bestand
|
|
|
|
1. **`ProductIngredient` hasMany-Relation:** Falscher zweiter Parameter (`'product_ingredients'` statt `'product_id'`). Muss korrigiert werden.
|
|
2. **`updateIngredients()`:** Fuegt nur neue INCIs hinzu, entfernt deselektierte nicht. Sync-Logik fehlt.
|
|
3. **`HTMLHelper::getProductIngredientsOptions()`:** Kein `orderBy('name')` - Dropdown nicht alphabetisch.
|
|
4. **Produkt-Kopierung:** INCI-Reihenfolge nicht deterministisch, da kein `orderBy` auf Pivot.
|
|
5. **Attribute-Kopierung:** `$this->model->attribute_type_id` existiert nicht auf Product-Model.
|
|
|
|
---
|
|
|
|
## 2. Architektur-Entscheidungen
|
|
|
|
### Frontend-Stack: jQuery + bestehende Libraries
|
|
|
|
**Begruendung:** Das Projekt nutzt durchgehend jQuery + Bootstrap 4 + Laravel Mix. Die Einfuehrung von Livewire waere ein Architekturbruch und wuerde die Komplexitaet ohne Mehrwert erhoehen.
|
|
|
|
| Anforderung | Loesung |
|
|
|-------------|--------|
|
|
| Drag & Drop Sortierung | **SortableJS** (vanilla JS, kein jQuery noetig) |
|
|
| Autosuggest / Dropdown | **Select2** (bereits im Projekt-Oekosystem Bootstrap 4 kompatibel) |
|
|
| AJAX-Operationen | jQuery `$.ajax` / `$.post` (konsistent mit Bestand) |
|
|
| DataTables | **jQuery DataTables** (bereits im Projekt verwendet) |
|
|
|
|
### Backend-Pattern: Repositories + Services
|
|
|
|
**Grundregel:** Konsistent mit der bestehenden Architektur (`ProductRepository`, `InvoiceRepository` etc.).
|
|
|
|
| Pattern | Einsatz | Beispiele |
|
|
|---------|---------|-----------|
|
|
| **Repositories** | Datenbank-Zugriff und CRUD-Logik, direkt von Controllern verwendet | `SupplierRepository`, `StockEntryRepository`, `ProductionRepository` |
|
|
| **Services** | Uebergreifende Business-Logik, Berechnungen, Hilfsfunktionen die mehrere Models/Repositories orchestrieren | `InventoryService` (Bestandsberechnung), `ProductionService` (Produktions-Workflow) |
|
|
|
|
Controller -> Repository (CRUD) -> Model
|
|
Controller -> Service (komplexe Logik) -> Repository/Model
|
|
|
|
### Weitere Backend-Entscheidungen
|
|
|
|
| Aspekt | Entscheidung |
|
|
|--------|-------------|
|
|
| Controller-Erstellung | `php artisan make:controller` mit `--no-interaction` |
|
|
| Modelle | `php artisan make:model` mit Migrations und Factories |
|
|
| Validierung | Dedizierte FormRequest-Klassen |
|
|
| Menue-Badges | Laravel **View Composer** fuer globale Badge-Zaehler |
|
|
| Einstellungen | Bestehendes `Setting`-Model mit Key-Value-Store |
|
|
|
|
### Datentypen & Praezision
|
|
|
|
| Typ | Speicherung | Begruendung |
|
|
|-----|-------------|------------|
|
|
| Gewichte | `DECIMAL(12,2)` in Gramm | Ausreichende Praezision fuer Kosmetik-Rezepturen |
|
|
| Preise | `DECIMAL(10,4)` in Euro | Netto-Preis/kg kann Nachkommastellen haben |
|
|
| Faktoren | `DECIMAL(4,2)` | Default 1.10, Bereich 1.00-9.99 |
|
|
| Mengen (Packaging) | `INT UNSIGNED` | Stueckzahlen (Tiegel, Deckel etc.) |
|
|
|
|
> **Hinweis:** Im initialen Konzept war "Milli-Cent / Milli-Gramm als Integer" vorgeschlagen. Das wird hier bewusst zu DECIMAL geaendert, weil: (a) das bestehende System durchgehend DECIMAL fuer Preise nutzt, (b) die Kosmetik-Produktion keine Nanogramm-Praezision erfordert, (c) DECIMAL(12,2) in Gramm bereits eine Genauigkeit auf 0,01g bietet.
|
|
|
|
---
|
|
|
|
## 3. Datenmodell
|
|
|
|
### Uebersicht neue Tabellen
|
|
|
|
```
|
|
STAMMDATEN
|
|
locations Lagerorte (Koeln, Waldboel ...)
|
|
supplier_categories Lieferanten-Kategorien
|
|
supplier_supplier_category Pivot: Lieferant <-> Kategorie (n:m)
|
|
suppliers Lieferanten
|
|
material_qualities Rohstoff-Qualitaeten (bio, konventionell ...)
|
|
packaging_materials Verpackungs-Wertstoffe (Glas, Pappe ...)
|
|
packaging_items Verpackungs-Artikel (Glastiegel 50ml ...)
|
|
|
|
BESTEHEND (ERWEITERT)
|
|
ingredients + default_factor, min_stock_alert
|
|
product_ingredients + pos, gram, factor
|
|
products + shelf_life_type, shelf_life_months
|
|
|
|
WARENEINGANG
|
|
stock_entries Eingangs-Datensaetze (Pending/Received)
|
|
|
|
PRODUKT-PACKAGING (BOM)
|
|
product_packagings Pivot: Produkt -> Packaging-Artikel
|
|
|
|
PRODUKTION
|
|
productions Produktionslauf (Produkt, Datum, Menge ...)
|
|
production_ingredients Zuordnung INCI -> Charge(n) pro Produktion
|
|
production_packagings Snapshot: Packaging-Verbrauch pro Produktion
|
|
|
|
AUSGANG / AUSSCHUSS
|
|
stock_disposals Entsorgungen / Ausschuss
|
|
|
|
AUDIT / LOGGING
|
|
inventory_logs Jede Bestandsveraenderung protokolliert
|
|
```
|
|
|
|
### Detaillierte Tabellenstruktur
|
|
|
|
#### `locations`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| name | VARCHAR(255) | z.B. "Koeln", "Waldboel" |
|
|
| active | BOOLEAN | Default true |
|
|
| timestamps | | |
|
|
|
|
#### `supplier_categories`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| name | VARCHAR(255) | z.B. "Rohstoffe", "Versandmaterial" |
|
|
| pos | TINYINT | Sortierung |
|
|
| timestamps | | |
|
|
|
|
#### `supplier_supplier_category` (Pivot)
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| supplier_id | BIGINT FK | -> suppliers |
|
|
| supplier_category_id | BIGINT FK | -> supplier_categories |
|
|
| timestamps | | |
|
|
|
|
> **Begruendung n:m:** Ein Lieferant kann in mehreren Kategorien taetig sein (z.B. liefert Rohstoffe *und* Verpackungen).
|
|
|
|
#### `suppliers`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| name | VARCHAR(255) | Firmenname |
|
|
| url | VARCHAR(255) NULL | Webseite |
|
|
| contact_person | VARCHAR(255) NULL | Ansprechpartner |
|
|
| email | VARCHAR(255) NULL | |
|
|
| phone | VARCHAR(100) NULL | |
|
|
| country_id | UNSIGNED INT FK | -> countries (Default: DE) |
|
|
| notes | TEXT NULL | Freitextnotizen |
|
|
| active | BOOLEAN | Default true |
|
|
| timestamps | | |
|
|
| deleted_at | TIMESTAMP NULL | SoftDeletes |
|
|
|
|
#### `material_qualities`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| name | VARCHAR(255) | z.B. "bio kaltgepresst", "konventionell raffiniert" |
|
|
| pos | TINYINT | Sortierung |
|
|
| timestamps | | |
|
|
|
|
#### `packaging_materials`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| name | VARCHAR(255) | z.B. "Glas", "Pappe/Papier", "Kunststoff" |
|
|
| pos | TINYINT | Sortierung |
|
|
| timestamps | | |
|
|
|
|
#### `packaging_items`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| packaging_material_id | BIGINT FK | -> packaging_materials |
|
|
| supplier_id | BIGINT FK NULL | -> suppliers |
|
|
| name | VARCHAR(255) | z.B. "Glastiegel 50ml", "Bambusdeckel" |
|
|
| category | ENUM | 'packaging', 'label', 'shipping_office' |
|
|
| weight_grams | DECIMAL(10,2) | Gewicht in Gramm (fuer LUCID-Abfallwirtschaft) |
|
|
| min_stock_alert | INT UNSIGNED NULL | Mindestbestand fuer Alarm (in Stueck) |
|
|
| product_id | BIGINT FK NULL | Nur fuer Etiketten: Bindung an Produkt |
|
|
| active | BOOLEAN | Default true |
|
|
| timestamps | | |
|
|
| deleted_at | TIMESTAMP NULL | SoftDeletes |
|
|
|
|
#### `ingredients` (Erweiterung)
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| default_factor | DECIMAL(4,2) | **NEU** - Default-Schwundfaktor fuer diesen Rohstoff, Default 1.10. Wird beim Zuweisen an ein Produkt in `product_ingredients.factor` uebernommen. |
|
|
| min_stock_alert | DECIMAL(12,2) NULL | **NEU** - Globaler Meldebestand in Gramm. Bei Unterschreitung: Badge + E-Mail. |
|
|
|
|
> **Warum der Faktor am Ingredient?** Wenn z.B. Sheabutter generell einen Verschleiss von 1,2 hat, wird das einmal am Rohstoff-Stammdatum gesetzt. Bei jeder Produkt-Zuweisung wird der Wert automatisch uebernommen. Wird bei einer Produktion festgestellt, dass der Verschleiss anders ist, kann `ingredients.default_factor` nachkorrigiert werden - zukuenftige Zuweisungen erben dann den neuen Wert. Bestehende `product_ingredients.factor`-Werte bleiben unveraendert.
|
|
|
|
#### `product_ingredients` (Erweiterung)
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| pos | SMALLINT UNSIGNED | **NEU** - Sortier-Reihenfolge |
|
|
| gram | DECIMAL(12,2) NULL | **NEU** - Grammzahl pro Einheit (Rezeptur) |
|
|
| factor | DECIMAL(4,2) | **NEU** - Individueller Schwund-Faktor pro Produkt-INCI-Kombination. Default wird aus `ingredients.default_factor` uebernommen. |
|
|
|
|
#### `products` (Erweiterung)
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| shelf_life_type | ENUM('pao','fixed') NULL | **NEU** - PAO (12M-Tiegel) oder festes MHD |
|
|
| shelf_life_months | TINYINT UNSIGNED NULL | **NEU** - 6/12/18/24/30/36 Monate |
|
|
|
|
> **Kein globaler `waste_factor` am Produkt.** Der Faktor wird individuell pro INCI gefuehrt (`product_ingredients.factor`), mit Default aus dem Rohstoff-Stammdatum (`ingredients.default_factor`). Das ist flexibler und erfordert nur einmaliges Anlegen pro Rohstoff.
|
|
|
|
#### `product_packagings`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| product_id | BIGINT FK | -> products |
|
|
| packaging_item_id | BIGINT FK | -> packaging_items |
|
|
| quantity | INT UNSIGNED | Stueck pro Produkteinheit (Default 1) |
|
|
| timestamps | | |
|
|
|
|
#### `stock_entries`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| entry_type | ENUM | 'ingredient', 'packaging', 'label', 'shipping_office' |
|
|
| ingredient_id | BIGINT FK NULL | -> ingredients (wenn Rohstoff) |
|
|
| packaging_item_id | BIGINT FK NULL | -> packaging_items (wenn Packaging/Etikett/Versand) |
|
|
| supplier_id | BIGINT FK | -> suppliers |
|
|
| location_id | BIGINT FK | -> locations (Lagerort) |
|
|
| unit | ENUM('gram','piece') | Mengeneinheit: 'gram' fuer Rohstoffe, 'piece' fuer Packaging |
|
|
| **Stufe 1 - Einkauf** | | |
|
|
| ordered_by | BIGINT FK | -> users (Einkaeufer) |
|
|
| ordered_at | DATE | Kaufdatum |
|
|
| ordered_quantity | DECIMAL(12,2) | Bestellte Menge (in Gramm oder Stueck, je nach `unit`) |
|
|
| price_per_kg | DECIMAL(10,4) NULL | Netto-Preis pro kg (nur Rohstoffe) |
|
|
| price_total | DECIMAL(10,4) NULL | Gesamtpreis netto |
|
|
| **Stufe 2 - Eingang** | | |
|
|
| received_by | BIGINT FK NULL | -> users (Wareneingaenger) |
|
|
| received_at | DATE NULL | Eingangsdatum |
|
|
| received_quantity | DECIMAL(12,2) NULL | Tatsaechliche Menge |
|
|
| batch_number | VARCHAR(100) NULL | Chargen-Nr. |
|
|
| best_before | DATE NULL | MHD |
|
|
| quality_id | BIGINT FK NULL | -> material_qualities |
|
|
| **Status** | | |
|
|
| status | ENUM('pending','received') | Default 'pending' |
|
|
| timestamps | | |
|
|
|
|
> **Kein `min_stock_alert` am Wareneingang.** Der Meldebestand wird am Stammdaten-Objekt gefuehrt: `ingredients.min_stock_alert` fuer Rohstoffe, `packaging_items.min_stock_alert` fuer Packaging. Das stellt sicher, dass der Schwellenwert global und einheitlich gilt.
|
|
|
|
#### `productions`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| product_id | BIGINT FK | -> products |
|
|
| location_id | BIGINT FK | -> locations (Produktionsstandort) |
|
|
| produced_by | BIGINT FK | -> users |
|
|
| produced_at | DATE | Produktionsdatum |
|
|
| quantity | INT UNSIGNED | Produzierte Stueckzahl |
|
|
| notes | TEXT NULL | |
|
|
| timestamps | | |
|
|
|
|
#### `production_ingredients`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| production_id | BIGINT FK | -> productions |
|
|
| ingredient_id | BIGINT FK | -> ingredients |
|
|
| stock_entry_id | BIGINT FK | -> stock_entries (Chargen-Zuordnung) |
|
|
| quantity_used | DECIMAL(12,2) | Verbrauchte Menge in Gramm |
|
|
| timestamps | | |
|
|
|
|
> **Chargen-Splitting:** Wenn eine Charge leer wird und die naechste verwendet wird, entstehen zwei Eintraege in `production_ingredients` fuer denselben `ingredient_id` mit unterschiedlichen `stock_entry_id`.
|
|
|
|
#### `production_packagings` (Snapshot)
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| production_id | BIGINT FK | -> productions |
|
|
| packaging_item_id | BIGINT FK | -> packaging_items |
|
|
| quantity_used | INT UNSIGNED | Verbrauchte Stueckzahl (= productions.quantity x product_packagings.quantity zum Zeitpunkt der Produktion) |
|
|
| timestamps | | |
|
|
|
|
> **Warum ein Snapshot?** Ohne diese Tabelle muesste der Packaging-Verbrauch aus `productions.quantity x product_packagings.quantity` berechnet werden. Wird die BOM eines Produkts nachtraeglich geaendert (z.B. Tiegel-Wechsel), waeren historische Bestandsberechnungen falsch. `production_packagings` speichert den tatsaechlichen Verbrauch zum Zeitpunkt der Produktion.
|
|
|
|
#### `stock_disposals`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| disposal_type | ENUM | 'ingredient', 'packaging', 'label', 'shipping_office' |
|
|
| ingredient_id | BIGINT FK NULL | -> ingredients |
|
|
| packaging_item_id | BIGINT FK NULL | -> packaging_items |
|
|
| stock_entry_id | BIGINT FK NULL | -> stock_entries (Charge, falls bekannt) |
|
|
| location_id | BIGINT FK | -> locations |
|
|
| quantity | DECIMAL(12,2) | Entsorgte Menge |
|
|
| unit | ENUM('gram','piece') | Mengeneinheit |
|
|
| reason | TEXT | Begruendung |
|
|
| disposed_by | BIGINT FK | -> users |
|
|
| disposed_at | DATE | |
|
|
| timestamps | | |
|
|
|
|
#### `inventory_logs`
|
|
| Spalte | Typ | Beschreibung |
|
|
|--------|-----|-------------|
|
|
| id | BIGINT PK | |
|
|
| loggable_type | VARCHAR(255) | Polymorphe Relation (StockEntry, Production, StockDisposal) |
|
|
| loggable_id | BIGINT | |
|
|
| action | ENUM | 'created', 'updated', 'received', 'produced', 'disposed' |
|
|
| user_id | BIGINT FK | -> users |
|
|
| changes | JSON NULL | Geaenderte Felder (alte/neue Werte) |
|
|
| timestamps | | |
|
|
|
|
---
|
|
|
|
## 4. Modul A: INCI-Management
|
|
|
|
### A1. Bugfixes (Voraussetzung)
|
|
|
|
- **`Product::product_ingredients()`** - Falschen Parameter korrigieren
|
|
- **`Ingredient::product_ingredients()`** - Falschen Parameter korrigieren
|
|
- **`ProductRepository::updateIngredients()`** - `sync()`-Logik implementieren statt nur hinzufuegen
|
|
- **`ProductRepository::copy()`** - Attribut-Kopierung fixen (`$attribute->type_id`)
|
|
|
|
### A2. Migration: Pivot erweitern
|
|
|
|
```sql
|
|
ALTER TABLE product_ingredients
|
|
ADD COLUMN pos SMALLINT UNSIGNED DEFAULT 0 AFTER ingredient_id,
|
|
ADD COLUMN gram DECIMAL(12,2) NULL AFTER pos,
|
|
ADD COLUMN factor DECIMAL(4,2) DEFAULT 1.10 AFTER gram;
|
|
```
|
|
|
|
### A3. Migration: Ingredients erweitern
|
|
|
|
```sql
|
|
ALTER TABLE ingredients
|
|
ADD COLUMN default_factor DECIMAL(4,2) DEFAULT 1.10,
|
|
ADD COLUMN min_stock_alert DECIMAL(12,2) NULL;
|
|
```
|
|
|
|
### A4. Migration: Products erweitern (Haltbarkeit)
|
|
|
|
```sql
|
|
ALTER TABLE products
|
|
ADD COLUMN shelf_life_type ENUM('pao','fixed') NULL,
|
|
ADD COLUMN shelf_life_months TINYINT UNSIGNED NULL;
|
|
```
|
|
|
|
### A5. Drag & Drop Sortierung
|
|
|
|
- **Frontend:** SortableJS auf der INCI-Tabelle im Produkt-Formular
|
|
- **Workflow:** User ordnet INCIs per Drag & Drop -> klickt "Speichern" -> AJAX-Request sendet sortierte IDs -> Backend aktualisiert `pos`-Spalte
|
|
- Kein Live-Speichern pro Verschiebung
|
|
|
|
### A6. Alphabetisches Dropdown + Klick-Reihenfolge
|
|
|
|
- `HTMLHelper::getProductIngredientsOptions()` erhaelt `orderBy('name')`
|
|
- Select2-Widget mit Mehrfachauswahl
|
|
- Die Reihenfolge der Auswahl bestimmt die initiale `pos` beim Hinzufuegen
|
|
|
|
### A7. Gramm & Faktor-Felder
|
|
|
|
- In der INCI-Tabelle erscheinen pro Zeile zusaetzlich:
|
|
- Eingabefeld "Gramm" (DECIMAL)
|
|
- Eingabefeld "Faktor" (DECIMAL, Default aus `ingredients.default_factor`)
|
|
- Berechnete Spalte "Effektiv" zeigt `gram x factor`
|
|
- Beim Hinzufuegen eines INCI zum Produkt wird `factor` automatisch aus `ingredients.default_factor` uebernommen
|
|
|
|
### A8. Haltbarkeit (MHD)
|
|
|
|
- Neue Felder im Produkt-Formular (Abschnitt "Warenwirtschaft"):
|
|
- Radio-Button: "12M-Tiegel (PAO)" / "Festes MHD"
|
|
- Dropdown bei "Festes MHD": 6, 12, 18, 24, 30, 36 Monate
|
|
- Gespeichert in `products.shelf_life_type` + `products.shelf_life_months`
|
|
|
|
### A9. Kopierfunktion erweitern
|
|
|
|
- `ProductRepository::copy()` muss die neuen Pivot-Felder `pos`, `gram`, `factor` mitkopieren
|
|
- Deterministisches `orderBy('pos')` beim Lesen der INCIs
|
|
|
|
---
|
|
|
|
## 5. Modul B: Stammdaten
|
|
|
|
### B1. Lagerorte (`locations`)
|
|
|
|
Einfache CRUD-Seite. Felder: Name, Aktiv-Status.
|
|
|
|
### B2. Lieferanten-Kategorien (`supplier_categories`)
|
|
|
|
Einfache CRUD-Seite. Felder: Name, Sortierung.
|
|
|
|
### B3. Lieferanten (`suppliers`)
|
|
|
|
- Listenansicht mit DataTable + Suchfeld
|
|
- Formular: Kategorien (**Mehrfachauswahl** via Select2 -> Pivot `supplier_supplier_category`), Name, URL, Ansprechpartner, E-Mail, Telefon, Land (Dropdown, Default DE), Notizen
|
|
- SoftDeletes fuer Datenintegritaet
|
|
|
|
### B4. Rohstoff-Qualitaeten (`material_qualities`)
|
|
|
|
Einfache CRUD-Seite. Vorbelegte Eintraege:
|
|
- konventionell
|
|
- bio kaltgepresst
|
|
- bio raffiniert
|
|
- konventionell kaltgepresst
|
|
- konventionell raffiniert
|
|
|
|
### B5. Verpackungs-Materialien (`packaging_materials`)
|
|
|
|
Einfache CRUD-Seite. Vorbelegte Eintraege:
|
|
- Glas
|
|
- Holz/Bambus
|
|
- Pappe/Papier
|
|
- Kunststoff
|
|
|
|
### B6. Verpackungs-Artikel (`packaging_items`)
|
|
|
|
- Formular: Name, Material (Dropdown -> packaging_materials), Kategorie (Packaging/Etikett/Versand & Office), Gewicht (Gramm), Lieferant (Dropdown), Mindestbestand
|
|
- Etiketten: Zusaetzliches Feld fuer Produkt-Zuordnung
|
|
- Listenansicht mit Filter nach Kategorie
|
|
|
|
---
|
|
|
|
## 6. Modul C: Wareneingang
|
|
|
|
### Zwei-Stufen-Workflow
|
|
|
|
**Stufe 1 - Einkaeufer erstellt Bestellung:**
|
|
- Dropdown: Kategorie (Rohstoff / Packaging / Etikett / Versand & Office)
|
|
- `unit` wird automatisch gesetzt: 'gram' bei Rohstoff, 'piece' bei Packaging/Etikett/Versand
|
|
- Je nach Kategorie:
|
|
- **Rohstoff:** Lieferant, Inhaltsstoff (Select2 Autosuggest), Menge (g), Netto-Preis/kg, Lagerort
|
|
- **Packaging etc.:** Lieferant, Artikel (Select2), Menge (Stueck), Preis, Lagerort
|
|
- Status: **Pending** (oranger Punkt)
|
|
|
|
**Stufe 2 - Wareneingang buchen:**
|
|
- Ivonne oeffnet den Pending-Eintrag und ergaenzt:
|
|
- Eingangsdatum, tatsaechliche Menge, Chargen-Nr., MHD, Qualitaet/Sorte (nur Rohstoffe)
|
|
- Status wechselt zu: **Received** (gruener Punkt)
|
|
|
|
### Sortierung der Liste
|
|
1. Pending-Eintraege oben, sortiert nach Bestelldatum (neueste oben)
|
|
2. Eingetroffene Ware darunter, sortiert nach Eingangsdatum
|
|
|
|
### Alarm-Logik
|
|
- Meldebestand am Stammdatum: `ingredients.min_stock_alert` fuer Rohstoffe, `packaging_items.min_stock_alert` fuer Packaging
|
|
- Bestand-Check: `SUM(received) - SUM(produced) - SUM(disposed)`
|
|
- Bei Unterschreitung: roter Badge im Menue + E-Mail an konfigurierbare Adresse (Setting)
|
|
|
|
---
|
|
|
|
## 7. Modul D: Rezeptur & BOM
|
|
|
|
### Rezeptur (Rohstoffe)
|
|
|
|
Bereits in Modul A abgedeckt: INCI-Pivot mit `gram` und `factor`.
|
|
|
|
**Verbrauchsberechnung bei Produktion:**
|
|
```
|
|
Verbrauch pro Einheit = gram x factor
|
|
Verbrauch gesamt = gram x factor x produzierte_stueckzahl
|
|
```
|
|
|
|
### BOM - Bill of Materials (Packaging)
|
|
|
|
Am Produkt werden ueber `product_packagings` die Verpackungskomponenten zugewiesen:
|
|
|
|
| Produkt: Pechsalbe | Packaging-Artikel | Menge |
|
|
|---------------------|-------------------|-------|
|
|
| | Glastiegel 50ml | 1 |
|
|
| | Bambusdeckel | 1 |
|
|
| | Umverpackung Pechsalbe | 1 |
|
|
| | Flyer Pechsalbe | 1 |
|
|
|
|
**UI im Produkt-Formular:**
|
|
- Neuer Abschnitt "Verpackung & Material"
|
|
- Select2-Dropdown zur Auswahl von Packaging-Artikeln
|
|
- Pro Zeile: Artikelname, Material (readonly aus Stammdaten), Gewicht (readonly), Menge pro Einheit
|
|
- Sortierbar
|
|
|
|
### LUCID-Report (Abfallwirtschaft)
|
|
|
|
Aus den Produktionsdaten laesst sich berechnen:
|
|
```
|
|
Produzierte Menge x Packaging-Gewicht -> kg pro Material
|
|
```
|
|
Beispiel: 500 Pechsalben x 120g Glas = 60 kg Glas in Verkehr gebracht.
|
|
|
|
> Report-Funktion wird als spaetere Erweiterung empfohlen (nicht im MVP).
|
|
|
|
---
|
|
|
|
## 8. Modul E: Produktion
|
|
|
|
### Workflow
|
|
|
|
1. User klickt "Neue Produktion"
|
|
2. Dropdown: Produkt auswaehlen
|
|
3. System laedt automatisch:
|
|
- Alle INCIs des Produkts (mit Grammzahl und Faktor)
|
|
- Alle Packaging-Artikel des Produkts
|
|
4. User fuellt aus:
|
|
- Datum (Default: heute)
|
|
- Stueckzahl
|
|
- Produktionsstandort (Dropdown -> locations)
|
|
5. Pro INCI: Auswahl der Charge(n) aus Dropdown der letzten 3 empfangenen `stock_entries` dieses Inhaltsstoffs am gewaehlten Standort
|
|
- Mehrfachauswahl moeglich (Chargen-Splitting)
|
|
- Bei Splitting: User gibt pro Charge die verwendete Menge ein
|
|
|
|
### Bestandsabzug
|
|
|
|
Bei Speicherung:
|
|
1. **Rohstoffe:** `production_ingredients`-Eintraege werden erstellt. Bestand wird rechnerisch reduziert.
|
|
2. **Packaging:** `production_packagings`-Eintraege werden als Snapshot erstellt (= `productions.quantity x product_packagings.quantity`). Bestand wird rechnerisch reduziert.
|
|
|
|
> **Kein physisches Bestandsfeld:** Der aktuelle Bestand wird immer berechnet:
|
|
> `Bestand = SUM(Eingaenge received) - SUM(Produktion consumed) - SUM(Ausgang disposed)`
|
|
> Das vermeidet Inkonsistenzen durch doppelte Datenhaltung.
|
|
|
|
> **Warum Snapshots fuer Packaging?** Wird die BOM eines Produkts nachtraeglich geaendert (z.B. Tiegel-Wechsel), bleiben historische Produktionen korrekt, weil der tatsaechliche Verbrauch zum Produktionszeitpunkt in `production_packagings` festgehalten ist.
|
|
|
|
### MHD-Warnung
|
|
|
|
Wenn bei der Chargen-Auswahl ein Rohstoff-MHD (aus `stock_entries.best_before`) kuerzer ist als das geplante Produkt-MHD, erscheint eine gelbe Warnung.
|
|
|
|
---
|
|
|
|
## 9. Modul F: Bestandsuebersicht & Alarme
|
|
|
|
### Getrennte Views
|
|
|
|
Vier separate Bestandsseiten (je ein Menuepunkt):
|
|
1. **Bestand Rohstoffe**
|
|
2. **Bestand Packaging**
|
|
3. **Bestand Etiketten**
|
|
4. **Bestand Versand & Office**
|
|
|
|
### Darstellung pro Seite
|
|
|
|
Die Lager-Spalten werden **dynamisch** aus der `locations`-Tabelle generiert. Kein Hardcoding von Standortnamen.
|
|
|
|
| Spalte | Beschreibung |
|
|
|--------|-------------|
|
|
| Artikel | Name des Rohstoffs / Packaging-Artikels |
|
|
| *Lager [Name]* | Bestand am Standort (dynamisch pro Location) |
|
|
| **Gesamt** | Summe aller Standorte |
|
|
| Meldebestand | Schwellenwert |
|
|
| Status | OK / Unter Meldebestand |
|
|
|
|
- DataTable mit Sortierung und Suche
|
|
- Zeilen unter Meldebestand werden rot hervorgehoben
|
|
- Wenn neue Lagerorte angelegt werden, erscheinen automatisch neue Spalten
|
|
|
|
### Menue-Badges (View Composer)
|
|
|
|
Ein `InventoryBadgeComposer` wird als Laravel View Composer registriert und an das Sidenav-Layout gebunden:
|
|
|
|
```php
|
|
// AppServiceProvider::boot()
|
|
View::composer('layouts.includes.layout-sidenav', InventoryBadgeComposer::class);
|
|
```
|
|
|
|
Der Composer zaehlt Artikel unter Meldebestand pro Kategorie und gibt die Zahlen an die View weiter. Im Menue werden rote Badges angezeigt (z.B. "Bestand Rohstoffe (2)").
|
|
|
|
**Performance:** Query-Ergebnis wird per Cache (5 Minuten) zwischengespeichert.
|
|
|
|
### E-Mail-Alarm
|
|
|
|
- Cron-Job (taeglich): Prueft alle Bestaende gegen Meldebestand
|
|
- Bei Unterschreitung: E-Mail an konfigurierbare Adresse (Setting: `inventory_alert_email`, Default: `service@gruene-seele.bio`)
|
|
- Registrierung in `app/Console/Kernel.php` (konsistent mit bestehenden Cron-Jobs)
|
|
|
|
---
|
|
|
|
## 10. Modul G: Ausgang / Ausschuss
|
|
|
|
### Felder
|
|
|
|
- Typ (Rohstoff / Packaging / Etikett / Versand & Office)
|
|
- Artikel (Select2, abhaengig vom Typ)
|
|
- Charge (Dropdown, falls bekannt)
|
|
- Lagerort (Dropdown)
|
|
- Menge
|
|
- Einheit (automatisch: gram/piece je nach Typ)
|
|
- Grund (Pflicht-Textfeld)
|
|
- Datum (Default: heute)
|
|
- User: Automatisch aus Session
|
|
|
|
### Listenansicht
|
|
|
|
DataTable mit allen Entsorgungen, filterbar nach Typ und Zeitraum.
|
|
|
|
---
|
|
|
|
## 11. Modul H: Rollen, Berechtigungen & Audit-Trail
|
|
|
|
### Berechtigungs-Konzept
|
|
|
|
Das bestehende Rollen-System ist hierarchisch und ausreichend:
|
|
|
|
| Rolle | Level | Warenwirtschafts-Rechte |
|
|
|-------|-------|------------------------|
|
|
| CopyReader (Redakteur/Ivonne) | admin >= 1 | Wareneingang buchen, Produktion eintragen, Ausgang erfassen, Bestaende einsehen |
|
|
| Admin | admin >= 7 | Wie CopyReader + Lieferanten verwalten, Einkauf anlegen, Preise sehen |
|
|
| SuperAdmin | admin >= 8 | Wie Admin + Stammdaten verwalten (Lagerorte, Qualitaeten, Materialien), Einstellungen |
|
|
|
|
**Umsetzung:** Sensible Felder (Netto-Preise) und Stammdaten-Verwaltung werden ueber bestehende Middleware geschuetzt. Fuer die Stufe-2-Felder (Wareneingang buchen) reicht `copyreader`.
|
|
|
|
### Audit-Trail
|
|
|
|
Jede Bestandsveraenderung wird in `inventory_logs` protokolliert:
|
|
- **Polymorphe Relation:** Verknuepft mit `StockEntry`, `Production`, oder `StockDisposal`
|
|
- **User-ID:** Automatisch aus `Auth::id()`
|
|
- **Changes:** JSON mit alten und neuen Werten
|
|
- Implementierung ueber Laravel Model Events (Observer-Pattern)
|
|
|
|
---
|
|
|
|
## 12. Modul I: Warenwirtschafts-Einstellungen
|
|
|
|
### Konzept
|
|
|
|
Konfigurierbare Einstellungen fuer die Warenwirtschaft werden ueber das bestehende `Setting`-Model (Key-Value-Store) verwaltet. Die Oberflaeche wird analog zum bestehenden `SettingController` (`app/Http/Controllers/SettingController.php`) aufgebaut.
|
|
|
|
### Settings
|
|
|
|
| Slug | Typ | Default | Beschreibung |
|
|
|------|-----|---------|-------------|
|
|
| `inventory_alert_email` | text | `service@gruene-seele.bio` | E-Mail-Adresse fuer Bestands-Alarme |
|
|
| `inventory_alert_enabled` | bool | true | Bestands-Alarm aktiviert? |
|
|
| `inventory_default_location` | int | (erste Location) | Standard-Lagerort fuer neue Eingaenge |
|
|
|
|
### Zugriff
|
|
|
|
- View: `resources/views/admin/inventory/settings.blade.php`
|
|
- Controller: `Admin/Inventory/InventorySettingController`
|
|
- Middleware: `superadmin`
|
|
- Menuepunkt unter "Warenwirtschaft" ganz unten
|
|
|
|
### Erweiterbarkeit
|
|
|
|
Weitere Einstellungen koennen jederzeit ueber `Setting::setContentBySlug()` ergaenzt werden, ohne Migrationen oder Code-Aenderungen am Settings-Mechanismus.
|
|
|
|
---
|
|
|
|
## 13. Menuestruktur
|
|
|
|
Neuer Block im Sidenav zwischen "Produkte" (CopyReader) und dem Admin-Bereich:
|
|
|
|
```
|
|
Warenwirtschaft
|
|
Eingang [copyreader] Badge: Pending-Anzahl
|
|
Ausgang [copyreader]
|
|
Produktion [copyreader]
|
|
----
|
|
Bestand Rohstoffe [copyreader] Badge: Unter Meldebestand
|
|
Bestand Packaging [copyreader] Badge: Unter Meldebestand
|
|
Bestand Etiketten [copyreader] Badge: Unter Meldebestand
|
|
Bestand Versand & Office [copyreader] Badge: Unter Meldebestand
|
|
----
|
|
Lieferanten [admin]
|
|
Lieferanten Kategorien [admin]
|
|
----
|
|
Lagerorte [superadmin]
|
|
Qualitaeten / Sorten [superadmin]
|
|
Verpackungs-Materialien [superadmin]
|
|
Einstellungen [superadmin]
|
|
```
|
|
|
|
---
|
|
|
|
## 14. Abweichungen vom initialen Konzept
|
|
|
|
| Thema | Initiales Konzept | Finale Entscheidung | Begruendung |
|
|
|-------|-------------------|---------------------|------------|
|
|
| Frontend-Stack | Livewire empfohlen | jQuery + Select2 + SortableJS | Konsistenz mit bestehendem Stack, kein Architekturbruch |
|
|
| Datentypen | Milli-Cent / Milli-Gramm (Integer) | DECIMAL | Konsistenz mit bestehendem Schema, ausreichende Praezision |
|
|
| Bestandsfuehrung | Nicht spezifiziert | Berechneter Bestand (kein physisches Feld) | Vermeidet Inkonsistenzen |
|
|
| Etiketten | Eigenstaendige Tabelle | Ueber `packaging_items` mit `category='label'` + optionaler `product_id` | Weniger Tabellen, gleiche Funktionalitaet |
|
|
| Meldebestand | Am Wareneingang | Am Stammdaten-Objekt (`ingredients` / `packaging_items`) | Global gueltig, nicht pro Eingang |
|
|
| Waste-Faktor | Globaler Faktor am Produkt + pro INCI | Nur pro INCI (`product_ingredients.factor`), Default aus `ingredients.default_factor` | Flexibler, einmaliges Anlegen pro Rohstoff, nachkorrigierbar |
|
|
| Lieferanten-Kategorien | 1:1 (FK) | n:m (Pivot `supplier_supplier_category`) | Ein Lieferant kann mehrere Kategorien bedienen |
|
|
| Packaging-Bestandsabzug | Implizit berechnet aus BOM | Snapshot in `production_packagings` | BOM-Aenderungen verfaelschen sonst historische Bestaende |
|
|
| Mengeneinheiten | Implizit aus entry_type | Explizite `unit`-Spalte (ENUM: 'gram', 'piece') | Eindeutig, keine fragile Ableitung noetig |
|
|
| Lager-Spalten | Hardcoded | Dynamisch aus `locations`-Tabelle | Erweiterbar auf n Standorte ohne Code-Aenderung |
|
|
| Einstellungen | Nicht vorgesehen | Settings via bestehendem `Setting`-Model | Nichts hardcoden, erweiterbar |
|
|
| LUCID-Report | Als Feature | Als spaetere Erweiterung | MVP-Fokus |
|
|
| Neue Redakteur-Rolle | Neue Rolle "Redakteur" | Bestehende CopyReader-Rolle (admin >= 1) | Existiert bereits, deckt den Use-Case ab |
|
|
| Architektur-Pattern | Nicht spezifiziert | Repositories fuer CRUD, Services fuer uebergreifende Logik | Konsistent mit bestehendem Code |
|