ExportReclamationsService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. namespace App\Services;
  3. use App\Models\File;
  4. use App\Models\PricingCode;
  5. use App\Models\Product;
  6. use App\Models\Reclamation;
  7. use App\Models\SparePart;
  8. use App\Helpers\DateHelper;
  9. use Illuminate\Support\Facades\Storage;
  10. use PhpOffice\PhpSpreadsheet\Cell\DataType;
  11. use PhpOffice\PhpSpreadsheet\IOFactory;
  12. use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  13. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  14. use PhpOffice\PhpSpreadsheet\Style\Border;
  15. use PhpOffice\PhpSpreadsheet\Style\Color;
  16. use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  17. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  18. class ExportReclamationsService
  19. {
  20. private const TEMPLATE = './templates/ReclamationsExport.xlsx';
  21. private const BASE_PRICING_CODE = '14.20-38-1';
  22. public function handle(array $reclamationIds, int $userId): string
  23. {
  24. $reader = IOFactory::createReader('Xlsx');
  25. $spreadsheet = $reader->load(self::TEMPLATE);
  26. $sheet = $spreadsheet->getActiveSheet();
  27. $templateLastRow = $sheet->getHighestRow();
  28. $templateStyle = $sheet->getStyle('A2:X2');
  29. $reclamations = Reclamation::query()
  30. ->whereIn('id', $reclamationIds)
  31. ->with([
  32. 'order.district',
  33. 'order.area',
  34. 'skus.product' => fn($q) => $q->withoutGlobalScopes(),
  35. 'spareParts.pricingCodes',
  36. ])
  37. ->get()
  38. ->keyBy('id');
  39. $row = 2;
  40. foreach ($reclamationIds as $reclamationId) {
  41. $reclamation = $reclamations->get($reclamationId);
  42. if (!$reclamation) {
  43. continue;
  44. }
  45. $base = $this->buildBaseRow($reclamation);
  46. $groupRows = $this->buildGroupRows($reclamation, $base);
  47. foreach ($groupRows as $group) {
  48. $count = max(count($group), 1);
  49. for ($i = 0; $i < $count; $i++) {
  50. $data = $group[$i] ?? $base;
  51. $sheet->duplicateStyle($templateStyle, 'A' . $row . ':X' . $row);
  52. $this->writeRow($sheet, $row, $data);
  53. $row++;
  54. }
  55. }
  56. }
  57. if ($row <= $templateLastRow) {
  58. $sheet->removeRow($row, $templateLastRow - $row + 1);
  59. }
  60. $this->applyBorders($sheet, 1, $row - 1);
  61. $this->applyDefaultStyles($sheet, 2, $row - 1);
  62. $fileName = fileName('Рекламации ' . date('Y-m-d H-i-s') . '.xlsx');
  63. $fd = 'export/reclamations';
  64. Storage::disk('public')->makeDirectory($fd);
  65. $fp = storage_path('app/public/' . $fd . '/') . $fileName;
  66. Storage::disk('public')->delete($fd . '/' . $fileName);
  67. (new Xlsx($spreadsheet))->save($fp);
  68. $link = url('/storage/' . $fd . '/' . $fileName);
  69. File::query()->create([
  70. 'link' => $link,
  71. 'path' => $fp,
  72. 'user_id' => $userId,
  73. 'original_name' => $fileName,
  74. 'mime_type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  75. ]);
  76. return $link;
  77. }
  78. private function buildBaseRow(Reclamation $reclamation): array
  79. {
  80. $order = $reclamation->order;
  81. return [
  82. 'A' => null, // № папки
  83. 'B' => $order?->district?->shortname ?? $order?->district_name ?? null, // Округ
  84. 'C' => $order?->area?->name ?? $order?->area_name ?? null, // Район
  85. 'D' => $order?->object_address ?? null, // Адрес
  86. 'E' => null, // Артикул
  87. 'F' => null, // Тип
  88. 'G' => null, // Наименование МАФ по паспорту
  89. 'H' => $reclamation->whats_done ?? null, // Что сделали
  90. 'I' => $reclamation->create_date ?? null, // Дата заявки
  91. 'J' => $order?->year ?? null, // Год поставки МАФ
  92. 'K' => $reclamation->reason ?? null, // Тип обращения
  93. 'L' => null, // Применяемая деталь
  94. 'M' => null, // Кол-во
  95. 'N' => null, // Цена по КС
  96. 'O' => null, // Лимит по МАФ
  97. 'P' => null, // Цена детали
  98. 'Q' => null, // Подтверждение цены
  99. 'R' => null, // Шифр 1
  100. 'S' => null, // Наименование 1
  101. 'T' => null, // Шифр 2
  102. 'U' => null, // Наименование 2
  103. 'V' => null, // Шифр 3
  104. 'W' => null, // Наименование 3
  105. 'X' => $reclamation->comment ?? null, // Комментарий
  106. ];
  107. }
  108. private function buildGroupRows(Reclamation $reclamation, array $base): array
  109. {
  110. $skus = $reclamation->skus;
  111. $spareParts = $reclamation->spareParts;
  112. $details = $reclamation->details;
  113. $groups = [];
  114. if ($skus->isEmpty()) {
  115. $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
  116. $codes = [
  117. [self::BASE_PRICING_CODE, $baseDescription],
  118. ];
  119. $groups[] = [$this->applyPricingCodes($this->buildRowForMaf($base, null), $codes)];
  120. return $groups;
  121. }
  122. foreach ($skus as $idx => $sku) {
  123. $mafRow = $this->buildRowForMaf($base, $sku);
  124. if ($idx > 0) {
  125. $this->blankColumns($mafRow, ['A', 'B', 'C', 'D']);
  126. }
  127. $detailRows = $this->buildDetailRows($mafRow, $spareParts, $details);
  128. $groups[] = $detailRows;
  129. }
  130. return $groups;
  131. }
  132. private function buildRowForMaf(array $base, $sku): array
  133. {
  134. if (!$sku || !$sku->product) {
  135. return $base;
  136. }
  137. $product = $sku->product;
  138. $year = $sku->year ?? $product->year;
  139. $passportName = null;
  140. if ($product->article && $year) {
  141. $productForYear = Product::withoutGlobalScopes()
  142. ->where('year', $year)
  143. ->where('article', $product->article)
  144. ->first();
  145. $passportName = $productForYear?->passport_name;
  146. }
  147. $row = $base;
  148. $row['E'] = $product->article;
  149. $row['F'] = $product->article;
  150. $row['G'] = $passportName;
  151. return $row;
  152. }
  153. private function buildDetailRows(array $mafRow, $spareParts, $details): array
  154. {
  155. $rows = [];
  156. $items = [];
  157. foreach ($spareParts as $sp) {
  158. $items[] = ['type' => 'spare', 'data' => $sp];
  159. }
  160. foreach ($details as $detail) {
  161. $items[] = ['type' => 'detail', 'data' => $detail];
  162. }
  163. if (empty($items)) {
  164. $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
  165. $codes = [
  166. [self::BASE_PRICING_CODE, $baseDescription],
  167. ];
  168. $rows[] = $this->applyPricingCodes($mafRow, $codes);
  169. return $rows;
  170. }
  171. foreach ($items as $index => $item) {
  172. $row = $mafRow;
  173. if ($index > 0) {
  174. $this->blankColumns($row, ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'X']);
  175. }
  176. if ($item['type'] === 'spare') {
  177. $rows[] = $this->buildRowsForSparePart($row, $item['data']);
  178. } else {
  179. $rows[] = $this->buildRowForDetail($row, $item['data']);
  180. }
  181. }
  182. return $rows;
  183. }
  184. private function buildRowsForSparePart(array $mafRow, SparePart $sp): array
  185. {
  186. $row = $mafRow;
  187. $row['L'] = $sp->article;
  188. $row['M'] = $sp->pivot?->quantity ?? null;
  189. $row['P'] = $sp->expertise_price;
  190. $row['Q'] = $sp->tsn_number;
  191. $codes = [];
  192. $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
  193. $codes[] = [self::BASE_PRICING_CODE, $baseDescription];
  194. foreach ($sp->pricingCodes as $pc) {
  195. if ($pc->type !== PricingCode::TYPE_PRICING_CODE) {
  196. continue;
  197. }
  198. if ($pc->code === self::BASE_PRICING_CODE) {
  199. continue;
  200. }
  201. $codes[] = [$pc->code, $pc->description];
  202. }
  203. return $this->applyPricingCodes($row, $codes);
  204. }
  205. private function buildRowForDetail(array $mafRow, $detail): array
  206. {
  207. $row = $mafRow;
  208. $row['L'] = $detail->name;
  209. $row['M'] = $detail->quantity;
  210. $row['P'] = null;
  211. $row['Q'] = null;
  212. $baseDescription = PricingCode::getPricingCodeDescription(self::BASE_PRICING_CODE);
  213. $codes = [
  214. [self::BASE_PRICING_CODE, $baseDescription],
  215. ];
  216. return $this->applyPricingCodes($row, $codes);
  217. }
  218. private function applyPricingCodes(array $row, array $codes): array
  219. {
  220. $slots = [
  221. ['R', 'S'],
  222. ['T', 'U'],
  223. ['V', 'W'],
  224. ];
  225. for ($i = 0; $i < count($slots); $i++) {
  226. $row[$slots[$i][0]] = null;
  227. $row[$slots[$i][1]] = null;
  228. }
  229. $idx = 0;
  230. foreach ($codes as $code) {
  231. if ($idx >= count($slots)) {
  232. break;
  233. }
  234. $row[$slots[$idx][0]] = $code[0] ?? null;
  235. $row[$slots[$idx][1]] = $code[1] ?? null;
  236. $idx++;
  237. }
  238. return $row;
  239. }
  240. private function blankColumns(array &$row, array $cols): void
  241. {
  242. foreach ($cols as $col) {
  243. $row[$col] = null;
  244. }
  245. }
  246. private function writeRow(Worksheet $sheet, int $row, array $data): void
  247. {
  248. $rowRange = 'A' . $row . ':X' . $row;
  249. $sheet->getStyle($rowRange)->getAlignment()->setWrapText(true);
  250. $sheet->getStyle($rowRange)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  251. $sheet->getStyle($rowRange)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
  252. foreach ($data as $col => $value) {
  253. if ($col === 'I') {
  254. if (!empty($value) && DateHelper::isDate($value)) {
  255. $excelDate = DateHelper::ISODateToExcelDate($value);
  256. $sheet->setCellValue('I' . $row, $excelDate);
  257. $sheet->getStyle('I' . $row)->getNumberFormat()->setFormatCode('m/d/yyyy');
  258. } else {
  259. $sheet->setCellValueExplicit('I' . $row, (string)$value, DataType::TYPE_STRING);
  260. $sheet->getStyle('I' . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT);
  261. }
  262. continue;
  263. }
  264. if ($col === 'P') {
  265. if(isset($value) && is_numeric($value)) {
  266. $sheet->setCellValue('P' . $row, (float)$value);
  267. }
  268. $sheet->getStyle('P' . $row)->getNumberFormat()->setFormatCode('#,##0.00\\₽');
  269. continue;
  270. }
  271. if ($value === null || $value === '') {
  272. $sheet->setCellValue($col . $row, null);
  273. continue;
  274. }
  275. $sheet->setCellValueExplicit($col . $row, (string)$value, DataType::TYPE_STRING);
  276. $sheet->getStyle($col . $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT);
  277. }
  278. }
  279. private function applyBorders(Worksheet $sheet, int $fromRow, int $toRow): void
  280. {
  281. $highestCol = $sheet->getHighestColumn();
  282. $range = 'A' . $fromRow . ':' . $highestCol . $toRow;
  283. $sheet->getStyle($range)
  284. ->getBorders()
  285. ->getAllBorders()
  286. ->setBorderStyle(Border::BORDER_THIN)
  287. ->setColor(new Color('777777'));
  288. }
  289. private function applyDefaultStyles(Worksheet $sheet, int $fromRow, int $toRow): void
  290. {
  291. if ($toRow < $fromRow) {
  292. return;
  293. }
  294. $highestCol = $sheet->getHighestColumn();
  295. $range = 'A' . $fromRow . ':' . $highestCol . $toRow;
  296. $sheet->getStyle($range)->getAlignment()->setWrapText(true);
  297. $sheet->getStyle($range)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
  298. $sheet->getStyle($range)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
  299. }
  300. }