ImportYearDataService.php 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  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::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
  172. $productIds = Product::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->pluck('id');
  173. $productSkuIds = ProductSKU::withoutGlobalScopes()->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::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
  193. // Заказы
  194. Order::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
  195. // МАФ заказы
  196. MafOrder::withoutGlobalScopes()->withTrashed()->where('year', $this->year)->forceDelete();
  197. // Продукты
  198. Product::withoutGlobalScopes()->withTrashed()
  199. ->whereIn('id', $productIds)
  200. ->update(['certificate_id' => null]);
  201. Product::withoutGlobalScopes()->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::withoutGlobalScopes()->withTrashed()
  224. ->whereIn('id', $productIds)
  225. ->whereNotNull('certificate_id')
  226. ->pluck('certificate_id')
  227. );
  228. $fileIds = $fileIds->merge(
  229. ProductSKU::withoutGlobalScopes()->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 ($value === null || $value === '') {
  260. return $default;
  261. }
  262. return $value;
  263. }
  264. /**
  265. * Получает строковое значение (пустая строка вместо null)
  266. */
  267. private function getStringValue(array $row, array $headerMap, string $key, string $default = ''): string
  268. {
  269. $value = $this->getValue($row, $headerMap, $key);
  270. return $value !== null ? (string) $value : $default;
  271. }
  272. /**
  273. * Получает числовое значение
  274. */
  275. private function getNumericValue(array $row, array $headerMap, string $key, int|float $default = 0): int|float
  276. {
  277. $value = $this->getValue($row, $headerMap, $key);
  278. if ($value === null) {
  279. return $default;
  280. }
  281. return is_numeric($value) ? $value : $default;
  282. }
  283. private function importProducts(): void
  284. {
  285. $this->log("Импорт продуктов...");
  286. $filePath = $this->tempDir . '/data/products.xlsx';
  287. if (!file_exists($filePath)) {
  288. $this->log("Файл products.xlsx не найден, пропуск", 'WARNING');
  289. return;
  290. }
  291. $spreadsheet = IOFactory::load($filePath);
  292. $sheet = $spreadsheet->getActiveSheet();
  293. $rows = $sheet->toArray();
  294. $headers = array_shift($rows);
  295. $headerMap = array_flip($headers);
  296. $count = 0;
  297. foreach ($rows as $row) {
  298. if (empty($row[$headerMap['id']])) {
  299. continue;
  300. }
  301. $oldId = $this->getValue($row, $headerMap, 'id');
  302. $nomenclatureNumber = $this->getStringValue($row, $headerMap, 'nomenclature_number', '');
  303. // Проверяем существует ли продукт
  304. $existing = Product::withoutGlobalScopes()
  305. ->where('year', $this->year)
  306. ->where('nomenclature_number', $nomenclatureNumber)
  307. ->first();
  308. // Импорт сертификата если есть
  309. $certificateId = null;
  310. $certificatePath = $this->getValue($row, $headerMap, 'certificate_file');
  311. if ($certificatePath) {
  312. $certificateId = $this->importFile($certificatePath);
  313. }
  314. $productData = [
  315. 'year' => $this->year,
  316. 'article' => $this->getStringValue($row, $headerMap, 'article'),
  317. 'name_tz' => $this->getValue($row, $headerMap, 'name_tz'),
  318. 'type_tz' => $this->getValue($row, $headerMap, 'type_tz'),
  319. 'nomenclature_number' => $nomenclatureNumber,
  320. 'sizes' => $this->getValue($row, $headerMap, 'sizes'),
  321. 'manufacturer' => $this->getValue($row, $headerMap, 'manufacturer'),
  322. 'unit' => $this->getStringValue($row, $headerMap, 'unit', 'шт.'),
  323. 'type' => $this->getStringValue($row, $headerMap, 'type', 'Товар'),
  324. 'product_price' => $this->getNumericValue($row, $headerMap, 'product_price', 0),
  325. 'installation_price' => $this->getNumericValue($row, $headerMap, 'installation_price', 0),
  326. 'total_price' => $this->getNumericValue($row, $headerMap, 'total_price', 0),
  327. 'manufacturer_name' => $this->getValue($row, $headerMap, 'manufacturer_name'),
  328. 'note' => $this->getValue($row, $headerMap, 'note'),
  329. 'passport_name' => $this->getValue($row, $headerMap, 'passport_name'),
  330. 'statement_name' => $this->getValue($row, $headerMap, 'statement_name'),
  331. 'service_life' => $this->getValue($row, $headerMap, 'service_life'),
  332. 'certificate_number' => $this->getValue($row, $headerMap, 'certificate_number'),
  333. 'certificate_date' => $this->getValue($row, $headerMap, 'certificate_date'),
  334. 'certificate_issuer' => $this->getValue($row, $headerMap, 'certificate_issuer'),
  335. 'certificate_type' => $this->getValue($row, $headerMap, 'certificate_type'),
  336. 'weight' => $this->getNumericValue($row, $headerMap, 'weight', 0),
  337. 'volume' => $this->getNumericValue($row, $headerMap, 'volume', 0),
  338. 'places' => $this->getNumericValue($row, $headerMap, 'places', 0),
  339. 'certificate_id' => $certificateId,
  340. ];
  341. if ($existing) {
  342. $existing->update($productData);
  343. $this->productIdMapping[$oldId] = $existing->id;
  344. } else {
  345. $product = Product::withoutGlobalScopes()->create($productData);
  346. $this->productIdMapping[$oldId] = $product->id;
  347. }
  348. $count++;
  349. }
  350. $this->log("Импортировано продуктов: {$count}");
  351. }
  352. private function importMafOrders(): void
  353. {
  354. $this->log("Импорт заказов МАФ...");
  355. $filePath = $this->tempDir . '/data/maf_orders.xlsx';
  356. if (!file_exists($filePath)) {
  357. $this->log("Файл maf_orders.xlsx не найден, пропуск", 'WARNING');
  358. return;
  359. }
  360. $spreadsheet = IOFactory::load($filePath);
  361. $sheet = $spreadsheet->getActiveSheet();
  362. $rows = $sheet->toArray();
  363. $headers = array_shift($rows);
  364. $headerMap = array_flip($headers);
  365. $count = 0;
  366. foreach ($rows as $row) {
  367. if (empty($row[$headerMap['id']])) {
  368. continue;
  369. }
  370. $oldId = $this->getValue($row, $headerMap, 'id');
  371. $oldProductId = $this->getValue($row, $headerMap, 'product_id');
  372. $orderNumber = $this->getStringValue($row, $headerMap, 'order_number', '');
  373. // Получаем новый product_id
  374. $newProductId = $this->productIdMapping[$oldProductId] ?? null;
  375. if (!$newProductId) {
  376. // Пробуем найти по номенклатуре
  377. $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
  378. if ($nomenclature) {
  379. $product = Product::withoutGlobalScopes()
  380. ->where('year', $this->year)
  381. ->where('nomenclature_number', $nomenclature)
  382. ->first();
  383. $newProductId = $product?->id;
  384. }
  385. }
  386. if (!$newProductId) {
  387. $this->log("Пропуск MafOrder {$oldId}: продукт не найден", 'WARNING');
  388. continue;
  389. }
  390. // Проверяем существует ли
  391. $existing = MafOrder::withoutGlobalScopes()
  392. ->where('year', $this->year)
  393. ->where('product_id', $newProductId)
  394. ->where('order_number', $orderNumber)
  395. ->first();
  396. // Получаем user_id
  397. $userName = $this->getValue($row, $headerMap, 'user_name');
  398. $userId = $this->userMapping[$userName] ?? $this->userId;
  399. $mafOrderData = [
  400. 'year' => $this->year,
  401. 'order_number' => $orderNumber,
  402. 'status' => $this->getValue($row, $headerMap, 'status'),
  403. 'user_id' => $userId,
  404. 'product_id' => $newProductId,
  405. 'quantity' => $this->getNumericValue($row, $headerMap, 'quantity', 0),
  406. 'in_stock' => $this->getNumericValue($row, $headerMap, 'in_stock', 0),
  407. ];
  408. if ($existing) {
  409. $existing->update($mafOrderData);
  410. $this->mafOrderIdMapping[$oldId] = $existing->id;
  411. } else {
  412. $mafOrder = MafOrder::withoutGlobalScopes()->create($mafOrderData);
  413. $this->mafOrderIdMapping[$oldId] = $mafOrder->id;
  414. }
  415. $count++;
  416. }
  417. $this->log("Импортировано заказов МАФ: {$count}");
  418. }
  419. private function importOrders(): void
  420. {
  421. $this->log("Импорт заказов (площадок)...");
  422. $filePath = $this->tempDir . '/data/orders.xlsx';
  423. if (!file_exists($filePath)) {
  424. $this->log("Файл orders.xlsx не найден, пропуск", 'WARNING');
  425. return;
  426. }
  427. $spreadsheet = IOFactory::load($filePath);
  428. $sheet = $spreadsheet->getActiveSheet();
  429. $rows = $sheet->toArray();
  430. $headers = array_shift($rows);
  431. $headerMap = array_flip($headers);
  432. $count = 0;
  433. foreach ($rows as $row) {
  434. if (empty($row[$headerMap['id']])) {
  435. continue;
  436. }
  437. $oldId = $this->getValue($row, $headerMap, 'id');
  438. $objectAddress = $this->getStringValue($row, $headerMap, 'object_address', '');
  439. // Проверяем существует ли
  440. $existing = Order::withoutGlobalScopes()
  441. ->where('year', $this->year)
  442. ->where('object_address', $objectAddress)
  443. ->first();
  444. // Маппинг справочников
  445. $districtShortname = $this->getValue($row, $headerMap, 'district_shortname');
  446. $districtId = $this->districtMapping[$districtShortname] ?? null;
  447. $areaName = $this->getValue($row, $headerMap, 'area_name');
  448. $areaId = $this->areaMapping[$areaName] ?? null;
  449. $userName = $this->getValue($row, $headerMap, 'user_name');
  450. $userId = $this->userMapping[$userName] ?? $this->userId;
  451. $objectTypeName = $this->getValue($row, $headerMap, 'object_type_name');
  452. $objectTypeId = $this->objectTypeMapping[$objectTypeName] ?? null;
  453. $brigadierName = $this->getValue($row, $headerMap, 'brigadier_name');
  454. $brigadierId = $this->userMapping[$brigadierName] ?? null;
  455. $orderStatusName = $this->getValue($row, $headerMap, 'order_status_name');
  456. $orderStatusId = $this->orderStatusMapping[$orderStatusName] ?? Order::STATUS_NEW;
  457. $orderData = [
  458. 'year' => $this->year,
  459. 'name' => $this->getValue($row, $headerMap, 'name'),
  460. 'user_id' => $userId,
  461. 'district_id' => $districtId,
  462. 'area_id' => $areaId,
  463. 'object_address' => $objectAddress,
  464. 'object_type_id' => $objectTypeId,
  465. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  466. 'installation_date' => $this->getValue($row, $headerMap, 'installation_date'),
  467. 'ready_date' => $this->getValue($row, $headerMap, 'ready_date'),
  468. 'brigadier_id' => $brigadierId,
  469. 'order_status_id' => $orderStatusId,
  470. 'tg_group_name' => $this->getValue($row, $headerMap, 'tg_group_name'),
  471. 'tg_group_link' => $this->getValue($row, $headerMap, 'tg_group_link'),
  472. 'ready_to_mount' => $this->getValue($row, $headerMap, 'ready_to_mount'),
  473. 'install_days' => $this->getValue($row, $headerMap, 'install_days'),
  474. ];
  475. if ($existing) {
  476. $existing->update($orderData);
  477. $this->orderIdMapping[$oldId] = $existing->id;
  478. } else {
  479. $order = Order::withoutGlobalScopes()->create($orderData);
  480. $this->orderIdMapping[$oldId] = $order->id;
  481. }
  482. $count++;
  483. }
  484. $this->log("Импортировано заказов: {$count}");
  485. }
  486. private function importProductsSku(): void
  487. {
  488. $this->log("Импорт SKU продуктов...");
  489. $filePath = $this->tempDir . '/data/products_sku.xlsx';
  490. if (!file_exists($filePath)) {
  491. $this->log("Файл products_sku.xlsx не найден, пропуск", 'WARNING');
  492. return;
  493. }
  494. $spreadsheet = IOFactory::load($filePath);
  495. $sheet = $spreadsheet->getActiveSheet();
  496. $rows = $sheet->toArray();
  497. $headers = array_shift($rows);
  498. $headerMap = array_flip($headers);
  499. $count = 0;
  500. foreach ($rows as $row) {
  501. if (empty($row[$headerMap['id']])) {
  502. continue;
  503. }
  504. $oldId = $this->getValue($row, $headerMap, 'id');
  505. // Маппинг product_id
  506. $oldProductId = $this->getValue($row, $headerMap, 'product_id');
  507. $newProductId = $this->productIdMapping[$oldProductId] ?? null;
  508. if (!$newProductId) {
  509. // Пробуем найти по номенклатуре
  510. $nomenclature = $this->getValue($row, $headerMap, 'product_nomenclature');
  511. if ($nomenclature) {
  512. $product = Product::withoutGlobalScopes()
  513. ->where('year', $this->year)
  514. ->where('nomenclature_number', $nomenclature)
  515. ->first();
  516. $newProductId = $product?->id;
  517. }
  518. }
  519. if (!$newProductId) {
  520. $this->log("Пропуск SKU {$oldId}: продукт не найден", 'WARNING');
  521. continue;
  522. }
  523. // Маппинг order_id
  524. $oldOrderId = $this->getValue($row, $headerMap, 'order_id');
  525. $newOrderId = $this->orderIdMapping[$oldOrderId] ?? null;
  526. if (!$newOrderId) {
  527. // Пробуем найти по адресу
  528. $orderAddress = $this->getValue($row, $headerMap, 'order_address');
  529. if ($orderAddress) {
  530. $order = Order::withoutGlobalScopes()
  531. ->where('year', $this->year)
  532. ->where('object_address', $orderAddress)
  533. ->first();
  534. $newOrderId = $order?->id;
  535. }
  536. }
  537. if (!$newOrderId) {
  538. $this->log("Пропуск SKU {$oldId}: заказ не найден", 'WARNING');
  539. continue;
  540. }
  541. // Маппинг maf_order_id
  542. $oldMafOrderId = $this->getValue($row, $headerMap, 'maf_order_id');
  543. $newMafOrderId = null;
  544. if ($oldMafOrderId) {
  545. $newMafOrderId = $this->mafOrderIdMapping[$oldMafOrderId] ?? null;
  546. if (!$newMafOrderId) {
  547. // Пробуем найти по номеру заказа
  548. $mafOrderNumber = $this->getValue($row, $headerMap, 'maf_order_number');
  549. if ($mafOrderNumber) {
  550. $mafOrder = MafOrder::withoutGlobalScopes()
  551. ->where('year', $this->year)
  552. ->where('product_id', $newProductId)
  553. ->where('order_number', $mafOrderNumber)
  554. ->first();
  555. $newMafOrderId = $mafOrder?->id;
  556. }
  557. }
  558. }
  559. // Импорт паспорта если есть
  560. $passportId = null;
  561. $passportPath = $this->getValue($row, $headerMap, 'passport_file');
  562. if ($passportPath) {
  563. $passportId = $this->importFile($passportPath);
  564. }
  565. $skuData = [
  566. 'year' => $this->year,
  567. 'product_id' => $newProductId,
  568. 'order_id' => $newOrderId,
  569. 'maf_order_id' => $newMafOrderId,
  570. 'status' => $this->getValue($row, $headerMap, 'status'),
  571. 'rfid' => $this->getValue($row, $headerMap, 'rfid'),
  572. 'factory_number' => $this->getValue($row, $headerMap, 'factory_number'),
  573. 'manufacture_date' => $this->getValue($row, $headerMap, 'manufacture_date'),
  574. 'statement_number' => $this->getValue($row, $headerMap, 'statement_number'),
  575. 'statement_date' => $this->getValue($row, $headerMap, 'statement_date'),
  576. 'upd_number' => $this->getValue($row, $headerMap, 'upd_number'),
  577. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  578. 'passport_id' => $passportId,
  579. ];
  580. $sku = ProductSKU::withoutGlobalScopes()->create($skuData);
  581. $this->productSkuIdMapping[$oldId] = $sku->id;
  582. $count++;
  583. }
  584. $this->log("Импортировано SKU: {$count}");
  585. }
  586. private function importReclamations(): void
  587. {
  588. $this->log("Импорт рекламаций...");
  589. $filePath = $this->tempDir . '/data/reclamations.xlsx';
  590. if (!file_exists($filePath)) {
  591. $this->log("Файл reclamations.xlsx не найден, пропуск", 'WARNING');
  592. return;
  593. }
  594. $spreadsheet = IOFactory::load($filePath);
  595. // Лист 1: Reclamations
  596. $sheet = $spreadsheet->getSheetByName('Reclamations');
  597. if (!$sheet) {
  598. $this->log("Лист Reclamations не найден", 'WARNING');
  599. return;
  600. }
  601. $rows = $sheet->toArray();
  602. $headers = array_shift($rows);
  603. $headerMap = array_flip($headers);
  604. $count = 0;
  605. foreach ($rows as $row) {
  606. if (empty($row[$headerMap['id']])) {
  607. continue;
  608. }
  609. $oldId = $this->getValue($row, $headerMap, 'id');
  610. // Маппинг order_id
  611. $oldOrderId = $this->getValue($row, $headerMap, 'order_id');
  612. $newOrderId = $this->orderIdMapping[$oldOrderId] ?? null;
  613. if (!$newOrderId) {
  614. // Пробуем найти по адресу
  615. $orderAddress = $this->getValue($row, $headerMap, 'order_address');
  616. if ($orderAddress) {
  617. $order = Order::withoutGlobalScopes()
  618. ->where('year', $this->year)
  619. ->where('object_address', $orderAddress)
  620. ->first();
  621. $newOrderId = $order?->id;
  622. }
  623. }
  624. if (!$newOrderId) {
  625. $this->log("Пропуск рекламации {$oldId}: заказ не найден", 'WARNING');
  626. continue;
  627. }
  628. $userName = $this->getValue($row, $headerMap, 'user_name');
  629. $userId = $this->userMapping[$userName] ?? $this->userId;
  630. $brigadierName = $this->getValue($row, $headerMap, 'brigadier_name');
  631. $brigadierId = $this->userMapping[$brigadierName] ?? null;
  632. $statusName = $this->getValue($row, $headerMap, 'status_name');
  633. $statusId = $this->reclamationStatusMapping[$statusName] ?? Reclamation::STATUS_NEW;
  634. $reclamationData = [
  635. 'order_id' => $newOrderId,
  636. 'user_id' => $userId,
  637. 'status_id' => $statusId,
  638. 'reason' => $this->getValue($row, $headerMap, 'reason'),
  639. 'guarantee' => $this->getValue($row, $headerMap, 'guarantee'),
  640. 'whats_done' => $this->getValue($row, $headerMap, 'whats_done'),
  641. 'create_date' => $this->getValue($row, $headerMap, 'create_date'),
  642. 'finish_date' => $this->getValue($row, $headerMap, 'finish_date'),
  643. 'start_work_date' => $this->getValue($row, $headerMap, 'start_work_date'),
  644. 'work_days' => $this->getValue($row, $headerMap, 'work_days'),
  645. 'brigadier_id' => $brigadierId,
  646. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  647. ];
  648. $reclamation = Reclamation::create($reclamationData);
  649. $this->reclamationIdMapping[$oldId] = $reclamation->id;
  650. $count++;
  651. }
  652. $this->log("Импортировано рекламаций: {$count}");
  653. // Лист 2: ReclamationDetails
  654. $this->importReclamationDetails($spreadsheet);
  655. // Лист 3: ReclamationSKU
  656. $this->importReclamationSkuRelations($spreadsheet);
  657. }
  658. private function importReclamationDetails(Spreadsheet $spreadsheet): void
  659. {
  660. $sheet = $spreadsheet->getSheetByName('ReclamationDetails');
  661. if (!$sheet) {
  662. return;
  663. }
  664. $rows = $sheet->toArray();
  665. $headers = array_shift($rows);
  666. $headerMap = array_flip($headers);
  667. $count = 0;
  668. foreach ($rows as $row) {
  669. if (empty($row[$headerMap['id']])) {
  670. continue;
  671. }
  672. $oldReclamationId = $this->getValue($row, $headerMap, 'reclamation_id');
  673. $newReclamationId = $this->reclamationIdMapping[$oldReclamationId] ?? null;
  674. if (!$newReclamationId) {
  675. continue;
  676. }
  677. ReclamationDetail::create([
  678. 'reclamation_id' => $newReclamationId,
  679. 'name' => $this->getStringValue($row, $headerMap, 'name', ''),
  680. 'quantity' => $this->getNumericValue($row, $headerMap, 'quantity', 0),
  681. ]);
  682. $count++;
  683. }
  684. $this->log("Импортировано деталей рекламаций: {$count}");
  685. }
  686. private function importReclamationSkuRelations(Spreadsheet $spreadsheet): void
  687. {
  688. $sheet = $spreadsheet->getSheetByName('ReclamationSKU');
  689. if (!$sheet) {
  690. return;
  691. }
  692. $rows = $sheet->toArray();
  693. $headers = array_shift($rows);
  694. $headerMap = array_flip($headers);
  695. $count = 0;
  696. foreach ($rows as $row) {
  697. $oldReclamationId = $this->getValue($row, $headerMap, 'reclamation_id');
  698. $oldProductSkuId = $this->getValue($row, $headerMap, 'product_sku_id');
  699. if (!$oldReclamationId || !$oldProductSkuId) {
  700. continue;
  701. }
  702. $newReclamationId = $this->reclamationIdMapping[$oldReclamationId] ?? null;
  703. $newProductSkuId = $this->productSkuIdMapping[$oldProductSkuId] ?? null;
  704. if (!$newReclamationId || !$newProductSkuId) {
  705. continue;
  706. }
  707. DB::table('reclamation_product_sku')->insert([
  708. 'reclamation_id' => $newReclamationId,
  709. 'product_sku_id' => $newProductSkuId,
  710. ]);
  711. $count++;
  712. }
  713. $this->log("Импортировано связей рекламация-SKU: {$count}");
  714. }
  715. private function importSchedules(): void
  716. {
  717. $this->log("Импорт расписаний...");
  718. $filePath = $this->tempDir . '/data/schedules.xlsx';
  719. if (!file_exists($filePath)) {
  720. $this->log("Файл schedules.xlsx не найден, пропуск", 'WARNING');
  721. return;
  722. }
  723. $spreadsheet = IOFactory::load($filePath);
  724. $sheet = $spreadsheet->getActiveSheet();
  725. $rows = $sheet->toArray();
  726. $headers = array_shift($rows);
  727. $headerMap = array_flip($headers);
  728. $count = 0;
  729. foreach ($rows as $row) {
  730. if (empty($row[$headerMap['id']])) {
  731. continue;
  732. }
  733. $oldOrderId = $this->getValue($row, $headerMap, 'order_id');
  734. $newOrderId = $this->orderIdMapping[$oldOrderId] ?? null;
  735. if (!$newOrderId) {
  736. continue;
  737. }
  738. $districtShortname = $this->getValue($row, $headerMap, 'district_shortname');
  739. $districtId = $this->districtMapping[$districtShortname] ?? null;
  740. $areaName = $this->getValue($row, $headerMap, 'area_name');
  741. $areaId = $this->areaMapping[$areaName] ?? null;
  742. $brigadierName = $this->getValue($row, $headerMap, 'brigadier_name');
  743. $brigadierId = $this->userMapping[$brigadierName] ?? null;
  744. Schedule::create([
  745. 'installation_date' => $this->getValue($row, $headerMap, 'installation_date'),
  746. 'address_code' => $this->getValue($row, $headerMap, 'address_code'),
  747. 'manual' => $this->getValue($row, $headerMap, 'manual'),
  748. 'source' => $this->getValue($row, $headerMap, 'source'),
  749. 'order_id' => $newOrderId,
  750. 'district_id' => $districtId,
  751. 'area_id' => $areaId,
  752. 'object_address' => $this->getValue($row, $headerMap, 'object_address'),
  753. 'object_type' => $this->getValue($row, $headerMap, 'object_type'),
  754. 'mafs' => $this->getValue($row, $headerMap, 'mafs'),
  755. 'mafs_count' => $this->getNumericValue($row, $headerMap, 'mafs_count', 0),
  756. 'brigadier_id' => $brigadierId,
  757. 'comment' => $this->getValue($row, $headerMap, 'comment'),
  758. ]);
  759. $count++;
  760. }
  761. $this->log("Импортировано расписаний: {$count}");
  762. }
  763. private function importContracts(): void
  764. {
  765. $this->log("Импорт контрактов...");
  766. $filePath = $this->tempDir . '/data/contracts.xlsx';
  767. if (!file_exists($filePath)) {
  768. $this->log("Файл contracts.xlsx не найден, пропуск", 'WARNING');
  769. return;
  770. }
  771. $spreadsheet = IOFactory::load($filePath);
  772. $sheet = $spreadsheet->getActiveSheet();
  773. $rows = $sheet->toArray();
  774. $headers = array_shift($rows);
  775. $headerMap = array_flip($headers);
  776. $count = 0;
  777. foreach ($rows as $row) {
  778. if (empty($row[$headerMap['id']])) {
  779. continue;
  780. }
  781. Contract::create([
  782. 'year' => $this->year,
  783. 'contract_number' => $this->getStringValue($row, $headerMap, 'contract_number', ''),
  784. 'contract_date' => $this->getValue($row, $headerMap, 'contract_date'),
  785. ]);
  786. $count++;
  787. }
  788. $this->log("Импортировано контрактов: {$count}");
  789. }
  790. private function importTtn(): void
  791. {
  792. $this->log("Импорт ТТН...");
  793. $filePath = $this->tempDir . '/data/ttn.xlsx';
  794. if (!file_exists($filePath)) {
  795. $this->log("Файл ttn.xlsx не найден, пропуск", 'WARNING');
  796. return;
  797. }
  798. $spreadsheet = IOFactory::load($filePath);
  799. $sheet = $spreadsheet->getActiveSheet();
  800. $rows = $sheet->toArray();
  801. $headers = array_shift($rows);
  802. $headerMap = array_flip($headers);
  803. $count = 0;
  804. foreach ($rows as $row) {
  805. if (empty($row[$headerMap['id']])) {
  806. continue;
  807. }
  808. // Импорт файла ТТН если есть
  809. $fileId = null;
  810. $ttnFilePath = $this->getValue($row, $headerMap, 'file_path');
  811. if ($ttnFilePath) {
  812. $fileId = $this->importFile($ttnFilePath);
  813. }
  814. Ttn::create([
  815. 'year' => $this->year,
  816. 'ttn_number' => $this->getStringValue($row, $headerMap, 'ttn_number', ''),
  817. 'ttn_number_suffix' => $this->getStringValue($row, $headerMap, 'ttn_number_suffix', ''),
  818. 'order_number' => $this->getStringValue($row, $headerMap, 'order_number', ''),
  819. 'order_date' => $this->getValue($row, $headerMap, 'order_date'),
  820. 'order_sum' => $this->getNumericValue($row, $headerMap, 'order_sum', 0),
  821. 'skus' => $this->getValue($row, $headerMap, 'skus'),
  822. 'file_id' => $fileId,
  823. ]);
  824. $count++;
  825. }
  826. $this->log("Импортировано ТТН: {$count}");
  827. }
  828. private function importPivotTables(): void
  829. {
  830. $this->log("Импорт pivot таблиц (связей файлов)...");
  831. $filePath = $this->tempDir . '/data/pivot_tables.xlsx';
  832. if (!file_exists($filePath)) {
  833. $this->log("Файл pivot_tables.xlsx не найден, пропуск", 'WARNING');
  834. return;
  835. }
  836. $spreadsheet = IOFactory::load($filePath);
  837. // Order photos
  838. $this->importPivotSheet($spreadsheet, 'order_photo', 'order_id', $this->orderIdMapping);
  839. // Order documents
  840. $this->importPivotSheet($spreadsheet, 'order_document', 'order_id', $this->orderIdMapping);
  841. // Order statements
  842. $this->importPivotSheet($spreadsheet, 'order_statement', 'order_id', $this->orderIdMapping);
  843. // Reclamation photos before
  844. $this->importPivotSheet($spreadsheet, 'reclamation_photo_before', 'reclamation_id', $this->reclamationIdMapping);
  845. // Reclamation photos after
  846. $this->importPivotSheet($spreadsheet, 'reclamation_photo_after', 'reclamation_id', $this->reclamationIdMapping);
  847. // Reclamation documents
  848. $this->importPivotSheet($spreadsheet, 'reclamation_document', 'reclamation_id', $this->reclamationIdMapping);
  849. // Reclamation acts
  850. $this->importPivotSheet($spreadsheet, 'reclamation_act', 'reclamation_id', $this->reclamationIdMapping);
  851. }
  852. private function importPivotSheet(
  853. Spreadsheet $spreadsheet,
  854. string $sheetName,
  855. string $foreignKey,
  856. array $idMapping
  857. ): void {
  858. $sheet = $spreadsheet->getSheetByName($sheetName);
  859. if (!$sheet) {
  860. return;
  861. }
  862. $rows = $sheet->toArray();
  863. $headers = array_shift($rows);
  864. $headerMap = array_flip($headers);
  865. $count = 0;
  866. foreach ($rows as $row) {
  867. $oldEntityId = $row[$headerMap[$foreignKey]] ?? null;
  868. $archivePath = $row[$headerMap['file_archive_path']] ?? null;
  869. if (!$oldEntityId || !$archivePath) {
  870. continue;
  871. }
  872. $newEntityId = $idMapping[$oldEntityId] ?? null;
  873. if (!$newEntityId) {
  874. continue;
  875. }
  876. // Импортируем файл
  877. $fileId = $this->importFile($archivePath);
  878. if (!$fileId) {
  879. continue;
  880. }
  881. // Вставляем связь
  882. DB::table($sheetName)->insert([
  883. $foreignKey => $newEntityId,
  884. 'file_id' => $fileId,
  885. ]);
  886. $count++;
  887. }
  888. if ($count > 0) {
  889. $this->log("Импортировано связей {$sheetName}: {$count}");
  890. }
  891. }
  892. private function importFile(string $archivePath): ?int
  893. {
  894. // Проверяем кэш
  895. if (isset($this->fileIdMapping[$archivePath])) {
  896. return $this->fileIdMapping[$archivePath];
  897. }
  898. $sourcePath = $this->tempDir . '/' . $archivePath;
  899. if (!file_exists($sourcePath)) {
  900. $this->log("Файл не найден: {$archivePath}", 'WARNING');
  901. return null;
  902. }
  903. // Определяем путь для сохранения
  904. $pathParts = explode('/', $archivePath);
  905. array_shift($pathParts); // Убираем 'files'
  906. $relativePath = implode('/', $pathParts);
  907. $fileName = basename($archivePath);
  908. $targetPath = dirname($relativePath) . '/' . $fileName;
  909. // Сохраняем файл
  910. $content = file_get_contents($sourcePath);
  911. Storage::disk('public')->put($targetPath, $content);
  912. // Создаем запись в БД
  913. $originalName = preg_replace('/^\d+_/', '', $fileName); // Убираем ID из имени
  914. $mimeType = mime_content_type($sourcePath) ?: 'application/octet-stream';
  915. $file = File::create([
  916. 'user_id' => $this->userId,
  917. 'original_name' => $originalName,
  918. 'mime_type' => $mimeType,
  919. 'path' => $targetPath,
  920. 'link' => url('/storage/' . $targetPath),
  921. ]);
  922. $this->fileIdMapping[$archivePath] = $file->id;
  923. return $file->id;
  924. }
  925. private function cleanupTempDirectory(): void
  926. {
  927. if (is_dir($this->tempDir)) {
  928. $this->deleteDirectory($this->tempDir);
  929. }
  930. }
  931. private function deleteDirectory(string $dir): void
  932. {
  933. if (!is_dir($dir)) {
  934. return;
  935. }
  936. $files = array_diff(scandir($dir), ['.', '..']);
  937. foreach ($files as $file) {
  938. $path = $dir . '/' . $file;
  939. is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
  940. }
  941. rmdir($dir);
  942. }
  943. private function log(string $message, string $level = 'INFO'): void
  944. {
  945. $this->logs[] = [
  946. 'level' => $level,
  947. 'message' => $message,
  948. 'timestamp' => now()->toIso8601String(),
  949. ];
  950. if ($level === 'ERROR') {
  951. Log::error("ImportYearDataService: {$message}");
  952. } else {
  953. Log::info("ImportYearDataService: {$message}");
  954. }
  955. }
  956. public function getLogs(): array
  957. {
  958. return $this->logs;
  959. }
  960. }