Warenwirtschaft: N-02 — Rezeptur-Phasen beim Artikelspeichern aktualisieren statt neu anlegen
Phasen werden über Name/Reihenfolge den bestehenden zugeordnet und aktualisiert; nur entfernte Phasen werden gelöscht. Produktions-Verbrauchszeilen behalten so ihre recipe_phase_id. Derselbe Rohstoff je Phase nur einmal (Validierung). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
dd93a99265
commit
520d4e2deb
1 changed files with 61 additions and 11 deletions
|
|
@ -277,12 +277,13 @@ class ProductRepository extends BaseRepository
|
|||
ProductIngredient::where('product_id', $this->model->id)
|
||||
->where('recipe_type', 'manufacturer')
|
||||
->delete();
|
||||
RecipePhase::where('product_id', $this->model->id)
|
||||
->where('recipe_type', 'manufacturer')
|
||||
->delete();
|
||||
|
||||
// Audit H-5: Phasen auch ohne Rohstoffzeilen persistieren — vorher
|
||||
// gingen Namen/Notizen verloren, wenn (noch) keine Zeile existierte.
|
||||
// N-02 (Nachprüfung 08.09.2026): bestehende Phasen aktualisieren statt
|
||||
// löschen/neu anlegen — Produktions-Verbrauchszeilen referenzieren die Phase
|
||||
// (production_ingredients.recipe_phase_id, nullOnDelete) und verlören sonst
|
||||
// bei jedem Artikelspeichern ihre Zuordnung.
|
||||
$phaseIds = $this->persistRecipePhases($data);
|
||||
|
||||
if (! isset($data['mfg_ingredient_id']) || ! is_array($data['mfg_ingredient_id'])) {
|
||||
|
|
@ -305,11 +306,22 @@ class ProductRepository extends BaseRepository
|
|||
]);
|
||||
}
|
||||
|
||||
// N-02: Derselbe Rohstoff darf je Phase nur einmal vorkommen (der Picker verhindert
|
||||
// das; Datenbestand/Direktaufrufe müssen es ebenfalls einhalten, sonst ist die
|
||||
// Zeilenidentität „Rohstoff + Phase“ im Produktionsformular nicht eindeutig).
|
||||
$seen = [];
|
||||
foreach ($ids as $index => $ingredientId) {
|
||||
$ingredientId = (int) $ingredientId;
|
||||
if ($ingredientId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$phaseKey = $ingredientId.'|'.(int) ($rowPhases[$index] ?? 0);
|
||||
if (isset($seen[$phaseKey])) {
|
||||
throw ValidationException::withMessages([
|
||||
'mfg_ingredient_id' => __('Rohstoff-ID :id kommt in derselben Phase mehrfach vor — bitte die Mengen in einer Zeile zusammenfassen.', ['id' => $ingredientId]),
|
||||
]);
|
||||
}
|
||||
$seen[$phaseKey] = true;
|
||||
ProductIngredient::create([
|
||||
'product_id' => $this->model->id,
|
||||
'ingredient_id' => $ingredientId,
|
||||
|
|
@ -341,24 +353,62 @@ class ProductRepository extends BaseRepository
|
|||
&& trim((string) ($names[0] ?? '')) === ''
|
||||
&& trim((string) ($notes[0] ?? '')) === '';
|
||||
|
||||
$existing = RecipePhase::query()
|
||||
->where('product_id', $this->model->id)
|
||||
->where('recipe_type', 'manufacturer')
|
||||
->orderBy('pos')->orderBy('id')
|
||||
->get();
|
||||
|
||||
if ($implicitSingle) {
|
||||
$existing->each->delete();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$phaseIds = [];
|
||||
// N-02: Zuordnung neue → bestehende Phase zuerst über den (effektiven) Namen,
|
||||
// danach in Reihenfolge; nur wirklich entfernte Phasen werden gelöscht.
|
||||
$blocks = [];
|
||||
foreach ($names as $index => $name) {
|
||||
$name = trim((string) $name);
|
||||
$note = trim((string) ($notes[$index] ?? ''));
|
||||
|
||||
$phase = RecipePhase::create([
|
||||
'product_id' => $this->model->id,
|
||||
'recipe_type' => 'manufacturer',
|
||||
$blocks[$index] = [
|
||||
// Audit H-4: Spaltenlänge varchar(100) absichern (strict mode).
|
||||
'name' => $name !== '' ? mb_substr($name, 0, 100) : $this->autoPhaseName($index),
|
||||
'note' => $note !== '' ? $note : null,
|
||||
'pos' => $index,
|
||||
]);
|
||||
$phaseIds[$index] = $phase->id;
|
||||
];
|
||||
}
|
||||
|
||||
$unmatched = $existing->keyBy('id');
|
||||
$assigned = [];
|
||||
foreach ($blocks as $index => $block) {
|
||||
$match = $unmatched->first(fn (RecipePhase $p) => $p->name === $block['name']);
|
||||
if ($match !== null) {
|
||||
$assigned[$index] = $match;
|
||||
$unmatched->forget($match->id);
|
||||
}
|
||||
}
|
||||
foreach ($blocks as $index => $block) {
|
||||
if (isset($assigned[$index])) {
|
||||
continue;
|
||||
}
|
||||
$next = $unmatched->first();
|
||||
if ($next !== null) {
|
||||
$assigned[$index] = $next;
|
||||
$unmatched->forget($next->id);
|
||||
}
|
||||
}
|
||||
$unmatched->each->delete();
|
||||
|
||||
$phaseIds = [];
|
||||
foreach ($blocks as $index => $block) {
|
||||
$attributes = $block + ['pos' => $index];
|
||||
if (isset($assigned[$index])) {
|
||||
$assigned[$index]->fill($attributes)->save();
|
||||
$phaseIds[$index] = (int) $assigned[$index]->id;
|
||||
} else {
|
||||
$phase = RecipePhase::create($attributes + ['product_id' => $this->model->id, 'recipe_type' => 'manufacturer']);
|
||||
$phaseIds[$index] = (int) $phase->id;
|
||||
}
|
||||
}
|
||||
|
||||
return $phaseIds;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue