Warenwirtschaft: AP-42 — Rohstoff-Dichte (g/ml) für ml-Erfassung im Wareneingang
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>
This commit is contained in:
parent
d51860af39
commit
fa852340ee
13 changed files with 332 additions and 11 deletions
128
tests/Feature/IngredientDensityTest.php
Normal file
128
tests/Feature/IngredientDensityTest.php
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<?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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue