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).
203 lines
7.3 KiB
PHP
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'));
|
|
});
|