Warenwirtschaft: AP-00 bis AP-08 + aktualisierter Entwicklungsplan
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).
This commit is contained in:
parent
ca3eb663fe
commit
78679e0c55
67 changed files with 3523 additions and 101 deletions
126
tests/Feature/DeliveryTimeSettingsTest.php
Normal file
126
tests/Feature/DeliveryTimeSettingsTest.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
use App\Models\DeliveryTime;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function deliveryTimeUser(int $adminLevel = 8): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('dt_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
test('allgemein-seite zeigt lieferzeit-vorlagen', function () {
|
||||
$this->actingAs(deliveryTimeUser(8), 'user');
|
||||
|
||||
DeliveryTime::create(['label' => '3–5 Werktage', 'active' => true, 'pos' => 0]);
|
||||
|
||||
$response = $this->get(route('admin.inventory.general'));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('Lieferzeit-Vorlagen', false);
|
||||
$response->assertSee('3–5 Werktage', false);
|
||||
$response->assertSee(route('admin.inventory.delivery-times.create'), false);
|
||||
});
|
||||
|
||||
test('superadmin legt lieferzeit-vorlage an', function () {
|
||||
$this->actingAs(deliveryTimeUser(8), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.delivery-times.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.delivery-times.store'), [
|
||||
'label' => '1–2 Wochen',
|
||||
'days' => 14,
|
||||
'active' => '1',
|
||||
'pos' => 0,
|
||||
])->assertRedirect(route('admin.inventory.general'));
|
||||
|
||||
$deliveryTime = DeliveryTime::query()->where('label', '1–2 Wochen')->firstOrFail();
|
||||
expect($deliveryTime->active)->toBeTrue()
|
||||
->and($deliveryTime->days)->toBe(14);
|
||||
});
|
||||
|
||||
test('lieferzeit-vorlage ohne tage bleibt erlaubt', function () {
|
||||
$this->actingAs(deliveryTimeUser(8), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.delivery-times.store'), [
|
||||
'label' => 'Auf Anfrage',
|
||||
'days' => '',
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.general'));
|
||||
|
||||
$deliveryTime = DeliveryTime::query()->where('label', 'Auf Anfrage')->firstOrFail();
|
||||
expect($deliveryTime->days)->toBeNull();
|
||||
});
|
||||
|
||||
test('tage muss eine ganze zahl sein', function () {
|
||||
$this->actingAs(deliveryTimeUser(8), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.delivery-times.store'), [
|
||||
'label' => 'Falsche Tage',
|
||||
'days' => 'abc',
|
||||
])->assertSessionHasErrors('days');
|
||||
});
|
||||
|
||||
test('lieferzeit-vorlage kann bearbeitet und deaktiviert werden', function () {
|
||||
$this->actingAs(deliveryTimeUser(8), 'user');
|
||||
|
||||
$deliveryTime = DeliveryTime::create(['label' => '3–5 Werktage', 'active' => true, 'pos' => 1]);
|
||||
|
||||
$this->get(route('admin.inventory.delivery-times.edit', $deliveryTime))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.delivery-times.update', $deliveryTime), [
|
||||
'label' => '5–7 Werktage',
|
||||
'pos' => 1,
|
||||
])->assertRedirect(route('admin.inventory.general'));
|
||||
|
||||
$deliveryTime->refresh();
|
||||
expect($deliveryTime->label)->toBe('5–7 Werktage');
|
||||
expect($deliveryTime->active)->toBeFalse();
|
||||
});
|
||||
|
||||
test('lieferzeit-vorlage kann geloescht werden', function () {
|
||||
$this->actingAs(deliveryTimeUser(8), 'user');
|
||||
|
||||
$deliveryTime = DeliveryTime::create(['label' => 'Express', 'active' => true, 'pos' => 2]);
|
||||
|
||||
$this->delete(route('admin.inventory.delivery-times.destroy', $deliveryTime))
|
||||
->assertRedirect(route('admin.inventory.general'));
|
||||
|
||||
expect(DeliveryTime::query()->whereKey($deliveryTime->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('bezeichnung ist pflicht', function () {
|
||||
$this->actingAs(deliveryTimeUser(8), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.delivery-times.store'), [
|
||||
'label' => '',
|
||||
])->assertSessionHasErrors('label');
|
||||
});
|
||||
|
||||
test('nur aktive vorlagen ueber active-scope', function () {
|
||||
DeliveryTime::create(['label' => 'Aktiv', 'active' => true, 'pos' => 0]);
|
||||
DeliveryTime::create(['label' => 'Inaktiv', 'active' => false, 'pos' => 1]);
|
||||
|
||||
expect(DeliveryTime::query()->active()->pluck('label')->all())->toBe(['Aktiv']);
|
||||
});
|
||||
|
||||
test('nicht-superadmin hat keinen zugriff auf lieferzeit-anlage', function () {
|
||||
$this->actingAs(deliveryTimeUser(7), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.delivery-times.create'))->assertRedirect('/home');
|
||||
});
|
||||
135
tests/Feature/IngredientOrderFieldsTest.php
Normal file
135
tests/Feature/IngredientOrderFieldsTest.php
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
use App\Models\DeliveryTime;
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\TaxRate;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function ingredientOrderUser(int $adminLevel): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('ing_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
function ingredientOrderSupplier(string $name): Supplier
|
||||
{
|
||||
return Supplier::factory()->create([
|
||||
'name' => $name,
|
||||
'active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
test('ingredient form shows suppliers, tax rates and delivery time templates', function () {
|
||||
$supplier = ingredientOrderSupplier('Aktiv-Lieferant');
|
||||
TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]);
|
||||
DeliveryTime::query()->create(['label' => '3–5 Werktage', 'days' => 5, 'active' => true, 'pos' => 0]);
|
||||
DeliveryTime::query()->create(['label' => 'Inaktiv-Vorlage', 'active' => false, 'pos' => 1]);
|
||||
|
||||
$this->actingAs(ingredientOrderUser(7), 'user');
|
||||
|
||||
$this->get(route('admin_product_ingredient_edit', 'new'))
|
||||
->assertSuccessful()
|
||||
->assertSee('supplier_ids')
|
||||
->assertSee('tax_rate_id')
|
||||
->assertSee('Aktiv-Lieferant')
|
||||
->assertSee('3–5 Werktage')
|
||||
->assertSee('data-days="5"', false)
|
||||
->assertDontSee('Inaktiv-Vorlage');
|
||||
});
|
||||
|
||||
test('ingredient can be stored with tax rate, delivery time and suppliers', function () {
|
||||
$taxRate = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]);
|
||||
$supplierA = ingredientOrderSupplier('Lieferant A');
|
||||
$supplierB = ingredientOrderSupplier('Lieferant B');
|
||||
|
||||
$this->actingAs(ingredientOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin_product_ingredient_store'), [
|
||||
'id' => 'new',
|
||||
'name' => 'Sheabutter',
|
||||
'active' => '1',
|
||||
'tax_rate_id' => (string) $taxRate->id,
|
||||
'delivery_time' => '3–5 Werktage',
|
||||
'delivery_time_days' => '5',
|
||||
'supplier_ids' => [(string) $supplierA->id, (string) $supplierB->id],
|
||||
])->assertRedirect(route('admin_product_ingredients'));
|
||||
|
||||
$ingredient = Ingredient::query()->where('name', 'Sheabutter')->firstOrFail();
|
||||
|
||||
expect($ingredient->tax_rate_id)->toBe($taxRate->id)
|
||||
->and($ingredient->delivery_time)->toBe('3–5 Werktage')
|
||||
->and($ingredient->delivery_time_days)->toBe(5)
|
||||
->and($ingredient->suppliers()->pluck('suppliers.id')->all())
|
||||
->toEqualCanonicalizing([$supplierA->id, $supplierB->id]);
|
||||
});
|
||||
|
||||
test('ingredient suppliers are synced on update', function () {
|
||||
$supplierA = ingredientOrderSupplier('Lieferant A');
|
||||
$supplierB = ingredientOrderSupplier('Lieferant B');
|
||||
|
||||
$ingredient = Ingredient::query()->create([
|
||||
'name' => 'Lavendelöl',
|
||||
'active' => true,
|
||||
]);
|
||||
$ingredient->suppliers()->sync([$supplierA->id]);
|
||||
|
||||
$this->actingAs(ingredientOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin_product_ingredient_store'), [
|
||||
'id' => (string) $ingredient->id,
|
||||
'name' => 'Lavendelöl',
|
||||
'active' => '1',
|
||||
'supplier_ids' => [(string) $supplierB->id],
|
||||
])->assertRedirect(route('admin_product_ingredients'));
|
||||
|
||||
expect($ingredient->fresh()->suppliers()->pluck('suppliers.id')->all())
|
||||
->toEqualCanonicalizing([$supplierB->id]);
|
||||
});
|
||||
|
||||
test('ingredient delivery time days must be an integer', function () {
|
||||
$this->actingAs(ingredientOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin_product_ingredient_store'), [
|
||||
'id' => 'new',
|
||||
'name' => 'Falsche-Tage',
|
||||
'active' => '1',
|
||||
'delivery_time_days' => 'viele',
|
||||
])->assertSessionHasErrors('delivery_time_days');
|
||||
});
|
||||
|
||||
test('ingredient tax rate must exist', function () {
|
||||
$this->actingAs(ingredientOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin_product_ingredient_store'), [
|
||||
'id' => 'new',
|
||||
'name' => 'Falscher-Steuersatz',
|
||||
'active' => '1',
|
||||
'tax_rate_id' => '999999',
|
||||
])->assertSessionHasErrors('tax_rate_id');
|
||||
});
|
||||
|
||||
test('ingredient name is required', function () {
|
||||
$this->actingAs(ingredientOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin_product_ingredient_store'), [
|
||||
'id' => 'new',
|
||||
'name' => '',
|
||||
'active' => '1',
|
||||
])->assertSessionHasErrors('name');
|
||||
});
|
||||
100
tests/Feature/InventoryUrlFieldsTest.php
Normal file
100
tests/Feature/InventoryUrlFieldsTest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\PackagingItem;
|
||||
use App\Models\PackagingMaterial;
|
||||
use App\Models\Supplier;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Konfigurator-URL mit kodierten Parametern (Versandverpackung), siehe docs/Todos.md.
|
||||
*/
|
||||
function configuratorUrl(): string
|
||||
{
|
||||
return 'https://www.kartonsaufmass.de/bestellen?bom_configuration=%7B%2522length%2522:125,%2522width%2522:125,%2522height%2522:30,%2522amount%2522:100%7D';
|
||||
}
|
||||
|
||||
function urlTestUser(int $adminLevel): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('urlfield_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
function urlTestCountry(): Country
|
||||
{
|
||||
return Country::query()->firstOrCreate(
|
||||
['code' => 'DE'],
|
||||
[
|
||||
'phone' => '00',
|
||||
'en' => 'Germany',
|
||||
'de' => 'Deutschland',
|
||||
'es' => 'Germany',
|
||||
'fr' => 'Germany',
|
||||
'it' => 'Germany',
|
||||
'ru' => 'Germany',
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
test('supplier speichert URL mit Parametern (B1/B2)', function () {
|
||||
$country = urlTestCountry();
|
||||
$user = urlTestUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'Kartons-auf-Mass',
|
||||
'country_id' => (string) $country->id,
|
||||
'url' => configuratorUrl(),
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
$supplier = Supplier::query()->where('name', 'Kartons-auf-Mass')->firstOrFail();
|
||||
expect($supplier->url)->toBe(configuratorUrl());
|
||||
});
|
||||
|
||||
test('supplier ohne URL bleibt speicherbar', function () {
|
||||
$country = urlTestCountry();
|
||||
$user = urlTestUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'Ohne-URL',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
expect(Supplier::query()->where('name', 'Ohne-URL')->value('url'))->toBeNull();
|
||||
});
|
||||
|
||||
test('packaging item speichert URL mit Parametern (B2)', function () {
|
||||
$material = PackagingMaterial::factory()->create();
|
||||
$user = urlTestUser(7);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.packaging-items.store'), [
|
||||
'packaging_material_id' => (string) $material->id,
|
||||
'name' => 'Versandkarton Konfigurator',
|
||||
'category' => 'shipping',
|
||||
'url' => configuratorUrl(),
|
||||
'active' => '1',
|
||||
])->assertRedirect(route('admin.inventory.packaging-items.index', ['category' => 'shipping']));
|
||||
|
||||
$item = PackagingItem::query()->where('name', 'Versandkarton Konfigurator')->firstOrFail();
|
||||
expect($item->url)->toBe(configuratorUrl());
|
||||
});
|
||||
263
tests/Feature/ProductPhase51Test.php
Normal file
263
tests/Feature/ProductPhase51Test.php
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
<?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);
|
||||
});
|
||||
203
tests/Feature/StockEntryPriceTest.php
Normal file
203
tests/Feature/StockEntryPriceTest.php
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\Location;
|
||||
use App\Models\MaterialQuality;
|
||||
use App\Models\StockEntry;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\TaxRate;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function stockPriceAdmin(): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('sep_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => 7,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
function stockPriceIngredient(): Ingredient
|
||||
{
|
||||
return Ingredient::query()->create(['name' => 'INCI-'.uniqid('', true), 'active' => true, 'pos' => 0]);
|
||||
}
|
||||
|
||||
test('net price derives gross from tax rate on store', function () {
|
||||
$admin = stockPriceAdmin();
|
||||
$ingredient = stockPriceIngredient();
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$quality = MaterialQuality::factory()->create();
|
||||
$tax = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]);
|
||||
|
||||
$this->actingAs($admin, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.stock-entries.store'), [
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => (string) $ingredient->id,
|
||||
'supplier_id' => (string) $supplier->id,
|
||||
'location_id' => (string) $location->id,
|
||||
'quality_id' => (string) $quality->id,
|
||||
'tax_rate_id' => (string) $tax->id,
|
||||
'ordered_at' => '2026-06-01',
|
||||
'ordered_quantity' => '1000',
|
||||
'price_per_kg' => '10,00',
|
||||
])->assertRedirect(route('admin.inventory.stock-entries.index'));
|
||||
|
||||
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
||||
|
||||
expect((float) $entry->price_per_kg)->toBe(10.0)
|
||||
->and((float) $entry->price_per_kg_gross)->toBe(11.9)
|
||||
->and((float) $entry->tax_rate_percent)->toBe(19.0);
|
||||
});
|
||||
|
||||
test('gross price derives net from tax rate on store', function () {
|
||||
$admin = stockPriceAdmin();
|
||||
$ingredient = stockPriceIngredient();
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$quality = MaterialQuality::factory()->create();
|
||||
$tax = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]);
|
||||
|
||||
$this->actingAs($admin, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.stock-entries.store'), [
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => (string) $ingredient->id,
|
||||
'supplier_id' => (string) $supplier->id,
|
||||
'location_id' => (string) $location->id,
|
||||
'quality_id' => (string) $quality->id,
|
||||
'tax_rate_id' => (string) $tax->id,
|
||||
'ordered_at' => '2026-06-01',
|
||||
'ordered_quantity' => '1000',
|
||||
'price_per_kg_gross' => '11,90',
|
||||
])->assertRedirect(route('admin.inventory.stock-entries.index'));
|
||||
|
||||
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
||||
|
||||
expect((float) $entry->price_per_kg)->toBe(10.0)
|
||||
->and((float) $entry->price_per_kg_gross)->toBe(11.9);
|
||||
});
|
||||
|
||||
test('without tax rate net equals gross', function () {
|
||||
$admin = stockPriceAdmin();
|
||||
$ingredient = stockPriceIngredient();
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$quality = MaterialQuality::factory()->create();
|
||||
|
||||
$this->actingAs($admin, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.stock-entries.store'), [
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => (string) $ingredient->id,
|
||||
'supplier_id' => (string) $supplier->id,
|
||||
'location_id' => (string) $location->id,
|
||||
'quality_id' => (string) $quality->id,
|
||||
'ordered_at' => '2026-06-01',
|
||||
'ordered_quantity' => '500',
|
||||
'price_per_kg' => '8,00',
|
||||
])->assertRedirect(route('admin.inventory.stock-entries.index'));
|
||||
|
||||
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
||||
|
||||
expect((float) $entry->price_per_kg)->toBe(8.0)
|
||||
->and((float) $entry->price_per_kg_gross)->toBe(8.0)
|
||||
->and($entry->tax_rate_percent)->toBeNull();
|
||||
});
|
||||
|
||||
test('ingredient requires net or gross price', function () {
|
||||
$admin = stockPriceAdmin();
|
||||
$ingredient = stockPriceIngredient();
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$quality = MaterialQuality::factory()->create();
|
||||
|
||||
$this->actingAs($admin, 'user');
|
||||
|
||||
$this->post(route('admin.inventory.stock-entries.store'), [
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => (string) $ingredient->id,
|
||||
'supplier_id' => (string) $supplier->id,
|
||||
'location_id' => (string) $location->id,
|
||||
'quality_id' => (string) $quality->id,
|
||||
'ordered_at' => '2026-06-01',
|
||||
'ordered_quantity' => '500',
|
||||
])->assertSessionHasErrors('price_per_kg');
|
||||
});
|
||||
|
||||
test('copy creates pending duplicate without receiving data', function () {
|
||||
$admin = stockPriceAdmin();
|
||||
$ingredient = stockPriceIngredient();
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$quality = MaterialQuality::factory()->create();
|
||||
$tax = TaxRate::query()->create(['name' => 'Standard', 'percent' => 19, 'active' => true, 'pos' => 0]);
|
||||
|
||||
$source = StockEntry::query()->create([
|
||||
'entry_type' => 'ingredient',
|
||||
'ingredient_id' => $ingredient->id,
|
||||
'supplier_id' => $supplier->id,
|
||||
'location_id' => $location->id,
|
||||
'unit' => 'gram',
|
||||
'ordered_by' => $admin->id,
|
||||
'ordered_at' => '2026-01-01',
|
||||
'ordered_quantity' => 1000,
|
||||
'price_per_kg' => 10,
|
||||
'price_per_kg_gross' => 11.9,
|
||||
'tax_rate_id' => $tax->id,
|
||||
'tax_rate_percent' => 19,
|
||||
'quality_id' => $quality->id,
|
||||
'received_at' => '2026-01-10',
|
||||
'received_quantity' => 1000,
|
||||
'batch_number' => 'CHARGE-1',
|
||||
'best_before' => '2027-01-01',
|
||||
'status' => 'received',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'user');
|
||||
|
||||
$this->get(route('admin.inventory.stock-entries.copy', $source))
|
||||
->assertRedirect();
|
||||
|
||||
$copy = StockEntry::query()->where('status', 'pending')->latest('id')->firstOrFail();
|
||||
|
||||
expect($copy->id)->not->toBe($source->id)
|
||||
->and($copy->ingredient_id)->toBe($ingredient->id)
|
||||
->and($copy->supplier_id)->toBe($supplier->id)
|
||||
->and((float) $copy->price_per_kg)->toBe(10.0)
|
||||
->and((float) $copy->price_per_kg_gross)->toBe(11.9)
|
||||
->and($copy->quality_id)->toBe($quality->id)
|
||||
->and($copy->batch_number)->toBeNull()
|
||||
->and($copy->best_before)->toBeNull()
|
||||
->and($copy->received_quantity)->toBeNull()
|
||||
->and($copy->ordered_by)->toBe($admin->id);
|
||||
});
|
||||
|
||||
test('copy is blocked for non admin', function () {
|
||||
$admin = stockPriceAdmin();
|
||||
$source = StockEntry::factory()->create(['ordered_by' => $admin->id]);
|
||||
|
||||
$copyreader = User::query()->create([
|
||||
'email' => uniqid('cr_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$copyreader->forceFill(['admin' => 1, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
|
||||
|
||||
$this->actingAs($copyreader->fresh(), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.stock-entries.copy', $source))
|
||||
->assertRedirect(route('home'));
|
||||
});
|
||||
114
tests/Feature/SupplierDetailsTest.php
Normal file
114
tests/Feature/SupplierDetailsTest.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\PackagingItem;
|
||||
use App\Models\Supplier;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function supplierDetailsUser(int $adminLevel): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('det_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
test('supplier show displays assigned ingredients and packaging items', function () {
|
||||
$supplier = Supplier::factory()->create(['name' => 'Detail-Lieferant']);
|
||||
$ingredient = Ingredient::query()->create(['name' => 'Sheabutter', 'active' => true]);
|
||||
$supplier->ingredients()->attach($ingredient->id);
|
||||
$item = PackagingItem::factory()->create(['name' => 'Braunglas 50ml', 'supplier_id' => $supplier->id]);
|
||||
|
||||
$this->actingAs(supplierDetailsUser(7), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.suppliers.show', $supplier))
|
||||
->assertSuccessful()
|
||||
->assertSee('Detail-Lieferant')
|
||||
->assertSee('Sheabutter')
|
||||
->assertSee('Braunglas 50ml')
|
||||
->assertSee('Zugeordnete INCIs')
|
||||
->assertSee('Zugeordnete Verpackungsartikel');
|
||||
});
|
||||
|
||||
test('ingredient can be attached to supplier from details', function () {
|
||||
$supplier = Supplier::factory()->create();
|
||||
$ingredient = Ingredient::query()->create(['name' => 'Lavendelöl', 'active' => true]);
|
||||
|
||||
$this->actingAs(supplierDetailsUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.ingredients.attach', $supplier), [
|
||||
'ingredient_id' => $ingredient->id,
|
||||
])->assertSuccessful()->assertSee('Lavendelöl');
|
||||
|
||||
expect($supplier->ingredients()->pluck('ingredients.id')->all())->toContain($ingredient->id);
|
||||
});
|
||||
|
||||
test('ingredient can be detached from supplier', function () {
|
||||
$supplier = Supplier::factory()->create();
|
||||
$ingredient = Ingredient::query()->create(['name' => 'Jojobaöl', 'active' => true]);
|
||||
$supplier->ingredients()->attach($ingredient->id);
|
||||
|
||||
$this->actingAs(supplierDetailsUser(7), 'user');
|
||||
|
||||
$this->delete(route('admin.inventory.suppliers.ingredients.detach', [$supplier, $ingredient]))
|
||||
->assertSuccessful();
|
||||
|
||||
expect($supplier->ingredients()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('packaging item can be attached to supplier from details', function () {
|
||||
$supplier = Supplier::factory()->create();
|
||||
$item = PackagingItem::factory()->create(['name' => 'Pumpspender', 'supplier_id' => null]);
|
||||
|
||||
$this->actingAs(supplierDetailsUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.packaging-items.attach', $supplier), [
|
||||
'packaging_item_id' => $item->id,
|
||||
])->assertSuccessful()->assertSee('Pumpspender');
|
||||
|
||||
expect($item->fresh()->supplier_id)->toBe($supplier->id);
|
||||
});
|
||||
|
||||
test('packaging item can be detached from supplier', function () {
|
||||
$supplier = Supplier::factory()->create();
|
||||
$item = PackagingItem::factory()->create(['name' => 'Faltschachtel', 'supplier_id' => $supplier->id]);
|
||||
|
||||
$this->actingAs(supplierDetailsUser(7), 'user');
|
||||
|
||||
$this->delete(route('admin.inventory.suppliers.packaging-items.detach', [$supplier, $item]))
|
||||
->assertSuccessful();
|
||||
|
||||
expect($item->fresh()->supplier_id)->toBeNull();
|
||||
});
|
||||
|
||||
test('attach ingredient validates ingredient exists', function () {
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$this->actingAs(supplierDetailsUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.ingredients.attach', $supplier), [
|
||||
'ingredient_id' => 999999,
|
||||
])->assertSessionHasErrors('ingredient_id');
|
||||
});
|
||||
|
||||
test('supplier details require admin level', function () {
|
||||
$supplier = Supplier::factory()->create();
|
||||
|
||||
$this->actingAs(supplierDetailsUser(1), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.suppliers.show', $supplier))
|
||||
->assertRedirect();
|
||||
});
|
||||
176
tests/Feature/SupplierOrderFieldsTest.php
Normal file
176
tests/Feature/SupplierOrderFieldsTest.php
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\DeliveryTime;
|
||||
use App\Models\Supplier;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function supplierOrderUser(int $adminLevel): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('sup_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
function supplierOrderCountry(): Country
|
||||
{
|
||||
return Country::query()->firstOrCreate(
|
||||
['code' => 'DE'],
|
||||
[
|
||||
'phone' => '00',
|
||||
'en' => 'Germany',
|
||||
'de' => 'Deutschland',
|
||||
'es' => 'Germany',
|
||||
'fr' => 'Germany',
|
||||
'it' => 'Germany',
|
||||
'ru' => 'Germany',
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
test('supplier form shows order fields and active delivery time templates', function () {
|
||||
supplierOrderCountry();
|
||||
DeliveryTime::query()->create(['label' => '3–5 Werktage', 'active' => true, 'pos' => 0]);
|
||||
DeliveryTime::query()->create(['label' => 'Inaktiv-Vorlage', 'active' => false, 'pos' => 1]);
|
||||
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.suppliers.create'))
|
||||
->assertSuccessful()
|
||||
->assertSee('order_method')
|
||||
->assertSee('delivery_time')
|
||||
->assertSee('3–5 Werktage')
|
||||
->assertDontSee('Inaktiv-Vorlage');
|
||||
});
|
||||
|
||||
test('supplier can be stored with email order method', function () {
|
||||
$country = supplierOrderCountry();
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'Mail-Lieferant',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'order_method' => 'email',
|
||||
'order_email' => 'bestellung@example.test',
|
||||
'delivery_time' => '3–5 Werktage',
|
||||
'delivery_time_days' => '5',
|
||||
])->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
$supplier = Supplier::query()->where('name', 'Mail-Lieferant')->firstOrFail();
|
||||
|
||||
expect($supplier->order_method)->toBe('email')
|
||||
->and($supplier->order_email)->toBe('bestellung@example.test')
|
||||
->and($supplier->delivery_time)->toBe('3–5 Werktage')
|
||||
->and($supplier->delivery_time_days)->toBe(5);
|
||||
});
|
||||
|
||||
test('supplier delivery time days must be an integer', function () {
|
||||
$country = supplierOrderCountry();
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'Falsche-Tage',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'delivery_time_days' => 'viele',
|
||||
])->assertSessionHasErrors('delivery_time_days');
|
||||
});
|
||||
|
||||
test('supplier form exposes delivery time template days for autofill', function () {
|
||||
supplierOrderCountry();
|
||||
DeliveryTime::query()->create(['label' => '3–5 Werktage', 'days' => 5, 'active' => true, 'pos' => 0]);
|
||||
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.suppliers.create'))
|
||||
->assertSuccessful()
|
||||
->assertSee('data-days="5"', false)
|
||||
->assertSee('delivery_time_days');
|
||||
});
|
||||
|
||||
test('supplier can be stored with online shop order method', function () {
|
||||
$country = supplierOrderCountry();
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'Shop-Lieferant',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'order_method' => 'online_shop',
|
||||
'order_url' => 'https://shop.example.test/order?id=5&ref=abc',
|
||||
])->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
$supplier = Supplier::query()->where('name', 'Shop-Lieferant')->firstOrFail();
|
||||
|
||||
expect($supplier->order_method)->toBe('online_shop')
|
||||
->and($supplier->order_url)->toBe('https://shop.example.test/order?id=5&ref=abc');
|
||||
});
|
||||
|
||||
test('supplier order fields can be updated', function () {
|
||||
$country = supplierOrderCountry();
|
||||
$supplier = Supplier::query()->create([
|
||||
'name' => 'Update-Lieferant',
|
||||
'country_id' => $country->id,
|
||||
'active' => true,
|
||||
'order_method' => 'email',
|
||||
'order_email' => 'alt@example.test',
|
||||
]);
|
||||
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->put(route('admin.inventory.suppliers.update', $supplier), [
|
||||
'name' => 'Update-Lieferant',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'order_method' => 'online_shop',
|
||||
'order_url' => 'https://neu.example.test',
|
||||
'delivery_time' => '1–2 Wochen',
|
||||
])->assertRedirect(route('admin.inventory.suppliers.index'));
|
||||
|
||||
$supplier->refresh();
|
||||
|
||||
expect($supplier->order_method)->toBe('online_shop')
|
||||
->and($supplier->order_url)->toBe('https://neu.example.test')
|
||||
->and($supplier->delivery_time)->toBe('1–2 Wochen');
|
||||
});
|
||||
|
||||
test('supplier order method rejects invalid value', function () {
|
||||
$country = supplierOrderCountry();
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'Falscher-Bestellweg',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'order_method' => 'fax',
|
||||
])->assertSessionHasErrors('order_method');
|
||||
});
|
||||
|
||||
test('supplier order email must be valid email', function () {
|
||||
$country = supplierOrderCountry();
|
||||
$this->actingAs(supplierOrderUser(7), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.suppliers.store'), [
|
||||
'name' => 'Falsche-Mail',
|
||||
'country_id' => (string) $country->id,
|
||||
'active' => '1',
|
||||
'order_method' => 'email',
|
||||
'order_email' => 'keine-mail',
|
||||
])->assertSessionHasErrors('order_email');
|
||||
});
|
||||
111
tests/Feature/TaxRateSettingsTest.php
Normal file
111
tests/Feature/TaxRateSettingsTest.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
use App\Models\TaxRate;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function taxRateUser(int $adminLevel = 8): User
|
||||
{
|
||||
$user = User::query()->create([
|
||||
'email' => uniqid('tax_', true).'@test.example',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
$user->forceFill([
|
||||
'admin' => $adminLevel,
|
||||
'confirmed' => true,
|
||||
'active' => true,
|
||||
'wizard' => 100,
|
||||
'blocked' => false,
|
||||
])->save();
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
test('allgemein-seite rendert und zeigt umsatzsteuersaetze', function () {
|
||||
$this->actingAs(taxRateUser(8), 'user');
|
||||
|
||||
TaxRate::create(['name' => 'Standard', 'percent' => 19.00, 'active' => true, 'pos' => 0]);
|
||||
|
||||
$response = $this->get(route('admin.inventory.general'));
|
||||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('Umsatzsteuersätze', false);
|
||||
$response->assertSee('Standard');
|
||||
$response->assertSee(route('admin.inventory.tax-rates.create'), false);
|
||||
});
|
||||
|
||||
test('superadmin legt umsatzsteuersatz an', function () {
|
||||
$this->actingAs(taxRateUser(8), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.tax-rates.create'))->assertSuccessful();
|
||||
|
||||
$this->post(route('admin.inventory.tax-rates.store'), [
|
||||
'name' => 'Standard',
|
||||
'percent' => '19',
|
||||
'active' => '1',
|
||||
'pos' => 0,
|
||||
])->assertRedirect(route('admin.inventory.general'));
|
||||
|
||||
$taxRate = TaxRate::query()->where('name', 'Standard')->firstOrFail();
|
||||
expect((float) $taxRate->percent)->toBe(19.00);
|
||||
expect($taxRate->active)->toBeTrue();
|
||||
});
|
||||
|
||||
test('umsatzsteuersatz kann bearbeitet und deaktiviert werden', function () {
|
||||
$this->actingAs(taxRateUser(8), 'user');
|
||||
|
||||
$taxRate = TaxRate::create(['name' => 'Ermäßigt', 'percent' => 7.00, 'active' => true, 'pos' => 1]);
|
||||
|
||||
$this->get(route('admin.inventory.tax-rates.edit', $taxRate))->assertSuccessful();
|
||||
|
||||
$this->put(route('admin.inventory.tax-rates.update', $taxRate), [
|
||||
'name' => 'Ermäßigt (alt)',
|
||||
'percent' => '7',
|
||||
'pos' => 1,
|
||||
])->assertRedirect(route('admin.inventory.general'));
|
||||
|
||||
$taxRate->refresh();
|
||||
expect($taxRate->name)->toBe('Ermäßigt (alt)');
|
||||
expect($taxRate->active)->toBeFalse();
|
||||
});
|
||||
|
||||
test('umsatzsteuersatz kann geloescht werden', function () {
|
||||
$this->actingAs(taxRateUser(8), 'user');
|
||||
|
||||
$taxRate = TaxRate::create(['name' => 'Steuerfrei', 'percent' => 0.00, 'active' => true, 'pos' => 2]);
|
||||
|
||||
$this->delete(route('admin.inventory.tax-rates.destroy', $taxRate))
|
||||
->assertRedirect(route('admin.inventory.general'));
|
||||
|
||||
expect(TaxRate::query()->whereKey($taxRate->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('prozentsatz ist pflicht und muss numerisch sein', function () {
|
||||
$this->actingAs(taxRateUser(8), 'user');
|
||||
|
||||
$this->post(route('admin.inventory.tax-rates.store'), [
|
||||
'name' => 'Ohne Satz',
|
||||
'percent' => '',
|
||||
])->assertSessionHasErrors('percent');
|
||||
|
||||
$this->post(route('admin.inventory.tax-rates.store'), [
|
||||
'name' => 'Zu hoch',
|
||||
'percent' => '150',
|
||||
])->assertSessionHasErrors('percent');
|
||||
});
|
||||
|
||||
test('nur aktive saetze ueber active-scope', function () {
|
||||
TaxRate::create(['name' => 'Aktiv', 'percent' => 19.00, 'active' => true, 'pos' => 0]);
|
||||
TaxRate::create(['name' => 'Inaktiv', 'percent' => 16.00, 'active' => false, 'pos' => 1]);
|
||||
|
||||
expect(TaxRate::query()->active()->pluck('name')->all())->toBe(['Aktiv']);
|
||||
});
|
||||
|
||||
test('nicht-superadmin hat keinen zugriff auf allgemein-seite', function () {
|
||||
$this->actingAs(taxRateUser(7), 'user');
|
||||
|
||||
$this->get(route('admin.inventory.general'))->assertRedirect('/home');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue