gruene-seele/tests/Feature/ProductFillWeightTest.php
Kevin Adametz 2d5bc6518e Warenwirtschaft: AP-47 — Regressionstest für die Checkbox „produktionsrelevant" über das Produktformular
Nachfrage 09.09.2026 (Zukaufartikel wie Aloe sollen in der Produktion nicht auftauchen): Die Funktion existiert seit AP-47 (Checkbox am Produkt, Filter im Planungs-Dropdown und in „Enthalten in"). Bisher war nur der Dropdown-Filter getestet; der neue Test sichert den ganzen Weg Formular → Speichern → Produktions-Auswahl ab.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:01:31 +00:00

138 lines
7.4 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>');
});
test('AP-47: checkbox „produktionsrelevant" im produktformular wird gespeichert und steuert die produktions-auswahl', function () {
$this->actingAs(fwUser(), 'user');
// Neuanlage ohne Häkchen (Hidden-Fallback 0) => Zukaufartikel wie Aloe verschwindet aus der Planung.
$this->post(route('admin_product_store'), ['id' => 'new', 'name' => 'AP47-Aloe-Zukauf', 'active' => 1, 'production_relevant' => '0'])
->assertRedirect()->assertSessionHasNoErrors();
$product = Product::query()->where('name', 'AP47-Aloe-Zukauf')->firstOrFail();
expect($product->production_relevant)->toBeFalse();
$html = $this->get(route('admin.inventory.productions.create'))->assertSuccessful()->getContent();
$planningSelect = substr($html, strpos($html, 'id="product_id"'));
$planningSelect = substr($planningSelect, 0, strpos($planningSelect, '</select>'));
expect($planningSelect)->not->toContain('AP47-Aloe-Zukauf');
// Häkchen wieder setzen => wieder wählbar.
$this->post(route('admin_product_store'), ['id' => $product->id, 'name' => 'AP47-Aloe-Zukauf', 'active' => 1, 'production_relevant' => '1'])
->assertSessionHasNoErrors();
expect($product->fresh()->production_relevant)->toBeTrue();
$html = $this->get(route('admin.inventory.productions.create'))->assertSuccessful()->getContent();
$planningSelect = substr($html, strpos($html, 'id="product_id"'));
$planningSelect = substr($planningSelect, 0, strpos($planningSelect, '</select>'));
expect($planningSelect)->toContain('AP47-Aloe-Zukauf');
});