ImportYearDataService.php 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199
  1. <?php
  2. namespace App\Services\Import;
  3. use App\Models\Contract;
  4. use App\Models\File;
  5. use App\Models\MafOrder;
  6. use App\Models\Order;
  7. use App\Models\Product;
  8. use App\Models\ProductSKU;
  9. use App\Models\Reclamation;
  10. use App\Models\ReclamationDetail;
  11. use App\Models\Schedule;
  12. use App\Models\Ttn;
  13. use App\Models\User;
  14. use Exception;
  15. use Illuminate\Support\Facades\DB;
  16. use Illuminate\Support\Facades\Log;
  17. use Illuminate\Support\Facades\Storage;
  18. use Illuminate\Support\Str;
  19. use PhpOffice\PhpSpreadsheet\IOFactory;
  20. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  21. use ZipArchive;
  22. class ImportYearDataService
  23. {
  24. private string $tempDir;
  25. private array $manifest;
  26. private array $logs = [];
  27. // ID маппинги: old_id => new_id
  28. private array $productIdMapping = [];
  29. private array $mafOrderIdMapping = [];
  30. private array $orderIdMapping = [];
  31. private array $productSkuIdMapping = [];
  32. private array $reclamationIdMapping = [];
  33. private array $fileIdMapping = [];
  34. // Справочники для маппинга
  35. private array $districtMapping = [];
  36. private array $areaMapping = [];
  37. private array $userMapping = [];
  38. private array $objectTypeMapping = [];
  39. private array $orderStatusMapping = [];
  40. private array $reclamationStatusMapping = [];
  41. public function __construct(
  42. private readonly string $archivePath,
  43. private readonly int $year,
  44. private readonly int $userId,
  45. private readonly bool $clearExisting = false,
  46. ) {}
  47. public function handle(): bool
  48. {
  49. $this->prepareTempDirectory();
  50. try {
  51. $this->log("Начало импорта данных за {$this->year} год");
  52. // Распаковка архива
  53. $this->extractArchive();
  54. // Валидация манифеста
  55. $this->validateManifest();
  56. // Загрузка справочников для маппинга
  57. $this->loadDictionaries();
  58. // Очистка существующих данных (опционально)
  59. if ($this->clearExisting) {
  60. $this->log("Очистка существующих данных за {$this->year} год...");
  61. $this->clearExistingData();
  62. }
  63. DB::beginTransaction();
  64. try {
  65. // Импорт данных в правильном порядке
  66. $this->importProducts();
  67. $this->importMafOrders();
  68. $this->importOrders();
  69. $this->importProductsSku();
  70. $this->importReclamations();
  71. $this->importSchedules();
  72. $this->importContracts();
  73. $this->importTtn();
  74. // Импорт pivot таблиц (файлы заказов и рекламаций)
  75. $this->importPivotTables();
  76. DB::commit();
  77. $this->log("Импорт успешно завершён");
  78. return true;
  79. } catch (Exception $e) {
  80. DB::rollBack();
  81. throw $e;
  82. }
  83. } catch (Exception $e) {
  84. $this->log("Ошибка импорта: " . $e->getMessage(), 'ERROR');
  85. Log::error("ImportYearDataService error: " . $e->getMessage(), [
  86. 'trace' => $e->getTraceAsString()
  87. ]);
  88. return false;
  89. } finally {
  90. $this->cleanupTempDirectory();
  91. }
  92. }
  93. private function prepareTempDirectory(): void
  94. {
  95. $this->tempDir = storage_path('app/temp/import_year_' . $this->year . '_' . Str::random(8));
  96. if (!is_dir($this->tempDir)) {
  97. mkdir($this->tempDir, 0755, true);
  98. }
  99. }
  100. private function extractArchive(): void
  101. {
  102. $this->log("Распаковка архива...");
  103. $zip = new ZipArchive();
  104. if ($zip->open($this->archivePath) !== true) {
  105. throw new Exception("Не удалось открыть архив");
  106. }
  107. $zip->extractTo($this->tempDir);
  108. $zip->close();
  109. $this->log("Архив распакован");
  110. }
  111. private function validateManifest(): void
  112. {
  113. $manifestPath = $this->tempDir . '/manifest.json';
  114. if (!file_exists($manifestPath)) {
  115. throw new Exception("Файл manifest.json не найден в архиве");
  116. }
  117. $this->manifest = json_decode(file_get_contents($manifestPath), true);
  118. if (!$this->manifest) {
  119. throw new Exception("Некорректный формат manifest.json");
  120. }
  121. if (!isset($this->manifest['version']) || !isset($this->manifest['year'])) {
  122. throw new Exception("Отсутствуют обязательные поля в manifest.json");
  123. }
  124. $this->log("Манифест валиден. Версия: {$this->manifest['version']}, Год экспорта: {$this->manifest['year']}");
  125. // Показываем статистику из манифеста
  126. if (isset($this->manifest['stats'])) {
  127. $this->log("Статистика из архива:");
  128. foreach ($this->manifest['stats'] as $entity => $count) {
  129. $this->log(" - {$entity}: {$count}");
  130. }
  131. }
  132. }
  133. private function loadDictionaries(): void
  134. {
  135. $this->log("Загрузка справочников...");
  136. // Округа
  137. $districts = DB::table('districts')->get();
  138. foreach ($districts as $d) {
  139. $this->districtMapping[$d->shortname] = $d->id;
  140. }
  141. // Районы
  142. $areas = DB::table('areas')->get();
  143. foreach ($areas as $a) {
  144. $this->areaMapping[$a->name] = $a->id;
  145. }
  146. // Пользователи
  147. $users = User::all();
  148. foreach ($users as $u) {
  149. $this->userMapping[$u->name] = $u->id;
  150. }
  151. // Типы объектов
  152. $objectTypes = DB::table('object_types')->get();
  153. foreach ($objectTypes as $ot) {
  154. $this->objectTypeMapping[$ot->name] = $ot->id;
  155. }
  156. // Статусы заказов
  157. $orderStatuses = DB::table('order_statuses')->get();
  158. foreach ($orderStatuses as $os) {
  159. $this->orderStatusMapping[$os->name] = $os->id;
  160. }
  161. // Статусы рекламаций
  162. $reclamationStatuses = DB::table('reclamation_statuses')->get();
  163. foreach ($reclamationStatuses as $rs) {
  164. $this->reclamationStatusMapping[$rs->name] = $rs->id;
  165. }
  166. $this->log("Справочники загружены");
  167. }
  168. private function clearExistingData(): void
  169. {
  170. // Используем логику из ClearYearDataJob
  171. $orderIds = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
  172. $productIds = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
  173. $productSkuIds = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->pluck('id');
  174. $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
  175. // Собираем файлы до удаления связей
  176. $fileIds = $this->collectFileIds($orderIds, $productIds, $productSkuIds);
  177. // Рекламации
  178. DB::table('reclamation_details')->whereIn('reclamation_id', $reclamationIds)->delete();
  179. DB::table('reclamation_product_sku')->whereIn('reclamation_id', $reclamationIds)->delete();
  180. DB::table('reclamation_photo_before')->whereIn('reclamation_id', $reclamationIds)->delete();
  181. DB::table('reclamation_photo_after')->whereIn('reclamation_id', $reclamationIds)->delete();
  182. DB::table('reclamation_document')->whereIn('reclamation_id', $reclamationIds)->delete();
  183. DB::table('reclamation_act')->whereIn('reclamation_id', $reclamationIds)->delete();
  184. Reclamation::whereIn('id', $reclamationIds)->delete();
  185. // Связи заказов
  186. DB::table('order_photo')->whereIn('order_id', $orderIds)->delete();
  187. DB::table('order_document')->whereIn('order_id', $orderIds)->delete();
  188. DB::table('order_statement')->whereIn('order_id', $orderIds)->delete();
  189. // Расписания
  190. Schedule::whereIn('order_id', $orderIds)->delete();
  191. // SKU
  192. ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
  193. // Заказы
  194. Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
  195. // МАФ заказы
  196. MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
  197. // Продукты
  198. Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
  199. ->whereIn('id', $productIds)
  200. ->update(['certificate_id' => null]);
  201. Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()->where('year', $this->year)->forceDelete();
  202. // ТТН
  203. Ttn::where('year', $this->year)->update(['file_id' => null]);
  204. Ttn::where('year', $this->year)->delete();
  205. // Контракты
  206. Contract::where('year', $this->year)->delete();
  207. // Файлы
  208. $this->deleteFiles($fileIds);
  209. $this->log("Существующие данные очищены");
  210. }
  211. private function collectFileIds($orderIds, $productIds, $productSkuIds): \Illuminate\Support\Collection
  212. {
  213. $fileIds = collect();
  214. $fileIds = $fileIds->merge(DB::table('order_photo')->whereIn('order_id', $orderIds)->pluck('file_id'));
  215. $fileIds = $fileIds->merge(DB::table('order_document')->whereIn('order_id', $orderIds)->pluck('file_id'));
  216. $fileIds = $fileIds->merge(DB::table('order_statement')->whereIn('order_id', $orderIds)->pluck('file_id'));
  217. $reclamationIds = Reclamation::whereIn('order_id', $orderIds)->pluck('id');
  218. $fileIds = $fileIds->merge(DB::table('reclamation_photo_before')->whereIn('reclamation_id', $reclamationIds)->pluck('file_id'));
  219. $fileIds = $fileIds->merge(DB::table('reclamation_photo_after')->whereIn('reclamation_id', $reclamationIds)->pluck('file_id'));
  220. $fileIds = $fileIds->merge(DB::table('reclamation_document')->whereIn('reclamation_id', $reclamationIds)->pluck('file_id'));
  221. $fileIds = $fileIds->merge(DB::table('reclamation_act')->whereIn('reclamation_id', $reclamationIds)->pluck('file_id'));
  222. $fileIds = $fileIds->merge(
  223. Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
  224. ->whereIn('id', $productIds)
  225. ->whereNotNull('certificate_id')
  226. ->pluck('certificate_id')
  227. );
  228. $fileIds = $fileIds->merge(
  229. ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->withTrashed()
  230. ->whereIn('id', $productSkuIds)
  231. ->whereNotNull('passport_id')
  232. ->pluck('passport_id')
  233. );
  234. $fileIds = $fileIds->merge(
  235. Ttn::where('year', $this->year)->whereNotNull('file_id')->pluck('file_id')
  236. );
  237. return $fileIds->unique();
  238. }
  239. private function deleteFiles($fileIds): void
  240. {
  241. $files = File::whereIn('id', $fileIds)->get();
  242. foreach ($files as $file) {
  243. if ($file->path && Storage::disk('public')->exists($file->path)) {
  244. Storage::disk('public')->delete($file->path);
  245. }
  246. }
  247. File::whereIn('id', $fileIds)->delete();
  248. }
  249. /**
  250. * Безопасно получает значение из строки Excel
  251. * Возвращает null для пустых строк, или значение по умолчанию
  252. */
  253. private function getValue(array $row, array $headerMap, string $key, mixed $default = null): mixed
  254. {
  255. if (!isset($headerMap[$key])) {
  256. return $default;
  257. }
  258. $value = $row[$headerMap[$key]] ?? null;
  259. if (is_string($value)) {
  260. $value = trim($value);
  261. }
  262. if ($value === null || $value === '') {
  263. return $default;
  264. }
  265. return $value;
  266. }
  267. /**
  268. * Получает строковое значение (пустая строка вместо null)
  269. */
  270. private function getStringValue(array $row, array $headerMap, string $key, string $default = ''): string
  271. {
  272. $value = $this->getValue($row, $headerMap, $key);
  273. return $value !== null ? (string) $value : $default;
  274. }
  275. /**
  276. * Получает числовое значение
  277. */
  278. private function getNumericValue(array $row, array $headerMap, string $key, int|float $default = 0): int|float
  279. {
  280. $value = $this->getValue($row, $headerMap, $key);
  281. if ($value === null) {
  282. return $default;
  283. }
  284. return is_numeric($value) ? $value : $default;
  285. }
  286. private function importProducts(): void
  287. {
  288. $this->log("Импорт продуктов...");
  289. $filePath = $this->tempDir . '/data/products.xlsx';
  290. if (!file_exists($filePath)) {
  291. $this->log("Файл products.xlsx не найден, пропуск", 'WARNING');
  292. return;
  293. }
  294. $spreadsheet = IOFactory::load($filePath);
  295. $sheet = $spreadsheet->getActiveSheet();
  296. $rows = $sheet->toArray();
  297. $headers = array_shift($rows);
  298. $headerMap = array_flip($headers);
  299. $count = 0;
  300. foreach ($rows as $row) {
  301. if (empty($row[$headerMap['id']])) {
  302. continue;
  303. }
  304. $oldId = $this->getValue($row, $headerMap, 'id');
  305. $nomenclatureNumber = $this->getStringValue($row, $headerMap, 'nomenclature_number', '');
  306. // Проверяем существует ли продукт
  307. $existing = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  308. ->where('year', $this->year)
  309. ->where('nomenclature_number', $nomenclatureNumber)
  310. ->first();
  311. // Импорт сертификата если есть
  312. $certificateId = null;
  313. $certificatePath = $this->getValue($row, $headerMap, 'certificate_file');
  314. if ($certificatePath) {
  315. $certificateId = $this->importFile($certificatePath);
  316. }
  317. $productData = [
  318. 'year' => $this->year,
  319. 'article' => $this->getStringValue($row, $headerMap, 'article'),
  320. 'name_tz' => $this->getValue($row, $headerMap, 'name_tz'),
  321. 'type_tz' => $this->getValue($row, $headerMap, 'type_tz'),
  322. 'nomenclature_number' => $nomenclatureNumber,
  323. 'sizes' => $this->getValue($row, $headerMap, 'sizes'),
  324. 'manufacturer' => $this->getValue($row, $headerMap, 'manufacturer'),
  325. 'unit' => $this->getStringValue($row, $headerMap, 'unit', 'шт.'),
  326. 'type' => $this->getStringValue($row, $headerMap, 'type', 'Товар'),
  327. 'product_price' => $this->getNumericValue($row, $headerMap, 'product_price', 0),
  328. 'installation_price' => $this->getNumericValue($row, $headerMap, 'installation_price', 0),
  329. 'total_price' => $this->getNumericValue($row, $headerMap, 'total_price', 0),
  330. 'manufacturer_name' => $this->getValue($row, $headerMap, 'manufacturer_name'),
  331. 'note' => $this->getValue($row, $headerMap, 'note'),
  332. 'passport_name' => $this->getValue($row, $headerMap, 'passport_name'),
  333. 'statement_name' => $this->getValue($row, $headerMap, 'statement_name'),
  334. 'service_life' => $this->getValue($row, $headerMap, 'service_life'),
  335. 'certificate_number' => $this->getValue($row, $headerMap, 'certificate_number'),
  336. 'certificate_date' => $this->getValue($row, $headerMap, 'certificate_date'),
  337. 'certificate_issuer' => $this->getValue($row, $headerMap, 'certificate_issuer'),
  338. 'certificate_type' => $this->getValue($row, $headerMap, 'certificate_type'),
  339. 'weight' => $this->getNumericValue($row, $headerMap, 'weight', 0),
  340. 'volume' => $this->getNumericValue($row, $headerMap, 'volume', 0),
  341. 'places' => $this->getNumericValue($row, $headerMap, 'places', 0),
  342. 'certificate_id' => $certificateId,
  343. ];
  344. if ($existing) {
  345. $existing->update($productData);
  346. $this->productIdMapping[$oldId] = $existing->id;
  347. } else {
  348. $product = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($productData);
  349. $this->productIdMapping[$oldId] = $product->id;
  350. }
  351. $count++;
  352. }
  353. $this->log("Импортировано продуктов: {$count}");
  354. }
  355. private function importMafOrders(): void
  356. {
  357. $this->log("Импорт заказов МАФ...");
  358. $filePath = $this->tempDir . '/data/maf_orders.xlsx';
  359. if (!file_exists($filePath)) {
  360. $this->log("Файл maf_orders.xlsx не найден, пропуск", 'WARNING');
  361. return;
  362. }
  363. $spreadsheet = IOFactory::load($filePath);
  364. $sheet = $spreadsheet->getActiveSheet();
  365. $rows = $sheet->toArray();
  366. $headers = array_shift($rows);
  367. $headerMap = array_flip($headers);
  368. $count = 0;
  369. $touchedOrderIds = [];
  370. foreach ($rows as $row) {
  371. if (empty($row[$headerMap['id']])) {
  372. continue;
  373. }
  374. $oldId = $this->getValue($row, $headerMap, 'id');
  375. $oldProductId = $this->getValue($row, $headerMap, 'product_id');
  376. $orderNumber = $this->getStringValue($row, $headerMap, 'order_number', '');
  377. // Получаем новый product_id
  378. $newProductId = $this->productIdMapping[$oldProductId] ?? null;
  379. if (!$newProductId) {
  380. // Пробуем найти по номенклатуре
  381. $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
  382. if ($nomenclature) {
  383. $product = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  384. ->where('year', $this->year)
  385. ->where('nomenclature_number', $nomenclature)
  386. ->first();
  387. $newProductId = $product?->id;
  388. }
  389. }
  390. if (!$newProductId) {
  391. $this->log("Пропуск MafOrder {$oldId}: продукт не найден", 'WARNING');
  392. continue;
  393. }
  394. // Проверяем существует ли
  395. $existing = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  396. ->where('year', $this->year)
  397. ->where('product_id', $newProductId)
  398. ->where('order_number', $orderNumber)
  399. ->first();
  400. // Получаем user_id
  401. $userName = $this->getValue($row, $headerMap, 'user_name');
  402. $userId = $this->userMapping[$userName] ?? $this->userId;
  403. $mafOrderData = [
  404. 'year' => $this->year,
  405. 'order_number' => $orderNumber,
  406. 'status' => $this->getValue($row, $headerMap, 'status'),
  407. 'user_id' => $userId,
  408. 'product_id' => $newProductId,
  409. 'quantity' => $this->getNumericValue($row, $headerMap, 'quantity', 0),
  410. 'in_stock' => $this->getNumericValue($row, $headerMap, 'in_stock', 0),
  411. ];
  412. if ($existing) {
  413. $existing->update($mafOrderData);
  414. $this->mafOrderIdMapping[$oldId] = $existing->id;
  415. } else {
  416. $mafOrder = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($mafOrderData);
  417. $this->mafOrderIdMapping[$oldId] = $mafOrder->id;
  418. }
  419. $count++;
  420. }
  421. $this->log("Импортировано заказов МАФ: {$count}");
  422. }
  423. private function importOrders(): void
  424. {
  425. $this->log("Импорт заказов (площадок)...");
  426. $filePath = $this->tempDir . '/data/orders.xlsx';
  427. if (!file_exists($filePath)) {
  428. $this->log("Файл orders.xlsx не найден, пропуск", 'WARNING');
  429. return;
  430. }
  431. $spreadsheet = IOFactory::load($filePath);
  432. $sheet = $spreadsheet->getActiveSheet();
  433. $rows = $sheet->toArray();
  434. $headers = array_shift($rows);
  435. $headerMap = array_flip($headers);
  436. $count = 0;
  437. foreach ($rows as $row) {
  438. if (empty($row[$headerMap['id']])) {
  439. continue;
  440. }
  441. $oldId = $this->getValue($row, $headerMap, 'id');
  442. $objectAddress = $this->getStringValue($row, $headerMap, 'object_address', '');
  443. // Проверяем существует ли
  444. $existing = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  445. ->where('year', $this->year)
  446. ->where('object_address', $objectAddress)
  447. ->first();
  448. // Маппинг справочников
  449. $districtShortname = $this->getValue($row, $headerMap, 'district_shortname');
  450. $districtId = $this->districtMapping[$districtShortname] ?? null;
  451. $areaName = $this->getValue($row, $headerMap, 'area_name');
  452. $areaId = $this->areaMapping[$areaName] ?? null;
  453. $userName = $this->getValue($row, $headerMap, 'user_name');
  454. $userId = $this->userMapping[$userName] ?? $this->userId;
  455. $objectTypeName = $this->getValue($row, $headerMap, 'object_type_name');
  456. $objectTypeId = $this->objectTypeMapping[$objectTypeName] ?? null;
  457. $brigadierName = $this->getValue($row, $headerMap, 'brigadier_name');
  458. $brigadierId = $this->userMapping[$brigadierName] ?? null;
  459. $orderStatusName = $this->getValue($row, $headerMap, 'order_status_name');
  460. $orderStatusId = $this->orderStatusMapping[$orderStatusName] ?? Order::STATUS_NEW;
  461. $orderData = [
  462. 'year' => $this->year,
  463. 'name' => $this->getValue($row, $headerMap, 'name'),
  464. 'user_id' => $userId,
  465. 'district_id' => $districtId,
  466. 'area_id' => $areaId,
  467. 'object_address' => $objectAddress,
  468. 'object_type_id' => $objectTypeId,
  469. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  470. 'installation_date' => $this->getValue($row, $headerMap, 'installation_date'),
  471. 'ready_date' => $this->getValue($row, $headerMap, 'ready_date'),
  472. 'brigadier_id' => $brigadierId,
  473. 'order_status_id' => $orderStatusId,
  474. 'tg_group_name' => $this->getValue($row, $headerMap, 'tg_group_name'),
  475. 'tg_group_link' => $this->getValue($row, $headerMap, 'tg_group_link'),
  476. 'ready_to_mount' => $this->getValue($row, $headerMap, 'ready_to_mount'),
  477. 'install_days' => $this->getValue($row, $headerMap, 'install_days'),
  478. ];
  479. if ($existing) {
  480. $existing->update($orderData);
  481. $this->orderIdMapping[$oldId] = $existing->id;
  482. } else {
  483. $order = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($orderData);
  484. $this->orderIdMapping[$oldId] = $order->id;
  485. }
  486. $count++;
  487. }
  488. $this->log("Импортировано заказов: {$count}");
  489. }
  490. private function importProductsSku(): void
  491. {
  492. $this->log("Импорт SKU продуктов...");
  493. $filePath = $this->tempDir . '/data/products_sku.xlsx';
  494. if (!file_exists($filePath)) {
  495. $this->log("Файл products_sku.xlsx не найден, пропуск", 'WARNING');
  496. return;
  497. }
  498. $spreadsheet = IOFactory::load($filePath);
  499. $sheet = $spreadsheet->getActiveSheet();
  500. $rows = $sheet->toArray();
  501. $headers = array_shift($rows);
  502. $headerMap = array_flip($headers);
  503. $count = 0;
  504. foreach ($rows as $row) {
  505. if (empty($row[$headerMap['id']])) {
  506. continue;
  507. }
  508. $oldId = $this->getValue($row, $headerMap, 'id');
  509. // Маппинг product_id
  510. $oldProductId = $this->getValue($row, $headerMap, 'product_id');
  511. $newProductId = $this->productIdMapping[$oldProductId] ?? null;
  512. if (!$newProductId) {
  513. // Пробуем найти по номенклатуре
  514. $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
  515. if ($nomenclature) {
  516. $product = Product::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  517. ->where('year', $this->year)
  518. ->where('nomenclature_number', $nomenclature)
  519. ->first();
  520. $newProductId = $product?->id;
  521. }
  522. }
  523. if (!$newProductId) {
  524. $this->log("Пропуск SKU {$oldId}: продукт не найден", 'WARNING');
  525. continue;
  526. }
  527. // Маппинг order_id
  528. $oldOrderId = $this->getValue($row, $headerMap, 'order_id');
  529. $newOrderId = $this->orderIdMapping[$oldOrderId] ?? null;
  530. if (!$newOrderId) {
  531. // Пробуем найти по адресу
  532. $orderAddress = $this->getValue($row, $headerMap, 'order_address');
  533. if ($orderAddress) {
  534. $order = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  535. ->where('year', $this->year)
  536. ->where('object_address', $orderAddress)
  537. ->first();
  538. $newOrderId = $order?->id;
  539. }
  540. }
  541. if (!$newOrderId) {
  542. $this->log("Пропуск SKU {$oldId}: заказ не найден", 'WARNING');
  543. continue;
  544. }
  545. // Маппинг maf_order_id
  546. $oldMafOrderId = $this->getValue($row, $headerMap, 'maf_order_id');
  547. $newMafOrderId = null;
  548. if ($oldMafOrderId) {
  549. $newMafOrderId = $this->mafOrderIdMapping[$oldMafOrderId] ?? null;
  550. if (!$newMafOrderId) {
  551. // Пробуем найти по номеру заказа
  552. $mafOrderNumber = $this->getValue($row, $headerMap, 'maf_order_number');
  553. if ($mafOrderNumber) {
  554. $mafOrder = MafOrder::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  555. ->where('year', $this->year)
  556. ->where('product_id', $newProductId)
  557. ->where('order_number', $mafOrderNumber)
  558. ->first();
  559. $newMafOrderId = $mafOrder?->id;
  560. }
  561. }
  562. }
  563. // Импорт паспорта если есть
  564. $passportId = null;
  565. $passportPath = $this->getValue($row, $headerMap, 'passport_file');
  566. if ($passportPath) {
  567. $passportId = $this->importFile($passportPath);
  568. }
  569. $skuData = [
  570. 'year' => $this->year,
  571. 'product_id' => $newProductId,
  572. 'order_id' => $newOrderId,
  573. 'maf_order_id' => $newMafOrderId,
  574. 'status' => $this->getValue($row, $headerMap, 'status'),
  575. 'rfid' => $this->getValue($row, $headerMap, 'rfid'),
  576. 'factory_number' => $this->getValue($row, $headerMap, 'factory_number'),
  577. 'manufacture_date' => $this->getValue($row, $headerMap, 'manufacture_date'),
  578. 'statement_number' => $this->getValue($row, $headerMap, 'statement_number'),
  579. 'statement_date' => $this->getValue($row, $headerMap, 'statement_date'),
  580. 'upd_number' => $this->getValue($row, $headerMap, 'upd_number'),
  581. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  582. 'passport_id' => $passportId,
  583. ];
  584. $sku = ProductSKU::withoutGlobalScope(\App\Models\Scopes\YearScope::class)->create($skuData);
  585. $this->productSkuIdMapping[$oldId] = $sku->id;
  586. $touchedOrderIds[$newOrderId] = $newOrderId;
  587. $count++;
  588. }
  589. $paymentStatusService = app(\App\Services\OrderPaymentStatusService::class);
  590. foreach ($touchedOrderIds as $orderId) {
  591. $paymentStatusService->markPaidIfAllMafsHavePaymentData((int) $orderId);
  592. }
  593. $this->log("Импортировано SKU: {$count}");
  594. }
  595. private function importReclamations(): void
  596. {
  597. $this->log("Импорт рекламаций...");
  598. $filePath = $this->tempDir . '/data/reclamations.xlsx';
  599. if (!file_exists($filePath)) {
  600. $this->log("Файл reclamations.xlsx не найден, пропуск", 'WARNING');
  601. return;
  602. }
  603. $spreadsheet = IOFactory::load($filePath);
  604. // Лист 1: Reclamations
  605. $sheet = $spreadsheet->getSheetByName('Reclamations');
  606. if (!$sheet) {
  607. $this->log("Лист Reclamations не найден", 'WARNING');
  608. return;
  609. }
  610. $rows = $sheet->toArray();
  611. $headers = array_shift($rows);
  612. $headerMap = array_flip($headers);
  613. $count = 0;
  614. foreach ($rows as $row) {
  615. if (empty($row[$headerMap['id']])) {
  616. continue;
  617. }
  618. $oldId = $this->getValue($row, $headerMap, 'id');
  619. // Маппинг order_id
  620. $oldOrderId = $this->getValue($row, $headerMap, 'order_id');
  621. $newOrderId = $this->orderIdMapping[$oldOrderId] ?? null;
  622. if (!$newOrderId) {
  623. // Пробуем найти по адресу
  624. $orderAddress = $this->getValue($row, $headerMap, 'order_address');
  625. if ($orderAddress) {
  626. $order = Order::withoutGlobalScope(\App\Models\Scopes\YearScope::class)
  627. ->where('year', $this->year)
  628. ->where('object_address', $orderAddress)
  629. ->first();
  630. $newOrderId = $order?->id;
  631. }
  632. }
  633. if (!$newOrderId) {
  634. $this->log("Пропуск рекламации {$oldId}: заказ не найден", 'WARNING');
  635. continue;
  636. }
  637. $userName = $this->getValue($row, $headerMap, 'user_name');
  638. $userId = $this->userMapping[$userName] ?? $this->userId;
  639. $brigadierName = $this->getValue($row, $headerMap, 'brigadier_name');
  640. $brigadierId = $this->userMapping[$brigadierName] ?? null;
  641. $statusName = $this->getValue($row, $headerMap, 'status_name');
  642. $statusId = $this->reclamationStatusMapping[$statusName] ?? Reclamation::STATUS_NEW;
  643. $reclamationData = [
  644. 'order_id' => $newOrderId,
  645. 'user_id' => $userId,
  646. 'status_id' => $statusId,
  647. 'reason' => $this->getValue($row, $headerMap, 'reason'),
  648. 'guarantee' => $this->getValue($row, $headerMap, 'guarantee'),
  649. 'whats_done' => $this->getValue($row, $headerMap, 'whats_done'),
  650. 'create_date' => $this->getValue($row, $headerMap, 'create_date'),
  651. 'finish_date' => $this->getValue($row, $headerMap, 'finish_date'),
  652. 'start_work_date' => $this->getValue($row, $headerMap, 'start_work_date'),
  653. 'work_days' => $this->getValue($row, $headerMap, 'work_days'),
  654. 'brigadier_id' => $brigadierId,
  655. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  656. ];
  657. $reclamation = Reclamation::create($reclamationData);
  658. $this->reclamationIdMapping[$oldId] = $reclamation->id;
  659. $count++;
  660. }
  661. $this->log("Импортировано рекламаций: {$count}");
  662. // Лист 2: ReclamationDetails
  663. $this->importReclamationDetails($spreadsheet);
  664. // Лист 3: ReclamationSKU
  665. $this->importReclamationSkuRelations($spreadsheet);
  666. }
  667. private function importReclamationDetails(Spreadsheet $spreadsheet): void
  668. {
  669. $sheet = $spreadsheet->getSheetByName('ReclamationDetails');
  670. if (!$sheet) {
  671. return;
  672. }
  673. $rows = $sheet->toArray();
  674. $headers = array_shift($rows);
  675. $headerMap = array_flip($headers);
  676. $count = 0;
  677. foreach ($rows as $row) {
  678. if (empty($row[$headerMap['id']])) {
  679. continue;
  680. }
  681. $oldReclamationId = $this->getValue($row, $headerMap, 'reclamation_id');
  682. $newReclamationId = $this->reclamationIdMapping[$oldReclamationId] ?? null;
  683. if (!$newReclamationId) {
  684. continue;
  685. }
  686. ReclamationDetail::create([
  687. 'reclamation_id' => $newReclamationId,
  688. 'name' => $this->getStringValue($row, $headerMap, 'name', ''),
  689. 'quantity' => $this->getNumericValue($row, $headerMap, 'quantity', 0),
  690. ]);
  691. $count++;
  692. }
  693. $this->log("Импортировано деталей рекламаций: {$count}");
  694. }
  695. private function importReclamationSkuRelations(Spreadsheet $spreadsheet): void
  696. {
  697. $sheet = $spreadsheet->getSheetByName('ReclamationSKU');
  698. if (!$sheet) {
  699. return;
  700. }
  701. $rows = $sheet->toArray();
  702. $headers = array_shift($rows);
  703. $headerMap = array_flip($headers);
  704. $count = 0;
  705. foreach ($rows as $row) {
  706. $oldReclamationId = $this->getValue($row, $headerMap, 'reclamation_id');
  707. $oldProductSkuId = $this->getValue($row, $headerMap, 'product_sku_id');
  708. if (!$oldReclamationId || !$oldProductSkuId) {
  709. continue;
  710. }
  711. $newReclamationId = $this->reclamationIdMapping[$oldReclamationId] ?? null;
  712. $newProductSkuId = $this->productSkuIdMapping[$oldProductSkuId] ?? null;
  713. if (!$newReclamationId || !$newProductSkuId) {
  714. continue;
  715. }
  716. DB::table('reclamation_product_sku')->insert([
  717. 'reclamation_id' => $newReclamationId,
  718. 'product_sku_id' => $newProductSkuId,
  719. ]);
  720. $count++;
  721. }
  722. $this->log("Импортировано связей рекламация-SKU: {$count}");
  723. }
  724. private function importSchedules(): void
  725. {
  726. $this->log("Импорт расписаний...");
  727. $filePath = $this->tempDir . '/data/schedules.xlsx';
  728. if (!file_exists($filePath)) {
  729. $this->log("Файл schedules.xlsx не найден, пропуск", 'WARNING');
  730. return;
  731. }
  732. $spreadsheet = IOFactory::load($filePath);
  733. $sheet = $spreadsheet->getActiveSheet();
  734. $rows = $sheet->toArray();
  735. $headers = array_shift($rows);
  736. $headerMap = array_flip($headers);
  737. $count = 0;
  738. foreach ($rows as $row) {
  739. if (empty($row[$headerMap['id']])) {
  740. continue;
  741. }
  742. $oldOrderId = $this->getValue($row, $headerMap, 'order_id');
  743. $newOrderId = $this->orderIdMapping[$oldOrderId] ?? null;
  744. if (!$newOrderId) {
  745. continue;
  746. }
  747. $districtShortname = $this->getValue($row, $headerMap, 'district_shortname');
  748. $districtId = $this->districtMapping[$districtShortname] ?? null;
  749. $areaName = $this->getValue($row, $headerMap, 'area_name');
  750. $areaId = $this->areaMapping[$areaName] ?? null;
  751. $brigadierName = $this->getValue($row, $headerMap, 'brigadier_name');
  752. $brigadierId = $this->userMapping[$brigadierName] ?? null;
  753. Schedule::create([
  754. 'installation_date' => $this->getValue($row, $headerMap, 'installation_date'),
  755. 'address_code' => $this->getValue($row, $headerMap, 'address_code'),
  756. 'manual' => $this->getValue($row, $headerMap, 'manual'),
  757. 'source' => $this->getValue($row, $headerMap, 'source'),
  758. 'order_id' => $newOrderId,
  759. 'district_id' => $districtId,
  760. 'area_id' => $areaId,
  761. 'object_address' => $this->getValue($row, $headerMap, 'object_address'),
  762. 'object_type' => $this->getValue($row, $headerMap, 'object_type'),
  763. 'mafs' => $this->getValue($row, $headerMap, 'mafs'),
  764. 'mafs_count' => $this->getNumericValue($row, $headerMap, 'mafs_count', 0),
  765. 'brigadier_id' => $brigadierId,
  766. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  767. ]);
  768. $count++;
  769. }
  770. $this->log("Импортировано расписаний: {$count}");
  771. }
  772. private function importContracts(): void
  773. {
  774. $this->log("Импорт контрактов...");
  775. $filePath = $this->tempDir . '/data/contracts.xlsx';
  776. if (!file_exists($filePath)) {
  777. $this->log("Файл contracts.xlsx не найден, пропуск", 'WARNING');
  778. return;
  779. }
  780. $spreadsheet = IOFactory::load($filePath);
  781. $sheet = $spreadsheet->getActiveSheet();
  782. $rows = $sheet->toArray();
  783. $headers = array_shift($rows);
  784. $headerMap = array_flip($headers);
  785. $count = 0;
  786. foreach ($rows as $row) {
  787. if (empty($row[$headerMap['id']])) {
  788. continue;
  789. }
  790. Contract::create([
  791. 'year' => $this->year,
  792. 'contract_number' => $this->getStringValue($row, $headerMap, 'contract_number', ''),
  793. 'contract_date' => $this->getValue($row, $headerMap, 'contract_date'),
  794. ]);
  795. $count++;
  796. }
  797. $this->log("Импортировано контрактов: {$count}");
  798. }
  799. private function importTtn(): void
  800. {
  801. $this->log("Импорт ТТН...");
  802. $filePath = $this->tempDir . '/data/ttn.xlsx';
  803. if (!file_exists($filePath)) {
  804. $this->log("Файл ttn.xlsx не найден, пропуск", 'WARNING');
  805. return;
  806. }
  807. $spreadsheet = IOFactory::load($filePath);
  808. $sheet = $spreadsheet->getActiveSheet();
  809. $rows = $sheet->toArray();
  810. $headers = array_shift($rows);
  811. $headerMap = array_flip($headers);
  812. $count = 0;
  813. foreach ($rows as $row) {
  814. if (empty($row[$headerMap['id']])) {
  815. continue;
  816. }
  817. // Импорт файла ТТН если есть
  818. $fileId = null;
  819. $ttnFilePath = $this->getValue($row, $headerMap, 'file_path');
  820. if ($ttnFilePath) {
  821. $fileId = $this->importFile($ttnFilePath);
  822. }
  823. Ttn::create([
  824. 'year' => $this->year,
  825. 'ttn_number' => $this->getStringValue($row, $headerMap, 'ttn_number', ''),
  826. 'ttn_number_suffix' => $this->getStringValue($row, $headerMap, 'ttn_number_suffix', ''),
  827. 'order_number' => $this->getStringValue($row, $headerMap, 'order_number', ''),
  828. 'order_date' => $this->getValue($row, $headerMap, 'order_date'),
  829. 'departure_date' => $this->getValue($row, $headerMap, 'departure_date'),
  830. 'order_sum' => $this->getNumericValue($row, $headerMap, 'order_sum', 0),
  831. 'skus' => $this->getValue($row, $headerMap, 'skus'),
  832. 'file_id' => $fileId,
  833. ]);
  834. $count++;
  835. }
  836. $this->log("Импортировано ТТН: {$count}");
  837. }
  838. private function importPivotTables(): void
  839. {
  840. $this->log("Импорт pivot таблиц (связей файлов)...");
  841. $filePath = $this->tempDir . '/data/pivot_tables.xlsx';
  842. if (!file_exists($filePath)) {
  843. $this->log("Файл pivot_tables.xlsx не найден, пропуск", 'WARNING');
  844. return;
  845. }
  846. $spreadsheet = IOFactory::load($filePath);
  847. // Order photos
  848. $this->importPivotSheet($spreadsheet, 'order_photo', 'order_id', $this->orderIdMapping);
  849. // Order documents
  850. $this->importPivotSheet($spreadsheet, 'order_document', 'order_id', $this->orderIdMapping);
  851. // Order statements
  852. $this->importPivotSheet($spreadsheet, 'order_statement', 'order_id', $this->orderIdMapping);
  853. // Reclamation photos before
  854. $this->importPivotSheet($spreadsheet, 'reclamation_photo_before', 'reclamation_id', $this->reclamationIdMapping);
  855. // Reclamation photos after
  856. $this->importPivotSheet($spreadsheet, 'reclamation_photo_after', 'reclamation_id', $this->reclamationIdMapping);
  857. // Reclamation documents
  858. $this->importPivotSheet($spreadsheet, 'reclamation_document', 'reclamation_id', $this->reclamationIdMapping);
  859. // Reclamation acts
  860. $this->importPivotSheet($spreadsheet, 'reclamation_act', 'reclamation_id', $this->reclamationIdMapping);
  861. }
  862. private function importPivotSheet(
  863. Spreadsheet $spreadsheet,
  864. string $sheetName,
  865. string $foreignKey,
  866. array $idMapping
  867. ): void {
  868. $sheet = $spreadsheet->getSheetByName($sheetName);
  869. if (!$sheet) {
  870. return;
  871. }
  872. $rows = $sheet->toArray();
  873. $headers = array_shift($rows);
  874. $headerMap = array_flip($headers);
  875. $count = 0;
  876. foreach ($rows as $row) {
  877. $oldEntityId = $row[$headerMap[$foreignKey]] ?? null;
  878. $archivePath = $row[$headerMap['file_archive_path']] ?? null;
  879. if (!$oldEntityId || !$archivePath) {
  880. continue;
  881. }
  882. $newEntityId = $idMapping[$oldEntityId] ?? null;
  883. if (!$newEntityId) {
  884. continue;
  885. }
  886. // Импортируем файл
  887. $fileId = $this->importFile($archivePath);
  888. if (!$fileId) {
  889. continue;
  890. }
  891. // Вставляем связь
  892. DB::table($sheetName)->insert([
  893. $foreignKey => $newEntityId,
  894. 'file_id' => $fileId,
  895. ]);
  896. $count++;
  897. }
  898. if ($count > 0) {
  899. $this->log("Импортировано связей {$sheetName}: {$count}");
  900. }
  901. }
  902. private function importFile(string $archivePath): ?int
  903. {
  904. // Проверяем кэш
  905. if (isset($this->fileIdMapping[$archivePath])) {
  906. return $this->fileIdMapping[$archivePath];
  907. }
  908. $sourcePath = $this->tempDir . '/' . $archivePath;
  909. if (!file_exists($sourcePath)) {
  910. $this->log("Файл не найден: {$archivePath}", 'WARNING');
  911. return null;
  912. }
  913. // Определяем путь для сохранения
  914. $pathParts = explode('/', $archivePath);
  915. array_shift($pathParts); // Убираем 'files'
  916. $relativePath = implode('/', $pathParts);
  917. $fileName = basename($archivePath);
  918. $targetPath = dirname($relativePath) . '/' . $fileName;
  919. // Сохраняем файл
  920. $content = file_get_contents($sourcePath);
  921. Storage::disk('public')->put($targetPath, $content);
  922. // Создаем запись в БД
  923. $originalName = preg_replace('/^\d+_/', '', $fileName); // Убираем ID из имени
  924. $mimeType = mime_content_type($sourcePath) ?: 'application/octet-stream';
  925. $file = File::create([
  926. 'user_id' => $this->userId,
  927. 'original_name' => $originalName,
  928. 'mime_type' => $mimeType,
  929. 'path' => $targetPath,
  930. 'link' => url('/storage/' . $targetPath),
  931. ]);
  932. $this->fileIdMapping[$archivePath] = $file->id;
  933. return $file->id;
  934. }
  935. private function cleanupTempDirectory(): void
  936. {
  937. if (is_dir($this->tempDir)) {
  938. $this->deleteDirectory($this->tempDir);
  939. }
  940. }
  941. private function deleteDirectory(string $dir): void
  942. {
  943. if (!is_dir($dir)) {
  944. return;
  945. }
  946. $files = array_diff(scandir($dir), ['.', '..']);
  947. foreach ($files as $file) {
  948. $path = $dir . '/' . $file;
  949. is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
  950. }
  951. rmdir($dir);
  952. }
  953. private function log(string $message, string $level = 'INFO'): void
  954. {
  955. $this->logs[] = [
  956. 'level' => $level,
  957. 'message' => $message,
  958. 'timestamp' => now()->toIso8601String(),
  959. ];
  960. if ($level === 'ERROR') {
  961. Log::error("ImportYearDataService: {$message}");
  962. } else {
  963. Log::info("ImportYearDataService: {$message}");
  964. }
  965. }
  966. public function getLogs(): array
  967. {
  968. return $this->logs;
  969. }
  970. }