Warenwirtschaft: F-14 — „Enthalten in“ mit Anteil (%) und Verbrauch je Stück

Nur Hersteller-Rezeptur, alle Phasenzeilen je Produkt summiert, Skalierung
über Füllgewicht und Faktor wie in der Produktion; kein roher Pivot-Wert mehr.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-08 11:07:22 +00:00
parent 1e7ebfb77b
commit 8808ff538f
4 changed files with 92 additions and 9 deletions

View file

@ -5,9 +5,12 @@ namespace App\Http\Controllers\Admin\Inventory;
use App\Http\Controllers\Controller;
use App\Models\Ingredient;
use App\Models\Location;
use App\Models\Product;
use App\Models\ProductIngredient;
use App\Models\TaxRate;
use App\Repositories\StockEntryRepository;
use App\Services\InventoryService;
use App\Services\ProductionService;
use App\Services\ProductStockService;
use Illuminate\Http\Request;
use Illuminate\View\View;
@ -18,6 +21,7 @@ class RawMaterialStockController extends Controller
protected InventoryService $inventoryService,
protected ProductStockService $productStockService,
protected StockEntryRepository $stockEntryRepository,
protected ProductionService $productionService,
) {}
public function index(Request $request): View
@ -90,11 +94,12 @@ class RawMaterialStockController extends Controller
'products' => fn ($q) => $q->where('active', true)->where('production_relevant', true),
]);
// AP-37: Jedes Produkt hat i. d. R. zwei product_ingredients-Zeilen
// (recipe_type „manufacturer" + „product"), wodurch die belongsToMany-
// Relation dasselbe Produkt doppelt liefert. Für die „Enthalten in"-Anzeige
// (und den darauf basierenden Produktbestand) nach Produkt-ID deduplizieren.
$ingredient->setRelation('products', $ingredient->products->unique('id')->values());
// AP-37/Prüfung 08.09.2026 F-14: „Enthalten in" aus der Hersteller-Rezeptur —
// alle Zeilen (Phasen) je Produkt summieren; Anteil (%) und Verbrauch je Stück
// (Anteil × Füllgewicht-Skala × Faktor, wie in der Produktion) statt roher Pivot-Wert.
$recipeUsage = $this->recipeUsageByProduct($ingredient);
$ingredient->setRelation('products', $ingredient->products->unique('id')
->filter(fn (Product $p) => isset($recipeUsage[$p->id]))->values());
// AP-27: Chargen je batch_number zusammengeführt (jeder Einkauf bleibt eigener stock_entry).
$chargeGroups = $this->inventoryService->chargeGroupsForIngredient($ingredient->id);
@ -133,10 +138,45 @@ class RawMaterialStockController extends Controller
'expectedEmpty' => $this->inventoryService->expectedEmptyDate($remaining, $daily),
'status' => $this->inventoryService->stockStatus($minAlert, $remaining, $daily, $leadDays, $openTotal > 0),
'lastPriceBySupplier' => $lastPriceBySupplier,
'recipeUsage' => $recipeUsage,
'taxRates' => TaxRate::query()->active()->orderBy('pos')->orderBy('name')->get(),
]);
}
/**
* F-14: Anteil und Verbrauch je Stück aus der Hersteller-Rezeptur je Produkt
* (nur aktive, produktionsrelevante Produkte — AP-47).
*
* @return array<int, array{percent: float, grams_per_piece: ?float, consumption_per_piece: ?float}>
*/
private function recipeUsageByProduct(Ingredient $ingredient): array
{
$rows = ProductIngredient::query()
->with('product')
->where('ingredient_id', $ingredient->id)
->where('recipe_type', 'manufacturer')
->get()
->filter(fn (ProductIngredient $row) => $row->product !== null
&& (bool) $row->product->active && (bool) $row->product->production_relevant);
$usage = [];
foreach ($rows->groupBy('product_id') as $productId => $productRows) {
$product = $productRows->first()->product;
$percent = (float) $productRows->sum(fn (ProductIngredient $r) => (float) ($r->gram ?? 0));
$withFactor = (float) $productRows->sum(fn (ProductIngredient $r) => (float) ($r->gram ?? 0) * (float) ($r->factor ?? 1.1));
$scale = $this->productionService->recipeGramScale($product);
$hasMassBasis = $this->productionService->hasMassBasis($product);
$usage[(int) $productId] = [
'percent' => round($percent, 3),
'grams_per_piece' => $hasMassBasis ? round($percent * $scale, 2) : null,
'consumption_per_piece' => $hasMassBasis ? round($withFactor * $scale, 2) : null,
];
}
return $usage;
}
/**
* @return array<int, string> [days => label]
*/