Warenwirtschaft: N-06 — Dichte-Snapshot der ml-Bestellung gilt auch im Bearbeiten
Formular, Update-Validierung und Repository nutzen bei unverändertem Rohstoff den Snapshot als Rückfall; bei Rohstoffwechsel wird keine fremde Dichte übernommen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
9acb05380a
commit
5d82875d2f
4 changed files with 73 additions and 1 deletions
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Requests\Inventory;
|
||||
|
||||
use App\Models\Ingredient;
|
||||
use App\Models\StockEntry;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
|
@ -72,6 +73,12 @@ class UpdateStockEntryRequest extends FormRequest
|
|||
// AP-42: ml-Erfassung nur mit hinterlegter Dichte.
|
||||
if ($this->input('ordered_unit') === 'ml') {
|
||||
$density = Ingredient::query()->whereKey($this->input('ingredient_id'))->value('density');
|
||||
// N-06: bei unverändertem Rohstoff gilt der Dichte-Snapshot der Bestellung als Rückfall.
|
||||
$entry = $this->route('stock_entry');
|
||||
if (($density === null || (float) $density <= 0) && $entry instanceof StockEntry
|
||||
&& (int) $entry->ingredient_id === (int) $this->input('ingredient_id') && (float) $entry->density_used > 0) {
|
||||
$density = $entry->density_used;
|
||||
}
|
||||
if ($density === null || (float) $density <= 0) {
|
||||
$validator->errors()->add('ordered_quantity', __('Für diesen Rohstoff ist keine Dichte hinterlegt — bitte in Gramm erfassen oder zuerst die Dichte pflegen.'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,13 @@ class StockEntryRepository
|
|||
{
|
||||
if (array_key_exists('entry_type', $data)) {
|
||||
$data['unit'] = ($data['entry_type'] ?? '') === 'ingredient' ? 'gram' : 'piece';
|
||||
// N-06: Dichte-Snapshot nur bei unverändertem Rohstoff als Rückfall mitgeben —
|
||||
// eine fremde alte Dichte darf bei Rohstoffwechsel nicht übernommen werden.
|
||||
if (($data['ordered_unit'] ?? null) === 'ml'
|
||||
&& (int) ($data['ingredient_id'] ?? 0) === (int) $stockEntry->ingredient_id
|
||||
&& (float) $stockEntry->density_used > 0) {
|
||||
$data['density_used'] = (float) $stockEntry->density_used;
|
||||
}
|
||||
$data = $this->applyPurchaseUnit($data);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
<select name="ingredient_id" id="ingredient_id" class="w-100"
|
||||
data-search-url="{{ route('admin.inventory.api.ingredients.search') }}">
|
||||
@if ($model->ingredient_id && $model->ingredient)
|
||||
<option value="{{ $model->ingredient_id }}" data-density="{{ $model->ingredient->density }}" selected>{{ $model->ingredient->name }}@if ($model->ingredient->inci)
|
||||
{{-- N-06: fehlt die Stammdaten-Dichte inzwischen, gilt der Dichte-Snapshot der Bestellung --}}
|
||||
<option value="{{ $model->ingredient_id }}" data-density="{{ $model->ingredient->density ?? $model->density_used }}" selected>{{ $model->ingredient->name }}@if ($model->ingredient->inci)
|
||||
({{ $model->ingredient->inci }})
|
||||
@endif
|
||||
</option>
|
||||
|
|
|
|||
|
|
@ -196,3 +196,60 @@ test('F-16: wareneingang prueft „offen“ innerhalb der sperre — zweite buch
|
|||
expect((float) $entry->fresh()->received_quantity)->toBe(500.0)
|
||||
->and($entry->fresh()->batch_number)->toBe('F16-1');
|
||||
});
|
||||
|
||||
test('N-06: ml-kopie ohne stammdatendichte übersteht bearbeiten, speichern und wareneingang', 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();
|
||||
$original = StockEntry::query()->latest('id')->firstOrFail();
|
||||
$ingredient->forceFill(['density' => null])->save();
|
||||
|
||||
$this->post(route('admin.inventory.stock-entries.copy', $original))->assertRedirect();
|
||||
$copy = StockEntry::query()->latest('id')->firstOrFail();
|
||||
|
||||
// Bearbeitungsformular: ml bleibt wählbar (Dichte-Snapshot als data-density), Menge 100,00 ml.
|
||||
$this->get(route('admin.inventory.stock-entries.edit', $copy))->assertOk()
|
||||
->assertSee('data-density="0.79', false)
|
||||
->assertSee('value="100,00"', false);
|
||||
|
||||
// Unverändert speichern (wie die Kopiermeldung es verlangt).
|
||||
$this->put(route('admin.inventory.stock-entries.update', $copy), [
|
||||
'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-02',
|
||||
'ordered_quantity' => '100', 'ordered_unit' => 'ml', 'price_per_kg' => '10,00',
|
||||
])->assertRedirect()->assertSessionHasNoErrors();
|
||||
|
||||
$copy->refresh();
|
||||
expect($copy->purchase_unit)->toBe('ml')
|
||||
->and((float) $copy->density_used)->toBe(0.79)
|
||||
->and((float) $copy->ordered_quantity)->toBe(79.0);
|
||||
|
||||
$this->put(route('admin.inventory.stock-entries.receive', $copy), [
|
||||
'received_at' => '2026-08-05', 'received_quantity' => '100', 'batch_number' => 'N06-1', 'best_before' => '2030-12-31',
|
||||
])->assertRedirect();
|
||||
expect((float) $copy->fresh()->received_quantity)->toBe(79.0);
|
||||
});
|
||||
|
||||
test('N-06: bei rohstoffwechsel wird keine fremde alte dichte übernommen', function () {
|
||||
[$admin, $ingredient, $location, $supplier, $quality] = densSetup(0.79);
|
||||
$other = Ingredient::query()->create(['name' => 'INCI-ohne-Dichte', 'active' => true, 'pos' => 0, 'density' => 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',
|
||||
])->assertRedirect();
|
||||
$entry = StockEntry::query()->latest('id')->firstOrFail();
|
||||
|
||||
$this->from(route('admin.inventory.stock-entries.edit', $entry))->put(route('admin.inventory.stock-entries.update', $entry), [
|
||||
'entry_type' => 'ingredient', 'ingredient_id' => (string) $other->id, 'supplier_id' => (string) $supplier->id,
|
||||
'location_id' => (string) $location->id, 'quality_id' => (string) $quality->id, 'ordered_at' => '2026-08-02',
|
||||
'ordered_quantity' => '100', 'ordered_unit' => 'ml', 'price_per_kg' => '10,00',
|
||||
])->assertSessionHasErrors('ordered_quantity');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue