Warenwirtschaft: AP-48 — Produktbestand-Buchungsgründe pflegbar, getrennt Ein/Aus

Kundenentscheid 26.08. (D-5): Eingang und Ausgang haben eigene, pflegbare
Gründe-Listen statt einer festen Liste im Controller.

- Neues Modell/Tabelle stock_movement_reasons (label, direction in/out, pos,
  active, SoftDeletes) inkl. Seed der bisherigen Gründe sinnvoll auf Ein-/Ausgang
  aufgeteilt.
- CRUD wie DisposalReason (SettingCrudController + FormRequest + Formular mit
  Richtungs-Auswahl); zwei Listen (Eingangs-/Ausgangs-Gründe) auf der Seite
  Einstellungen → Allgemeines, „Neu anlegen" mit vorbelegter Richtung.
- Produktbestand-Maske: das Grund-Dropdown wird jetzt je nach Eingang/Ausgang
  per JS aus den passenden, gepflegten Gründen befüllt (manualReasons entfernt).
- DeletionGuard: StockMovementReason immer löschbar.

Migration create_stock_movement_reasons auf Dev-/Live-DB. 4 Tests, Suite 326 grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-28 09:22:32 +00:00
parent e08c8dec40
commit 190713c21b
12 changed files with 381 additions and 22 deletions

View file

@ -0,0 +1,57 @@
<?php
use App\Models\StockMovementReason;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function smrAdmin(): User
{
$user = User::query()->create(['email' => uniqid('smr_', true).'@test.example', 'password' => bcrypt('password')]);
$user->forceFill(['admin' => 9, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
return $user->fresh();
}
test('AP-48: seed liefert getrennte gründe für eingang und ausgang', function () {
expect(StockMovementReason::query()->where('direction', 'in')->count())->toBeGreaterThan(0)
->and(StockMovementReason::query()->where('direction', 'out')->count())->toBeGreaterThan(0)
// „Testervergabe" ist ein Ausgangs-, kein Eingangsgrund
->and(StockMovementReason::query()->where('direction', 'out')->where('label', 'Testervergabe')->exists())->toBeTrue()
->and(StockMovementReason::query()->where('direction', 'in')->where('label', 'Testervergabe')->exists())->toBeFalse();
});
test('AP-48: grund kann mit richtung angelegt werden', function () {
$this->actingAs(smrAdmin(), 'user');
$this->post(route('admin.inventory.stock-movement-reasons.store'), [
'label' => 'AP48-Sonderentnahme',
'direction' => 'out',
'active' => '1',
'pos' => '3',
])->assertRedirect(route('admin.inventory.general'));
$reason = StockMovementReason::query()->where('label', 'AP48-Sonderentnahme')->firstOrFail();
expect($reason->direction)->toBe('out')->and($reason->active)->toBeTrue();
});
test('AP-48: produktbestand-maske liefert die gründe je richtung', function () {
$this->actingAs(smrAdmin(), 'user');
StockMovementReason::query()->create(['label' => 'AP48-NurEingang', 'direction' => 'in', 'active' => true, 'pos' => 9]);
$this->get(route('admin.inventory.product-stock.index'))
->assertSuccessful()
->assertSee('AP48-NurEingang');
});
test('AP-48: grund ist immer löschbar', function () {
$this->actingAs(smrAdmin(), 'user');
$reason = StockMovementReason::query()->create(['label' => 'AP48-Weg', 'direction' => 'in', 'active' => true, 'pos' => 9]);
$this->delete(route('admin.inventory.stock-movement-reasons.destroy', $reason))
->assertRedirect(route('admin.inventory.general'));
expect(StockMovementReason::query()->find($reason->id))->toBeNull();
});