29 KiB
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
- Ist-Analyse & identifizierte Probleme
- Architektur-Entscheidungen
- Datenmodell
- Modul A: INCI-Management (Bugfix & Erweiterung)
- Modul B: Stammdaten
- Modul C: Wareneingang (Zwei-Stufen-System)
- Modul D: Rezeptur & BOM (Bill of Materials)
- Modul E: Produktion & Bestandsabzug
- Modul F: Bestandsuebersicht & Alarme
- Modul G: Ausgang / Ausschuss
- Modul H: Rollen, Berechtigungen & Audit-Trail
- Modul I: Warenwirtschafts-Einstellungen
- Menuestruktur
- Abweichungen vom initialen Konzept
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
ProductIngredienthasMany-Relation: Falscher zweiter Parameter ('product_ingredients'statt'product_id'). Muss korrigiert werden.updateIngredients(): Fuegt nur neue INCIs hinzu, entfernt deselektierte nicht. Sync-Logik fehlt.HTMLHelper::getProductIngredientsOptions(): KeinorderBy('name')- Dropdown nicht alphabetisch.- Produkt-Kopierung: INCI-Reihenfolge nicht deterministisch, da kein
orderByauf Pivot. - Attribute-Kopierung:
$this->model->attribute_type_idexistiert 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Ⓜ️ 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 |
| 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_factornachkorrigiert werden - zukuenftige Zuweisungen erben dann den neuen Wert. Bestehendeproduct_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_factoram 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_alertam Wareneingang. Der Meldebestand wird am Stammdaten-Objekt gefuehrt:ingredients.min_stock_alertfuer Rohstoffe,packaging_items.min_stock_alertfuer 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_ingredientsfuer denselbeningredient_idmit unterschiedlichenstock_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.quantityberechnet werden. Wird die BOM eines Produkts nachtraeglich geaendert (z.B. Tiegel-Wechsel), waeren historische Bestandsberechnungen falsch.production_packagingsspeichert 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 korrigierenIngredient::product_ingredients()- Falschen Parameter korrigierenProductRepository::updateIngredients()-sync()-Logik implementieren statt nur hinzufuegenProductRepository::copy()- Attribut-Kopierung fixen ($attribute->type_id)
A2. Migration: Pivot erweitern
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
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)
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()erhaeltorderBy('name')- Select2-Widget mit Mehrfachauswahl
- Die Reihenfolge der Auswahl bestimmt die initiale
posbeim 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
factorautomatisch ausingredients.default_factoruebernommen
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-Felderpos,gram,factormitkopieren- 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)
unitwird 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
- Pending-Eintraege oben, sortiert nach Bestelldatum (neueste oben)
- Eingetroffene Ware darunter, sortiert nach Eingangsdatum
Alarm-Logik
- Meldebestand am Stammdatum:
ingredients.min_stock_alertfuer Rohstoffe,packaging_items.min_stock_alertfuer 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
- User klickt "Neue Produktion"
- Dropdown: Produkt auswaehlen
- System laedt automatisch:
- Alle INCIs des Produkts (mit Grammzahl und Faktor)
- Alle Packaging-Artikel des Produkts
- User fuellt aus:
- Datum (Default: heute)
- Stueckzahl
- Produktionsstandort (Dropdown -> locations)
- Pro INCI: Auswahl der Charge(n) aus Dropdown der letzten 3 empfangenen
stock_entriesdieses Inhaltsstoffs am gewaehlten Standort- Mehrfachauswahl moeglich (Chargen-Splitting)
- Bei Splitting: User gibt pro Charge die verwendete Menge ein
Bestandsabzug
Bei Speicherung:
- Rohstoffe:
production_ingredients-Eintraege werden erstellt. Bestand wird rechnerisch reduziert. - 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_packagingsfestgehalten 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):
- Bestand Rohstoffe
- Bestand Packaging
- Bestand Etiketten
- 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:
// 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, oderStockDisposal - 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 |