> */ public function rules(): array { $received = $this->isReceivedEntry(); return [ 'entry_type' => ['required', Rule::in(['ingredient', 'packaging', 'shipping'])], 'ingredient_id' => ['nullable', 'integer', 'exists:ingredients,id'], 'packaging_item_id' => ['nullable', 'integer', 'exists:packaging_items,id'], 'supplier_id' => ['required', 'integer', 'exists:suppliers,id'], 'location_id' => ['required', 'integer', 'exists:locations,id'], 'ordered_at' => ['required', 'date'], 'ordered_quantity' => ['required', 'numeric', 'min:0.001', 'max:'.InventoryService::QUANTITY_MAX], 'ordered_unit' => ['nullable', Rule::in(['gram', 'ml'])], 'quality_id' => ['nullable', 'integer', 'exists:material_qualities,id'], 'tax_rate_id' => ['nullable', 'integer', 'exists:tax_rates,id'], 'price_per_kg' => ['nullable', 'numeric', 'min:0'], 'price_per_kg_gross' => ['nullable', 'numeric', 'min:0'], 'price_total' => ['nullable', 'numeric', 'min:0'], // Anforderung 14.09.2026: Korrektur gebuchter Eingänge inkl. Eingangsdaten, // Grund ist bei gebuchten Eingängen Pflicht (Änderungsprotokoll). 'received_at' => [$received ? 'required' : 'nullable', 'date'], 'received_quantity' => [$received ? 'required' : 'nullable', 'numeric', 'min:0.001', 'max:'.InventoryService::QUANTITY_MAX], 'batch_number' => ['nullable', 'string', 'max:100'], 'best_before' => ['nullable', 'date'], 'change_reason' => [$received ? 'required' : 'nullable', 'string', 'max:500'], ]; } protected function stockEntry(): ?StockEntry { $entry = $this->route('stock_entry') ?? $this->route('stockEntry'); return $entry instanceof StockEntry ? $entry : null; } protected function isReceivedEntry(): bool { return (bool) $this->stockEntry()?->isReceived(); } /** * @return array */ public function messages(): array { return [ 'quality_id.required' => __('Bitte eine Rohstoffqualität wählen.'), 'price_per_kg.required' => __('Bitte den Netto- oder Brutto-Preis pro kg angeben.'), 'price_total.required' => __('Bitte den Gesamtpreis netto angeben.'), 'change_reason.required' => __('Bitte den Grund der Korrektur angeben — er wird im Änderungsprotokoll gespeichert.'), 'received_at.required' => __('Bitte das Eingangsdatum angeben.'), 'received_quantity.required' => __('Bitte die eingegangene Menge angeben.'), ]; } protected function prepareForValidation(): void { $this->merge([ 'ordered_quantity' => normalizeWawiQuantity($this->input('ordered_quantity')), 'received_quantity' => $this->filled('received_quantity') ? normalizeWawiQuantity($this->input('received_quantity')) : null, 'change_reason' => $this->filled('change_reason') ? trim((string) $this->input('change_reason')) : null, 'price_per_kg' => $this->filled('price_per_kg') ? reFormatWawiNumber($this->input('price_per_kg')) : null, 'price_per_kg_gross' => $this->filled('price_per_kg_gross') ? reFormatWawiNumber($this->input('price_per_kg_gross')) : null, 'price_total' => $this->filled('price_total') ? reFormatWawiNumber($this->input('price_total')) : null, ]); } public function withValidator(Validator $validator): void { $validator->after(function (Validator $validator): void { $type = $this->input('entry_type'); if ($type === 'ingredient') { if (empty($this->input('ingredient_id'))) { $validator->errors()->add('ingredient_id', __('Bitte einen Inhaltsstoff wählen.')); } if (empty($this->input('quality_id'))) { $validator->errors()->add('quality_id', __('Bitte eine Rohstoffqualität wählen.')); } // Einheit ist Pflicht: bewusst g oder ml wählen (kein stiller Gramm-Default). if (! in_array($this->input('ordered_unit'), ['gram', 'ml'], true)) { $validator->errors()->add('ordered_unit', __('Bitte die Einheit der bestellten Menge wählen (Gramm oder Milliliter).')); } // 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.')); } elseif (is_numeric($this->input('ordered_quantity')) && roundWawiQuantity((float) $this->input('ordered_quantity') * (float) $density) <= 0) { // Prüfung AP-53 (4.2): positive ml-Menge darf nach Umrechnung nicht auf 0,000 g fallen. $validator->errors()->add('ordered_quantity', __('Die Menge ergibt umgerechnet weniger als 0,001 g — bitte mindestens 0,001 g erfassen.')); } } if ($this->isReceivedEntry()) { if (empty($this->input('batch_number'))) { $validator->errors()->add('batch_number', __('Bitte eine Chargennummer angeben.')); } if (empty($this->input('best_before'))) { $validator->errors()->add('best_before', __('Bitte die Mindesthaltbarkeit angeben.')); } if ($this->input('ordered_unit') === 'ml' && is_numeric($this->input('received_quantity'))) { $density = Ingredient::query()->whereKey($this->input('ingredient_id'))->value('density') ?? $this->stockEntry()?->density_used; if ($density !== null && (float) $density > 0 && roundWawiQuantity((float) $this->input('received_quantity') * (float) $density) <= 0) { $validator->errors()->add('received_quantity', __('Die Menge ergibt umgerechnet weniger als 0,001 g — bitte mindestens 0,001 g erfassen.')); } } } $net = $this->input('price_per_kg'); $gross = $this->input('price_per_kg_gross'); $hasNet = is_numeric($net) && (float) $net > 0; $hasGross = is_numeric($gross) && (float) $gross > 0; if (! $hasNet && ! $hasGross) { $validator->errors()->add('price_per_kg', __('Bitte den Netto- oder Brutto-Preis pro kg angeben.')); } } elseif (! empty($type)) { if (empty($this->input('packaging_item_id'))) { $validator->errors()->add('packaging_item_id', __('Bitte einen Verpackungsartikel wählen.')); } if (! is_numeric($this->input('price_total')) || (float) $this->input('price_total') <= 0) { $validator->errors()->add('price_total', __('Bitte den Gesamtpreis netto angeben.')); } } }); } /** * @return array */ public function validatedPayload(): array { $data = $this->validated(); unset($data['change_reason']); if (($data['entry_type'] ?? '') === 'ingredient') { $data['packaging_item_id'] = null; } else { $data['ingredient_id'] = null; $data['quality_id'] = null; $data['batch_number'] = null; $data['best_before'] = null; } if (! $this->isReceivedEntry()) { // Offene Bestellung: Eingangsfelder gehören nicht ins Formular. unset($data['received_at'], $data['received_quantity'], $data['batch_number'], $data['best_before']); } return $data; } }