gruene-seele/tests/Feature/StockEntryPriceTest.php
Kevin Adametz 78679e0c55 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).
2026-06-02 16:30:42 +00:00

203 lines
7.3 KiB
PHP

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