first(); if (!$sparePart) { return new ReservationResult( reserved: 0, missing: $quantity, reservations: collect(), shortage: null ); } return $this->reservationService->reserve( $sparePart->id, $quantity, $withDocuments, $reclamationId ); } /** * @deprecated Используйте SparePartReservationService::reserve() */ public function deductForReclamation( string $article, int $quantity, bool $withDocuments, int $reclamationId ): bool { $result = $this->reserveForReclamation($article, $quantity, $withDocuments, $reclamationId); return $result->reserved > 0; } /** * Получить список запчастей с критическим дефицитом (открытые дефициты) */ public function getCriticalShortages(): Collection { return $this->shortageService->getCriticalShortages(); } /** * Получить список запчастей ниже минимального остатка */ public function getBelowMinStock(): Collection { return SparePart::all()->filter(function ($sparePart) { return $sparePart->isBelowMinStock(); }); } /** * Рассчитать сколько нужно заказать для покрытия дефицита */ public function calculateOrderQuantity(SparePart $sparePart, bool $withDocuments): int { return $this->shortageService->calculateOrderQuantity($sparePart->id, $withDocuments); } /** * Рассчитать сколько нужно заказать для достижения минимального остатка */ public function calculateMinStockOrderQuantity(SparePart $sparePart): int { $freeStock = $sparePart->total_free_stock; $minStock = $sparePart->min_stock; if ($freeStock >= $minStock) { return 0; } return $minStock - $freeStock; } /** * Получить полную информацию по остаткам запчасти */ public function getStockInfo(SparePart $sparePart): array { return [ 'spare_part_id' => $sparePart->id, 'article' => $sparePart->article, // Физические остатки 'physical_without_docs' => $sparePart->physical_stock_without_docs, 'physical_with_docs' => $sparePart->physical_stock_with_docs, 'physical_total' => $sparePart->total_physical_stock, // Зарезервировано 'reserved_without_docs' => $sparePart->reserved_without_docs, 'reserved_with_docs' => $sparePart->reserved_with_docs, 'reserved_total' => $sparePart->total_reserved, // Свободно 'free_without_docs' => $sparePart->free_stock_without_docs, 'free_with_docs' => $sparePart->free_stock_with_docs, 'free_total' => $sparePart->total_free_stock, // Дефициты 'has_shortages' => $sparePart->hasOpenShortages(), 'shortage_qty' => $sparePart->open_shortages_qty, // Минимальный остаток 'min_stock' => $sparePart->min_stock, 'below_min_stock' => $sparePart->isBelowMinStock(), // Рекомендации 'recommended_order_without_docs' => $this->calculateOrderQuantity($sparePart, false), 'recommended_order_with_docs' => $this->calculateOrderQuantity($sparePart, true), 'recommended_for_min_stock' => $this->calculateMinStockOrderQuantity($sparePart), ]; } /** * Получить сводку по всем запчастям */ public function getInventorySummary(): array { $spareParts = SparePart::all(); $criticalCount = 0; $belowMinCount = 0; $totalPhysical = 0; $totalReserved = 0; foreach ($spareParts as $sparePart) { if ($sparePart->hasOpenShortages()) { $criticalCount++; } if ($sparePart->isBelowMinStock()) { $belowMinCount++; } $totalPhysical += $sparePart->total_physical_stock; $totalReserved += $sparePart->total_reserved; } return [ 'total_spare_parts' => $spareParts->count(), 'critical_shortages_count' => $criticalCount, 'below_min_stock_count' => $belowMinCount, 'total_physical_stock' => $totalPhysical, 'total_reserved' => $totalReserved, 'total_free' => $totalPhysical - $totalReserved, ]; } /** * Получить резервы для рекламации */ public function getReservationsForReclamation(int $reclamationId): Collection { return $this->reservationService->getReservationsForReclamation($reclamationId); } /** * Получить дефициты для рекламации */ public function getShortagesForReclamation(int $reclamationId): Collection { return Shortage::query() ->where('reclamation_id', $reclamationId) ->with('sparePart') ->get(); } }