Warenwirtschaft: OPT-7 Buchungs-Härtung (Audit H-2, H-3, M-1, M-9)
- H-2: quantity_used muss > 0 sein (FormRequest-Regel + Service-Guard) — negative Zeilen konnten sich in der Summenprüfung aufheben und Chargen-Restbestände aufblähen. - H-3: expandBatchLines() dekrementiert die gecachten Restmengen — zwei Zeilen derselben Charge belasten nicht mehr denselben Einkauf doppelt (FEFO korrekt über beide Zeilen). - M-1: Ausschuss validiert Chargen-Konsistenz (gehört zum Rohstoff, received) und Restbestand (je Charge bzw. gesamt). - M-9: lockForUpdate auf die Produktionszeile bei Update und Löschen. Tests: neuer ProductionHardeningTest (4). Suite: 223 grün, 806 Assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
84450385e3
commit
c69721d174
5 changed files with 231 additions and 1 deletions
|
|
@ -30,7 +30,14 @@ class StoreProductionRequest extends FormRequest
|
|||
// AP-27: Das Formular übergibt Chargen (batch_key); Einzel-Einträge (stock_entry_id) bleiben unterstützt.
|
||||
'ingredient_lines.*.batch_key' => ['required_without:ingredient_lines.*.stock_entry_id', 'nullable', 'string', 'max:120'],
|
||||
'ingredient_lines.*.stock_entry_id' => ['required_without:ingredient_lines.*.batch_key', 'nullable', 'integer', 'exists:stock_entries,id'],
|
||||
'ingredient_lines.*.quantity_used' => ['required', 'string'],
|
||||
// Audit H-2: Vorzeichen-/Zahlprüfung — negative Zeilen konnten sich
|
||||
// in der Summenprüfung gegenseitig aufheben und Bestand aufblähen.
|
||||
'ingredient_lines.*.quantity_used' => ['required', 'string', function (string $attribute, mixed $value, \Closure $fail) {
|
||||
$n = reFormatNumber((string) $value);
|
||||
if (! is_numeric($n) || (float) $n <= 0) {
|
||||
$fail(__('Die Menge je Chargen-Zeile muss größer als 0 sein.'));
|
||||
}
|
||||
}],
|
||||
// AP-28: Produktionsergebnis — bucht den Produktbestand; Planung oben ist unverbindlich.
|
||||
'outputs' => ['required', 'array', 'min:1'],
|
||||
'outputs.*.product_id' => ['required', 'integer', 'exists:products,id'],
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace App\Http\Requests\Inventory;
|
||||
|
||||
use App\Models\StockEntry;
|
||||
use App\Services\InventoryService;
|
||||
use App\Services\ProductionService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
|
@ -66,6 +69,7 @@ class StoreStockDisposalRequest extends FormRequest
|
|||
if (empty($this->input('ingredient_id'))) {
|
||||
$validator->errors()->add('ingredient_id', __('Bitte einen Rohstoff wählen.'));
|
||||
}
|
||||
$this->validateChargeConsistency($validator);
|
||||
} elseif ($this->input('disposal_type') === 'packaging') {
|
||||
if (empty($this->input('packaging_item_id'))) {
|
||||
$validator->errors()->add('packaging_item_id', __('Bitte einen Verpackungsartikel wählen.'));
|
||||
|
|
@ -74,6 +78,48 @@ class StoreStockDisposalRequest extends FormRequest
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit M-1: Charge muss zum gewählten Rohstoff gehören (eingegangen,
|
||||
* Typ ingredient), und die Menge darf den Restbestand nicht übersteigen.
|
||||
*/
|
||||
protected function validateChargeConsistency(Validator $validator): void
|
||||
{
|
||||
$quantity = (float) ($this->input('quantity') ?: 0);
|
||||
$ingredientId = (int) $this->input('ingredient_id');
|
||||
|
||||
if (! $this->filled('stock_entry_id')) {
|
||||
if ($ingredientId > 0 && $quantity > 0) {
|
||||
$remaining = app(InventoryService::class)
|
||||
->remainingByIngredient([$ingredientId])[$ingredientId] ?? 0.0;
|
||||
if ($quantity > $remaining) {
|
||||
$validator->errors()->add('quantity', __('Die Menge (:qty g) übersteigt den Restbestand (:rest g).', [
|
||||
'qty' => number_format($quantity, 2, ',', '.'),
|
||||
'rest' => number_format(max(0, $remaining), 2, ',', '.'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$entry = StockEntry::query()->find((int) $this->input('stock_entry_id'));
|
||||
if (! $entry || $entry->entry_type !== 'ingredient' || $entry->status !== 'received'
|
||||
|| (int) $entry->ingredient_id !== $ingredientId) {
|
||||
$validator->errors()->add('stock_entry_id', __('Die gewählte Charge gehört nicht zum gewählten Rohstoff.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$consumed = app(ProductionService::class)->consumedByStockEntry([$entry->id])[$entry->id] ?? 0.0;
|
||||
$remaining = round((float) $entry->received_quantity - $consumed, 2);
|
||||
if ($quantity > $remaining) {
|
||||
$validator->errors()->add('quantity', __('Die Menge (:qty g) übersteigt den Restbestand der Charge (:rest g).', [
|
||||
'qty' => number_format($quantity, 2, ',', '.'),
|
||||
'rest' => number_format(max(0, $remaining), 2, ',', '.'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue