Je Verpackungsartikel Bestand (Stück), Differenz zu Bedarf (rot/grün wie bei Rohstoffen, AP-40) und Lagerplatz; Spalte einheitlich „Lagerort“ für Rohstoffe (INCI-Tabelle, vorher „Ablage“) und Verpackung. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
113 lines
5.9 KiB
PHP
113 lines
5.9 KiB
PHP
<?php
|
||
|
||
use App\Models\Location;
|
||
use App\Models\PackagingItem;
|
||
use App\Models\PackagingMaterial;
|
||
use App\Models\Product;
|
||
use App\Models\StockEntry;
|
||
use App\Models\StorageRoom;
|
||
use App\Models\StorageShelf;
|
||
use App\Models\Supplier;
|
||
use App\Services\ProductionService;
|
||
use App\User;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Tests\TestCase;
|
||
|
||
uses(TestCase::class, RefreshDatabase::class);
|
||
|
||
/**
|
||
* Prüfung 08.09.2026 F-08: Füllgewicht mit Nachkommastellen (Dezimalspalte, deutsche Eingabe, Validierung).
|
||
*/
|
||
function fwUser(): User
|
||
{
|
||
$user = User::query()->create(['email' => uniqid('fw_', true).'@test.example', 'password' => bcrypt('password')]);
|
||
$user->forceFill(['admin' => 7, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
|
||
|
||
return $user->fresh();
|
||
}
|
||
|
||
test('F-08: füllgewicht 39,5 wird mit nachkommastellen gespeichert und skaliert die rezeptur exakt', function () {
|
||
$this->actingAs(fwUser(), 'user');
|
||
|
||
$this->post(route('admin_product_store'), ['id' => 'new', 'name' => 'FW-Dezimal', 'fill_weight' => '39,5'])
|
||
->assertRedirect()->assertSessionHasNoErrors();
|
||
|
||
$product = Product::query()->where('name', 'FW-Dezimal')->firstOrFail();
|
||
expect($product->fill_weight)->toBe(39.5)
|
||
->and(app(ProductionService::class)->recipeGramScale($product))->toBe(0.395);
|
||
|
||
// Punkt mit 1–2 Nachkommastellen gilt als Dezimaltrenner (WaWi-Konvention), leer = null.
|
||
$this->post(route('admin_product_store'), ['id' => $product->id, 'name' => 'FW-Dezimal', 'fill_weight' => '4.5'])->assertSessionHasNoErrors();
|
||
expect($product->fresh()->fill_weight)->toBe(4.5);
|
||
|
||
$this->post(route('admin_product_store'), ['id' => $product->id, 'name' => 'FW-Dezimal', 'fill_weight' => ''])->assertSessionHasNoErrors();
|
||
expect($product->fresh()->fill_weight)->toBeNull();
|
||
});
|
||
|
||
test('F-08: ungültiges oder negatives füllgewicht wird abgelehnt', function () {
|
||
$this->actingAs(fwUser(), 'user');
|
||
$product = Product::query()->create(['name' => 'FW-Invalid', 'title' => 'x', 'active' => true, 'fill_weight' => 10]);
|
||
|
||
// Der Produkt-Controller rendert bei Fehlern das Formular direkt (kein Redirect).
|
||
foreach (['abc', '-5', '0'] as $bad) {
|
||
$this->post(route('admin_product_store'), ['id' => $product->id, 'name' => 'FW-Invalid', 'fill_weight' => $bad])
|
||
->assertOk()
|
||
->assertSee('is-invalid', false);
|
||
}
|
||
expect($product->fresh()->fill_weight)->toBe(10.0);
|
||
});
|
||
|
||
test('F-07: produktionsformular liefert die verpackungs-stückliste je ergebnisprodukt für die vorschau', function () {
|
||
$this->actingAs(fwUser(), 'user');
|
||
$supplier = Supplier::factory()->create();
|
||
$material = PackagingMaterial::factory()->create();
|
||
$pk = PackagingItem::factory()->create(['packaging_material_id' => $material->id, 'supplier_id' => $supplier->id, 'name' => 'F07-Tiegel', 'category' => 'packaging']);
|
||
$small = Product::query()->create(['name' => 'F07-5ml', 'title' => 'x', 'active' => true, 'no_recipe_required' => true, 'production_relevant' => false]);
|
||
$small->packagings()->sync([$pk->id => ['quantity' => 2, 'pos' => 0]]);
|
||
Location::factory()->create();
|
||
|
||
$html = $this->get(route('admin.inventory.productions.create'))->assertOk()->getContent();
|
||
$start = strpos($html, "data-packaging-bom='") + strlen("data-packaging-bom='");
|
||
$bom = json_decode(substr($html, $start, strpos($html, "'", $start) - $start), true);
|
||
|
||
expect($bom)->toHaveKey((string) $small->id)
|
||
->and($bom[$small->id][0]['name'])->toBe('F07-Tiegel')
|
||
->and($bom[$small->id][0]['per_unit'])->toEqual(2)
|
||
->and($html)->toContain('renderPackagingPreview');
|
||
});
|
||
|
||
test('AP-52: verpackungsvorschau liefert bestand und lagerort je verpackungsartikel; spalten heißen einheitlich Lagerort', function () {
|
||
$user = fwUser();
|
||
$this->actingAs($user, 'user');
|
||
$supplier = Supplier::factory()->create();
|
||
$material = PackagingMaterial::factory()->create();
|
||
$room = StorageRoom::query()->create(['label' => 'Lager 1', 'pos' => 0, 'active' => true]);
|
||
$shelf = StorageShelf::query()->create(['label' => 'Regal B', 'pos' => 0, 'active' => true]);
|
||
$pk = PackagingItem::factory()->create([
|
||
'packaging_material_id' => $material->id, 'supplier_id' => $supplier->id, 'name' => 'AP52-Tiegel', 'category' => 'packaging',
|
||
'storage_room_id' => $room->id, 'storage_shelf_id' => $shelf->id,
|
||
]);
|
||
$location = Location::factory()->create();
|
||
// 120 Stück Wareneingang, 20 bereits verbraucht → Bestand 100.
|
||
StockEntry::query()->create([
|
||
'entry_type' => 'packaging', 'packaging_item_id' => $pk->id, 'supplier_id' => $supplier->id, 'location_id' => $location->id, 'unit' => 'piece',
|
||
'ordered_by' => $user->id, 'ordered_at' => '2026-01-01', 'ordered_quantity' => 120, 'price_per_kg' => 1, 'status' => 'received',
|
||
'received_by' => $user->id, 'received_at' => '2026-01-15', 'received_quantity' => 120,
|
||
]);
|
||
$product = Product::query()->create(['name' => 'AP52-Produkt', 'title' => 'x', 'active' => true, 'no_recipe_required' => true]);
|
||
$product->packagings()->sync([$pk->id => ['quantity' => 1, 'pos' => 0]]);
|
||
app(ProductionService::class)->store(
|
||
['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 20],
|
||
[], $user->id, [['product_id' => $product->id, 'quantity' => 20, 'batch_number' => null]]
|
||
);
|
||
|
||
$html = $this->get(route('admin.inventory.productions.create'))->assertOk()->getContent();
|
||
$start = strpos($html, "data-packaging-info='") + strlen("data-packaging-info='");
|
||
$info = json_decode(substr($html, $start, strpos($html, "'", $start) - $start), true);
|
||
|
||
expect($info[$pk->id]['stock'])->toEqual(100)
|
||
->and($info[$pk->id]['storage_label'])->toBe('Lager 1 | Regal B')
|
||
->and($html)->toContain('packagingRow(')
|
||
->and(substr_count($html, '<th>Lagerort</th>'))->toBe(2)
|
||
->and($html)->not->toContain('<th>Ablage</th>');
|
||
});
|