diff --git a/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php b/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php index c4d6a56..af50c0c 100644 --- a/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php +++ b/app/Http/Controllers/Admin/Inventory/RawMaterialStockController.php @@ -110,7 +110,7 @@ class RawMaterialStockController extends Controller $remainingByLocation = $this->inventoryService->remainingByLocationForIngredient($ingredient->id); $openOrders = $this->inventoryService->openOrdersForIngredient($ingredient->id); - $openTotal = round((float) $openOrders->sum('ordered_quantity'), 2); + $openTotal = roundWawiQuantity((float) $openOrders->sum('ordered_quantity')); $daily = ($this->inventoryService->dailyConsumptionByIngredient([$ingredient->id]))[$ingredient->id] ?? null; $minAlert = $ingredient->min_stock_alert !== null ? (float) $ingredient->min_stock_alert : null; @@ -128,7 +128,7 @@ class RawMaterialStockController extends Controller 'ingredient' => $ingredient, 'productStock' => $productStock, 'chargeGroups' => $chargeGroups, - 'remaining' => round($remaining, 2), + 'remaining' => roundWawiQuantity($remaining), 'remainingByLocation' => $remainingByLocation, 'locations' => Location::query()->where('active', true)->orderBy('name')->get(), 'openOrders' => $openOrders, @@ -169,8 +169,8 @@ class RawMaterialStockController extends Controller $usage[(int) $productId] = [ 'percent' => round($percent, 3), - 'grams_per_piece' => $hasMassBasis ? round($percent * $scale, 2) : null, - 'consumption_per_piece' => $hasMassBasis ? round($withFactor * $scale, 2) : null, + 'grams_per_piece' => $hasMassBasis ? roundWawiQuantity($percent * $scale) : null, + 'consumption_per_piece' => $hasMassBasis ? roundWawiQuantity($withFactor * $scale) : null, ]; } diff --git a/app/Http/Controllers/Admin/Inventory/StockDisposalController.php b/app/Http/Controllers/Admin/Inventory/StockDisposalController.php index bf9f14d..a49c1fc 100644 --- a/app/Http/Controllers/Admin/Inventory/StockDisposalController.php +++ b/app/Http/Controllers/Admin/Inventory/StockDisposalController.php @@ -109,19 +109,19 @@ class StockDisposalController extends Controller } $ingredientId = (int) $data['ingredient_id']; - $quantity = round((float) $data['quantity'], 2); + $quantity = roundWawiQuantity((float) $data['quantity']); if (! empty($data['stock_entry_id'])) { $entry = StockEntry::query()->lockForUpdate()->findOrFail((int) $data['stock_entry_id']); $available = $productionService->availableStockEntriesForIngredient($ingredientId, (int) $entry->location_id)->firstWhere('id', $entry->id); - $remaining = $available !== null ? round((float) $available->getAttribute('remaining_quantity'), 2) : 0.0; + $remaining = $available !== null ? roundWawiQuantity((float) $available->getAttribute('remaining_quantity')) : 0.0; } else { // Alle Eingänge des Rohstoffs am Standort sperren, dann Standortrest neu berechnen. StockEntry::query()->lockForUpdate() ->where('entry_type', 'ingredient')->where('ingredient_id', $ingredientId) ->where('location_id', (int) $data['location_id']) ->get(); - $remaining = round((float) ($this->inventoryService->remainingByLocationForIngredient($ingredientId)[(int) $data['location_id']] ?? 0.0), 2); + $remaining = roundWawiQuantity((float) ($this->inventoryService->remainingByLocationForIngredient($ingredientId)[(int) $data['location_id']] ?? 0.0)); } if ($quantity > $remaining) { @@ -156,15 +156,15 @@ class StockDisposalController extends Controller } $rows = []; - $needed = round((float) $data['quantity'], 2); + $needed = roundWawiQuantity((float) $data['quantity']); $lastIndex = $entries->count() - 1; foreach ($entries as $index => $entry) { $capacity = (float) $entry->getAttribute('remaining_quantity'); - $take = $index === $lastIndex ? $needed : round(min($needed, $capacity), 2); + $take = $index === $lastIndex ? $needed : roundWawiQuantity(min($needed, $capacity)); if ($take > 0) { $rows[] = array_merge($data, ['stock_entry_id' => (int) $entry->id, 'quantity' => $take]); } - $needed = round($needed - $take, 2); + $needed = roundWawiQuantity($needed - $take); if ($needed <= 0) { break; } diff --git a/app/Http/Controllers/Admin/Inventory/StockEntryController.php b/app/Http/Controllers/Admin/Inventory/StockEntryController.php index dfcc420..cc2ddf4 100644 --- a/app/Http/Controllers/Admin/Inventory/StockEntryController.php +++ b/app/Http/Controllers/Admin/Inventory/StockEntryController.php @@ -165,7 +165,7 @@ class StockEntryController extends Controller // die Kopie zur Gramm-Bestellung und ein Wareneingang „100“ bucht 100 g statt 79 g. if ($stockEntry->purchase_unit === 'ml' && $stockEntry->density_used !== null && (float) $stockEntry->density_used > 0) { $data['ordered_unit'] = 'ml'; - $data['ordered_quantity'] = round((float) $stockEntry->ordered_quantity / (float) $stockEntry->density_used, 2); + $data['ordered_quantity'] = roundWawiQuantity((float) $stockEntry->ordered_quantity / (float) $stockEntry->density_used); $data['density_used'] = (float) $stockEntry->density_used; } @@ -270,7 +270,7 @@ class StockEntryController extends Controller return response()->json([ 'exists' => true, 'purchases' => $entries->count(), - 'remaining' => round((float) $remaining, 2), + 'remaining' => roundWawiQuantity((float) $remaining), 'best_before' => $latest->best_before?->format('d.m.Y'), 'location' => $latest->location?->name, 'quality_id' => $latest->quality_id, diff --git a/app/Http/Requests/Inventory/StoreStockDisposalRequest.php b/app/Http/Requests/Inventory/StoreStockDisposalRequest.php index 5429935..f5b56b6 100644 --- a/app/Http/Requests/Inventory/StoreStockDisposalRequest.php +++ b/app/Http/Requests/Inventory/StoreStockDisposalRequest.php @@ -28,7 +28,7 @@ class StoreStockDisposalRequest extends FormRequest 'packaging_item_id' => ['nullable', 'integer', 'exists:packaging_items,id'], 'stock_entry_id' => ['nullable', 'integer', 'exists:stock_entries,id'], 'location_id' => ['required', 'integer', 'exists:locations,id'], - 'quantity' => ['required', 'numeric', 'min:0.01'], + 'quantity' => ['required', 'numeric', 'min:0.001'], 'reason' => ['required', 'string', 'max:100'], 'note' => ['nullable', 'string', 'max:255'], 'disposed_at' => ['required', 'date'], @@ -116,7 +116,7 @@ class StoreStockDisposalRequest extends FormRequest // N-05: Verfügbarkeit der Charge wie in der Produktion (inkl. FEFO-Anrechnung von Alt-Ausschuss ohne Charge). $available = app(ProductionService::class)->availableStockEntriesForIngredient($ingredientId, (int) $entry->location_id) ->firstWhere('id', $entry->id); - $remaining = $available !== null ? round((float) $available->getAttribute('remaining_quantity'), 2) : 0.0; + $remaining = $available !== null ? roundWawiQuantity((float) $available->getAttribute('remaining_quantity')) : 0.0; if ($quantity > $remaining) { $validator->errors()->add('quantity', __('Die Menge (:qty g) übersteigt den Restbestand der Charge (:rest g).', [ 'qty' => number_format($quantity, 2, ',', '.'), diff --git a/app/Models/ProductionIngredient.php b/app/Models/ProductionIngredient.php index 8988eb9..e3aab23 100644 --- a/app/Models/ProductionIngredient.php +++ b/app/Models/ProductionIngredient.php @@ -48,7 +48,7 @@ class ProductionIngredient extends Model protected function casts(): array { return [ - 'quantity_used' => 'decimal:2', + 'quantity_used' => 'decimal:3', ]; } diff --git a/app/Models/StockDisposal.php b/app/Models/StockDisposal.php index b46147e..0972348 100644 --- a/app/Models/StockDisposal.php +++ b/app/Models/StockDisposal.php @@ -32,7 +32,7 @@ class StockDisposal extends Model { return [ 'disposed_at' => 'date', - 'quantity' => 'decimal:2', + 'quantity' => 'decimal:3', ]; } diff --git a/app/Models/StockEntry.php b/app/Models/StockEntry.php index 61be1c3..0550b4a 100644 --- a/app/Models/StockEntry.php +++ b/app/Models/StockEntry.php @@ -51,8 +51,8 @@ class StockEntry extends Model 'ordered_at' => 'date', 'received_at' => 'date', 'best_before' => 'date', - 'ordered_quantity' => 'decimal:2', - 'received_quantity' => 'decimal:2', + 'ordered_quantity' => 'decimal:3', + 'received_quantity' => 'decimal:3', 'density_used' => 'decimal:4', 'price_per_kg' => 'decimal:4', 'price_per_kg_gross' => 'decimal:4', diff --git a/app/Repositories/StockEntryRepository.php b/app/Repositories/StockEntryRepository.php index 35565b9..784d231 100644 --- a/app/Repositories/StockEntryRepository.php +++ b/app/Repositories/StockEntryRepository.php @@ -80,7 +80,7 @@ class StockEntryRepository $data['purchase_unit'] = 'ml'; $data['density_used'] = (float) $density; if (isset($data['ordered_quantity'])) { - $data['ordered_quantity'] = round((float) $data['ordered_quantity'] * (float) $density, 2); + $data['ordered_quantity'] = roundWawiQuantity((float) $data['ordered_quantity'] * (float) $density); } } } @@ -157,7 +157,7 @@ class StockEntryRepository // AP-42: Wurde in ml eingekauft, wird auch die gelieferte Menge in ml erfasst // und über die (beim Bestellen gespeicherte) Dichte in Gramm umgerechnet. if ($locked->purchase_unit === 'ml' && $locked->density_used !== null && isset($data['received_quantity'])) { - $data['received_quantity'] = round((float) $data['received_quantity'] * (float) $locked->density_used, 2); + $data['received_quantity'] = roundWawiQuantity((float) $data['received_quantity'] * (float) $locked->density_used); } $locked->update($data); diff --git a/app/Services/InventoryService.php b/app/Services/InventoryService.php index 501e694..1fc6450 100644 --- a/app/Services/InventoryService.php +++ b/app/Services/InventoryService.php @@ -13,6 +13,12 @@ use Illuminate\Database\Eloquent\Collection; class InventoryService { + /** + * AP-53 (09.09.2026): Gramm-Mengen (Produktion, Wareneingang, Ausschuss, + * Restbestände) werden einheitlich auf drei Nachkommastellen geführt. + */ + public const QUANTITY_DECIMALS = 3; + public function __construct( protected ProductionService $productionService, ) {} @@ -68,7 +74,7 @@ class InventoryService $result = []; foreach ($ids as $id) { - $result[$id] = round((float) ($received[$id] ?? 0) - (float) ($consumed[$id] ?? 0) - (float) ($disposed[$id] ?? 0), 2); + $result[$id] = roundWawiQuantity((float) ($received[$id] ?? 0) - (float) ($consumed[$id] ?? 0) - (float) ($disposed[$id] ?? 0)); } return $result; @@ -92,7 +98,7 @@ class InventoryService $result = []; foreach ($rows as $id => $qty) { - $result[(int) $id] = round((float) $qty, 2); + $result[(int) $id] = roundWawiQuantity((float) $qty); } return $result; @@ -142,7 +148,7 @@ class InventoryService $result = []; foreach ($ids as $locationId) { - $result[$locationId] = round((float) ($received[$locationId] ?? 0) - (float) ($consumed[$locationId] ?? 0) - (float) ($disposed[$locationId] ?? 0), 2); + $result[$locationId] = roundWawiQuantity((float) ($received[$locationId] ?? 0) - (float) ($consumed[$locationId] ?? 0) - (float) ($disposed[$locationId] ?? 0)); } return $result; @@ -185,7 +191,7 @@ class InventoryService $result = []; foreach ($ids as $id) { - $result[$id] = round((float) ($received[$id] ?? 0) - (float) ($consumed[$id] ?? 0) - (float) ($disposed[$id] ?? 0), 2); + $result[$id] = roundWawiQuantity((float) ($received[$id] ?? 0) - (float) ($consumed[$id] ?? 0) - (float) ($disposed[$id] ?? 0)); } return $result; @@ -209,7 +215,7 @@ class InventoryService $result = []; foreach ($rows as $id => $qty) { - $result[(int) $id] = round((float) $qty, 2); + $result[(int) $id] = roundWawiQuantity((float) $qty); } return $result; @@ -234,7 +240,7 @@ class InventoryService $result = []; foreach ($rows as $id => $qty) { - $result[(int) $id] = round((float) $qty, 2); + $result[(int) $id] = roundWawiQuantity((float) $qty); } return $result; @@ -345,7 +351,7 @@ class InventoryService $entries = $entries->map(function (StockEntry $entry) use ($consumed) { $received = $entry->received_quantity !== null ? (float) $entry->received_quantity : 0.0; - $entry->setAttribute('remaining_quantity', round($received - ($consumed[(int) $entry->id] ?? 0.0), 2)); + $entry->setAttribute('remaining_quantity', roundWawiQuantity($received - ($consumed[(int) $entry->id] ?? 0.0))); return $entry; }); @@ -375,7 +381,7 @@ class InventoryService 'key' => $key, 'batch_number' => $first->batch_number, 'entries' => $entries->values(), - 'remaining' => round($entries->sum(fn (StockEntry $e) => (float) $e->getAttribute('remaining_quantity')), 2), + 'remaining' => roundWawiQuantity($entries->sum(fn (StockEntry $e) => (float) $e->getAttribute('remaining_quantity'))), 'best_before' => $entries->pluck('best_before')->filter()->min(), 'suppliers' => $entries->map(fn (StockEntry $e) => $e->supplier?->name)->filter()->unique()->values(), 'locations' => $entries->map(fn (StockEntry $e) => $e->location?->name)->filter()->unique()->values(), diff --git a/app/Services/ProductionService.php b/app/Services/ProductionService.php index 45eba38..147b96d 100644 --- a/app/Services/ProductionService.php +++ b/app/Services/ProductionService.php @@ -291,13 +291,13 @@ class ProductionService // passen — gebucht wird der reale Ist-Wert. Toleranz ±5 % (mit kleinem // absolutem Sockel gegen Float-/Kleinmengen-Effekte); größere // Abweichungen fängt der Zahlendreher-Schutz weiterhin ab. - $tolerance = max((float) $req * 0.05, 0.05); + $tolerance = max((float) $req * 0.05, 0.005); // AP-53: Sockel 5 mg if (abs($sum - $req) > $tolerance) { throw ValidationException::withMessages([ 'ingredient_lines' => __('Die Ist-Menge je INCI darf höchstens 5 % vom Soll abweichen (INCI :id: Soll :req g, Ist :sum g).', [ 'id' => $iid, - 'req' => number_format($req, 2, ',', '.'), - 'sum' => number_format($sum, 2, ',', '.'), + 'req' => formatWawiQuantity($req), + 'sum' => formatWawiQuantity($sum), ]), ]); } @@ -541,7 +541,7 @@ class ProductionService $entries = $entries->map(function (StockEntry $entry) use ($consumed) { $received = $entry->received_quantity !== null ? (float) $entry->received_quantity : 0.0; - $entry->setAttribute('remaining_quantity', round($received - ($consumed[(int) $entry->id] ?? 0.0), 2)); + $entry->setAttribute('remaining_quantity', roundWawiQuantity($received - ($consumed[(int) $entry->id] ?? 0.0))); return $entry; }); @@ -577,7 +577,7 @@ class ProductionService ->pluck('qty', 'location_id'); foreach ($unbound as $locationId => $qty) { - $needed = round((float) $qty, 2); + $needed = roundWawiQuantity((float) $qty); foreach ($entries as $entry) { if ($needed <= 0) { break; @@ -589,9 +589,9 @@ class ProductionService if ($remaining <= 0) { continue; } - $take = round(min($remaining, $needed), 2); - $entry->setAttribute('remaining_quantity', round($remaining - $take, 2)); - $needed = round($needed - $take, 2); + $take = roundWawiQuantity(min($remaining, $needed)); + $entry->setAttribute('remaining_quantity', roundWawiQuantity($remaining - $take)); + $needed = roundWawiQuantity($needed - $take); } } @@ -639,7 +639,7 @@ class ProductionService 'label' => implode(' - ', $labelParts), 'batch_number' => $first->batch_number, 'best_before' => $bestBefore?->format('Y-m-d'), - 'remaining' => round($group->sum(fn (StockEntry $e) => (float) $e->getAttribute('remaining_quantity')), 2), + 'remaining' => roundWawiQuantity($group->sum(fn (StockEntry $e) => (float) $e->getAttribute('remaining_quantity'))), 'purchases' => $group->count(), ]; }) @@ -685,12 +685,12 @@ class ProductionService ]); } - $needed = round($this->parseQuantity($line['quantity_used'] ?? null), 2); + $needed = roundWawiQuantity($this->parseQuantity($line['quantity_used'] ?? null)); $lastIndex = $batchEntries->count() - 1; foreach ($batchEntries as $index => $entry) { $capacity = (float) $entry->getAttribute('remaining_quantity'); - $take = $index === $lastIndex ? $needed : round(min($needed, $capacity), 2); + $take = $index === $lastIndex ? $needed : roundWawiQuantity(min($needed, $capacity)); if ($take > 0) { $expanded[] = [ 'ingredient_id' => $ingredientId, @@ -702,9 +702,9 @@ class ProductionService // Audit H-3: Restmenge im Cache dekrementieren — sonst rechnet // eine zweite Zeile derselben Charge erneut gegen die volle // Kapazität und belastet denselben Einkauf doppelt. - $entry->setAttribute('remaining_quantity', round($capacity - $take, 2)); + $entry->setAttribute('remaining_quantity', roundWawiQuantity($capacity - $take)); } - $needed = round($needed - $take, 2); + $needed = roundWawiQuantity($needed - $take); if ($needed <= 0) { break; } @@ -752,7 +752,7 @@ class ProductionService 'factor' => $factor, 'required_grams_total' => $req, // AP-40: aktueller Bestand des Rohstoffs (global, in Gramm). - 'current_stock' => round((float) ($stockByIngredient[(int) $ing->id] ?? 0.0), 2), + 'current_stock' => roundWawiQuantity((float) ($stockByIngredient[(int) $ing->id] ?? 0.0)), 'stock_entries' => $available ->map(function (StockEntry $se) { return [ diff --git a/app/helpers.php b/app/helpers.php index 790d45e..ccea3c5 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,5 +1,7 @@ $minDecimals && str_ends_with($asString, '0')) { + $asString = substr($asString, 0, -1); + $decimals--; + } + + return number_format($rounded, $decimals, ',', '.'); + } +} + if (! function_exists('formatPlural')) { function formatPlural($count, $singular, $plural) { diff --git a/database/migrations/2026_09_09_100000_change_quantities_to_three_decimals.php b/database/migrations/2026_09_09_100000_change_quantities_to_three_decimals.php new file mode 100644 index 0000000..7c43a65 --- /dev/null +++ b/database/migrations/2026_09_09_100000_change_quantities_to_three_decimals.php @@ -0,0 +1,43 @@ +decimal('quantity_used', 12, 3)->change(); + }); + Schema::table('stock_entries', function (Blueprint $table) { + $table->decimal('ordered_quantity', 12, 3)->change(); + $table->decimal('received_quantity', 12, 3)->nullable()->change(); + }); + Schema::table('stock_disposals', function (Blueprint $table) { + $table->decimal('quantity', 12, 3)->change(); + }); + } + + public function down(): void + { + Schema::table('production_ingredients', function (Blueprint $table) { + $table->decimal('quantity_used', 12, 2)->change(); + }); + Schema::table('stock_entries', function (Blueprint $table) { + $table->decimal('ordered_quantity', 12, 2)->change(); + $table->decimal('received_quantity', 12, 2)->nullable()->change(); + }); + Schema::table('stock_disposals', function (Blueprint $table) { + $table->decimal('quantity', 12, 2)->change(); + }); + } +}; diff --git a/dev/product management /01-entwicklungsplaene/entwicklungsplan-V7-2026-08-26.md b/dev/product management /01-entwicklungsplaene/entwicklungsplan-V7-2026-08-26.md index 4911736..9bd2447 100644 --- a/dev/product management /01-entwicklungsplaene/entwicklungsplan-V7-2026-08-26.md +++ b/dev/product management /01-entwicklungsplaene/entwicklungsplan-V7-2026-08-26.md @@ -32,9 +32,10 @@ Die mit **`#`** markierten Punkte hat der Kunde als **buggy/dringend** priorisie | **AP-44** Rezeptur-Darstellung (Optik/iPad) | ✅ erledigt (2a6daf2) | | **AP-38** WooCommerce-Ampel (Cron/Auth) | ✅ erledigt (0c8d321 ERP+Connector, b6ca332 Cron-Selbstheilung) — Live-Ursache behoben | | **AP-52** Verpackung: Bestand/Differenz/Lagerort in der Produktion (Nachtrag 09.09.) | ✅ erledigt (3abb9e6) — Spalte einheitlich „Lagerort" | +| **AP-53** Mengen mit drei Nachkommastellen (Nachtrag 09.09.) | ✅ erledigt — Migration `2026_09_09_100000` auf Dev-DB ausgeführt, Live ausstehend | | übrige APs | — | -Suite (26.08.2026): **338 grün**; nach Nacharbeit, Nachprüfung und Gegenprüfung (08./09.09.2026): **370 grün**. Erledigt: **AP-34…AP-51 vollständig** (AP-34/35/36/37/38/39/40/41/42/43/44/45/46/47/48/49/50/51). **Keine offenen APs mehr.** Verbleibend nur der **Connector-Rollout v1.6.0** in den Shop (AP-34 + AP-38). +Suite (26.08.2026): **338 grün**; nach Nacharbeit, Nachprüfung und Gegenprüfung (08./09.09.2026): **370 grün**; mit AP-52/AP-53 und AP-47-Nachtrag (09.09.2026): **378 grün**. Erledigt: **AP-34…AP-51 vollständig** (AP-34/35/36/37/38/39/40/41/42/43/44/45/46/47/48/49/50/51). **Keine offenen APs mehr.** Verbleibend nur der **Connector-Rollout v1.6.0** in den Shop (AP-34 + AP-38). > **AP-43-Hinweis:** „Zwischenspeichern" legt die Produktion als **Entwurf** ab (kein Bestand gebucht); erst **„Produktion abschließen"** bucht Roh-/Produkt-/Verpackungsbestand. Entwürfe zählen nirgends in den Bestand — auch nicht in die Chargenverfügbarkeit (F-01). Pflicht bleiben im Entwurf nur Produkt, Standort, Datum und Planmenge; Chargen- und Ergebniszeilen dürfen unvollständig sein und werden verlustfrei gesichert (F-03). Eine bereits abgeschlossene Produktion kann nicht zurück in den Entwurf. @@ -186,6 +187,18 @@ Aktuell nach `pos` sortiert (`Product::manufacturer_ingredients() ->orderByPivot **Lösung:** Verpackungs-Vorschau mit Spalten Artikel · Stück gesamt · Lagerort · Bestand · Differenz (rot = Fehlmenge, grün = ausreichend, gleiche Darstellung wie AP-40). Bestand aus `InventoryService::remainingByPackagingItem()`, Lagerort aus den AP-51-Feldern. Die INCI-Spalte „Ablage" heißt jetzt ebenfalls „Lagerort". Bei bereits gebuchten Produktionen (Snapshot, F-05/N-04) wird keine Differenz angezeigt, da der Bestand den Abzug bereits enthält. **Umsetzung:** ✅ 09.09.2026, Commit 3abb9e6. +### AP-53 — Mengen mit drei Nachkommastellen (Nachtrag 09.09.2026) +**Kundenwunsch:** Die Eingabe bei der Produktion soll drei Nachkommastellen zulassen (Milligramm, z. B. 0,125 g Konservierer). +**Befund (Prüfung 09.09.2026):** Chargenverbrauch, Wareneingang und Ausschuss waren als `decimal(12,2)` angelegt, sämtliche Bestandsrechnungen (Chargen-Rest, FEFO, Verfügbarkeit) rundeten auf zwei Stellen, die Anzeige ebenso; die Soll-Ist-Toleranz hatte einen Sockel von 0,05 g. Eine Eingabe „0,125" wurde still auf 0,13 gerundet; „0.125" (Punkt) wurde nach der Regel vom 15.07.2026 als 125 g gelesen. +**Lösung:** +- Migration `2026_09_09_100000_change_quantities_to_three_decimals`: `production_ingredients.quantity_used`, `stock_entries.ordered_quantity`/`received_quantity`, `stock_disposals.quantity` → `decimal(12,3)`; Model-Casts `decimal:3`. Altdaten unverändert. +- Zentrale Konstante `InventoryService::QUANTITY_DECIMALS = 3`; Helfer `roundWawiQuantity()` ersetzt alle `round(…, 2)` der Bestandslogik (InventoryService, ProductionService, StockDisposalController/-Request, StockEntryRepository/-Controller, RawMaterialStockController). Helfer `formatWawiQuantity($v, $min = 2)` für die Anzeige (bis 3 Stellen, mind. 2, optional 0). +- Eingaberegel `reFormatWawiNumber` + JS (`parseGerman`, Wareneingang `parseNumber`): zusätzlich `0.xxx` als Dezimalzahl; `1.500` bleibt Tausend. Produktions-JS formatiert gespeicherte Zahlen beim Wiederöffnen deutsch ins Feld (`fmtInput`), damit 1,125 nicht als 1.125 = 1125 gelesen wird; `fmt()` zeigt bis 3 Stellen. +- Toleranzsockel 0,05 → 0,005 g; Ausschuss-Validierung `min:0.001`. +- Anzeige umgestellt: Produktionsdetail, Einkaufs-Liste/-Detail/-Formular, Ausschuss-Liste, Chargen-Rest im Rohstoff-Detail. Übersichten (Rohstoffbestand gesamt, Verbrauch/Tag) bleiben ganzzahlig. +**Tests:** `tests/Feature/QuantityPrecisionTest.php` (6): Parsing-Regeln, Anzeigeformat, Produktion 0,125 g exakt inkl. Rest 9,875 g, Entwurf 1,125 als JSON-Zahl + fmtInput, Toleranz bei Kleinmengen, Ausschuss 0,005 g. +**Umsetzung:** ✅ 09.09.2026. + --- ## D. Klärungspunkte — beantwortet (Kundenentscheide 26.08.2026) diff --git a/dev/product management /README.md b/dev/product management /README.md index 14969db..bb2c401 100644 --- a/dev/product management /README.md +++ b/dev/product management /README.md @@ -2,7 +2,7 @@ > **Stand:** 09.09.2026 > **Status:** Modul ist **live** und in der **Kundentestphase** (Dev-Container mit eingespielter Live-Datenbank inkl. Testdaten). -> **Testsuite:** 371 Tests grün (1.512 Assertions, 0 Failures). +> **Testsuite:** 378 Tests grün (1.554 Assertions, 0 Failures). Dieses Dokument ist der Einstieg in den Ordner. Es skizziert den Gesamtstand, listet die erledigten und die noch offenen Arbeitspakete und verweist auf die @@ -101,7 +101,7 @@ WooCommerce-Ampel ist voraussichtlich **kein** Partnercenter-Fehler, sondern shopseitig (Status-Cron/Auth). Rohfassung des Feedbacks: [entwicklungsplan-aktualisiert-2026-08-26.md](01-entwicklungsplaene/entwicklungsplan-aktualisiert-2026-08-26.md). Offene Klärungspunkte (D-1…D-6) im Plan blockieren den Start einzelner Pakete. -**Stand 09.09.2026:** AP-34…AP-51 sind umgesetzt (siehe Umsetzungsstand in V7.0); Kundennachtrag **AP-52** (Verpackungs-Vorschau in der Produktion mit Bestand, Differenz und „Lagerort", einheitlich mit der INCI-Tabelle) ebenfalls umgesetzt. +**Stand 09.09.2026:** AP-34…AP-51 sind umgesetzt (siehe Umsetzungsstand in V7.0); Kundennachträge **AP-52** (Verpackungs-Vorschau in der Produktion mit Bestand, Differenz und „Lagerort", einheitlich mit der INCI-Tabelle) und **AP-53** (Gramm-Mengen mit drei Nachkommastellen in Produktion, Wareneingang und Ausschuss) ebenfalls umgesetzt; AP-47-Häkchen in die Karte „Warenwirtschaft" verschoben und ausformuliert („In der Produktion anzeigen"). ## Umsetzungsprüfung 08.09.2026 → Nacharbeit @@ -145,11 +145,11 @@ Offene Kundenfragen im Dokument. Details: --- -## Deployment-Checkliste (Stand 09.09.2026, Paket V7.0 + Nachbesserungen + AP-52) +## Deployment-Checkliste (Stand 09.09.2026, Paket V7.0 + Nachbesserungen + AP-52/AP-53) 1. **Code:** `master` bis Commit der Doku-Aktualisierung 09.09.2026 (siehe `git log`). -2. **Migrationen:** `php artisan migrate` — drei neue Migrationen `2026_09_08_100000` (batch_key, stock_entry_id nullable), `2026_09_08_110000` (recipe_phase_id), `2026_09_08_120000` (fill_weight decimal). Auf der Dev-DB (Live-Kopie, MySQL) am 09.09.2026 erfolgreich ausgeführt; bestehende Produktionszeilen bleiben unverändert (neue Spalten NULL). -3. **In-App-Doku:** `resources/docs/hinweise.md` (Stand 09.09.2026) und `resources/docs/testleitfaden.md` (Version 3, Teil C) sind aktualisiert und werden ohne Build ausgeliefert (Einstellungen → Hinweise / Leitfaden). +2. **Migrationen:** `php artisan migrate` — vier neue Migrationen `2026_09_08_100000` (batch_key, stock_entry_id nullable), `2026_09_08_110000` (recipe_phase_id), `2026_09_08_120000` (fill_weight decimal), `2026_09_09_100000` (AP-53: Chargenverbrauch, Bestell-/Eingangsmenge, Ausschussmenge → decimal(12,3)). Alle auf der Dev-DB (Live-Kopie, MySQL) am 09.09.2026 erfolgreich ausgeführt; bestehende Zeilen bleiben unverändert (neue Spalten NULL, dritte Nachkommastelle 0). +3. **In-App-Doku:** `resources/docs/hinweise.md` (Stand 09.09.2026) und `resources/docs/testleitfaden.md` (Version 3.1, Teil C) sind aktualisiert und werden ohne Build ausgeliefert (Einstellungen → Hinweise / Leitfaden). 4. **Kein Frontend-Build nötig:** alle Änderungen liegen in Blade/Inline-JS; Laravel Mix ist nicht betroffen. 5. **Nach dem Deploy (Pflicht):** `php artisan optimize:clear` und anschließend `php artisan route:cache` / `config:cache` / `view:cache` neu aufbauen. Live cached Routen — ein veralteter Route-Cache führte am 09.09.2026 zu einem 500er unter `/admin/inventory/general` (`Route [admin.inventory.stock-movement-reasons.create] not defined`, Route aus AP-48 seit 28.08.2026 im Code). Danach Testleitfaden Teil C mit dem Kunden durchgehen (insb. Abschnitte 19–21 auf dem iPad). 6. **Getrennt davon:** Connector-Plugin **v1.6.0** im WooCommerce-Shop einspielen (AP-34/AP-38), siehe `dev/partnercenter-api/README.md`. diff --git a/resources/docs/hinweise.md b/resources/docs/hinweise.md index b4bbff3..c805813 100644 --- a/resources/docs/hinweise.md +++ b/resources/docs/hinweise.md @@ -5,8 +5,8 @@ > später noch ausgebaut werden können. > > **Stand:** 09.09.2026 — ein geführter Rundgang durch die neuen Funktionen -> steht unter **Einstellungen → Leitfaden** (aktuell **Version 3**: Runde V7.0 -> plus Nachbesserungen und Nachtrag vom 08./09.09.2026). +> steht unter **Einstellungen → Leitfaden** (aktuell **Version 3.1**: Runde V7.0 +> plus Nachbesserungen und Nachträge AP-52/AP-53 vom 08./09.09.2026). --- @@ -220,11 +220,25 @@ Aus der Testphasen-Rückmeldung 26.08.2026 umgesetzt: Status-Mapping ist vollständig (Wartestellung, Abholung, Storno). *Details im Connector-README; benötigt v1.6.0.* -### Neu / geändert 08./09.09.2026 (Nachbesserungen + Nachtrag AP-52) +### Neu / geändert 08./09.09.2026 (Nachbesserungen + Nachträge AP-52/AP-53) Nach der unabhängigen Prüfung der V7-Umsetzung wurden alle Befunde behoben. Was sich dadurch für die Bedienung ändert: +- **Mengen mit drei Nachkommastellen (AP-53, Kundennachtrag 09.09.2026):** Die + Ist-Mengen je Charge in der **Produktion** lassen sich jetzt auf das Milligramm + eingeben, z. B. **0,125 g** Konservierer. Damit Restbestände ohne Rundungsdifferenz + aufgehen, gilt das genauso für die **Bestell-/Eingangsmenge im Wareneingang** und + die **Ausschussmenge**. Gespeichert, gerechnet (Chargen-Rest, FEFO-Verteilung, + Verfügbarkeit) und angezeigt wird einheitlich mit drei Stellen; Übersichten zeigen + weiterhin mindestens zwei Stellen („12,50 g") und die dritte nur, wenn sie belegt + ist („0,125 g"). Bestehende Buchungen bleiben unverändert (dritte Stelle 0). + **Eingaberegel:** Dezimaltrenner ist das **Komma** („0,125"). Ein **Punkt** mit + 1–2 Stellen („5.1", „5.12") oder „0.125" wird ebenfalls als Komma gelesen; ein Punkt + mit drei Stellen hinter einer anderen Ziffer bleibt der Tausenderpunkt + („1.500" = 1500 g). Die Soll-Ist-Toleranz von 5 % hat jetzt einen Sockel von 0,005 g + (vorher 0,05 g), damit auch Kleinstmengen sauber geprüft werden. + - **Häkchen „In der Produktion anzeigen" (AP-47, 09.09.2026 verschoben und ausformuliert):** Das bisherige Kästchen „Produktionsrelevant" oben im Produktformular war schwer zu finden. Es sitzt jetzt in der Karte **Warenwirtschaft** unter „Im Produktbestand diff --git a/resources/docs/testleitfaden.md b/resources/docs/testleitfaden.md index 3d94ff0..7820762 100644 --- a/resources/docs/testleitfaden.md +++ b/resources/docs/testleitfaden.md @@ -1,4 +1,4 @@ -# Testleitfaden Warenwirtschaft — Version 3 +# Testleitfaden Warenwirtschaft — Version 3.1 > **Stand:** 09.09.2026 > Dieser Leitfaden führt einmal durch die Funktionen der Warenwirtschaft — in der Reihenfolge eines typischen Arbeitsablaufs, sodass jede Stelle direkt im Browser nachgeschaut und getestet werden kann. @@ -6,7 +6,7 @@ > > - **Teil A** (Abschnitte 1–11) — die Basis-Funktionen der Warenwirtschaft (Runde 12.06.2026, Stand 16.07.2026). Unverändert gültig. > - **Teil B** (Abschnitte 12–18) — Runde V7.0 (Testphasen-Rückmeldung 26.–28.08.2026). -> - **Teil C** (Abschnitte 19–25) — **neu: Nachbesserungen aus der Umsetzungsprüfung und Nachtrag AP-52 (08./09.09.2026)**. Wer nur die neuen Punkte prüfen will, springt direkt zu **Teil C**. +> - **Teil C** (Abschnitte 19–26) — **neu: Nachbesserungen aus der Umsetzungsprüfung und Nachträge AP-52/AP-53 (08./09.09.2026)**. Wer nur die neuen Punkte prüfen will, springt direkt zu **Teil C**. --- @@ -401,6 +401,24 @@ --- +## 26. Mengen mit drei Nachkommastellen (AP-53) + +**Wo:** **[Produktion](/admin/inventory/productions)** (Ist-Menge je Charge), **[Einkauf & Wareneingang](/admin/inventory/stock-entries)** (Bestell-/Eingangsmenge), **[Ausschuss](/admin/inventory/stock-disposals)** (Menge) + +**Neu:** +- Gramm-Mengen werden auf **drei Nachkommastellen** (Milligramm) eingegeben, gespeichert, gerechnet und angezeigt — z. B. **0,125 g**. Bisher wurde auf zwei Stellen gerundet (0,125 → 0,13). +- Anzeige: mindestens zwei Stellen („12,50 g"), die dritte nur wenn belegt („0,125 g"); Chargen-Restbestände im Rohstoff-Detail zeigen sie ebenfalls. +- Eingabe: **Komma** als Dezimaltrenner. Punkt mit 1–2 Stellen („5.12") oder „0.125" wird als Komma gelesen; „1.500" bleibt 1500 g (Tausenderpunkt). +- Soll-Ist-Toleranz: 5 % mit Sockel 0,005 g (vorher 0,05 g). + +**So testen:** +1. Rezeptur mit einem Rohstoff zu 0,125 % (bzw. 0,125 g je Stück) anlegen, Produktion für 1 Stück: Ist-Menge **0,125** eintragen → Ampel grün, abschließen → Produktionsdetail zeigt **0,125 g**, Rohstoff-Detail: Chargen-Rest um genau 0,125 g kleiner. +2. Denselben Entwurf mit **1,125** speichern und wieder öffnen → im Feld steht **1,125** (nicht 1.125 oder 1,13). +3. Wareneingang mit **250,375 g** buchen → Detail zeigt 250,375 g; Ausschuss **0,005 g** buchen → geht durch, Restbestand rechnet auf drei Stellen. +4. Gegenprobe Tausenderpunkt: Ist-Menge **1.500** eingeben → Summe der Phase zeigt 1.500,00 g (nicht 1,5). + +--- + ## Hinweis am Rande Die Listen **Einkauf & Wareneingang**, **Produktionen** und die **Produktbestand-Historie** zeigen aus Performance-Gründen die neuesten 500 Einträge; darüber erscheint ein Link **„Alle anzeigen"**. Offene Bestellungen sind immer vollständig gelistet. diff --git a/resources/views/admin/inventory/productions/_scripts.blade.php b/resources/views/admin/inventory/productions/_scripts.blade.php index 64b6319..6afa058 100644 --- a/resources/views/admin/inventory/productions/_scripts.blade.php +++ b/resources/views/admin/inventory/productions/_scripts.blade.php @@ -29,8 +29,18 @@ return url; } + // AP-53: Gramm-Mengen mit bis zu drei Nachkommastellen (0,125), mindestens zwei (12,50). function fmt(value) { - return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}); + return Number(value).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 3}); + } + + // AP-53: Zahl → Eingabefeld (deutsches Komma, ohne Tausenderpunkt, max. 3 Stellen), + // damit gespeicherte Werte wie 1,125 beim Wiederöffnen nicht als 1.125 (= 1125) gelesen werden. + function fmtInput(value) { + if (typeof value !== 'number') { + return value == null ? '' : String(value); + } + return Number(value).toLocaleString('de-DE', {maximumFractionDigits: 3, useGrouping: false}); } // AP-52: Stückzahlen (Verpackung) ohne Nachkommastellen. @@ -44,8 +54,9 @@ return 0; } // Audit N-6: einzelner Punkt mit 1–2 Nachkommastellen = Dezimaltrenner - // ("5.1" = 5,1) — identische Regel wie serverseitig (reFormatWawiNumber). - if (/^-?\d+\.\d{1,2}$/.test(value)) { + // ("5.1" = 5,1); AP-53: zusätzlich "0.125" — identische Regel wie + // serverseitig (reFormatWawiNumber). "1.500" bleibt 1500. + if (/^-?(\d+\.\d{1,2}|0\.\d{3})$/.test(value)) { return parseFloat(value) || 0; } return parseFloat(value.replace(/\./g, '').replace(',', '.')) || 0; @@ -96,7 +107,7 @@ var $colQty = $('
'); var $group = $(''); - $group.append($('').val(preselectedQty || '')); + $group.append($('').val(fmtInput(preselectedQty))); $group.append('