Umsetzung der Warenwirtschafts-/Produktmanagement-Erweiterung gemaess Entwicklungsplan V4.0: - AP-00: Regressionsbasis fuer 5.1-Features (ProductPhase51Test) - AP-01: URL-Bugfixes B1/B2 (suppliers/packaging-items, breitere url-Spalten) - AP-04/04.1: iPad-taugliche, vereinheitlichte Tabellen-Aktionen - AP-05: Einstellungen "Allgemein" mit UST-Saetzen (tax_rates) und Lieferzeit-Vorlagen (delivery_times, inkl. Tage-Feld) - AP-06: Lieferanten um Bestellweg, Bestell-Mail/-URL und Lieferzeit erweitert - AP-07/07.1: INCI um Lieferanten-Mehrfachwahl, UST und Lieferzeit erweitert; Lieferanten-Detailansicht im Modal mit pflegbaren INCI-/Verpackungslisten - AP-08: Einkauf um UST-Snapshot, Netto/Brutto-Automatik und Duplizieren erweitert Entwicklungsplan aktualisiert: alle Klaerungspunkte (§5) vom Kunden beantwortet und in die jeweiligen APs eingearbeitet (AP-02/03/09/13/15), neues AP-18 (Hinweise-Doku unter Einstellungen) ergaenzt. Naechster Schritt eindeutig markiert: AP-09 (Produktion auf Hersteller-Rezeptur, kein Fallback, Warnung).
263 lines
7.8 KiB
PHP
263 lines
7.8 KiB
PHP
<?php
|
|
|
|
use App\Models\Country;
|
|
use App\Models\Ingredient;
|
|
use App\Models\Location;
|
|
use App\Models\MaterialQuality;
|
|
use App\Models\Product;
|
|
use App\Models\ProductIngredient;
|
|
use App\Models\StockEntry;
|
|
use App\Models\Supplier;
|
|
use App\Repositories\ProductRepository;
|
|
use App\Services\ProductionService;
|
|
use App\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
|
|
|
function p51MakeUser(int $adminLevel = 1): User
|
|
{
|
|
$user = User::query()->create([
|
|
'email' => uniqid('p51_', true).'@test.example',
|
|
'password' => bcrypt('password'),
|
|
]);
|
|
$user->forceFill([
|
|
'admin' => $adminLevel,
|
|
'confirmed' => true,
|
|
'active' => true,
|
|
'wizard' => 100,
|
|
'blocked' => false,
|
|
])->save();
|
|
|
|
return $user->fresh();
|
|
}
|
|
|
|
function p51EnsureCountry(): Country
|
|
{
|
|
return Country::query()->firstOrCreate(
|
|
['code' => 'DE'],
|
|
[
|
|
'phone' => '00',
|
|
'en' => 'Germany',
|
|
'de' => 'Deutschland',
|
|
'es' => 'Germany',
|
|
'fr' => 'Germany',
|
|
'it' => 'Germany',
|
|
'ru' => 'Germany',
|
|
'active' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array{0: Ingredient, 1: Ingredient}
|
|
*/
|
|
function p51MakeIngredients(): array
|
|
{
|
|
$base = [
|
|
'trans_name' => '',
|
|
'trans_inci' => '',
|
|
'effect' => '',
|
|
'trans_effect' => '',
|
|
'active' => true,
|
|
'pos' => 0,
|
|
];
|
|
|
|
$a = Ingredient::query()->create($base + ['name' => 'P51-Rohstoff-A', 'inci' => 'INCI-A', 'default_factor' => 1.10]);
|
|
$b = Ingredient::query()->create($base + ['name' => 'P51-Rohstoff-B', 'inci' => 'INCI-B', 'default_factor' => 1.10]);
|
|
|
|
return [$a, $b];
|
|
}
|
|
|
|
test('ingredient speichert und liefert rohstoffqualitaet', function () {
|
|
$quality = MaterialQuality::query()->create(['name' => 'bio kaltgepresst', 'pos' => 1]);
|
|
|
|
$ing = Ingredient::query()->create([
|
|
'name' => 'Sonnenblumenöl',
|
|
'trans_name' => '',
|
|
'inci' => 'Helianthus Annuus Seed Oil',
|
|
'trans_inci' => '',
|
|
'effect' => '',
|
|
'trans_effect' => '',
|
|
'active' => true,
|
|
'pos' => 0,
|
|
'material_quality_id' => $quality->id,
|
|
]);
|
|
|
|
expect($ing->materialQuality)->not->toBeNull();
|
|
expect($ing->materialQuality->name)->toBe('bio kaltgepresst');
|
|
});
|
|
|
|
test('produkt speichert hersteller-rezeptur getrennt von produkt-rezeptur', function () {
|
|
[$a, $b] = p51MakeIngredients();
|
|
|
|
$product = Product::query()->create([
|
|
'name' => 'P51-Produkt',
|
|
'title' => 'P51-Produkt',
|
|
'active' => true,
|
|
]);
|
|
|
|
$repo = new ProductRepository($product);
|
|
$repo->update([
|
|
'id' => (string) $product->id,
|
|
'name' => $product->name,
|
|
'title' => $product->title,
|
|
'active' => '1',
|
|
// Produkt-Rezeptur
|
|
'product_inci_sync_sent' => '1',
|
|
'pi_ingredient_id' => [(string) $a->id],
|
|
'pi_gram' => ['80,000'],
|
|
'pi_factor' => ['1,10'],
|
|
// Hersteller-Rezeptur
|
|
'manufacturer_inci_sync_sent' => '1',
|
|
'mfg_ingredient_id' => [(string) $b->id],
|
|
'mfg_gram' => ['20,000'],
|
|
'mfg_factor' => ['1,20'],
|
|
]);
|
|
|
|
$product->refresh();
|
|
|
|
expect($product->p_ingredients)->toHaveCount(1);
|
|
expect($product->p_ingredients->first()->id)->toBe($a->id);
|
|
|
|
expect($product->manufacturer_ingredients)->toHaveCount(1);
|
|
$mfg = $product->manufacturer_ingredients->first();
|
|
expect($mfg->id)->toBe($b->id);
|
|
expect((float) $mfg->pivot->gram)->toBe(20.0);
|
|
expect((float) $mfg->pivot->factor)->toBe(1.20);
|
|
});
|
|
|
|
test('produkt-kopie uebernimmt beide rezepturen', function () {
|
|
[$a, $b] = p51MakeIngredients();
|
|
|
|
$source = Product::query()->create([
|
|
'name' => 'P51-Quelle',
|
|
'title' => 'P51-Quelle',
|
|
'active' => true,
|
|
]);
|
|
|
|
ProductIngredient::query()->create([
|
|
'product_id' => $source->id,
|
|
'ingredient_id' => $a->id,
|
|
'pos' => 0,
|
|
'gram' => 70,
|
|
'factor' => 1.10,
|
|
'recipe_type' => 'product',
|
|
]);
|
|
|
|
ProductIngredient::query()->create([
|
|
'product_id' => $source->id,
|
|
'ingredient_id' => $b->id,
|
|
'pos' => 0,
|
|
'gram' => 30,
|
|
'factor' => 1.15,
|
|
'recipe_type' => 'manufacturer',
|
|
]);
|
|
|
|
$repo = new ProductRepository(new Product);
|
|
$copy = $repo->copy($source->fresh());
|
|
|
|
expect($copy->p_ingredients()->count())->toBe(1);
|
|
expect($copy->manufacturer_ingredients()->count())->toBe(1);
|
|
|
|
$mfg = $copy->manufacturer_ingredients()->first();
|
|
expect($mfg->id)->toBe($b->id);
|
|
expect((float) $mfg->pivot->gram)->toBe(30.0);
|
|
expect((float) $mfg->pivot->factor)->toBe(1.15);
|
|
});
|
|
|
|
test('produktions-formular zeigt nur aktive produkte', function () {
|
|
p51EnsureCountry();
|
|
Location::factory()->create(['name' => 'Köln']);
|
|
$user = p51MakeUser(1);
|
|
|
|
Product::query()->create(['name' => 'P51-AKTIV-PROD', 'title' => 'P51-AKTIV-PROD', 'active' => true]);
|
|
Product::query()->create(['name' => 'P51-INAKTIV-PROD', 'title' => 'P51-INAKTIV-PROD', 'active' => false]);
|
|
|
|
$this->actingAs($user, 'user');
|
|
$response = $this->get(route('admin.inventory.productions.create'));
|
|
|
|
$response->assertSuccessful();
|
|
$response->assertSee('P51-AKTIV-PROD');
|
|
$response->assertDontSee('P51-INAKTIV-PROD');
|
|
});
|
|
|
|
test('produktion kann bearbeitet und kopiert werden (views rendern)', function () {
|
|
p51EnsureCountry();
|
|
$user = p51MakeUser(7);
|
|
$location = Location::factory()->create();
|
|
$supplier = Supplier::factory()->create();
|
|
|
|
$ing = Ingredient::query()->create([
|
|
'name' => 'P51-Prod-Roh',
|
|
'trans_name' => '',
|
|
'inci' => 'PR',
|
|
'trans_inci' => '',
|
|
'effect' => '',
|
|
'trans_effect' => '',
|
|
'active' => true,
|
|
'pos' => 0,
|
|
]);
|
|
|
|
$product = Product::query()->create([
|
|
'name' => 'P51-Prod',
|
|
'title' => 'P51-Prod',
|
|
'active' => true,
|
|
]);
|
|
|
|
ProductIngredient::query()->create([
|
|
'product_id' => $product->id,
|
|
'ingredient_id' => $ing->id,
|
|
'pos' => 0,
|
|
'gram' => 10,
|
|
'factor' => 1.0,
|
|
'recipe_type' => 'product',
|
|
]);
|
|
|
|
$stock = StockEntry::query()->create([
|
|
'entry_type' => 'ingredient',
|
|
'ingredient_id' => $ing->id,
|
|
'packaging_item_id' => null,
|
|
'supplier_id' => $supplier->id,
|
|
'location_id' => $location->id,
|
|
'unit' => 'gram',
|
|
'ordered_by' => $user->id,
|
|
'ordered_at' => '2026-01-01',
|
|
'ordered_quantity' => 10000,
|
|
'price_per_kg' => 1,
|
|
'status' => 'received',
|
|
'received_by' => $user->id,
|
|
'received_at' => '2026-01-15',
|
|
'received_quantity' => 10000,
|
|
'batch_number' => 'P51-B',
|
|
'best_before' => '2030-12-31',
|
|
'quality_id' => null,
|
|
]);
|
|
|
|
$production = app(ProductionService::class)->store(
|
|
[
|
|
'product_id' => $product->id,
|
|
'location_id' => $location->id,
|
|
'produced_at' => '2026-03-01',
|
|
'quantity' => 5,
|
|
'notes' => null,
|
|
],
|
|
[
|
|
['ingredient_id' => $ing->id, 'stock_entry_id' => $stock->id, 'quantity_used' => '50'],
|
|
],
|
|
$user->id
|
|
);
|
|
|
|
$this->actingAs($user, 'user');
|
|
|
|
$this->get(route('admin.inventory.productions.edit', $production))->assertSuccessful();
|
|
$this->get(route('admin.inventory.productions.copy', $production))->assertSuccessful();
|
|
|
|
// Übersicht rendert mit Aktionsspalte (Ansicht, Bearbeiten, Kopieren)
|
|
$index = $this->get(route('admin.inventory.productions.index'));
|
|
$index->assertSuccessful();
|
|
$index->assertSee(route('admin.inventory.productions.show', $production), false);
|
|
$index->assertSee(route('admin.inventory.productions.edit', $production), false);
|
|
$index->assertSee(route('admin.inventory.productions.copy', $production), false);
|
|
});
|