Warenwirtschaft: AP-51 — Lagerplatz-Felder für Produkte & Verpackung
Lagerplatz-Zuordnung (Raum/Regal/Fach) gab es bisher nur bei Rohstoffen. Jetzt dieselbe Storage-Hierarchie (StorageRoom/Shelf/Slot) auch für Produkte und Verpackungsartikel, inkl. Anzeige an den passenden Stellen. - Migration: storage_room_id/shelf/slot an products + packaging_items (nullable, FK constrained, nullOnDelete). - Models Product + PackagingItem: fillable + storageRoom/Shelf/Slot-Relationen + storageLabel() („Raum 1 | Regal 3 | A"). - Produktformular (Inventar-Abschnitt) + Verpackungsformular: Auswahl-Zeile Raum/Regal/Fach; Optionen kommen aktiv/sortiert aus den zentral pflegbaren Bausteinen (Einstellungen → Allgemein). - Speichern: ProductRepository normalisiert leere Auswahl -> null (FK); PackagingItem-Requests + Repository um die drei Felder erweitert. - Anzeige: Produktbestand (Sub-Zeile mit Marker), Produktverpackung-Übersicht (neue Spalte „Lagerplatz"). - DeletionGuard: Lagerbaustein ist nicht löschbar, solange Produkte/Verpackung darauf verweisen (analog Rohstoffe). Tests: 4 neue (Produkt speichern + leer->null, Verpackung speichern + Anzeige, Löschsperre, freier Baustein löschbar). Migration auf Dev-/Live-DB gelaufen. Suite 331 grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
eb47c8a82d
commit
30d4e28696
16 changed files with 363 additions and 5 deletions
103
tests/Feature/StorageAssignmentTest.php
Normal file
103
tests/Feature/StorageAssignmentTest.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
use App\Models\PackagingItem;
|
||||
use App\Models\PackagingMaterial;
|
||||
use App\Models\Product;
|
||||
use App\Models\StorageRoom;
|
||||
use App\Models\StorageShelf;
|
||||
use App\Models\StorageSlot;
|
||||
use App\Repositories\ProductRepository;
|
||||
use App\Services\DeletionGuard;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function storageTestUser(int $adminLevel = 9): User
|
||||
{
|
||||
$user = User::query()->create(['email' => uniqid('storage_', true).'@test.example', 'password' => bcrypt('password')]);
|
||||
$user->forceFill(['admin' => $adminLevel, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
test('AP-51: produkt speichert lagerplatz und setzt leere auswahl auf null', function () {
|
||||
$room = StorageRoom::factory()->create(['label' => 'Raum 1']);
|
||||
$shelf = StorageShelf::factory()->create(['label' => 'Regal 3']);
|
||||
$slot = StorageSlot::factory()->create(['label' => 'A']);
|
||||
|
||||
$product = Product::query()->create(['name' => 'AP51 Produkt', 'title' => 'AP51 Produkt', 'active' => true, 'is_set' => false]);
|
||||
|
||||
(new ProductRepository($product))->update([
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'title' => $product->title,
|
||||
'active' => '1',
|
||||
'storage_room_id' => (string) $room->id,
|
||||
'storage_shelf_id' => (string) $shelf->id,
|
||||
'storage_slot_id' => (string) $slot->id,
|
||||
]);
|
||||
|
||||
$product->refresh();
|
||||
expect($product->storage_room_id)->toBe($room->id)
|
||||
->and($product->storage_shelf_id)->toBe($shelf->id)
|
||||
->and($product->storage_slot_id)->toBe($slot->id)
|
||||
->and($product->storageLabel())->toBe('Raum 1 | Regal 3 | A');
|
||||
|
||||
// Leere Auswahl (Select „— keine Angabe —" sendet "") => null, kein FK-Fehler.
|
||||
(new ProductRepository($product))->update([
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'title' => $product->title,
|
||||
'active' => '1',
|
||||
'storage_room_id' => '',
|
||||
'storage_shelf_id' => '',
|
||||
'storage_slot_id' => '',
|
||||
]);
|
||||
|
||||
$product->refresh();
|
||||
expect($product->storage_room_id)->toBeNull()
|
||||
->and($product->storageLabel())->toBeNull();
|
||||
});
|
||||
|
||||
test('AP-51: verpackungsartikel speichert lagerplatz und zeigt ihn an', function () {
|
||||
$material = PackagingMaterial::factory()->create();
|
||||
$room = StorageRoom::factory()->create(['label' => 'Raum 2']);
|
||||
$slot = StorageSlot::factory()->create(['label' => 'B']);
|
||||
$this->actingAs(storageTestUser(), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.packaging-items.store'), [
|
||||
'packaging_material_id' => (string) $material->id,
|
||||
'name' => 'AP51 Karton',
|
||||
'category' => 'packaging',
|
||||
'storage_room_id' => (string) $room->id,
|
||||
'storage_slot_id' => (string) $slot->id,
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.packaging-items.index', ['category' => 'packaging']));
|
||||
|
||||
$item = PackagingItem::query()->where('name', 'AP51 Karton')->firstOrFail();
|
||||
expect($item->storage_room_id)->toBe($room->id)
|
||||
->and($item->storage_shelf_id)->toBeNull()
|
||||
->and($item->storageLabel())->toBe('Raum 2 | B');
|
||||
|
||||
// Anzeige in der Übersicht
|
||||
$this->get(route('admin.inventory.packaging-items.index', ['category' => 'packaging']))
|
||||
->assertOk()
|
||||
->assertSee('Raum 2 | B');
|
||||
});
|
||||
|
||||
test('AP-51: lagerbaustein mit produkt-/verpackungs-referenz ist nicht löschbar', function () {
|
||||
$room = StorageRoom::factory()->create(['label' => 'Raum belegt']);
|
||||
Product::query()->create(['name' => 'AP51 Ref', 'title' => 'AP51 Ref', 'active' => true, 'is_set' => false, 'storage_room_id' => $room->id]);
|
||||
|
||||
$blockers = app(DeletionGuard::class)->blockersFor($room);
|
||||
expect($blockers)->not->toBeEmpty()
|
||||
->and(implode(' ', $blockers))->toContain('Produkt');
|
||||
});
|
||||
|
||||
test('AP-51: freier lagerbaustein bleibt löschbar', function () {
|
||||
$room = StorageRoom::factory()->create(['label' => 'Raum frei']);
|
||||
|
||||
expect(app(DeletionGuard::class)->blockersFor($room))->toBeEmpty();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue