From 9e745d144161b08b915069cf3b9c2a5ac815b5e4 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 8 Sep 2026 10:49:14 +0000 Subject: [PATCH] =?UTF-8?q?Warenwirtschaft:=20F-01=20=E2=80=94=20Entw?= =?UTF-8?q?=C3=BCrfe=20z=C3=A4hlen=20nicht=20mehr=20als=20Chargenverbrauch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit consumedByStockEntry() schließt Entwürfe jetzt wie remainingByIngredient() aus. Regression: Chargen-/Standortbestand, zweite Produktion, Ausschussprüfung. Co-Authored-By: Claude Fable 5.1 --- app/Services/ProductionService.php | 3 ++ tests/Feature/ProductionDraftTest.php | 47 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/app/Services/ProductionService.php b/app/Services/ProductionService.php index 16d73c4..9fc8416 100644 --- a/app/Services/ProductionService.php +++ b/app/Services/ProductionService.php @@ -455,6 +455,9 @@ class ProductionService $rows = ProductionIngredient::query() ->join('productions', 'productions.id', '=', 'production_ingredients.production_id') ->whereNull('productions.deleted_at') + // Prüfung 08.09.2026 F-01: Entwürfe sind nicht bestandswirksam — wie in + // InventoryService::remainingByIngredient(), sonst blockiert ein Entwurf die Charge. + ->where('productions.status', '!=', Production::STATUS_DRAFT) ->selectRaw('production_ingredients.stock_entry_id as stock_entry_id, SUM(production_ingredients.quantity_used) as used') ->whereIn('production_ingredients.stock_entry_id', $stockEntryIds) ->when($excludeProductionId !== null, fn ($q) => $q->where('production_ingredients.production_id', '!=', $excludeProductionId)) diff --git a/tests/Feature/ProductionDraftTest.php b/tests/Feature/ProductionDraftTest.php index 0dbab9f..4b50d3a 100644 --- a/tests/Feature/ProductionDraftTest.php +++ b/tests/Feature/ProductionDraftTest.php @@ -215,3 +215,50 @@ test('AP-43: HTTP zwischenspeichern ohne ergebnis speichert entwurf; abschließe 'quantity' => 3, ])->assertSessionHasErrors('outputs'); }); + +test('F-01: entwurf reduziert weder chargen- noch standortbestand und blockiert keine andere produktion', function () { + $user = draftUser(); + $location = Location::factory()->create(); + $supplier = Supplier::factory()->create(); + $ing = draftIngredient(); + $product = draftRecipeProduct($ing); + $stock = draftStock($ing, $location, $supplier, $user); + + $draft = app(ProductionService::class)->store( + ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 1, 'notes' => null], + [['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '10']], + $user->id, + [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => null]], + true // draft + ); + + $productionService = app(ProductionService::class); + $inventory = app(InventoryService::class); + + // Chargenverbrauch ohne Ausschluss der eigenen Produktion — wie ihn andere Produktionen/Ausschuss sehen + expect($productionService->consumedByStockEntry([$stock->id]))->toBe([]) + ->and((float) $productionService->availableStockEntriesForIngredient($ing->id, $location->id)->first()->getAttribute('remaining_quantity'))->toBe(1000.0) + ->and((float) $inventory->chargesWithRemainingForIngredient($ing->id)->first()->getAttribute('remaining_quantity'))->toBe(1000.0) + ->and((float) ($inventory->remainingByLocationForIngredient($ing->id)[$location->id] ?? 0.0))->toBe(1000.0); + + // Eine zweite Produktion kann die komplette Charge trotz Entwurf buchen + $completed = $productionService->store( + ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-02', 'quantity' => 100, 'notes' => null], + [['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '1000']], + $user->id, + [['product_id' => $product->id, 'quantity' => 100, 'batch_number' => null]] + ); + + expect($completed->isDraft())->toBeFalse() + ->and($productionService->consumedByStockEntry([$stock->id])[$stock->id])->toBe(1000.0) + ->and($productionService->availableStockEntriesForIngredient($ing->id, $location->id))->toHaveCount(0); + + // Ausschuss mit Charge: Entwurf darf die zulässige Menge nicht verkleinern (vor Abschluss der zweiten Produktion wäre 1000 g erlaubt) + $completed->delete(); + $this->actingAs($user, 'user') + ->post(route('admin.inventory.stock-disposals.store'), [ + 'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, + 'location_id' => $location->id, 'quantity' => '1000', 'disposed_at' => '2026-08-03', 'reason' => 'Test', + ]) + ->assertSessionHasNoErrors(); +});