gruene-seele/tests/Feature/ProductFillWeightTest.php
Kevin Adametz 3abb9e644e Warenwirtschaft: AP-52 — Verpackungsvorschau mit Bestand, Differenz und Lagerort (Kundennachtrag 09.09.2026)
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>
2026-09-09 08:48:30 +00:00

113 lines
5.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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>');
});