ImportYearDataService.php 45 KB

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