SparePartInventoryService.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Reservation;
  4. use App\Models\Shortage;
  5. use App\Models\SparePart;
  6. use App\Models\SparePartOrder;
  7. use Illuminate\Support\Collection;
  8. /**
  9. * Сервис контроля наличия запчастей.
  10. *
  11. * Предоставляет методы для:
  12. * - Получения критических дефицитов
  13. * - Получения запчастей ниже минимального остатка
  14. * - Расчёта рекомендованного количества для заказа
  15. *
  16. * ВАЖНО: Для резервирования и списания используйте
  17. * SparePartReservationService и SparePartIssueService.
  18. */
  19. class SparePartInventoryService
  20. {
  21. public function __construct(
  22. private readonly SparePartReservationService $reservationService,
  23. private readonly ShortageService $shortageService
  24. ) {}
  25. /**
  26. * Зарезервировать запчасть для рекламации
  27. *
  28. * @deprecated Используйте SparePartReservationService::reserve()
  29. */
  30. public function reserveForReclamation(
  31. string $article,
  32. int $quantity,
  33. bool $withDocuments,
  34. int $reclamationId
  35. ): ReservationResult {
  36. $sparePart = SparePart::where('article', $article)->first();
  37. if (!$sparePart) {
  38. return new ReservationResult(
  39. reserved: 0,
  40. missing: $quantity,
  41. reservations: collect(),
  42. shortage: null
  43. );
  44. }
  45. return $this->reservationService->reserve(
  46. $sparePart->id,
  47. $quantity,
  48. $withDocuments,
  49. $reclamationId
  50. );
  51. }
  52. /**
  53. * @deprecated Используйте SparePartReservationService::reserve()
  54. */
  55. public function deductForReclamation(
  56. string $article,
  57. int $quantity,
  58. bool $withDocuments,
  59. int $reclamationId
  60. ): bool {
  61. $result = $this->reserveForReclamation($article, $quantity, $withDocuments, $reclamationId);
  62. return $result->reserved > 0;
  63. }
  64. /**
  65. * Получить список запчастей с критическим дефицитом (открытые дефициты)
  66. */
  67. public function getCriticalShortages(): Collection
  68. {
  69. return $this->shortageService->getCriticalShortages();
  70. }
  71. /**
  72. * Получить список запчастей ниже минимального остатка
  73. */
  74. public function getBelowMinStock(): Collection
  75. {
  76. return SparePart::all()->filter(function ($sparePart) {
  77. return $sparePart->isBelowMinStock();
  78. });
  79. }
  80. /**
  81. * Рассчитать сколько нужно заказать для покрытия дефицита
  82. */
  83. public function calculateOrderQuantity(SparePart $sparePart, bool $withDocuments): int
  84. {
  85. return $this->shortageService->calculateOrderQuantity($sparePart->id, $withDocuments);
  86. }
  87. /**
  88. * Рассчитать сколько нужно заказать для достижения минимального остатка
  89. */
  90. public function calculateMinStockOrderQuantity(SparePart $sparePart): int
  91. {
  92. $freeStock = $sparePart->total_free_stock;
  93. $minStock = $sparePart->min_stock;
  94. if ($freeStock >= $minStock) {
  95. return 0;
  96. }
  97. return $minStock - $freeStock;
  98. }
  99. /**
  100. * Получить полную информацию по остаткам запчасти
  101. */
  102. public function getStockInfo(SparePart $sparePart): array
  103. {
  104. return [
  105. 'spare_part_id' => $sparePart->id,
  106. 'article' => $sparePart->article,
  107. // Физические остатки
  108. 'physical_without_docs' => $sparePart->physical_stock_without_docs,
  109. 'physical_with_docs' => $sparePart->physical_stock_with_docs,
  110. 'physical_total' => $sparePart->total_physical_stock,
  111. // Зарезервировано
  112. 'reserved_without_docs' => $sparePart->reserved_without_docs,
  113. 'reserved_with_docs' => $sparePart->reserved_with_docs,
  114. 'reserved_total' => $sparePart->total_reserved,
  115. // Свободно
  116. 'free_without_docs' => $sparePart->free_stock_without_docs,
  117. 'free_with_docs' => $sparePart->free_stock_with_docs,
  118. 'free_total' => $sparePart->total_free_stock,
  119. // Дефициты
  120. 'has_shortages' => $sparePart->hasOpenShortages(),
  121. 'shortage_qty' => $sparePart->open_shortages_qty,
  122. // Минимальный остаток
  123. 'min_stock' => $sparePart->min_stock,
  124. 'below_min_stock' => $sparePart->isBelowMinStock(),
  125. // Рекомендации
  126. 'recommended_order_without_docs' => $this->calculateOrderQuantity($sparePart, false),
  127. 'recommended_order_with_docs' => $this->calculateOrderQuantity($sparePart, true),
  128. 'recommended_for_min_stock' => $this->calculateMinStockOrderQuantity($sparePart),
  129. ];
  130. }
  131. /**
  132. * Получить сводку по всем запчастям
  133. */
  134. public function getInventorySummary(): array
  135. {
  136. $spareParts = SparePart::all();
  137. $criticalCount = 0;
  138. $belowMinCount = 0;
  139. $totalPhysical = 0;
  140. $totalReserved = 0;
  141. foreach ($spareParts as $sparePart) {
  142. if ($sparePart->hasOpenShortages()) {
  143. $criticalCount++;
  144. }
  145. if ($sparePart->isBelowMinStock()) {
  146. $belowMinCount++;
  147. }
  148. $totalPhysical += $sparePart->total_physical_stock;
  149. $totalReserved += $sparePart->total_reserved;
  150. }
  151. return [
  152. 'total_spare_parts' => $spareParts->count(),
  153. 'critical_shortages_count' => $criticalCount,
  154. 'below_min_stock_count' => $belowMinCount,
  155. 'total_physical_stock' => $totalPhysical,
  156. 'total_reserved' => $totalReserved,
  157. 'total_free' => $totalPhysical - $totalReserved,
  158. ];
  159. }
  160. /**
  161. * Получить резервы для рекламации
  162. */
  163. public function getReservationsForReclamation(int $reclamationId): Collection
  164. {
  165. return $this->reservationService->getReservationsForReclamation($reclamationId);
  166. }
  167. /**
  168. * Получить дефициты для рекламации
  169. */
  170. public function getShortagesForReclamation(int $reclamationId): Collection
  171. {
  172. return Shortage::query()
  173. ->where('reclamation_id', $reclamationId)
  174. ->with('sparePart')
  175. ->get();
  176. }
  177. }