Warenwirtschaft: Plan V7.0 — Kundenentscheide D-1…D-6 eingearbeitet

Klärungspunkte beantwortet (26.08.): Varianten haben eigene Artikelnummer
(Connector sendet künftig Variations-Nr.); Gramm-Rechnung über ml+Dichte auf
Rohstoff- UND Produktebene (Produkt-Füllgewicht, Rohstoff-Dichte); getrennte
pflegbare Buchungsgründe Ein/Aus; Waldbröl auf Köln übernehmen, Funktion bleibt.
Nur D-3 (Shop-Status-Cron) noch kundenseitig in Prüfung. AP-34/35/42/48/50
entsprechend geschärft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-27 12:07:54 +00:00
parent d93c676b6b
commit 5c5078bf64

View file

@ -23,6 +23,7 @@ Die mit **`#`** markierten Punkte hat der Kunde als **buggy/dringend** priorisie
### AP-34 `#` — Artikelnummer-Zuordnung für Varianten
**Befund (bestätigt):** Bestellungen mit Produktvarianten (z. B. „Häld Deo Creme ohne Natron") werden im Versand nicht automatisch zugeordnet — aktuell manuell nachgepflegt.
**Kundenentscheid 26.08.2026 (D-1):** Jede Variante hat ihre **eigene Artikelnummer** (zusätzlich zur internen Varianten-ID von WooCommerce). ⇒ Der Connector muss die **Variations-Artikelnummer** senden; das genaue Feld wird bei der Umsetzung an einer echten Bestellung verifiziert.
**Ursache (zwei zusammenwirkende Fehler):**
1. **Connector schickt die falsche SKU.** In `dev/partnercenter-api/public/partnercenter-api/partnercenter-api-requests/order.php` liest `compose_cart_content()` mit `get_product_id()` (Zeile ~241) das **Elternprodukt** der Variation und sendet dessen `get_sku()` (Zeile ~257). Die **eigene Artikel-Nr. der Variation** (das „eigene Feld", das der Kunde meint) liegt am Variations-Objekt (`get_variation_id()` / `$order_item->get_product()`) und wird nie gelesen.
2. **Importer zerschneidet die SKU.** `ShoppingUserController::prepareOrder()` ([:728](../../../app/Http/Controllers/Api/ShoppingUserController.php)) nimmt nur den Teil **vor dem ersten „-"** (`explode('-', …)[0]`) und matcht gegen `wp_number`. Das unterstellt „eine Basis-Nr. + Suffix je Variante" — das Gegenteil der Realität: In der Live-DB ist **jede Variante eine eigene `products`-Zeile mit eigener `wp_number`** (z. B. Deocreme 50 ml = 100009, MINI 5 ml = 100029; kein `main_product_id` gesetzt).
@ -32,8 +33,9 @@ Die mit **`#`** markierten Punkte hat der Kunde als **buggy/dringend** priorisie
### AP-35 `#` — Produktionsmengen auf Füllmenge skalieren (statt 100 g)
**Befund (bestätigt):** Bei „HAELD Sanft 50 ml" werden die Rohstoffsummen auf **100 g** statt 50 g gerechnet.
**Ursache:** Die Rezeptur ist in **Prozent** hinterlegt (`product_ingredients.gram`, Spaltenkopf „Anteil %"); die Sollmenge = `gram(%) × factor × qty` **ohne** Multiplikation mit der Füllmenge des Produkts. `qty` ist die Stückzahl, keine Masse. Fundstellen: `ProductionService::buildRecipePayload` ([:562](../../../app/Services/ProductionService.php)), `ProductionService::requiredGramsByIngredient` ([:300](../../../app/Services/ProductionService.php)), JS-Spiegel `productions/_scripts.blade.php:119/138/189`. Live-DB: Produkt 134 hat `contents_total = 50`, Rezeptursumme = 100,0 %.
**Lösung:** Sollmenge = `(gram/100) × contents_total × factor × qty`, konsistent an allen drei Stellen + INCI-Spalten. Der Kunde erwartet für 50 ml = 50 g (⇒ Produktebene 1 g/ml angenommen; feinere Umrechnung siehe AP-42 nur für **Rohstoff**-Bestand).
**Aufwand:** 1–2 PT · **Klärung:** D-2 (Produkt-Nettomasse/1 g/ml-Annahme).
**Lösung:** Sollmenge = `(gram/100) × Füllgewicht(g) × factor × qty`, konsistent an allen drei Stellen + INCI-Spalten.
**Kundenentscheid 26.08.2026 (D-2/D-4):** Grundsätzlich wird in **Gramm** gerechnet, aber über **ml + Dichte**. Am Produkt daher optionales **Füllgewicht in Gramm** (bzw. Dichte g/ml auf `contents_total`); ist es leer, gilt `contents_total` als Gramm (1 g/ml). So ergibt „50 ml" korrekt 50 g bzw. das reale Gewicht bei abweichender Dichte.
**Aufwand:** 1–2 PT (inkl. Produkt-Füllgewichtsfeld) · **Bezug:** AP-42 (gleiches Gramm/ml/Dichte-Prinzip auf Rohstoffebene).
### AP-36 `#` — Rezeptur-Textphasen ohne Rohstoffe anzeigen
**Befund (bestätigt):** Phasen ohne Rohstoffzeile (z. B. „Vorbereitung"/„Nachbereitung" mit reinem Hinweistext) erscheinen in der Produktion nicht.
@ -70,8 +72,9 @@ Die mit **`#`** markierten Punkte hat der Kunde als **buggy/dringend** priorisie
### AP-42 `#` — Rohstoff-Dichte (g/ml) für realen Bestand
**Befund:** Rohstoffe werden in **Gramm** geführt; eine in ml eingekaufte Tinktur (z. B. Propolis 0,79 g/ml → 100 ml = 79 g) verfälscht sonst den Bestand.
**Lösung:** Nullable-Feld `ingredients.density` (g/ml). Ist es gesetzt und wird der Wareneingang in **ml** erfasst, wird bei der Buchung `ml × density` in Gramm umgerechnet und wie bisher als Gramm gespeichert (alle Aggregationen in `InventoryService` bleiben unverändert). Leeres Feld = Gewicht wie eingekauft (heutiges Verhalten).
**Aufwand:** 1,5–2,5 PT · **Klärung:** D-4 (ml-Erfassung im Wareneingang, Enum `unit`).
**Kundenentscheid 26.08.2026 (D-4, = Antwort auf D-2):** Es wird in **Gramm** geführt, aber Erfassung auch in **ml** möglich; über die hinterlegte **Dichte** ergeben sich die Gramm. Umsetzung bestätigt.
**Lösung:** Nullable-Feld `ingredients.density` (g/ml) + ml-Erfassung im Wareneingang (Enum `unit` um `ml` erweitern). Ist Dichte gesetzt und wird in ml erfasst, wird bei der Buchung `ml × density` in Gramm umgerechnet und wie bisher als Gramm gespeichert (Aggregationen in `InventoryService` unverändert). Leeres Feld = Gewicht wie eingekauft.
**Aufwand:** 1,5–2,5 PT.
---
@ -105,8 +108,9 @@ Aktuell nach `pos` sortiert (`Product::manufacturer_ingredients() ->orderByPivot
### AP-48 — Produktbestand: Eingangs-/Ausgangsgründe editierbar
**Befund:** Buchungsgründe sind hartkodiert (`ProductStockController::manualReasons()`), gleiche Liste für Ein-/Ausgang.
**Lösung:** Eigene CRUD unter Einstellungen → Allgemeines nach Vorbild `DisposalReasonController`/`SettingCrudController` (Modell + Tabelle + FormRequest + Views), mit den 7 heutigen Werten geseedet; optional `direction`-Spalte, falls Ein-/Ausgang divergieren sollen.
**Aufwand:** 1–1,5 PT · **Klärung:** D-5 (getrennte Listen Ein/Aus?).
**Kundenentscheid 26.08.2026 (D-5):** Eingang und Ausgang haben **getrennte** Gründe-Listen, beide pflegbar.
**Lösung:** Eigene CRUD unter Einstellungen → Allgemeines nach Vorbild `DisposalReasonController`/`SettingCrudController` (Modell + Tabelle + FormRequest + Views) mit **`direction`-Spalte** (in/out) → zwei getrennt pflegbare Listen; mit den heutigen Werten geseedet.
**Aufwand:** 1–1,5 PT.
### AP-49 — Inhaltsstoffe: Lieferanten-Dropdown nur Rohstoff-Lieferanten
**Befund:** `IngredientController` ([:49](../../../app/Http/Controllers/IngredientController.php)) lädt **alle** aktiven Lieferanten. Filtermechanik vorhanden: `Supplier::supplierCategories()` (Kategorie „Kosmetikrohstoffe", id 1).
@ -115,8 +119,9 @@ Aktuell nach `pos` sortiert (`Product::manufacturer_ingredients() ->orderByPivot
### AP-50 — Lagerorte: „Waldbröl" löschbar + „Köln" als Default
**Befund:** Löschsperre greift, weil „Waldbröl" (id 2) von **3 `stock_entries`** referenziert wird (`DeletionGuard::locationBlockers`). Produktion hat Köln-Default bereits, **Wareneingang nicht**.
**Lösung:** (a) Löschen ermöglichen, ohne die Guard-Idee aufzugeben — Alt-Wareneingänge auf Köln umhängen **oder** Soft-Delete zulassen und inaktive Orte aus Auswahllisten ausblenden (`Location` nutzt SoftDeletes). (b) Köln als Default auch im Wareneingang (`StockEntryController` + `_form.blade.php:88`) und in weiteren Auswahlorten.
**Aufwand:** 0,5–1 PT · **Klärung:** D-6 (Alt-Einträge umhängen vs. Soft-Delete).
**Kundenentscheid 26.08.2026 (D-6):** Waldbröl gibt es nicht mehr — alles ist Köln; die 3 Alt-Einträge auf **Köln umhängen**. Die Standort-Funktion muss aber **erhalten** bleiben (künftig könnte ein weiterer Standort dazukommen).
**Lösung:** (a) Alt-Wareneingänge von Waldbröl auf Köln umbuchen, dann Waldbröl löschen/soft-deleten; die Guard-Logik bleibt für belegte Orte bestehen. (b) Köln als Default auch im Wareneingang (`StockEntryController` + `_form.blade.php:88`) und in weiteren Auswahlorten.
**Aufwand:** 0,5–1 PT.
### AP-51 — Lagerplatz-Felder für Produkte & Verpackung
**Befund:** Lagerort-Felder (Raum/Regal/Fach) gibt es bisher nur bei Rohstoffen.
@ -125,16 +130,18 @@ Aktuell nach `pos` sortiert (`Product::manufacturer_ingredients() ->orderByPivot
---
## D. Klärungspunkte (vor Umsetzung)
## D. Klärungspunkte — beantwortet (Kundenentscheide 26.08.2026)
| # | Frage | Betrifft |
|---|---|---|
| **D-1** | Was steht in WooCommerce **je Variation** im SKU-Feld — exakt die ERP-`wp_number` (z. B. `100009`, `100029`) oder ein anderes Schema/Custom-Feld? Hat das Elternprodukt eine SKU, und soll sie je als Fallback dienen? Gibt es Altprodukte, die wirklich das „Basis-Nr. + Suffix"-Schema (`10042-1`) nutzen? | AP-34 |
| **D-2** | Produktebene: Reicht die Annahme **1 g/ml** (Rezeptursumme = Füllmenge in ml), oder braucht es ein separates **Netto-Gewichtsfeld (g)** je Produkt? Der Kunde erwartet 50 ml → 50 g. | AP-35 |
| **D-3** | Läuft im Shop der Cron `partnercenter_api_fetch_orders_status` und sind die API-Zugangsdaten gültig? (Zuerst prüfen — vermutlich die eigentliche Ampel-Ursache.) Farbsemantik für `Abholung`/`storniert`? | AP-38 |
| **D-4** | Soll der **Wareneingang** künftig eine **ml-Erfassung** bekommen (Enum `unit` um `ml` erweitern), damit die Dichte-Umrechnung greift? Für welche Rohstoffe ist Dichte relevant? | AP-42 |
| **D-5** | Produktbestand: sollen **Eingang** und **Ausgang** getrennte Gründe-Listen haben oder eine gemeinsame? | AP-48 |
| **D-6** | Lagerort „Waldbröl": die 3 alten Wareneingänge auf **Köln umhängen** oder Ort nur **soft-löschen** (historische Zuordnung bleibt)? | AP-50 |
| # | Frage | Antwort des Kunden | Betrifft |
|---|---|---|---|
| **D-1** | Wo im Shop liegt die Artikelnummer der Variante? | ✅ Jede Variante hat ihre **eigene Artikelnummer** (+ interne Varianten-ID). Connector sendet künftig die Variations-Artikelnummer; genaues Feld bei Umsetzung an echter Bestellung verifizieren. | AP-34 |
| **D-2** | Produktebene 1 g/ml oder eigenes Gewichtsfeld? | ✅ In **Gramm** rechnen, aber über **ml + Dichte**. Produkt bekommt optionales **Füllgewicht (g)**; leer = 1 g/ml. | AP-35 |
| **D-3** | Läuft der Shop-Status-Cron / sind die Zugangsdaten gültig? | ⏳ **Wird kundenseitig geprüft.** ERP-Mapping-Lücken (Status 5/pick_up/cancel) setzen wir unabhängig um. | AP-38 |
| **D-4** | ml-Erfassung im Wareneingang? | ✅ Ja — Erfassung in **ml** + **Dichte** je Rohstoff, Umrechnung auf Gramm. (= Antwort auf D-2.) | AP-42 |
| **D-5** | Ein-/Ausgang gemeinsame oder getrennte Gründe? | ✅ **Getrennte**, beide pflegbare Listen. | AP-48 |
| **D-6** | Waldbröl umhängen oder soft-löschen? | ✅ Alles auf **Köln** übernehmen, Waldbröl weg; Standort-Funktion für künftige Standorte **erhalten**. | AP-50 |
**Damit ist nur noch D-3 offen** (Shop-seitige Prüfung) — und blockiert lediglich die endgültige Ursachenbestätigung von AP-38, nicht die übrigen Pakete.
---