Kundenentscheid 26.08.: Rohstoffe werden in Gramm geführt, dürfen aber in ml erfasst werden; über die Dichte ergeben sich die Gramm (100 ml Propolistinktur × 0,79 = 79 g). - Neues Feld ingredients.density (g/ml, optional) inkl. Feld im INCI-Formular. - Wareneingang: Einheiten-Umschalter g/ml am Mengenfeld (nur wenn Dichte hinterlegt; Live-Hinweis mit Gramm-Umrechnung, Dichte kommt aus der Rohstoff-Suche mit). Erfassung in ml wird über die Dichte in Gramm umgerechnet und kanonisch als Gramm gespeichert — InventoryService und alle Aggregationen bleiben unverändert. purchase_unit + density_used (Snapshot) protokollieren die ml-Erfassung für Anzeige/Bearbeitung; Detailseite zeigt „790 g (= 1.000 ml)". Wareneingang-Buchung rechnet gelieferte ml ebenfalls um. - Serverseitige Validierung: ml nur mit hinterlegter Dichte. Migrationen add_density_* auf Dev-/Live-DB. 4 Tests, Suite 318 grün. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
4.8 KiB
PHP
128 lines
4.8 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\User;
|
||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
use Tests\TestCase;
|
||
|
||
uses(TestCase::class, RefreshDatabase::class);
|
||
|
||
/**
|
||
* AP-42: Rohstoffe werden in Gramm geführt, dürfen aber in ml erfasst werden.
|
||
* Über die Dichte (g/ml) ergeben sich die Gramm (100 ml Propolis × 0,79 = 79 g).
|
||
*/
|
||
function densAdmin(): User
|
||
{
|
||
$user = User::query()->create(['email' => uniqid('dens_', true).'@test.example', 'password' => bcrypt('password')]);
|
||
$user->forceFill(['admin' => 7, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save();
|
||
|
||
return $user->fresh();
|
||
}
|
||
|
||
/**
|
||
* @return array{0: User, 1: Ingredient, 2: Location, 3: Supplier, 4: MaterialQuality}
|
||
*/
|
||
function densSetup(?float $density): array
|
||
{
|
||
$admin = densAdmin();
|
||
$ingredient = Ingredient::query()->create(['name' => 'INCI-'.uniqid('', true), 'active' => true, 'pos' => 0, 'density' => $density]);
|
||
|
||
return [$admin, $ingredient, Location::factory()->create(), Supplier::factory()->create(), MaterialQuality::factory()->create()];
|
||
}
|
||
|
||
test('AP-42: ml-erfassung wird über die dichte in gramm gespeichert', function () {
|
||
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(0.79);
|
||
$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-08-01',
|
||
'ordered_quantity' => '100',
|
||
'ordered_unit' => 'ml',
|
||
'price_per_kg' => '10,00',
|
||
])->assertRedirect(route('admin.inventory.stock-entries.index'));
|
||
|
||
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
||
expect((float) $entry->ordered_quantity)->toBe(79.0)
|
||
->and($entry->purchase_unit)->toBe('ml')
|
||
->and((float) $entry->density_used)->toBe(0.79);
|
||
});
|
||
|
||
test('AP-42: gramm-erfassung bleibt unverändert', function () {
|
||
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(0.79);
|
||
$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-08-01',
|
||
'ordered_quantity' => '100',
|
||
'ordered_unit' => 'gram',
|
||
'price_per_kg' => '10,00',
|
||
])->assertRedirect();
|
||
|
||
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
||
expect((float) $entry->ordered_quantity)->toBe(100.0)
|
||
->and($entry->purchase_unit)->toBe('gram')
|
||
->and($entry->density_used)->toBeNull();
|
||
});
|
||
|
||
test('AP-42: ml-erfassung ohne hinterlegte dichte wird abgelehnt', function () {
|
||
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(null);
|
||
$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-08-01',
|
||
'ordered_quantity' => '100',
|
||
'ordered_unit' => 'ml',
|
||
'price_per_kg' => '10,00',
|
||
])->assertSessionHasErrors('ordered_quantity');
|
||
|
||
expect(StockEntry::query()->count())->toBe(0);
|
||
});
|
||
|
||
test('AP-42: wareneingang in ml wird über die dichte in gramm gebucht', function () {
|
||
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(0.80);
|
||
$this->actingAs($admin, 'user');
|
||
|
||
// Bestellung in ml (250 ml × 0,80 = 200 g)
|
||
$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-08-01',
|
||
'ordered_quantity' => '250',
|
||
'ordered_unit' => 'ml',
|
||
'price_per_kg' => '10,00',
|
||
])->assertRedirect();
|
||
|
||
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
||
|
||
// Wareneingang: 250 ml geliefert => 200 g gebucht
|
||
$this->put(route('admin.inventory.stock-entries.receive', $entry), [
|
||
'received_at' => '2026-08-05',
|
||
'received_quantity' => '250',
|
||
'batch_number' => 'DENS-1',
|
||
'best_before' => '2030-12-31',
|
||
])->assertRedirect();
|
||
|
||
expect((float) $entry->fresh()->received_quantity)->toBe(200.0);
|
||
});
|