Warenwirtschaft: F-05 — historischer Verpackungs-Snapshot bleibt bei Notizkorrektur erhalten

updateProduction() baut die Verpackungszeilen nur neu auf, wenn sich
Ergebnisprodukte/Stückzahlen ändern oder ein Entwurf abgeschlossen wird.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-08 10:55:51 +00:00
parent 104671e929
commit a803e956e1
2 changed files with 66 additions and 5 deletions

View file

@ -159,17 +159,31 @@ class ProductionService
$production->productionIngredients()->create($this->ingredientLineAttributes($line));
}
// F-05: Ergebnisdaten vor dem Neuaufbau festhalten — nur bei tatsächlicher
// Änderung von Produkt/Menge wird der historische Verpackungs-Snapshot ersetzt.
$hadSnapshot = ! $locked->isDraft();
$previousOutputs = $this->outputSignature($production);
$production->outputs()->delete();
if (! $draft || $outputs !== []) {
$this->persistOutputs($production, $product, $producedQty, $outputs, $draft);
}
// AP-43: Entwürfe buchen keinen Bestand. Ein zuvor gelöschter Verpackungs-
// Snapshot bleibt für den Entwurf leer.
$production->productionPackagings()->delete();
if (! $draft) {
if ($draft) {
// AP-43: Entwürfe buchen keinen Bestand. Ein zuvor gelöschter Verpackungs-
// Snapshot bleibt für den Entwurf leer.
$production->productionPackagings()->delete();
} else {
// AP-45: Verpackung folgt dem Produktionsergebnis, daher nach den Outputs.
$this->syncPackagingSnapshot($production);
// F-05: Der Snapshot ist historisch (Konzept „Warum ein Snapshot?“): Eine
// Notiz-/Datumskorrektur an einer alten Produktion darf ihn nicht aus der
// heutigen Stückliste neu berechnen. Regel: Snapshot bleibt, solange
// Ergebnisprodukte und Stückzahlen unverändert sind; bei einer echten
// Mengen-/Produktkorrektur wird er mit der aktuellen Stückliste neu gebildet.
if (! $hadSnapshot || $this->outputSignature($production) !== $previousOutputs) {
$production->productionPackagings()->delete();
$this->syncPackagingSnapshot($production);
}
$production->setRelation('product', $product);
$this->productStockService->recordProductionStock($production, $userId);
@ -328,6 +342,23 @@ class ProductionService
}
}
/**
* F-05: Vergleichbare Signatur des Produktionsergebnisses (Produkt + Stückzahl,
* reihenfolgeunabhängig).
*
* @return array<int, string>
*/
private function outputSignature(Production $production): array
{
$signature = $production->outputs()
->get(['product_id', 'quantity'])
->map(fn ($o) => (int) $o->product_id.':'.(int) $o->quantity)
->all();
sort($signature);
return $signature;
}
/**
* @return array<int, float>
*/

View file

@ -187,6 +187,36 @@ test('AP-45: verpackung wird je ergebnisprodukt aus dem produktionsergebnis gebu
->and($byItem[$pkB->id]->quantity_used)->toBe(100); // 2 × 50 (Ergebnis Other)
});
test('F-05: notizkorrektur behält den historischen verpackungs-snapshot, mengenkorrektur bildet ihn neu', function () {
$user = psUser();
$supplier = Supplier::factory()->create();
$location = Location::factory()->create();
$material = PackagingMaterial::factory()->create();
$pk = PackagingItem::factory()->create(['packaging_material_id' => $material->id, 'supplier_id' => $supplier->id, 'name' => 'PS-F05-Tiegel', 'category' => 'packaging']);
$product = Product::query()->create(['name' => uniqid('PS-F05-', true), 'title' => 'x', 'active' => true, 'no_recipe_required' => true]);
$product->packagings()->sync([$pk->id => ['quantity' => 1, 'pos' => 0]]);
$service = app(ProductionService::class);
$head = ['product_id' => $product->id, 'location_id' => $location->id, 'produced_at' => '2026-08-01', 'quantity' => 1];
$production = $service->store($head, [], $user->id, [['product_id' => $product->id, 'quantity' => 1, 'batch_number' => 'F05']]);
expect($production->productionPackagings->sum('quantity_used'))->toBe(1);
// Stückliste nachträglich auf 2 Tiegel je Stück ändern.
$product->packagings()->sync([$pk->id => ['quantity' => 2, 'pos' => 0]]);
// Nur die Notiz korrigieren: Snapshot bleibt bei 1.
$production = $service->updateProduction($production->fresh(), $head + ['notes' => 'Korrektur'], [], $user->id,
[['product_id' => $product->id, 'quantity' => 1, 'batch_number' => 'F05']]);
expect($production->productionPackagings->sum('quantity_used'))->toBe(1)
->and($production->notes)->toBe('Korrektur');
// Echte Mengenkorrektur (1 → 3 Stück): Snapshot wird mit der aktuellen Stückliste neu gebildet (2 × 3).
$production = $service->updateProduction($production->fresh(), $head + ['quantity' => 3], [], $user->id,
[['product_id' => $product->id, 'quantity' => 3, 'batch_number' => 'F05']]);
expect($production->productionPackagings->sum('quantity_used'))->toBe(6);
});
test('AP-47: produktions-auswahl zeigt nur produktionsrelevante produkte', function () {
$user = psUser();
$this->actingAs($user, 'user');