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(); +});