From a803e956e1f16f0059f364b1f3e57b5388bd9515 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 8 Sep 2026 10:55:51 +0000 Subject: [PATCH] =?UTF-8?q?Warenwirtschaft:=20F-05=20=E2=80=94=20historisc?= =?UTF-8?q?her=20Verpackungs-Snapshot=20bleibt=20bei=20Notizkorrektur=20er?= =?UTF-8?q?halten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updateProduction() baut die Verpackungszeilen nur neu auf, wenn sich Ergebnisprodukte/Stückzahlen ändern oder ein Entwurf abgeschlossen wird. Co-Authored-By: Claude Fable 5.1 --- app/Services/ProductionService.php | 41 ++++++++++++++++++++++--- tests/Feature/ProductionScalingTest.php | 30 ++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/Services/ProductionService.php b/app/Services/ProductionService.php index 918e6b0..3def773 100644 --- a/app/Services/ProductionService.php +++ b/app/Services/ProductionService.php @@ -159,17 +159,31 @@ class ProductionService $production->productionIngredients()->create($this->ingredientLineAttributes($line)); } + // F-05: Ergebnisdaten vor dem Neuaufbau festhalten — nur bei tatsächlicher + // Änderung von Produkt/Menge wird der historische Verpackungs-Snapshot ersetzt. + $hadSnapshot = ! $locked->isDraft(); + $previousOutputs = $this->outputSignature($production); + $production->outputs()->delete(); if (! $draft || $outputs !== []) { $this->persistOutputs($production, $product, $producedQty, $outputs, $draft); } - // AP-43: Entwürfe buchen keinen Bestand. Ein zuvor gelöschter Verpackungs- - // Snapshot bleibt für den Entwurf leer. - $production->productionPackagings()->delete(); - if (! $draft) { + if ($draft) { + // AP-43: Entwürfe buchen keinen Bestand. Ein zuvor gelöschter Verpackungs- + // Snapshot bleibt für den Entwurf leer. + $production->productionPackagings()->delete(); + } else { // AP-45: Verpackung folgt dem Produktionsergebnis, daher nach den Outputs. - $this->syncPackagingSnapshot($production); + // F-05: Der Snapshot ist historisch (Konzept „Warum ein Snapshot?“): Eine + // Notiz-/Datumskorrektur an einer alten Produktion darf ihn nicht aus der + // heutigen Stückliste neu berechnen. Regel: Snapshot bleibt, solange + // Ergebnisprodukte und Stückzahlen unverändert sind; bei einer echten + // Mengen-/Produktkorrektur wird er mit der aktuellen Stückliste neu gebildet. + if (! $hadSnapshot || $this->outputSignature($production) !== $previousOutputs) { + $production->productionPackagings()->delete(); + $this->syncPackagingSnapshot($production); + } $production->setRelation('product', $product); $this->productStockService->recordProductionStock($production, $userId); @@ -328,6 +342,23 @@ class ProductionService } } + /** + * F-05: Vergleichbare Signatur des Produktionsergebnisses (Produkt + Stückzahl, + * reihenfolgeunabhängig). + * + * @return array + */ + private function outputSignature(Production $production): array + { + $signature = $production->outputs() + ->get(['product_id', 'quantity']) + ->map(fn ($o) => (int) $o->product_id.':'.(int) $o->quantity) + ->all(); + sort($signature); + + return $signature; + } + /** * @return array */ diff --git a/tests/Feature/ProductionScalingTest.php b/tests/Feature/ProductionScalingTest.php index 8aed314..062ca10 100644 --- a/tests/Feature/ProductionScalingTest.php +++ b/tests/Feature/ProductionScalingTest.php @@ -187,6 +187,36 @@ test('AP-45: verpackung wird je ergebnisprodukt aus dem produktionsergebnis gebu ->and($byItem[$pkB->id]->quantity_used)->toBe(100); // 2 × 50 (Ergebnis Other) }); +test('F-05: notizkorrektur behält den historischen verpackungs-snapshot, mengenkorrektur bildet ihn neu', function () { + $user = psUser(); + $supplier = Supplier::factory()->create(); + $location = Location::factory()->create(); + $material = PackagingMaterial::factory()->create(); + $pk = PackagingItem::factory()->create(['packaging_material_id' => $material->id, 'supplier_id' => $supplier->id, 'name' => 'PS-F05-Tiegel', 'category' => 'packaging']); + + $product = Product::query()->create(['name' => uniqid('PS-F05-', true), 'title' => 'x', 'active' => true, 'no_recipe_required' => true]); + $product->packagings()->sync([$pk->id => ['quantity' => 1, 'pos' => 0]]); + + $service = app(ProductionService::class); + $head = ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 1]; + $production = $service->store($head, [], $user->id, [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => 'F05']]); + expect($production->productionPackagings->sum('quantity_used'))->toBe(1); + + // Stückliste nachträglich auf 2 Tiegel je Stück ändern. + $product->packagings()->sync([$pk->id => ['quantity' => 2, 'pos' => 0]]); + + // Nur die Notiz korrigieren: Snapshot bleibt bei 1. + $production = $service->updateProduction($production->fresh(), $head + ['notes' => 'Korrektur'], [], $user->id, + [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => 'F05']]); + expect($production->productionPackagings->sum('quantity_used'))->toBe(1) + ->and($production->notes)->toBe('Korrektur'); + + // Echte Mengenkorrektur (1 → 3 Stück): Snapshot wird mit der aktuellen Stückliste neu gebildet (2 × 3). + $production = $service->updateProduction($production->fresh(), $head + ['quantity' => 3], [], $user->id, + [['product_id' => $product->id, 'quantity' => 3, 'batch_number' => 'F05']]); + expect($production->productionPackagings->sum('quantity_used'))->toBe(6); +}); + test('AP-47: produktions-auswahl zeigt nur produktionsrelevante produkte', function () { $user = psUser(); $this->actingAs($user, 'user');