Warenwirtschaft: F-09 — Ausschuss ohne Charge standort- und chargenkonsistent

Ohne Charge wird gegen den Restbestand des gewählten Lagerorts geprüft und die
Menge FEFO auf die dort verfügbaren Chargen verteilt (je Charge gebucht, in
einer Transaktion) — wie der Produktionsverbrauch. Gesamt-, Standort- und
Chargenbestand stimmen damit überein.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-08 11:01:13 +00:00
parent 2796b1103e
commit db27f040e6
3 changed files with 107 additions and 5 deletions

View file

@ -136,3 +136,57 @@ test('standort-aggregation rechnet chargen-ausschuss am standort der charge ab',
expect($byLocation[$locationA->id])->toBe(900.0);
expect($byLocation)->not->toHaveKey($locationB->id);
});
test('F-09: ausschuss ohne charge prüft den gewählten lagerort, nicht den globalen bestand', function () {
$user = cdcUser();
$supplier = Supplier::factory()->create();
$locA = Location::factory()->create(['name' => 'F09-A']);
$locB = Location::factory()->create(['name' => 'F09-B']);
$ing = cdcIngredient('F09-Standort');
cdcStock($ing, $locA, $supplier, $user, ['received_quantity' => 1000, 'batch_number' => 'F09-1']);
$this->actingAs($user, 'user');
$this->from(route('admin.inventory.stock-disposals.create'))
->post(route('admin.inventory.stock-disposals.store'), [
'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $locB->id,
'quantity' => '500', 'reason' => 'Verfall / MHD überschritten', 'disposed_at' => '2026-09-01',
])->assertSessionHasErrors('quantity');
expect(app(InventoryService::class)->remainingByLocationForIngredient($ing->id)[$locB->id] ?? 0.0)->toBe(0.0);
});
test('F-09: ausschuss ohne charge wird FEFO auf die chargen des lagerorts verteilt und reduziert deren verfügbarkeit', function () {
$user = cdcUser();
$supplier = Supplier::factory()->create();
$loc = Location::factory()->create();
$ing = cdcIngredient('F09-FEFO');
$old = cdcStock($ing, $loc, $supplier, $user, ['received_quantity' => 300, 'batch_number' => 'F09-ALT', 'best_before' => '2027-01-01']);
$new = cdcStock($ing, $loc, $supplier, $user, ['received_quantity' => 700, 'batch_number' => 'F09-NEU', 'best_before' => '2029-01-01']);
$this->actingAs($user, 'user');
$this->post(route('admin.inventory.stock-disposals.store'), [
'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $loc->id,
'quantity' => '400', 'reason' => 'Verfall / MHD überschritten', 'disposed_at' => '2026-09-01',
])->assertRedirect()->assertSessionHasNoErrors();
// 300 g aus der älteren Charge (FEFO), 100 g aus der neueren — beides chargenbezogen.
$this->assertDatabaseHas('stock_disposals', ['stock_entry_id' => $old->id, 'quantity' => 300.00]);
$this->assertDatabaseHas('stock_disposals', ['stock_entry_id' => $new->id, 'quantity' => 100.00]);
$this->assertDatabaseMissing('stock_disposals', ['ingredient_id' => $ing->id, 'stock_entry_id' => null]);
$inventory = app(InventoryService::class);
$charges = app(ProductionService::class)->availableStockEntriesForIngredient($ing->id, $loc->id);
expect($inventory->remainingByIngredient([$ing->id])[$ing->id])->toBe(600.0)
->and($charges)->toHaveCount(1)
->and($charges->first()->id)->toBe($new->id)
->and((float) $charges->first()->getAttribute('remaining_quantity'))->toBe(600.0)
->and($inventory->chargesWithRemainingForIngredient($ing->id)->sum('remaining_quantity'))->toEqual(600);
// Gesamten Rest ohne Charge entsorgen: global 0 und keine Charge mehr verfügbar (D-07).
$this->post(route('admin.inventory.stock-disposals.store'), [
'disposal_type' => 'ingredient', 'ingredient_id' => $ing->id, 'location_id' => $loc->id,
'quantity' => '600', 'reason' => 'Verfall / MHD überschritten', 'disposed_at' => '2026-09-02',
])->assertSessionHasNoErrors();
expect($inventory->remainingByIngredient([$ing->id])[$ing->id])->toBe(0.0)
->and(app(ProductionService::class)->availableStockEntriesForIngredient($ing->id, $loc->id))->toHaveCount(0);
});