ImportStockOrdersService.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Services\Import;
  4. use App\Models\CommonCatalogItem;
  5. use App\Models\Import;
  6. use App\Models\StockOrder;
  7. use App\Models\User;
  8. use App\Services\StockInventoryService;
  9. use Illuminate\Support\Facades\Storage;
  10. use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
  11. use RuntimeException;
  12. use Throwable;
  13. class ImportStockOrdersService
  14. {
  15. public const HEADERS = [
  16. 'Номер заказа',
  17. 'Артикул',
  18. 'Статус',
  19. 'Кол-во заказано',
  20. 'Примечание',
  21. ];
  22. public function __construct(
  23. private readonly Import $import,
  24. private readonly int $userId,
  25. private readonly StockInventoryService $inventoryService,
  26. ) {}
  27. public function handle(): bool
  28. {
  29. try {
  30. $actor = User::query()->findOrFail($this->userId);
  31. $spreadsheet = (new Xlsx)->load(Storage::disk('upload')->path((string) $this->import->filename));
  32. $sheet = $spreadsheet->getActiveSheet();
  33. $headers = $sheet->rangeToArray('A1:E1', null, true, true, false)[0];
  34. if (array_map($this->normalize(...), $headers) !== array_map($this->normalize(...), self::HEADERS)) {
  35. throw new RuntimeException('Некорректные заголовки файла складских заказов.');
  36. }
  37. $created = 0;
  38. $updated = 0;
  39. $errors = 0;
  40. for ($row = 2; $row <= $sheet->getHighestDataRow(); $row++) {
  41. $orderNumber = trim((string) $sheet->getCell("A{$row}")->getFormattedValue());
  42. $article = trim((string) $sheet->getCell("B{$row}")->getFormattedValue());
  43. if ($orderNumber === '' && $article === '') {
  44. continue;
  45. }
  46. try {
  47. if ($orderNumber === '') {
  48. throw new RuntimeException('Не указан номер заказа.');
  49. }
  50. if ($article === '') {
  51. throw new RuntimeException('Не указан артикул.');
  52. }
  53. $items = CommonCatalogItem::query()
  54. ->where('article', $article)
  55. ->limit(2)
  56. ->get();
  57. if ($items->isEmpty()) {
  58. throw new RuntimeException("Позиция с артикулом {$article} не найдена.");
  59. }
  60. if ($items->count() > 1) {
  61. throw new RuntimeException(
  62. "Артикул {$article} соответствует нескольким вариантам. Добавьте заказ вручную, выбрав наименование.",
  63. );
  64. }
  65. $item = $items->first();
  66. $status = $this->status((string) $sheet->getCell("C{$row}")->getFormattedValue());
  67. $quantity = filter_var(
  68. $sheet->getCell("D{$row}")->getCalculatedValue(),
  69. FILTER_VALIDATE_INT,
  70. ['options' => ['min_range' => 1]],
  71. );
  72. if ($quantity === false) {
  73. throw new RuntimeException('Количество должно быть целым числом больше нуля.');
  74. }
  75. $data = [
  76. 'common_catalog_item_id' => $item->id,
  77. 'order_number' => $orderNumber,
  78. 'status' => $status,
  79. 'ordered_quantity' => $quantity,
  80. 'note' => $this->nullable((string) $sheet->getCell("E{$row}")->getValue()),
  81. ];
  82. $order = StockOrder::withTrashed()
  83. ->where('order_number', $orderNumber)
  84. ->where('common_catalog_item_id', $item->id)
  85. ->first();
  86. if ($order) {
  87. if ($order->trashed()) {
  88. $order->restore();
  89. }
  90. $this->inventoryService->updateOrder($order, $data, $actor);
  91. $updated++;
  92. } else {
  93. $this->inventoryService->createOrder($data, $actor);
  94. $created++;
  95. }
  96. } catch (Throwable $exception) {
  97. $errors++;
  98. $this->import->log("Строка {$row}: {$exception->getMessage()}", 'WARNING');
  99. }
  100. }
  101. $spreadsheet->disconnectWorksheets();
  102. $this->import->log("Создано: {$created}; обновлено: {$updated}; ошибок: {$errors}.");
  103. $this->import->update(['status' => 'DONE']);
  104. return true;
  105. } catch (Throwable $exception) {
  106. $this->import->log($exception->getMessage(), 'ERROR');
  107. $this->import->update(['status' => 'ERROR']);
  108. throw $exception;
  109. }
  110. }
  111. private function status(string $value): string
  112. {
  113. return match (mb_strtolower(trim($value))) {
  114. 'заказан', 'заказано', StockOrder::STATUS_ORDERED => StockOrder::STATUS_ORDERED,
  115. 'на складе', StockOrder::STATUS_IN_STOCK => StockOrder::STATUS_IN_STOCK,
  116. 'отгружено', 'отгружен', StockOrder::STATUS_SHIPPED => StockOrder::STATUS_SHIPPED,
  117. default => throw new RuntimeException("Неизвестный статус: {$value}."),
  118. };
  119. }
  120. private function normalize(mixed $value): string
  121. {
  122. return mb_strtolower(trim((string) preg_replace('/\s+/u', ' ', (string) $value)));
  123. }
  124. private function nullable(string $value): ?string
  125. {
  126. $value = trim($value);
  127. return $value === '' ? null : $value;
  128. }
  129. }