Warenwirtschaft: AP-45 + AP-47 — Verpackung aus Produktionsergebnis, Produktionsrelevant-Flag

AP-45: Verpackungsverbrauch (Etikett/Tiegel) wird jetzt aus dem tatsächlichen
Produktionsergebnis gebucht — je Ergebnisprodukt dessen eigene Stückliste ×
Ergebnismenge, über alle Outputs summiert — statt aus der geplanten Stückzahl.
syncPackagingSnapshot liest die production_outputs und läuft daher nach
persistOutputs (store + update umgestellt).

AP-47: Neues Produkt-Kennzeichen „produktionsrelevant" (Boolean, Default true).
Nur so markierte Produkte erscheinen in der Produktions-Auswahl
(ProductionController create/edit/copy) und in der „Enthalten in"-Box des
Rohstoffbestands (blendet z. B. Aloe-Handelsware und Zweitgrößen aus).
Checkbox im Produktformular (mit Hidden-Fallback 0). Migration Default true,
damit Bestandsdaten unverändert bleiben.

Migrationen add_production_relevant auf Dev-/Live-DB ausgeführt. 4 Tests, Suite 314 grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-08-28 08:38:59 +00:00
parent e6007efc41
commit d51860af39
7 changed files with 132 additions and 17 deletions

View file

@ -2,6 +2,8 @@
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\PackagingItem;
use App\Models\PackagingMaterial;
use App\Models\Product;
use App\Models\ProductIngredient;
use App\Models\Production;
@ -156,3 +158,57 @@ test('AP-40: recipeJson-payload enthält aktuellen bestand je INCI (für differe
expect($line['current_stock'])->toBe(800.0)
->and($line['required_grams_total'])->toBe(1000.0);
});
test('AP-45: verpackung wird je ergebnisprodukt aus dem produktionsergebnis gebucht', function () {
$user = psUser();
$supplier = Supplier::factory()->create();
$location = Location::factory()->create();
$material = PackagingMaterial::factory()->create();
$pkA = PackagingItem::factory()->create(['packaging_material_id' => $material->id, 'supplier_id' => $supplier->id, 'name' => 'PS-Tiegel-A', 'category' => 'packaging']);
$pkB = PackagingItem::factory()->create(['packaging_material_id' => $material->id, 'supplier_id' => $supplier->id, 'name' => 'PS-Tiegel-B', 'category' => 'packaging']);
// Beide Ergebnisprodukte ohne Rezepturpflicht (kein Rohstoff-Abwiegen nötig).
$planned = Product::query()->create(['name' => uniqid('PS-Plan-', true), 'title' => 'x', 'active' => true, 'no_recipe_required' => true]);
$planned->packagings()->sync([$pkA->id => ['quantity' => 1, 'pos' => 0]]);
$other = Product::query()->create(['name' => uniqid('PS-Other-', true), 'title' => 'y', 'active' => true, 'no_recipe_required' => true]);
$other->packagings()->sync([$pkB->id => ['quantity' => 2, 'pos' => 0]]);
// Eine Charge, Ergebnis: 40× Plan (Tiegel A) + 50× Other (Tiegel B).
$production = app(ProductionService::class)->store([
'product_id' => $planned->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 40,
], [], $user->id, [
['product_id' => $planned->id, 'quantity' => 40, 'batch_number' => 'PS-A1'],
['product_id' => $other->id, 'quantity' => 50, 'batch_number' => 'PS-B1'],
]);
$byItem = $production->productionPackagings->keyBy('packaging_item_id');
expect($byItem[$pkA->id]->quantity_used)->toBe(40) // 1 × 40 (Ergebnis Plan)
->and($byItem[$pkB->id]->quantity_used)->toBe(100); // 2 × 50 (Ergebnis Other)
});
test('AP-47: produktions-auswahl zeigt nur produktionsrelevante produkte', function () {
$user = psUser();
$this->actingAs($user, 'user');
$ing = psIngredient('PS-Rel');
psProduct(['name' => 'PS47-Sichtbar', 'no_recipe_required' => true, 'production_relevant' => true], $ing);
psProduct(['name' => 'PS47-Versteckt', 'no_recipe_required' => true, 'production_relevant' => false], $ing);
$this->get(route('admin.inventory.productions.create'))
->assertSuccessful()
->assertSee('PS47-Sichtbar')
->assertDontSee('PS47-Versteckt');
});
test('AP-47: enthalten-in blendet nicht-produktionsrelevante produkte aus', function () {
$user = psUser();
$this->actingAs($user, 'user');
$ing = psIngredient('PS-EnthaltenRel');
psProduct(['name' => 'PS47-EnthaltenSichtbar', 'production_relevant' => true], $ing);
psProduct(['name' => 'PS47-EnthaltenVersteckt', 'production_relevant' => false], $ing);
$this->get(route('admin.inventory.raw-material-stock.show', $ing))
->assertSuccessful()
->assertSee('PS47-EnthaltenSichtbar')
->assertDontSee('PS47-EnthaltenVersteckt');
});