Warenwirtschaft: F-01 — Entwürfe zählen nicht mehr als Chargenverbrauch

consumedByStockEntry() schließt Entwürfe jetzt wie remainingByIngredient()
aus. Regression: Chargen-/Standortbestand, zweite Produktion, Ausschussprüfung.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-08 10:49:14 +00:00
parent 0a1e796f7b
commit 9e745d1441
2 changed files with 50 additions and 0 deletions

View file

@ -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))

View file

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