- products.whitelabel_variant_id, product_images.position/size_cm mit Backfill aus Produktnamen
- WhiteLabelResolver: Farbe vom Produkt, Kundenlogo je Position, Fehler statt stillem Rückfall; Altlogik für Produkte ohne Variante
- Attribute: Variantenwert-Parsing und Farbname ohne Größe
- Produkt- und Kundenformular um Variante, Position und Größe ergänzt
- Lieferschein noch nicht umgestellt (WL-03, wartet auf K6/K7)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Etikettgröße im Lieferschein korrekt parsen ("40#3,5" ergab 3 statt 3,5 cm)
- Produktkopie optional inkl. Kundenzuordnungen und Logo-Dateien (zweiter Kopier-Link)
- whitelabel:transfer-assignments: gezielte, wiederholbare Übertragung ohne Dubletten
- whitelabel:migrate-colors: Zuordnungen anhand der Bildfarbe auf die Farbprodukte übertragen (Dry-Run ohne --execute)
- whitelabel:report: rein lesende Bestandsprüfung (fehlende Zuordnungen, Farbwidersprüche, Dateien, Dubletten)
- Migration für attributes.value (Schema-Drift, auf Live No-op)
- Analyse/Entwicklungsplan um Gegenprüfung, systemischen Befund und neue Paketreihenfolge ergänzt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neuer Klappbereich „Bestandsrechnung nachvollziehen" zeigt alle gebuchten
Eingänge – auch aufgebrauchte mit Rest 0 – mit Produktionsverbrauch,
Ausschuss mit Chargenbezug und dem nach FEFO zugerechneten Ausschuss ohne
Chargenbezug; Summenzeile bis zum Gesamtbestand identisch zur Kopfzahl.
Alt-Ausschuss ohne Chargenbezug wird einzeln mit Grund/Notiz/Bearbeiter
gelistet. Anlass: Live-Fall Sonnenblumenöl (doppelte Korrektur vom 28.07.).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bisher ließen sich nur offene Bestellungen bearbeiten; ein Tippfehler in
einem gebuchten Wareneingang war nicht korrigierbar (Anforderung 14.09.2026).
- Bearbeiten/Korrigieren für gebuchte Eingänge (Admin): Bestell- und
Eingangsdaten (Menge, Datum, Charge, MHD, Qualität, Preise, Lieferant,
Lagerort). Menge wirkt sofort auf den Bestand; ml-Eingänge werden in ml
korrigiert und über die gespeicherte Dichte in Gramm gebucht.
- Schutz: Menge nicht unter den bereits verbrauchten Anteil (Rohstoff je
Eingang, Verpackung je Artikel); bei Verbrauch sind Art und Rohstoff/
Artikel gesperrt (Formular + Server).
- Änderungsprotokoll (stock_entry_changes): wer/wann/Grund + geänderte
Felder alt → neu; Anzeige auf der Detailseite. Grund bei gebuchten
Eingängen Pflicht, bei offenen optional. Speichern ohne Änderung
erzeugt keine Zeile.
- Tests (6 neue), In-App-Doku (Hinweise + Leitfaden 3.2) nachgezogen.
Migration: 2026_09_14_110000_create_stock_entry_changes_table.
Suite: 396 grün.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Einheit hing bisher als Anhang rechts am Mengenfeld — auf breiten
Monitoren außer Sicht, und „g" war stiller Default. Jetzt:
- Eigenes Pflichtfeld „Einheit der bestellten Menge" direkt VOR der Menge
mit „Bitte wählen"; beim Neuanlegen muss bewusst Gramm oder Milliliter
gewählt werden, beim Bearbeiten ist die gespeicherte Einheit vorbelegt.
- Hinweis unter der Einheit sagt, ob ml möglich ist (Dichte hinterlegt,
Wert wird genannt) oder erst gepflegt werden muss; ml bleibt ohne Dichte
gesperrt. Mengen-Hinweis und ml→g-Live-Vorschau folgen der Auswahl.
- Server: Store-/UpdateStockEntryRequest weisen Rohstoff-Einkäufe ohne
Einheit ab (Verpackung bleibt ohne Einheit). Bestehende Tests ergänzt,
neuer Test für die Pflicht.
- In-App-Doku (Hinweise, Testleitfaden) nachgezogen.
Suite: 390 grün.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ergänzt die selbsterklärende Dichte um eine fette Live-Vorschau direkt am
Mengenfeld — in beiden Stufen des Wareneingangs:
- Einkauf erfassen (Stufe 1): bei Einheit „ml" zeigt sich beim Tippen
„<ml> ml × <Dichte> g/ml = <g> g"; der Hinweistext bleibt Führung.
- Wareneingang buchen (Stufe 2): gleiche Vorschau am Feld „Eingegangene
Menge", wenn in ml eingekauft wurde.
So ist an jeder Eingabestelle sofort sichtbar, wie und wann die Dichte
rechnet. Kein Mechanik-Wechsel; Speicherung bleibt in Gramm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die Dichte (g/ml) wirkte "unverbunden": Sie wird nur beim Einkauf in ml
gebraucht (ml × Dichte = Gramm), der Bestand läuft immer in Gramm. Wer in
Gramm erfasst, sah die Dichte nirgends wirken. Zudem verschwamm sie mit dem
daneben stehenden Schwundfaktor.
- Rohstoff-Formular: Hilfetexte trennen beide Faktoren eindeutig
(Schwundfaktor = Rezeptur/Produktion, Dichte = nur ml-Einkauf) und die
Dichte zeigt eine Live-Vorschau „1 ml = x g / 100 ml = y g".
- Rohstoffbestand-Detailseite: Dichte als Stammdatum mit Kurzerklärung
eingeblendet, damit der Wert dort sichtbar ist, wo der Bestand steht.
Kein Mechanik-Wechsel; Umrechnung/Speicherung in Gramm bleiben unverändert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- exclude_single_commission: Rollen (z. B. Barkunde 0 %) ziehen den
produktseitigen Kunden-Einzelrabatt (single_commission/value_commission)
nicht mehr ab. Behebt: 0-%-Rolle zog trotzdem die Produktprovision.
- hide_registration: interne Rollen (z. B. Mitarbeiter) werden im Wizard
und der Mitgliedschafts-Auswahl ausgeblendet, bleiben im Admin/Lead
(HTMLHelper) weiter zuweisbar.
- Migration 2026_09_11_100000 (beide Flags, Default false), Model-casts/
fillable, Admin-Formular (2 Checkboxen), Warenkorb-Gate in Yard.
- Test tests/Feature/UserLevelSingleCommissionTest.php.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Stats\Sales: alle Summenzeilen (Angezeigt/Ausgeblendet/Gesamt/Rabatte/Netto)
aus price_net × qty der Positionen statt aus subtotal_full/discount des
Bestellkopfs; WooCommerce-Importe (txaction extern) hatten dort NULL und
zählten seit 06/2026 mit 0 € (Aug 2026: 449 € statt 3.318 € angezeigt)
- Kompensationsprodukte (comp > 0) bleiben in der Menge, neue Spalten
„davon Kompensation“ (aktuell + Vorjahr) und Summenzeile 9993
- Gelöschte Produkte bleiben im Umsatz sichtbar (withTrashed, „(gelöscht)“)
- WP-Import setzt subtotal_full und discount jetzt wie der Checkout
- Migration füllt subtotal_full/discount bestehender Extern-Bestellungen nach
- Excel-Export um die beiden Kompensationsspalten erweitert
- Test tests/Feature/StatsSalesVolumeTest.php (Konsistenz, Filter, Jahr)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prüfbericht pruefung-ap53-mengenpraezision-2026-09-10.md, alle Punkte umgesetzt:
- AP53-01: Toleranz nur noch in ProductionService (TOLERANCE_RATIO 0,05, TOLERANCE_FLOOR 0,005, COMPARE_EPSILON); das Produktions-JS rendert die Konstanten aus dem Service statt eigener Zahlen (Ampel hatte noch Sockel 0,05 g).
- AP53-02: withinTolerance() vergleicht mit Float-Ausgleich 1e-9 — 0,025 g auf 0,020 g Soll ist wieder zulässig, 0,026 g nicht; Sollwerte werden nicht gerundet.
- AP53-03: Chargen-Hauptzeile und Standortsumme im Rohstoff-Detail mit formatWawiQuantity(…, 0) (0,125 g statt 0 g).
- AP53-04: Ausschuss-Fehlermeldungen (Request ohne/mit Charge, Controller-Wiederholung) mit formatWawiQuantity().
- 4.1: Obergrenze decimal(12,3) bewusst akzeptiert (Live max. 49.000 g) und als InventoryService::QUANTITY_MAX validiert; Bereichsprüfung + Rollback-Hinweis in der Deploy-Checkliste.
- 4.2: normalizeWawiQuantity() (parsen + drei Stellen) in Bestell-/Wareneingangs-/Ausschuss-Requests, parseQuantity() rundet kanonisch für batch_key- und stock_entry_id-Zeilen; Untergrenze 0,001 g; ml-Eingaben, die umgerechnet 0,000 g ergeben, werden abgelehnt (Store/Update/Receive).
- 4.3: Einkaufs-JS trennt formatQuantity (3), formatPrice (2, wie vor AP-53) und formatDensity (4).
- 4.4: Hinweise präzisiert (Übersichten ganzzahlig, Rundungs-/Untergrenzregel), Leitfaden §26 mit Sockelrand-, Hauptzeilen- und Meldungs-Prüfschritten, Plan V7 und README aktualisiert, Prüfbericht Abschnitt 6 mit Nacharbeitsstand.
Tests: QuantityPrecisionTest +6 (AP53-01…04, 4.2 Rundung/Untergrenze/Obergrenze, ml→0 g). Suite 384 grün / 1.592 Assertions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kundennachtrag 09.09.2026: Ist-Mengen je Charge in der Produktion sollen auf das Milligramm eingegeben werden können (z. B. 0,125 g). Bisher rundeten Datenbank (decimal 12,2), Bestandslogik, Anzeige und Toleranzsockel auf zwei Stellen; „0,125" wurde still zu 0,13, „0.125" nach der Punkt-Regel zu 125 g.
- Migration 2026_09_09_100000: production_ingredients.quantity_used, stock_entries.ordered_quantity/received_quantity, stock_disposals.quantity → decimal(12,3); Casts decimal:3. Auf der Dev-DB (Live-Kopie) ausgeführt, 53/78/7 Zeilen unverändert.
- InventoryService::QUANTITY_DECIMALS = 3; roundWawiQuantity() ersetzt alle round(…, 2) in InventoryService, ProductionService, StockDisposalController/-Request, StockEntryRepository/-Controller, RawMaterialStockController.
- formatWawiQuantity($v, $min = 2): bis drei Stellen, mindestens zwei — Produktionsdetail, Einkauf Liste/Detail/Formular, Ausschuss-Liste, Chargen-Rest im Rohstoff-Detail.
- Eingaberegel (reFormatWawiNumber, JS parseGerman/parseNumber): zusätzlich „0.xxx" als Dezimalzahl; „1.500" bleibt 1500. Produktions-JS: fmt() bis drei Stellen, fmtInput() schreibt gespeicherte Zahlen deutsch ins Feld (1,125 statt 1.125 = 1125).
- Soll-Ist-Toleranz: Sockel 0,05 → 0,005 g; Ausschuss min 0,001.
- Doku: Hinweise (Neu/geändert), Leitfaden Version 3.1 mit Abschnitt 26, Plan V7 AP-53, README (Deploy-Checkliste, Suite 378).
Tests: QuantityPrecisionTest (6) — Parsing, Anzeige, Produktion 0,125 g exakt + Rest 9,875 g, Entwurf 1,125 wiederöffnen, Toleranz Kleinmengen, Ausschuss 0,005 g.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kundenfeedback 09.09.2026: Das Kästchen „Produktionsrelevant" oben im Produktformular wurde nicht gefunden. Es sitzt jetzt in der Karte „Warenwirtschaft" direkt unter „Im Produktbestand anzeigen", heißt „In der Produktion anzeigen (Produkt wird selbst hergestellt)" und erklärt im Hilfetext die Wirkung (Zukaufartikel wie Aloe abwählen: keine Anzeige in der Produkt-Auswahl der Produktion und in „Enthalten in"; Produktionsergebnis, Produktbestand und Verkauf unberührt). Feld hält jetzt auch old()-Werte nach Validierungsfehlern.
In-App-Doku (Hinweise, Leitfaden §13/§20) entsprechend umformuliert; Test prüft Label und Platzierung.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Je Verpackungsartikel Bestand (Stück), Differenz zu Bedarf (rot/grün wie bei
Rohstoffen, AP-40) und Lagerplatz; Spalte einheitlich „Lagerort“ für Rohstoffe
(INCI-Tabelle, vorher „Ablage“) und Verpackung.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ob der Snapshot gilt, entscheidet der Buchungsstatus (nicht die Zeilenzahl);
ohne Verpackung gebuchte Produktionen zeigen „Kein Verpackungsverbrauch gebucht“.
Tests R-01/R-02.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mfg_phase_id[] je Phasenblock; Zuordnung bestehender Phasen über ID, dann Name,
dann gemeinsame Rohstoffe. Ein vorangestellter neuer Block übernimmt nicht mehr
die ID einer umbenannten Phase; Produktionszeilen behalten ihre Phase.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Formular, Update-Validierung und Repository nutzen bei unverändertem Rohstoff den
Snapshot als Rückfall; bei Rohstoffwechsel wird keine fremde Dichte übernommen.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bearbeitungsformular gebuchter Produktionen liefert Snapshot und Ergebnissummen;
die Vorschau folgt derselben Regel wie die Speicherung (F-05) und weist auf eine
Neuberechnung hin, sobald sich Produktsummen ändern. Tests N-01/N-02/N-04.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nur Hersteller-Rezeptur, alle Phasenzeilen je Produkt summiert, Skalierung
über Füllgewicht und Faktor wie in der Produktion; kein roher Pivot-Wert mehr.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Board-Statuswechsel auf „versendet“ prüft serverseitig isReadyToShip()
(Rechnung/API weiterhin erlaubt); Button im Board ohne Freigabe deaktiviert.
Konfektionieren bleibt vor Zahlungseingang möglich.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stückliste je Ergebnisprodukt wird ins Formular gegeben; die Vorschau rechnet
Outputs × Stückliste (wie die Buchung) und reagiert auf Hinzufügen/Entfernen,
Produkt- und Mengenwechsel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Vorbelegung der Chargen-Zeilen zentral in ProductionController::existingLines();
old('ingredient_lines') hat Vorrang vor dem gespeicherten Stand.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
production_ingredients.recipe_phase_id; Formular sendet die Phase je Zeile,
Vorbelegung je Rohstoff|Phase. Altbestand ohne Phase wird nur einmal eingesetzt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entwürfe speichern batch_key roh (stock_entry_id nullable), keine Auflösung
und keine Filterung unfertiger Zeilen mehr beim Zwischenspeichern. Eine nicht
mehr verfügbare Charge wird im Formular markiert statt alle Zeilen zu verwerfen.
Ergebniszeilen dürfen im Entwurf ohne Stückzahl bleiben. Auflösung, Prüfung und
Buchung erst beim Abschluss.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Planung oben bleibt auf produktionsrelevante Produkte gefiltert (AP-47);
Ergebniszeilen bieten alle aktiven Einzelprodukte plus bereits gespeicherte
Output-Produkte an (Bearbeiten/Kopieren verliert keine Zuordnung mehr).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reine Optik der Chargen-Erfassung (Produktions-Maske):
(a) Phasenkasten leicht hinterlegt (~3,5 %); der innere Kasten um die
Chargen-Dropdowns entfällt, stattdessen dezente Trennlinie je Rohstoff.
(b) Schriftgrößen vereinheitlicht und schwarz (wie die INCI-Liste) —
Rohstoffname, Soll-Label und Phasenname einheitlich.
(c) iPad/Touch: Eingabefelder (Charge/Menge) mind. 44 px hoch und 16 px
Schrift (verhindert iOS-Auto-Zoom) über @media (pointer: coarse).
(d) Phasenbeschreibung formatiert dargestellt wie auf Produktebene eingegeben
(Zeilenumbrüche erhalten via white-space: pre-wrap; weiterhin .text(),
also kein HTML-Injection-Risiko).
Umsetzung in _form_fields.blade (scoped <style>) + _scripts.blade
(recipe-item statt border-Kasten, recipe-soll-label, recipe-phase-note).
Test: Render-Smoke der Neu-Maske prüft Buttons (AP-43) + Optik-Hooks (AP-44).
Suite 337 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Speicher-Button heißt jetzt „Produktion abschließen"; daneben gibt es
„Zwischenspeichern (Entwurf)". Ein Entwurf sichert den Stand ohne Pflichtfeld-
und ohne Bestandsprüfung und bucht KEINEN Bestand (Roh-/Produkt-/Verpackung).
Erst „abschließen" prüft vollständig und bucht.
- Migration: productions.status (default 'completed'; Bestand ist bereits gebucht).
- Production-Model: STATUS_DRAFT/COMPLETED, isDraft(), scopeBooked().
- InventoryService: Entwürfe fallen aus allen Verbrauchs-/Bestandsjoins heraus
(status != draft an den 4 productions-Joins).
- ProductionService store()/updateProduction() bekommen $draft: Entwurf
überspringt Rezeptur-/Mengen-/Bestandsprüfung + Buchungen, sichert nur
vollständige Chargen-Zeilen (filterCompleteLines) und die angegebenen Outputs.
Ein abgeschlossene Produktion kann NICHT zurück in den Entwurf (sonst
verwaisen die Buchungen) — Guard wirft ValidationException.
- StoreProductionRequest: save_mode=draft => nur Kopfdaten Pflicht, Chargen/
Ergebnis optional; validatedPayload filtert unvollständige Zeilen/Outputs.
- Controller: Entwurf löst Chargen tolerant auf (nicht auflösbare Chargen werden
übersprungen, Entwurf ist immer speicherbar) und leitet zur Bearbeitung zurück.
- Views: create/edit „abschließen" + „Zwischenspeichern"-Button (nur solange
Entwurf); Entwurf-Badge in Liste + Bearbeiten-Kopf.
Tests: 5 neue (kein Produktbestand, kein Rohstoffverbrauch, Abschluss bucht,
Rück-Entwurf verboten, HTTP-Entwurf ohne Ergebnis ok / Abschluss erzwingt
Ergebnis). Migration auf Dev-/Live-DB. Suite 336 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lagerplatz-Zuordnung (Raum/Regal/Fach) gab es bisher nur bei Rohstoffen. Jetzt
dieselbe Storage-Hierarchie (StorageRoom/Shelf/Slot) auch für Produkte und
Verpackungsartikel, inkl. Anzeige an den passenden Stellen.
- Migration: storage_room_id/shelf/slot an products + packaging_items
(nullable, FK constrained, nullOnDelete).
- Models Product + PackagingItem: fillable + storageRoom/Shelf/Slot-Relationen +
storageLabel() („Raum 1 | Regal 3 | A").
- Produktformular (Inventar-Abschnitt) + Verpackungsformular: Auswahl-Zeile
Raum/Regal/Fach; Optionen kommen aktiv/sortiert aus den zentral pflegbaren
Bausteinen (Einstellungen → Allgemein).
- Speichern: ProductRepository normalisiert leere Auswahl -> null (FK);
PackagingItem-Requests + Repository um die drei Felder erweitert.
- Anzeige: Produktbestand (Sub-Zeile mit Marker), Produktverpackung-Übersicht
(neue Spalte „Lagerplatz").
- DeletionGuard: Lagerbaustein ist nicht löschbar, solange Produkte/Verpackung
darauf verweisen (analog Rohstoffe).
Tests: 4 neue (Produkt speichern + leer->null, Verpackung speichern + Anzeige,
Löschsperre, freier Baustein löschbar). Migration auf Dev-/Live-DB gelaufen.
Suite 331 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kundenentscheid 26.08. (D-5): Eingang und Ausgang haben eigene, pflegbare
Gründe-Listen statt einer festen Liste im Controller.
- Neues Modell/Tabelle stock_movement_reasons (label, direction in/out, pos,
active, SoftDeletes) inkl. Seed der bisherigen Gründe sinnvoll auf Ein-/Ausgang
aufgeteilt.
- CRUD wie DisposalReason (SettingCrudController + FormRequest + Formular mit
Richtungs-Auswahl); zwei Listen (Eingangs-/Ausgangs-Gründe) auf der Seite
Einstellungen → Allgemeines, „Neu anlegen" mit vorbelegter Richtung.
- Produktbestand-Maske: das Grund-Dropdown wird jetzt je nach Eingang/Ausgang
per JS aus den passenden, gepflegten Gründen befüllt (manualReasons entfernt).
- DeletionGuard: StockMovementReason immer löschbar.
Migration create_stock_movement_reasons auf Dev-/Live-DB. 4 Tests, Suite 326 grün.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kundenentscheid 26.08.: „Waldbröl" gibt es nicht mehr, alles wird Köln; die
Standort-Funktion bleibt für künftige Standorte.
- LocationController::destroy überschrieben: ein belegter Lagerort lässt sich
löschen, indem seine Buchungen (Wareneingänge, Produktionen, Ausschuss)
transaktional auf einen Ziel-Lagerort übernommen werden. Ohne Ziel-Lagerort
bleibt die Sperre bestehen; unbelegte Orte werden direkt gelöscht.
- Lagerort-Liste: Ziel-Auswahl („→ Köln" vorbelegt) im Lösch-Formular.
- Wareneingang: Köln als Standard-Lagerort vorbelegt (wie in der Produktion).
Außerdem AP-49-Folgeanpassung: Test-Helper hängt die Kategorie „Kosmetikrohstoffe"
an, damit Lieferanten im gefilterten INCI-Formular erscheinen; Lagerort-Sperr-Test
prüft die neue Meldung. Suite 322 grün.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kundenentscheid 26.08.: Rohstoffe werden in Gramm geführt, dürfen aber in ml
erfasst werden; über die Dichte ergeben sich die Gramm (100 ml Propolistinktur
× 0,79 = 79 g).
- Neues Feld ingredients.density (g/ml, optional) inkl. Feld im INCI-Formular.
- Wareneingang: Einheiten-Umschalter g/ml am Mengenfeld (nur wenn Dichte
hinterlegt; Live-Hinweis mit Gramm-Umrechnung, Dichte kommt aus der
Rohstoff-Suche mit). Erfassung in ml wird über die Dichte in Gramm
umgerechnet und kanonisch als Gramm gespeichert — InventoryService und alle
Aggregationen bleiben unverändert. purchase_unit + density_used (Snapshot)
protokollieren die ml-Erfassung für Anzeige/Bearbeitung; Detailseite zeigt
„790 g (= 1.000 ml)". Wareneingang-Buchung rechnet gelieferte ml ebenfalls um.
- Serverseitige Validierung: ml nur mit hinterlegter Dichte.
Migrationen add_density_* auf Dev-/Live-DB. 4 Tests, Suite 318 grün.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AP-45: Verpackungsverbrauch (Etikett/Tiegel) wird jetzt aus dem tatsächlichen
Produktionsergebnis gebucht — je Ergebnisprodukt dessen eigene Stückliste ×
Ergebnismenge, über alle Outputs summiert — statt aus der geplanten Stückzahl.
syncPackagingSnapshot liest die production_outputs und läuft daher nach
persistOutputs (store + update umgestellt).
AP-47: Neues Produkt-Kennzeichen „produktionsrelevant" (Boolean, Default true).
Nur so markierte Produkte erscheinen in der Produktions-Auswahl
(ProductionController create/edit/copy) und in der „Enthalten in"-Box des
Rohstoffbestands (blendet z. B. Aloe-Handelsware und Zweitgrößen aus).
Checkbox im Produktformular (mit Hidden-Fallback 0). Migration Default true,
damit Bestandsdaten unverändert bleiben.
Migrationen add_production_relevant auf Dev-/Live-DB ausgeführt. 4 Tests, Suite 314 grün.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Die INCI-Liste (Hersteller-Rezeptur) in der Produktionsmaske wird jetzt nach
Anteil % absteigend sortiert dargestellt (null-Anteile zuletzt). Reine
Anzeige-Sortierung einer Kopie — die Chargen-Zuordnung unten bleibt in
Phasen-/pos-Reihenfolge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AP-40: INCI-Tabelle der Produktion um zwei Spalten ergänzt — „Bestand" (aktueller
Rohstoffbestand je INCI, global) und „Differenz" (Bestand − Soll; negativ rot =
Fehlmenge, sonst grün). Spaltenkopf „Regal" → „Ablage". Bestand kommt aus
InventoryService::remainingByIngredient und wird als current_stock in den
recipeJson-Payload gehängt (lazy aufgelöst — InventoryService hängt selbst von
ProductionService ab, Konstruktor-Injektion wäre zirkulär).
AP-41: „Verpackung (Vorschau)" direkt unter die INCI-Tabelle verschoben (man sieht
auf einen Blick den Gesamtbedarf). Je Phase die Gesamt-Gramm hinter dem Namen
(„Phase A (345 g)"), aktualisiert sich mit Stückzahl und Füllgewicht-Skala.
Test für den current_stock-Payload ergänzt. Suite 311 grün.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AP-35 (Mengenberechnung): Die Hersteller-Rezeptur ist in Prozent hinterlegt; die
Sollmengen skalieren jetzt auf das Füllgewicht statt fest auf ~100 g. Neuer
Faktor recipeGramScale = Füllgewicht/100. Basis = neues, optionales Produkt-Feld
"fill_weight" (g); ist es leer, gilt bei ml-Produkten (unit=1) die Füllmenge
(contents_total) als Gramm (1 g/ml). Ohne Massenbasis (z. B. Stück-Produkte)
bleibt es beim Altverhalten (Anteil = Gramm), sodass Bestandsprodukte/Tests
unverändert rechnen. Skala wird an die Maske durchgereicht (recipe_gram_scale),
damit Client und Server identisch rechnen (Service + 4 JS-Stellen).
AP-39 (Abwiegen): Die Ist-Menge je INCI muss nicht mehr aufs Gramm passen —
assertLinesMatchRecipe erlaubt ±5 % (kleiner absoluter Sockel), gebucht wird der
reale Ist-Wert. Status-Ampel in der Maske: Orange = nichts eingetragen,
Grün = eingetragen und ±5 %, Rot = >5 % daneben (Zahlendreher-Schutz).
Migration add_fill_weight_to_products auf Dev-/Live-DB ausgeführt (nullable).
Product fillable/cast + Produktformular-Feld ergänzt. 5 Tests, Suite 310 grün.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phasen ohne Rohstoffzeile (reine Hinweisphasen wie „Vorbereitung"/„Nachbereitung")
verschwanden in der Produktionsmaske, weil buildPhaseGroups() leere Gruppen
wegfilterte. Filter so angepasst, dass benannte Phasen erhalten bleiben und nur
der namenlose „lose" Sammeltopf verworfen wird. Die Phasen kommen serverseitig
bereits in pos-Reihenfolge (recipePhases orderBy pos), sodass „Vorbereitung"
korrekt über „Phase A" steht. Test sichert den recipeJson-Datenvertrag ab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Das Lösch-Formular für gespeicherte Rezeptur-Inhaltsstoffe war in das
Produkt-Formular verschachtelt; Browser verwerfen das innere <form>-Tag,
wodurch _method=DELETE ins Speichern-Formular leakte. Jetzt Button, der
per JS ein eigenes Formular außerhalb erzeugt und abschickt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Indizes auf shopping_orders.shipped/shipped_at/created_at (Board-Filter,
Tages-Kacheln) und products.wp_number (SKU-Auflösung). Die
API-Bestellungen-Seite lud das komplette JSON-payload für alle 500 Zeilen
und serialisierte es in Inline-Attribute; jetzt persistierte
positions_count, Liste ohne payload, Inhalt on-demand über show-Endpoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Der Default-Scope 'recipe' blendet rezepturlose Rohstoffe aus, das
Sidebar-Badge zählt aber alle aktiven kritischen — Badge und Seite
konnten sich widersprechen. Die recipe-Ansicht zeigt jetzt einen Hinweis
mit 'Alle anzeigen', sobald das Badge mehr zählt; Empty-State-Text
scope-abhängig.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bestellnummern-Links auf admin_sales_detail (admin >= 7) waren für
Versand-Mitarbeiter tote Links (stiller Redirect) — jetzt rollenabhängig.
Kachel-Zähler und pick/ready-Filter rechnen über alle offenen
Bestellungen statt über die neuesten 500; gekappt wird nur die Anzeige,
der Hinweis-Banner zeigt 'neueste n von total' korrekt. whereDate durch
indexfähiges whereBetween ersetzt; Packlisten-Toggle über data-order-id;
Gewicht über formatNumber().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Die Brücke hätte live nicht funktioniert: Connector 1.4.0 sendete die
label_url aus dem per 'deny from all' geschützten DHL-Ordner (403), dazu
SSRF-Risiko durch serverseitigen Abruf einer Client-URL und Timeout-
Mismatch (20 s Download vs. 5 s WordPress-Default). Der Connector liest
die PDF jetzt vom Shop-Dateisystem (realpath-Pinning auf wp-uploads) und
sendet sie Base64; storeWpLabel() validiert und speichert ohne Http::get;
tote Shop-Link-Fallbacks aus den Views entfernt; Connector-Timeout 30 s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bestell-Übertragung (wp/order) akzeptiert wp_label_url + wp_tracking_number:
Label-PDF wird als lokale Kopie im privaten Storage gespeichert (Shop-URL ist
öffentlich erratbar; Kopie überlebt Löschung im Shop), Sendungsnummer landet
in shopping_orders.tracking. Neuer Endpoint wp/label zum Nachreichen
(Connector-Hook pr_save_dhl_label_tracking), wp/status liefert die
Sendungsnummer an den Shop zurück. Abwicklungs-Board + Bestell-Details zeigen
Label-Download (copyreader-Dokumentroute type=label, Fallback Shop-Link) und
Sendungsnummer. Download-Fehler blockieren die Übertragung nie.
Doku: Connector-Update-Anleitung (dev/product management), Hinweise,
Testleitfaden §11, Plan-Protokoll. pint.json schließt dev/ (Fremdcode) aus.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checkbox-Auswahl im Abwicklungs-Board (Alle-auswählen, Zähler in den Buttons)
+ zwei Sammel-Aktionen: „Lieferscheine drucken" und „+ Rechnungen" — je ein
zusammengeführtes PDF (PDFMerger/FPDI) im neuen Tab zum Stapeldruck. Fehlende
Dokumente werden dabei ohne Mailversand erzeugt, auf der Platte fehlende
Altdateien mit bestehender Nummer regeneriert. API-Bestellungen sind von der
Auswahl ausgenommen (deaktivierte Checkbox, Rechnung liegt im externen Shop)
und werden in gemischter Auswahl übersprungen; max. 100 je Druck.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>