ImportStockOrdersService.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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. $item = CommonCatalogItem::query()->where('article', $article)->firstOrFail();
  54. $status = $this->status((string) $sheet->getCell("C{$row}")->getFormattedValue());
  55. $quantity = filter_var(
  56. $sheet->getCell("D{$row}")->getCalculatedValue(),
  57. FILTER_VALIDATE_INT,
  58. ['options' => ['min_range' => 1]],
  59. );
  60. if ($quantity === false) {
  61. throw new RuntimeException('Количество должно быть целым числом больше нуля.');
  62. }
  63. $data = [
  64. 'common_catalog_item_id' => $item->id,
  65. 'order_number' => $orderNumber,
  66. 'status' => $status,
  67. 'ordered_quantity' => $quantity,
  68. 'note' => $this->nullable((string) $sheet->getCell("E{$row}")->getValue()),
  69. ];
  70. $order = StockOrder::withTrashed()
  71. ->where('order_number', $orderNumber)
  72. ->where('common_catalog_item_id', $item->id)
  73. ->first();
  74. if ($order) {
  75. if ($order->trashed()) {
  76. $order->restore();
  77. }
  78. $this->inventoryService->updateOrder($order, $data, $actor);
  79. $updated++;
  80. } else {
  81. $this->inventoryService->createOrder($data, $actor);
  82. $created++;
  83. }
  84. } catch (Throwable $exception) {
  85. $errors++;
  86. $this->import->log("Строка {$row}: {$exception->getMessage()}", 'WARNING');
  87. }
  88. }
  89. $spreadsheet->disconnectWorksheets();
  90. $this->import->log("Создано: {$created}; обновлено: {$updated}; ошибок: {$errors}.");
  91. $this->import->update(['status' => 'DONE']);
  92. return true;
  93. } catch (Throwable $exception) {
  94. $this->import->log($exception->getMessage(), 'ERROR');
  95. $this->import->update(['status' => 'ERROR']);
  96. throw $exception;
  97. }
  98. }
  99. private function status(string $value): string
  100. {
  101. return match (mb_strtolower(trim($value))) {
  102. 'заказан', 'заказано', StockOrder::STATUS_ORDERED => StockOrder::STATUS_ORDERED,
  103. 'на складе', StockOrder::STATUS_IN_STOCK => StockOrder::STATUS_IN_STOCK,
  104. 'отгружено', 'отгружен', StockOrder::STATUS_SHIPPED => StockOrder::STATUS_SHIPPED,
  105. default => throw new RuntimeException("Неизвестный статус: {$value}."),
  106. };
  107. }
  108. private function normalize(mixed $value): string
  109. {
  110. return mb_strtolower(trim((string) preg_replace('/\s+/u', ' ', (string) $value)));
  111. }
  112. private function nullable(string $value): ?string
  113. {
  114. $value = trim($value);
  115. return $value === '' ? null : $value;
  116. }
  117. }