Warenwirtschaft: Anforderungsrunde 12.06. — Plan V5.0 + AP-26/AP-25/AP-22

Neue Anforderungen (docs/) interpretiert und als Entwicklungsplan V5.0
(AP-20 bis AP-28) aufgenommen; erste drei Pakete umgesetzt:

AP-26 Ausschuss-Gründe konfigurierbar:
- Stammdaten-Tabelle disposal_reasons + CRUD unter Einstellungen → Allgemein
- StockDisposalController liest aktive DB-Gründe statt hartkodierter Liste
- Seeder übernimmt die bisherigen 6 Gründe idempotent

AP-25 Lieferbestand — Datum statt Tage:
- "Nicht vorrätig" wird über Datepicker "Wieder lieferbar ab" gepflegt;
  Resttage-Hinweis zählt täglich automatisch herunter
- Interne Bestellliste wieder kaufbar: Hinweis erscheint zusätzlich zu
  den Mengen-Buttons (VP entscheidet selbst)

AP-22 Produktbestand-Erweiterungen:
- Default-Sortierung nach Dringlichkeit, Status-Kopf toggelt
- Alle vier Status-Kacheln als Filter klickbar
- Neue Spalte "Verbrauch/Monat" (Ø Abgänge der letzten 6 Monate)
- Produkt-Flag "Im Produktbestand anzeigen" (products.show_in_product_stock)

Tests: 77 grün (DisposalReasonSettings 8, ProductOutOfStock 8,
ProductStock 13 + Regression). Hinweise-Doku + Plan-Protokoll fortgeschrieben;
nächster Schritt laut Plan: AP-21 (INCI-Erweiterungen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-12 16:28:45 +00:00
parent a8f6fef38e
commit e53201f229
32 changed files with 1377 additions and 94 deletions

View file

@ -189,3 +189,85 @@ test('historie rendert und filtert nach richtung', function () {
$response->assertSee('Produktbestand Historie');
$response->assertSee('Verkauf');
});
// --- AP-22: Produktbestand-Erweiterungen ---
test('flag blendet produkt aus uebersicht und kritisch-zaehler aus', function () {
$user = ap11MakeUser(1);
$service = app(ProductStockService::class);
$hidden = ap11MakeProduct('AP22-Versteckt', ['show_in_product_stock' => false, 'critical_product_stock' => 10]);
$service->recordMovement($hidden, 'in', 2, 'Initialbestand');
$visible = ap11MakeProduct('AP22-Sichtbar', ['critical_product_stock' => 10]);
$service->recordMovement($visible, 'in', 2, 'Initialbestand');
expect($service->criticalProductCount())->toBe(1);
$this->actingAs($user, 'user');
$response = $this->get(route('admin.inventory.product-stock.index'));
$response->assertSuccessful();
$response->assertSee('AP22-Sichtbar');
$response->assertDontSee('AP22-Versteckt');
});
test('verbrauch pro monat mittelt abgaenge der letzten 6 monate', function () {
$product = ap11MakeProduct('AP22-Verbrauch');
$service = app(ProductStockService::class);
// 60 Stück Abgang innerhalb des Fensters => Ø 10/Monat
$service->recordMovement($product, 'out', 24, 'Verkauf', 'sale');
$m = $service->recordMovement($product, 'out', 36, 'Verkauf', 'sale');
$m->forceFill(['created_at' => now()->subMonths(3)])->save();
// außerhalb des Fensters: zählt nicht
$old = $service->recordMovement($product, 'out', 600, 'Verkauf', 'sale');
$old->forceFill(['created_at' => now()->subMonths(7)])->save();
// Produktions-Gegenbuchung (Korrektur): zählt nicht als Verbrauch
$service->recordMovement($product, 'out', 100, 'Produktionskorrektur', 'production');
// Eingänge zählen nie
$service->recordMovement($product, 'in', 500, 'Initialbestand');
$result = $service->monthlyConsumptionByProduct([$product->id]);
expect($result[$product->id])->toBe(10.0);
});
test('uebersicht sortiert default nach dringlichkeit', function () {
$user = ap11MakeUser(1);
$service = app(ProductStockService::class);
$ok = ap11MakeProduct('AP22-Zustand-OK', ['critical_product_stock' => 10]);
$service->recordMovement($ok, 'in', 100, 'Initialbestand');
$critical = ap11MakeProduct('AP22-Zustand-Kritisch', ['critical_product_stock' => 10]);
$service->recordMovement($critical, 'in', 2, 'Initialbestand');
$this->actingAs($user, 'user');
$response = $this->get(route('admin.inventory.product-stock.index'));
$response->assertSuccessful();
// Kritisches Produkt erscheint vor dem OK-Produkt (Default-Sortierung nach Dringlichkeit)
$response->assertSeeInOrder(['AP22-Zustand-Kritisch', 'AP22-Zustand-OK']);
});
test('uebersicht zeigt verbrauch-spalte und alle kacheln klickbar', function () {
$user = ap11MakeUser(1);
ap11MakeProduct('AP22-Render');
$this->actingAs($user, 'user');
$response = $this->get(route('admin.inventory.product-stock.index'));
$response->assertSuccessful();
$response->assertSee('Verbrauch/Monat');
$response->assertSee('data-filter="all"', false);
$response->assertSee('data-filter="ok"', false);
$response->assertSee('data-filter="warning"', false);
$response->assertSee('data-filter="critical"', false);
// alle vier Kacheln tragen is-clickable
expect(substr_count($response->getContent(), 'wawi-stat is-clickable')
+ substr_count($response->getContent(), 'is-clickable" data-filter'))->toBeGreaterThanOrEqual(1);
});