$data */ public function create(array $data): StockEntry { $data['unit'] = ($data['entry_type'] ?? '') === 'ingredient' ? 'gram' : 'piece'; $data = $this->resolvePrices($data); return StockEntry::query()->create($data); } /** * @param array $data */ public function update(StockEntry $stockEntry, array $data): StockEntry { if (array_key_exists('entry_type', $data)) { $data['unit'] = ($data['entry_type'] ?? '') === 'ingredient' ? 'gram' : 'piece'; } $data = $this->resolvePrices($data); $stockEntry->update($data); return $stockEntry->fresh(); } /** * Ergänzt Netto-/Brutto-Preis pro kg und den UST-Snapshot. * Es genügt, Netto oder Brutto anzugeben; der jeweils fehlende Wert wird * aus dem gewählten Steuersatz berechnet. * * @param array $data * @return array */ protected function resolvePrices(array $data): array { if (($data['entry_type'] ?? '') !== 'ingredient') { $data['price_per_kg'] = null; $data['price_per_kg_gross'] = null; $data['tax_rate_id'] = null; $data['tax_rate_percent'] = null; return $data; } $percent = null; if (! empty($data['tax_rate_id'])) { $percent = TaxRate::query()->whereKey($data['tax_rate_id'])->value('percent'); } $data['tax_rate_percent'] = $percent; $factor = 1 + ((float) ($percent ?? 0)) / 100; $net = isset($data['price_per_kg']) && $data['price_per_kg'] !== null && $data['price_per_kg'] !== '' ? (float) $data['price_per_kg'] : null; $gross = isset($data['price_per_kg_gross']) && $data['price_per_kg_gross'] !== null && $data['price_per_kg_gross'] !== '' ? (float) $data['price_per_kg_gross'] : null; if ($net !== null && $gross === null) { $gross = round($net * $factor, 4); } elseif ($gross !== null && $net === null) { $net = $factor > 0 ? round($gross / $factor, 4) : $gross; } elseif ($net !== null && $gross !== null) { $gross = round($net * $factor, 4); } $data['price_per_kg'] = $net; $data['price_per_kg_gross'] = $gross; $data['price_total'] = null; return $data; } /** * @param array $data */ public function receive(StockEntry $stockEntry, array $data): StockEntry { $data['status'] = 'received'; $data['received_by'] = auth()->id(); $stockEntry->update($data); return $stockEntry->fresh(); } /** * @return Collection */ public function getByStatus(string $status): Collection { return StockEntry::query() ->where('status', $status) ->orderByDesc('ordered_at') ->get(); } /** * @return Collection */ public function getForIngredient(int $ingredientId): Collection { return StockEntry::query() ->where('ingredient_id', $ingredientId) ->where('status', 'received') ->orderByDesc('received_at') ->get(); } /** * Letzter Netto-kg-Preis je Lieferant für einen Rohstoff (neuester Einkauf zählt). * * @return array [supplier_id => price_per_kg_net] */ public function lastNetPricePerSupplier(int $ingredientId): array { $ranked = StockEntry::query() ->selectRaw('supplier_id, price_per_kg, ROW_NUMBER() OVER (PARTITION BY supplier_id ORDER BY ordered_at DESC, id DESC) as rn') ->where('entry_type', 'ingredient') // Audit N-2: nur tatsächliche Eingänge — offene, evtl. nie gelieferte // Bestellungen bestimmen den letzten Einkaufspreis nicht mehr. ->where('status', 'received') ->where('ingredient_id', $ingredientId) ->whereNotNull('supplier_id') ->whereNotNull('price_per_kg'); // DB::query statt StockEntry::query: der SoftDeletes-Global-Scope würde // sonst "stock_entries.deleted_at" auf dem Sub-Select-Alias referenzieren. return \DB::query() ->fromSub($ranked, 'ranked') ->where('rn', 1) ->pluck('price_per_kg', 'supplier_id') ->map(fn ($price) => (float) $price) ->all(); } /** * Liste: Pending zuerst (neuestes Bestelldatum), dann Received (neuester Eingang). * Eingegangene Einträge sind auf $receivedLimit gedeckelt (OPT-3); null = alle. * * @return Collection */ public function listForIndex(?int $receivedLimit = 500): Collection { $with = [ 'ingredient', 'packagingItem', 'supplier', 'location', 'quality', ]; $pending = StockEntry::query()->with($with) ->where('status', 'pending') ->orderByDesc('ordered_at') ->orderByDesc('id') ->get(); $received = StockEntry::query()->with($with) ->where('status', 'received') ->orderByDesc('received_at') ->orderByDesc('id') ->when($receivedLimit !== null, fn ($q) => $q->limit($receivedLimit)) ->get(); return $pending->concat($received)->values(); } }