Warenwirtschaft: F-03 — Entwurf sichert Chargenzeilen verlustfrei
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>
This commit is contained in:
parent
73512897ee
commit
859b1f083d
10 changed files with 200 additions and 41 deletions
|
|
@ -85,24 +85,21 @@ class ProductionController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* AP-27/AP-43: Chargen-Zeilen auf Einzel-Wareneingänge auflösen. Im Entwurf
|
||||
* darf das Auflösen scheitern (Charge nicht verfügbar) — dann bleiben nur die
|
||||
* bereits konkret gewählten Wareneingänge erhalten, der Entwurf ist immer speicherbar.
|
||||
* AP-27: Chargen-Zeilen auf Einzel-Wareneingänge auflösen (nur beim Abschluss).
|
||||
* Prüfung 08.09.2026 F-03: Im Entwurf wird nicht aufgelöst — die Zeilen werden
|
||||
* roh (batch_key) gesichert, damit eine nicht mehr verfügbare Charge nicht den
|
||||
* gesamten Arbeitsstand verwirft. Aufgelöst und geprüft wird erst beim Abschluss.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $lines
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function expandLines(array $lines, int $locationId, ?int $excludeProductionId, bool $isDraft): array
|
||||
{
|
||||
if (! $isDraft) {
|
||||
return $this->productionService->expandBatchLines($lines, $locationId, $excludeProductionId);
|
||||
if ($isDraft) {
|
||||
return $lines;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->productionService->expandBatchLines($lines, $locationId, $excludeProductionId);
|
||||
} catch (ValidationException) {
|
||||
return array_values(array_filter($lines, fn ($l) => (int) ($l['stock_entry_id'] ?? 0) > 0));
|
||||
}
|
||||
return $this->productionService->expandBatchLines($lines, $locationId, $excludeProductionId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ class StoreProductionRequest extends FormRequest
|
|||
'ingredient_lines.*.quantity_used' => ['nullable', 'string'],
|
||||
'outputs' => ['nullable', 'array'],
|
||||
'outputs.*.product_id' => ['nullable', 'integer', 'exists:products,id'],
|
||||
'outputs.*.quantity' => ['nullable', 'integer', 'min:1'],
|
||||
// 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'],
|
||||
];
|
||||
}
|
||||
|
|
@ -100,10 +101,12 @@ class StoreProductionRequest extends FormRequest
|
|||
&& (! empty($line['batch_key']) || (int) ($line['stock_entry_id'] ?? 0) > 0);
|
||||
}));
|
||||
|
||||
$outputs = array_values(array_filter($data['outputs'] ?? [], function ($output): bool {
|
||||
// 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
|
||||
&& (int) ($output['quantity'] ?? 0) > 0;
|
||||
&& ($isDraft || (int) ($output['quantity'] ?? 0) > 0);
|
||||
}));
|
||||
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -11,9 +11,23 @@ class ProductionIngredient extends Model
|
|||
'production_id',
|
||||
'ingredient_id',
|
||||
'stock_entry_id',
|
||||
'batch_key',
|
||||
'quantity_used',
|
||||
];
|
||||
|
||||
/**
|
||||
* F-03: Chargen-Schlüssel für das Formular — im Entwurf der roh gespeicherte
|
||||
* batch_key, sonst aus dem aufgelösten Wareneingang abgeleitet.
|
||||
*/
|
||||
public function formBatchKey(): string
|
||||
{
|
||||
if (! empty($this->batch_key)) {
|
||||
return (string) $this->batch_key;
|
||||
}
|
||||
|
||||
return $this->stockEntry?->batchGroupKey() ?? 'e:'.$this->stock_entry_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ class ProductionService
|
|||
$this->assertNotASet($product);
|
||||
|
||||
if ($draft) {
|
||||
// AP-43: Entwurf — keine Rezeptur-/Mengen-/Bestandsprüfung. Es werden
|
||||
// nur bereits vollständig ausgefüllte Chargen-Zeilen gesichert.
|
||||
$ingredientLines = $this->filterCompleteLines($ingredientLines);
|
||||
// AP-43/F-03: Entwurf — keine Rezeptur-/Mengen-/Bestandsprüfung; alle
|
||||
// Zeilen mit Rohstoff + Charge werden roh gesichert (auch ohne Menge).
|
||||
$ingredientLines = $this->draftLines($ingredientLines);
|
||||
} elseif ($product->no_recipe_required) {
|
||||
$ingredientLines = [];
|
||||
} else {
|
||||
|
|
@ -70,17 +70,13 @@ class ProductionService
|
|||
]);
|
||||
|
||||
foreach ($ingredientLines as $line) {
|
||||
$production->productionIngredients()->create([
|
||||
'ingredient_id' => (int) $line['ingredient_id'],
|
||||
'stock_entry_id' => (int) $line['stock_entry_id'],
|
||||
'quantity_used' => $this->parseQuantity($line['quantity_used'] ?? null),
|
||||
]);
|
||||
$production->productionIngredients()->create($this->ingredientLineAttributes($line));
|
||||
}
|
||||
|
||||
// AP-43: Für den Entwurf nur die tatsächlich angegebenen Ergebnis-Zeilen
|
||||
// sichern (kein Planungs-Fallback), sonst wie gehabt.
|
||||
if (! $draft || $outputs !== []) {
|
||||
$this->persistOutputs($production, $product, $producedQty, $outputs);
|
||||
$this->persistOutputs($production, $product, $producedQty, $outputs, $draft);
|
||||
}
|
||||
|
||||
// AP-43: Entwürfe buchen keinen Bestand (Roh-/Produkt-/Verpackung).
|
||||
|
|
@ -130,8 +126,8 @@ class ProductionService
|
|||
$this->assertNotASet($product);
|
||||
|
||||
if ($draft) {
|
||||
// AP-43: Entwurf — keine Rezeptur-/Mengen-/Bestandsprüfung.
|
||||
$ingredientLines = $this->filterCompleteLines($ingredientLines);
|
||||
// AP-43/F-03: Entwurf — keine Rezeptur-/Mengen-/Bestandsprüfung, Zeilen roh sichern.
|
||||
$ingredientLines = $this->draftLines($ingredientLines);
|
||||
} elseif ($product->no_recipe_required) {
|
||||
$ingredientLines = [];
|
||||
} else {
|
||||
|
|
@ -160,16 +156,12 @@ class ProductionService
|
|||
|
||||
$production->productionIngredients()->delete();
|
||||
foreach ($ingredientLines as $line) {
|
||||
$production->productionIngredients()->create([
|
||||
'ingredient_id' => (int) $line['ingredient_id'],
|
||||
'stock_entry_id' => (int) $line['stock_entry_id'],
|
||||
'quantity_used' => $this->parseQuantity($line['quantity_used'] ?? null),
|
||||
]);
|
||||
$production->productionIngredients()->create($this->ingredientLineAttributes($line));
|
||||
}
|
||||
|
||||
$production->outputs()->delete();
|
||||
if (! $draft || $outputs !== []) {
|
||||
$this->persistOutputs($production, $product, $producedQty, $outputs);
|
||||
$this->persistOutputs($production, $product, $producedQty, $outputs, $draft);
|
||||
}
|
||||
|
||||
// AP-43: Entwürfe buchen keinen Bestand. Ein zuvor gelöschter Verpackungs-
|
||||
|
|
@ -194,7 +186,7 @@ class ProductionService
|
|||
*
|
||||
* @param array<int, array{product_id: int, quantity: int, batch_number?: ?string}> $outputs
|
||||
*/
|
||||
private function persistOutputs(Production $production, Product $plannedProduct, int $plannedQty, array $outputs): void
|
||||
private function persistOutputs(Production $production, Product $plannedProduct, int $plannedQty, array $outputs, bool $draft = false): void
|
||||
{
|
||||
if ($outputs === []) {
|
||||
$producedAt = $production->produced_at instanceof Carbon
|
||||
|
|
@ -223,9 +215,11 @@ class ProductionService
|
|||
if ($outputProduct->is_set) {
|
||||
throw ValidationException::withMessages(['outputs' => __('Sets können nicht produziert werden. Bitte ein Einzelprodukt im Produktionsergebnis wählen.')]);
|
||||
}
|
||||
if ($quantity < 1) {
|
||||
// F-03: Im Entwurf darf die Stückzahl noch fehlen (0); beim Abschluss ist sie Pflicht.
|
||||
if ($quantity < 1 && ! $draft) {
|
||||
throw ValidationException::withMessages(['outputs' => __('Die produzierte Stückzahl muss je Ergebniszeile mindestens 1 sein.')]);
|
||||
}
|
||||
$quantity = max(0, $quantity);
|
||||
|
||||
$batchNumber = trim((string) ($output['batch_number'] ?? ''));
|
||||
|
||||
|
|
@ -741,18 +735,35 @@ class ProductionService
|
|||
}
|
||||
|
||||
/**
|
||||
* AP-43: Für den Entwurf nur vollständige Chargen-Zeilen sichern — eine Zeile
|
||||
* ist vollständig, wenn ein konkreter Wareneingang (stock_entry_id) gewählt ist
|
||||
* und eine Menge > 0 eingetragen wurde. Unfertige Zeilen werden verworfen.
|
||||
* AP-43/F-03: Entwurfszeilen verlustfrei sichern — jede Zeile mit Rohstoff und
|
||||
* Charge (batch_key) bzw. Wareneingang bleibt erhalten, auch ohne Menge.
|
||||
* Die Auflösung auf Wareneingänge und die fachliche Prüfung folgen beim Abschluss.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $ingredientLines
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function filterCompleteLines(array $ingredientLines): array
|
||||
private function draftLines(array $ingredientLines): array
|
||||
{
|
||||
return array_values(array_filter($ingredientLines, function (array $line): bool {
|
||||
return (int) ($line['stock_entry_id'] ?? 0) > 0
|
||||
&& $this->parseQuantity($line['quantity_used'] ?? null) > 0;
|
||||
return (int) ($line['ingredient_id'] ?? 0) > 0
|
||||
&& ((int) ($line['stock_entry_id'] ?? 0) > 0 || trim((string) ($line['batch_key'] ?? '')) !== '');
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $line
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function ingredientLineAttributes(array $line): array
|
||||
{
|
||||
$stockEntryId = (int) ($line['stock_entry_id'] ?? 0);
|
||||
$batchKey = trim((string) ($line['batch_key'] ?? ''));
|
||||
|
||||
return [
|
||||
'ingredient_id' => (int) $line['ingredient_id'],
|
||||
'stock_entry_id' => $stockEntryId > 0 ? $stockEntryId : null,
|
||||
'batch_key' => $stockEntryId > 0 ? null : ($batchKey !== '' ? $batchKey : null),
|
||||
'quantity_used' => $this->parseQuantity($line['quantity_used'] ?? null),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue