Warenwirtschaft: F-06 — Chargenzeilen nach Validierungsfehler aus old() wiederherstellen
Vorbelegung der Chargen-Zeilen zentral in ProductionController::existingLines();
old('ingredient_lines') hat Vorrang vor dem gespeicherten Stand.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a803e956e1
commit
122d28bc4f
5 changed files with 78 additions and 25 deletions
|
|
@ -52,6 +52,7 @@ class ProductionController extends Controller
|
|||
'defaultLocationId' => $defaultLocationId,
|
||||
'defaultProductId' => $defaultProductId,
|
||||
'model' => null,
|
||||
'existingLines' => $this->existingLines(null),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +161,7 @@ class ProductionController extends Controller
|
|||
...$this->productCatalogs($production),
|
||||
'locations' => Location::query()->where('active', true)->orderBy('name')->get(),
|
||||
'defaultLocationId' => $defaultLocationId,
|
||||
'existingLines' => $this->existingLines($production),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -217,9 +219,48 @@ class ProductionController extends Controller
|
|||
...$this->productCatalogs($production),
|
||||
'locations' => Location::query()->where('active', true)->orderBy('name')->get(),
|
||||
'defaultLocationId' => $defaultLocationId,
|
||||
'existingLines' => $this->existingLines($production),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorbelegung der Chargen-Zeilen im Formular, Schlüssel „Rohstoff|Phase“ (F-04).
|
||||
* Prüfung 08.09.2026 F-06: Nach einem Validierungsfehler haben die zuletzt
|
||||
* eingegebenen Zeilen (old()) Vorrang vor dem gespeicherten Stand — wie bei
|
||||
* Kopfdaten und Ergebniszeilen. Sonst AP-27: Zeilen je Charge (batch_key)
|
||||
* zusammenfassen, mehrere Einkäufe derselben Charge = eine Formularzeile.
|
||||
*
|
||||
* @return array<string, array<int, array{batch_key: string, quantity_used: float|string}>>
|
||||
*/
|
||||
private function existingLines(?Production $production): array
|
||||
{
|
||||
$old = old('ingredient_lines');
|
||||
if (is_array($old) && $old !== []) {
|
||||
return collect($old)
|
||||
->filter(fn ($l) => is_array($l)
|
||||
&& (int) ($l['ingredient_id'] ?? 0) > 0
|
||||
&& (trim((string) ($l['batch_key'] ?? '')) !== '' || trim((string) ($l['quantity_used'] ?? '')) !== ''))
|
||||
->groupBy(fn ($l) => (int) $l['ingredient_id'].'|'.(! empty($l['recipe_phase_id']) ? (int) $l['recipe_phase_id'] : ''))
|
||||
->map(fn ($group) => $group->map(fn ($l) => [
|
||||
'batch_key' => trim((string) ($l['batch_key'] ?? '')),
|
||||
'quantity_used' => (string) ($l['quantity_used'] ?? ''),
|
||||
])->values()->all())
|
||||
->all();
|
||||
}
|
||||
|
||||
if ($production === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $production->productionIngredients
|
||||
->groupBy(fn ($l) => $l->formGroupKey())
|
||||
->map(fn ($lines) => $lines
|
||||
->groupBy(fn ($l) => $l->formBatchKey())
|
||||
->map(fn ($group, $key) => ['batch_key' => (string) $key, 'quantity_used' => (float) $group->sum('quantity_used')])
|
||||
->values()->all())
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüfung 08.09.2026 F-02: Zwei getrennte Kataloge.
|
||||
* - `products`: Planungsprodukt oben — nur produktionsrelevante Produkte (AP-47),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
(function ($) {
|
||||
var recipeBase = @json(url('/admin/inventory/api/products'));
|
||||
var existingLines = @json($existingLines ?? (object) []);
|
||||
var existingLines = @json(($existingLines ?? []) === [] ? (object) [] : $existingLines);
|
||||
var excludeProductionId = @json($excludeProductionId ?? null);
|
||||
|
||||
var lineIndex = 0;
|
||||
|
|
|
|||
|
|
@ -19,17 +19,6 @@
|
|||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
@php
|
||||
// AP-27: Zeilen je Charge (batch_key) zusammenfassen — mehrere Einkäufe derselben Charge = eine Formularzeile.
|
||||
// F-04: Schlüssel = Rohstoff|Phase, damit ein Rohstoff in zwei Phasen je Phase seinen Anteil behält.
|
||||
$existingLines = $model
|
||||
? $model->productionIngredients->groupBy(fn ($l) => $l->formGroupKey())->map(function ($lines) {
|
||||
return $lines
|
||||
->groupBy(fn ($l) => $l->formBatchKey())
|
||||
->map(fn ($group, $key) => ['batch_key' => $key, 'quantity_used' => (float) $group->sum('quantity_used')])
|
||||
->values();
|
||||
})
|
||||
: [];
|
||||
@endphp
|
||||
@include('admin.inventory.productions._scripts', ['existingLines' => $existingLines, 'excludeProductionId' => null])
|
||||
{{-- Vorbelegung der Chargen-Zeilen: ProductionController::existingLines() (AP-27/F-04/F-06). --}}
|
||||
@include('admin.inventory.productions._scripts', ['existingLines' => $existingLines ?? [], 'excludeProductionId' => null])
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -27,15 +27,6 @@
|
|||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
@php
|
||||
// AP-27: Zeilen je Charge (batch_key) zusammenfassen — mehrere Einkäufe derselben Charge = eine Formularzeile.
|
||||
// F-04: Schlüssel = Rohstoff|Phase, damit ein Rohstoff in zwei Phasen je Phase seinen Anteil behält.
|
||||
$existingLines = $model->productionIngredients->groupBy(fn ($l) => $l->formGroupKey())->map(function ($lines) {
|
||||
return $lines
|
||||
->groupBy(fn ($l) => $l->formBatchKey())
|
||||
->map(fn ($group, $key) => ['batch_key' => $key, 'quantity_used' => (float) $group->sum('quantity_used')])
|
||||
->values();
|
||||
});
|
||||
@endphp
|
||||
@include('admin.inventory.productions._scripts', ['existingLines' => $existingLines, 'excludeProductionId' => $model->id])
|
||||
{{-- Vorbelegung der Chargen-Zeilen: ProductionController::existingLines() (AP-27/F-04/F-06). --}}
|
||||
@include('admin.inventory.productions._scripts', ['existingLines' => $existingLines ?? [], 'excludeProductionId' => $model->id])
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -393,3 +393,35 @@ test('F-04: derselbe rohstoff in zwei phasen behält beim wiederöffnen je phase
|
|||
->and($done->productionIngredients()->where('recipe_phase_id', $phaseB->id)->sum('quantity_used'))->toEqual(4)
|
||||
->and((float) app(InventoryService::class)->remainingByIngredient([$ing->id])[$ing->id])->toBe(990.0);
|
||||
});
|
||||
|
||||
test('F-06: nach einem validierungsfehler bleiben die eingegebenen chargenzeilen im formular erhalten', function () {
|
||||
$user = draftUser();
|
||||
$location = Location::factory()->create();
|
||||
$supplier = Supplier::factory()->create();
|
||||
$ing = draftIngredient();
|
||||
$product = draftRecipeProduct($ing); // Soll 10 g
|
||||
$stock = draftStock($ing, $location, $supplier, $user);
|
||||
$key = $stock->batchGroupKey();
|
||||
|
||||
Passport::actingAs($user);
|
||||
$this->actingAs($user, 'user');
|
||||
|
||||
// 20 g statt 10 g → Toleranzfehler beim Abschluss, Redirect zurück mit old().
|
||||
$this->from(route('admin.inventory.productions.create'))
|
||||
->post(route('admin.inventory.productions.store'), [
|
||||
'save_mode' => 'complete',
|
||||
'product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 1,
|
||||
'ingredient_lines' => [['ingredient_id' => $ing->id, 'batch_key' => $key, 'quantity_used' => '20,0']],
|
||||
'outputs' => [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => null]],
|
||||
])
|
||||
->assertRedirect(route('admin.inventory.productions.create'))
|
||||
->assertSessionHasErrors('ingredient_lines');
|
||||
|
||||
$html = $this->get(route('admin.inventory.productions.create'))->assertOk()->getContent();
|
||||
$start = strpos($html, 'var existingLines = ') + strlen('var existingLines = ');
|
||||
$existing = json_decode(substr($html, $start, strpos($html, ';', $start) - $start), true);
|
||||
|
||||
expect($existing)->toHaveKey($ing->id.'|')
|
||||
->and($existing[$ing->id.'|'][0]['batch_key'])->toBe($key)
|
||||
->and($existing[$ing->id.'|'][0]['quantity_used'])->toBe('20,0');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue