Entwürfe speichern batch_key roh (stock_entry_id nullable), keine Auflösung und keine Filterung unfertiger Zeilen mehr beim Zwischenspeichern. Eine nicht mehr verfügbare Charge wird im Formular markiert statt alle Zeilen zu verwerfen. Ergebniszeilen dürfen im Entwurf ohne Stückzahl bleiben. Auflösung, Prüfung und Buchung erst beim Abschluss. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
141 lines
6.5 KiB
PHP
141 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Requests\Inventory;
|
|
|
|
use App\Models\Product;
|
|
use Illuminate\Foundation\Http\FormRequest;
|
|
|
|
class StoreProductionRequest extends FormRequest
|
|
{
|
|
public function authorize(): bool
|
|
{
|
|
// Audit M-2: Produktionen buchen Produktbestand — gleiche Schranke
|
|
// wie manuelle Bestandsbewegungen (admin >= 7).
|
|
return (bool) $this->user()?->isAdmin();
|
|
}
|
|
|
|
/**
|
|
* AP-43: „Zwischenspeichern" schickt save_mode=draft und speichert einen
|
|
* Entwurf ohne Pflichtfeld-Validierung (Chargen/Ergebnis optional).
|
|
*/
|
|
public function isDraft(): bool
|
|
{
|
|
return $this->input('save_mode') === 'draft';
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array<int, mixed|string>>
|
|
*/
|
|
public function rules(): array
|
|
{
|
|
// AP-43: Entwurf — nur die Kopfdaten (DB-Pflichtspalten) prüfen; Rezeptur-
|
|
// Zeilen und Produktionsergebnis dürfen unvollständig bleiben.
|
|
if ($this->isDraft()) {
|
|
return [
|
|
'product_id' => ['required', 'integer', 'exists:products,id'],
|
|
'location_id' => ['required', 'integer', 'exists:locations,id'],
|
|
'produced_at' => ['required', 'date'],
|
|
'quantity' => ['required', 'integer', 'min:1'],
|
|
'notes' => ['nullable', 'string', 'max:2000'],
|
|
'ingredient_lines' => ['nullable', 'array'],
|
|
'ingredient_lines.*.ingredient_id' => ['nullable', 'integer', 'exists:ingredients,id'],
|
|
'ingredient_lines.*.batch_key' => ['nullable', 'string', 'max:120'],
|
|
'ingredient_lines.*.stock_entry_id' => ['nullable', 'integer', 'exists:stock_entries,id'],
|
|
'ingredient_lines.*.quantity_used' => ['nullable', 'string'],
|
|
'outputs' => ['nullable', 'array'],
|
|
'outputs.*.product_id' => ['nullable', 'integer', 'exists:products,id'],
|
|
// F-03: Im Entwurf darf die Stückzahl noch leer/0 sein.
|
|
'outputs.*.quantity' => ['nullable', 'integer', 'min:0'],
|
|
'outputs.*.batch_number' => ['nullable', 'string', 'max:100'],
|
|
];
|
|
}
|
|
|
|
$recipeRequired = $this->recipeRequired();
|
|
|
|
return [
|
|
'product_id' => ['required', 'integer', 'exists:products,id'],
|
|
'location_id' => ['required', 'integer', 'exists:locations,id'],
|
|
'produced_at' => ['required', 'date'],
|
|
'quantity' => ['required', 'integer', 'min:1'],
|
|
'notes' => ['nullable', 'string', 'max:2000'],
|
|
'ingredient_lines' => [$recipeRequired ? 'required' : 'nullable', 'array', $recipeRequired ? 'min:1' : 'min:0'],
|
|
'ingredient_lines.*.ingredient_id' => ['required', 'integer', 'exists:ingredients,id'],
|
|
// 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'],
|
|
// 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 = reFormatWawiNumber((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'],
|
|
'outputs.*.quantity' => ['required', 'integer', 'min:1'],
|
|
'outputs.*.batch_number' => ['nullable', 'string', 'max:100'],
|
|
];
|
|
}
|
|
|
|
private function recipeRequired(): bool
|
|
{
|
|
$product = Product::query()->find($this->input('product_id'));
|
|
|
|
return $product === null ? true : ! (bool) $product->no_recipe_required;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function validatedPayload(): array
|
|
{
|
|
$data = $this->validated();
|
|
|
|
// 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 {
|
|
return is_array($line)
|
|
&& (int) ($line['ingredient_id'] ?? 0) > 0
|
|
&& (! empty($line['batch_key']) || (int) ($line['stock_entry_id'] ?? 0) > 0);
|
|
}));
|
|
|
|
// 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
|
|
&& ($isDraft || (int) ($output['quantity'] ?? 0) > 0);
|
|
}));
|
|
|
|
return [
|
|
'is_draft' => $this->isDraft(),
|
|
'product_id' => (int) $data['product_id'],
|
|
'location_id' => (int) $data['location_id'],
|
|
'produced_at' => $data['produced_at'],
|
|
'quantity' => (int) $data['quantity'],
|
|
'notes' => $data['notes'] ?? null,
|
|
'ingredient_lines' => array_map(function (array $line): array {
|
|
$out = [
|
|
'ingredient_id' => (int) $line['ingredient_id'],
|
|
'quantity_used' => $line['quantity_used'] ?? null,
|
|
];
|
|
if (! empty($line['batch_key'])) {
|
|
$out['batch_key'] = (string) $line['batch_key'];
|
|
} else {
|
|
$out['stock_entry_id'] = (int) $line['stock_entry_id'];
|
|
}
|
|
|
|
return $out;
|
|
}, $ingredientLines),
|
|
'outputs' => array_map(function (array $output): array {
|
|
return [
|
|
'product_id' => (int) $output['product_id'],
|
|
'quantity' => (int) $output['quantity'],
|
|
'batch_number' => $output['batch_number'] ?? null,
|
|
];
|
|
}, $outputs),
|
|
];
|
|
}
|
|
}
|