Warenwirtschaft: N-01/N-03/N-05 — Entwurfsmenge ohne Charge, Snapshot-Signatur je Produkt, Alt-Ausschuss in Chargenverfügbarkeit

- N-01: Ist-Menge ohne gewählte Charge bleibt im Entwurf erhalten (Filter in
  Request und Service, formBatchKey liefert '' für chargenlose Zeilen).
- N-03: outputSignature summiert Stückzahlen je Produkt — ein Chargensplit
  ersetzt den historischen Verpackungs-Snapshot nicht mehr.
- N-05: Ausschuss ohne Chargenbezug (Altbestand) wird für die Verfügbarkeit je
  Standort FEFO auf die Chargen angerechnet (applyUnboundDisposals, lesend);
  Chargen-Ausschussprüfung nutzt dieselbe Verfügbarkeit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-08 11:48:02 +00:00
parent 520d4e2deb
commit 313c6277ce
8 changed files with 138 additions and 23 deletions

View file

@ -98,14 +98,17 @@ class StoreProductionRequest extends FormRequest
// AP-43: Im Entwurf dürfen Zeilen unvollständig sein — nur solche mit
// Rohstoff + (Charge oder Wareneingang) bzw. Ergebnis-Produkt übernehmen.
$ingredientLines = array_values(array_filter($data['ingredient_lines'] ?? [], function ($line): bool {
// N-01 (Nachprüfung 08.09.2026): Im Entwurf zählt auch eine Ist-Menge ohne
// gewählte Charge als Arbeitsstand; beim Abschluss verlangen die Regeln die Charge.
$isDraft = $this->isDraft();
$ingredientLines = array_values(array_filter($data['ingredient_lines'] ?? [], function ($line) use ($isDraft): bool {
return is_array($line)
&& (int) ($line['ingredient_id'] ?? 0) > 0
&& (! empty($line['batch_key']) || (int) ($line['stock_entry_id'] ?? 0) > 0);
&& (! empty($line['batch_key']) || (int) ($line['stock_entry_id'] ?? 0) > 0
|| ($isDraft && trim((string) ($line['quantity_used'] ?? '')) !== ''));
}));
// F-03: Im Entwurf zählt ein gewähltes Ergebnisprodukt auch ohne Stückzahl als Arbeitsstand.
$isDraft = $this->isDraft();
$outputs = array_values(array_filter($data['outputs'] ?? [], function ($output) use ($isDraft): bool {
return is_array($output)
&& (int) ($output['product_id'] ?? 0) > 0
@ -128,7 +131,8 @@ class StoreProductionRequest extends FormRequest
if (! empty($line['batch_key'])) {
$out['batch_key'] = (string) $line['batch_key'];
} else {
$out['stock_entry_id'] = (int) $line['stock_entry_id'];
// N-01: im Entwurf darf beides fehlen (Menge ohne Charge).
$out['stock_entry_id'] = (int) ($line['stock_entry_id'] ?? 0);
}
return $out;

View file

@ -113,8 +113,10 @@ class StoreStockDisposalRequest extends FormRequest
return;
}
$consumed = app(ProductionService::class)->consumedByStockEntry([$entry->id])[$entry->id] ?? 0.0;
$remaining = round((float) $entry->received_quantity - $consumed, 2);
// N-05: Verfügbarkeit der Charge wie in der Produktion (inkl. FEFO-Anrechnung von Alt-Ausschuss ohne Charge).
$available = app(ProductionService::class)->availableStockEntriesForIngredient($ingredientId, (int) $entry->location_id)
->firstWhere('id', $entry->id);
$remaining = $available !== null ? round((float) $available->getAttribute('remaining_quantity'), 2) : 0.0;
if ($quantity > $remaining) {
$validator->errors()->add('quantity', __('Die Menge (:qty g) übersteigt den Restbestand der Charge (:rest g).', [
'qty' => number_format($quantity, 2, ',', '.'),