ContractorPriceService.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Contractor;
  4. use App\Models\ContractorInstallationPrice;
  5. use App\Models\Product;
  6. use App\Models\Scopes\YearScope;
  7. use Illuminate\Http\UploadedFile;
  8. use Illuminate\Support\Collection;
  9. use PhpOffice\PhpSpreadsheet\IOFactory;
  10. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  11. use PhpOffice\PhpSpreadsheet\Style\Border;
  12. use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
  13. use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
  14. class ContractorPriceService
  15. {
  16. public function rowsForContractor(Contractor $contractor, int $year): Collection
  17. {
  18. $currentProducts = Product::query()
  19. ->where('year', $year)
  20. ->orderBy('article')
  21. ->get()
  22. ->keyBy('id');
  23. $prices = $contractor->prices()
  24. ->where('catalog_year', $year)
  25. ->with('product')
  26. ->get()
  27. ->keyBy('product_id');
  28. $rows = collect();
  29. foreach ($currentProducts as $product) {
  30. $rows->push($this->buildRow($product, $prices->get($product->id), true));
  31. }
  32. foreach ($prices as $price) {
  33. if ($currentProducts->has($price->product_id) || !$price->product) {
  34. continue;
  35. }
  36. $rows->push($this->buildRow($price->product, $price, false));
  37. }
  38. return $rows;
  39. }
  40. public function updatePrice(Contractor $contractor, Product $product, int $year, ?string $nameInSpec, float $price): ContractorInstallationPrice
  41. {
  42. return ContractorInstallationPrice::query()->updateOrCreate(
  43. [
  44. 'contractor_id' => $contractor->id,
  45. 'product_id' => $product->id,
  46. 'catalog_year' => $year,
  47. ],
  48. [
  49. 'name_in_spec' => $nameInSpec ?: null,
  50. 'price' => max(0, $price),
  51. ],
  52. );
  53. }
  54. public function import(Contractor $contractor, UploadedFile $file, int $year): array
  55. {
  56. $spreadsheet = IOFactory::load($file->getRealPath());
  57. $sheet = $spreadsheet->getActiveSheet();
  58. $highestRow = $sheet->getHighestDataRow();
  59. $updated = 0;
  60. $unchanged = 0;
  61. $errors = [];
  62. for ($row = 2; $row <= $highestRow; $row++) {
  63. $article = trim((string) $sheet->getCell('B' . $row)->getValue());
  64. if ($article === '') {
  65. continue;
  66. }
  67. $product = Product::query()
  68. ->where('year', $year)
  69. ->where('article', $article)
  70. ->first();
  71. if (!$product) {
  72. $errors[] = "Строка {$row}: артикул {$article} не найден в каталоге {$year} года";
  73. continue;
  74. }
  75. $nameInSpec = trim((string) $sheet->getCell('D' . $row)->getValue());
  76. $price = $this->parsePrice($sheet->getCell('E' . $row)->getCalculatedValue());
  77. $current = ContractorInstallationPrice::query()
  78. ->where('contractor_id', $contractor->id)
  79. ->where('product_id', $product->id)
  80. ->where('catalog_year', $year)
  81. ->first();
  82. $currentName = (string) ($current?->name_in_spec ?? '');
  83. $currentPrice = (float) ($current?->price ?? 0);
  84. if ($current && $currentName === $nameInSpec && abs($currentPrice - $price) < 0.001) {
  85. $unchanged++;
  86. continue;
  87. }
  88. $this->updatePrice($contractor, $product, $year, $nameInSpec, $price);
  89. $updated++;
  90. }
  91. return [
  92. 'updated' => $updated,
  93. 'unchanged' => $unchanged,
  94. 'errors' => $errors,
  95. ];
  96. }
  97. public function export(Contractor $contractor, int $year): string
  98. {
  99. $spreadsheet = new Spreadsheet();
  100. $sheet = $spreadsheet->getActiveSheet();
  101. $sheet->setTitle('Цены монтажа');
  102. $headers = [
  103. 'A' => 'Картинка МАФ',
  104. 'B' => 'Артикул МАФ',
  105. 'C' => 'Номер номенклатуры',
  106. 'D' => 'Наименование по спецификации',
  107. 'E' => 'Цена монтажа',
  108. ];
  109. foreach ($headers as $column => $title) {
  110. $sheet->setCellValue($column . '1', $title);
  111. }
  112. $rowNumber = 2;
  113. foreach ($this->rowsForContractor($contractor, $year) as $row) {
  114. $this->insertProductImage($sheet, $row['product'], $rowNumber);
  115. $sheet->setCellValue('B' . $rowNumber, $row['product']->article);
  116. $sheet->setCellValue('C' . $rowNumber, $row['product']->nomenclature_number);
  117. $sheet->setCellValue('D' . $rowNumber, $row['price']?->name_in_spec ?? '');
  118. $sheet->setCellValue('E' . $rowNumber, $row['price']?->price ?? 0);
  119. $sheet->getRowDimension($rowNumber)->setRowHeight(55);
  120. $rowNumber++;
  121. }
  122. $sheet->getStyle('A1:E1')->getFont()->setBold(true);
  123. $sheet->getStyle('A1:E' . max(1, $rowNumber - 1))->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
  124. $sheet->getStyle('E2:E' . max(2, $rowNumber))->getNumberFormat()->setFormatCode('#,##0.00');
  125. foreach (range('A', 'E') as $column) {
  126. $sheet->getColumnDimension($column)->setAutoSize(true);
  127. }
  128. $sheet->getColumnDimension('A')->setWidth(18);
  129. $path = storage_path('app/contractor-prices-' . $contractor->id . '-' . $year . '-' . time() . '.xlsx');
  130. (new Xlsx($spreadsheet))->save($path);
  131. return $path;
  132. }
  133. private function buildRow(Product $product, ?ContractorInstallationPrice $price, bool $available): array
  134. {
  135. return [
  136. 'product' => $product,
  137. 'price' => $price,
  138. 'available' => $available && is_null($product->deleted_at),
  139. ];
  140. }
  141. private function parsePrice(mixed $value): float
  142. {
  143. if ($value === null || $value === '') {
  144. return 0;
  145. }
  146. $normalized = str_replace([' ', "\xc2\xa0", '₽'], '', (string) $value);
  147. $normalized = str_replace(',', '.', $normalized);
  148. return is_numeric($normalized) ? (float) $normalized : 0;
  149. }
  150. private function insertProductImage($sheet, Product $product, int $row): void
  151. {
  152. $path = public_path('images/main/' . $product->article . '.0000.0000.jpg');
  153. if (!file_exists($path)) {
  154. return;
  155. }
  156. $drawing = new Drawing();
  157. $drawing->setPath($path);
  158. $drawing->setCoordinates('A' . $row);
  159. $drawing->setHeight(60);
  160. $drawing->setOffsetX(4);
  161. $drawing->setOffsetY(4);
  162. $drawing->setWorksheet($sheet);
  163. }
  164. }