ImportYearDataService.php 46 KB

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