class-studiou-wc-product-manage-product-import.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. <?php
  2. /**
  3. * Product import class
  4. *
  5. * Handles product import from CSV files
  6. *
  7. * @since 1.4.0
  8. * @package Studiou_WC_Product_Cat_Manage
  9. * @subpackage Studiou_WC_Product_Cat_Manage/includes
  10. */
  11. // If this file is called directly, abort.
  12. if (!defined('WPINC')) {
  13. die;
  14. }
  15. class Studiou_WC_Product_Manage_Product_Import {
  16. /**
  17. * @var Studiou_WC_Product_Manage_Product_DB
  18. */
  19. private $db;
  20. /**
  21. * Constructor
  22. *
  23. * @param Studiou_WC_Product_Manage_Product_DB $db Database instance
  24. */
  25. public function __construct($db) {
  26. $this->db = $db;
  27. }
  28. /**
  29. * Parse CSV file and store data for batch processing
  30. *
  31. * @param string $file_path Path to CSV file
  32. * @return array Result with total count and transient key
  33. */
  34. public function parse_csv_for_batch($file_path) {
  35. if (!file_exists($file_path)) {
  36. return array('error' => __('File not found', 'studiou-wc-product-cat-manage'));
  37. }
  38. // Open CSV file
  39. $handle = fopen($file_path, 'r');
  40. if (!$handle) {
  41. return array('error' => __('Could not open file', 'studiou-wc-product-cat-manage'));
  42. }
  43. // Read header row
  44. $header = fgetcsv($handle, 0, ',', '"');
  45. if (!$header) {
  46. fclose($handle);
  47. return array('error' => __('Invalid CSV format', 'studiou-wc-product-cat-manage'));
  48. }
  49. // Read all data rows
  50. $rows = array();
  51. $row_number = 1;
  52. while (($row = fgetcsv($handle, 0, ',', '"')) !== false) {
  53. $row_number++;
  54. // Skip empty rows
  55. if (empty(array_filter($row))) {
  56. continue;
  57. }
  58. // Combine header with row data
  59. $data = array_combine($header, $row);
  60. $rows[] = array(
  61. 'row_number' => $row_number,
  62. 'data' => $data
  63. );
  64. }
  65. fclose($handle);
  66. // Store data in transient (expires in 1 hour)
  67. $transient_key = 'studiou_wcpcm_import_' . uniqid();
  68. set_transient($transient_key, $rows, 3600);
  69. return array(
  70. 'transient_key' => $transient_key,
  71. 'total' => count($rows)
  72. );
  73. }
  74. /**
  75. * Process batch of products
  76. *
  77. * @param string $transient_key Transient key with CSV data
  78. * @param int $offset Starting offset
  79. * @param int $batch_size Number of items to process
  80. * @return array Result with statistics
  81. */
  82. public function process_batch($transient_key, $offset = 0, $batch_size = 5) {
  83. $rows = get_transient($transient_key);
  84. if (!$rows) {
  85. return array('error' => __('Import data not found or expired', 'studiou-wc-product-cat-manage'));
  86. }
  87. $result = array(
  88. 'success' => 0,
  89. 'failed' => 0,
  90. 'skipped' => 0,
  91. 'failed_items' => array(),
  92. 'messages' => array(),
  93. 'processed' => 0,
  94. 'total' => count($rows)
  95. );
  96. // Get batch of rows to process
  97. $batch = array_slice($rows, $offset, $batch_size);
  98. foreach ($batch as $item) {
  99. $row_number = $item['row_number'];
  100. $data = $item['data'];
  101. // Process the product
  102. $process_result = $this->process_product_row($data, $row_number);
  103. if ($process_result['status'] === 'success') {
  104. $result['success']++;
  105. // Don't add success messages to log
  106. } elseif ($process_result['status'] === 'skipped') {
  107. $result['skipped']++;
  108. $result['messages'][] = sprintf(
  109. __('Row %d: SKIPPED - %s', 'studiou-wc-product-cat-manage'),
  110. $row_number,
  111. $process_result['message']
  112. );
  113. // Add to failed items for re-processing
  114. $result['failed_items'][] = $data;
  115. } else {
  116. $result['failed']++;
  117. $result['messages'][] = sprintf(
  118. __('Row %d: FAILED - %s', 'studiou-wc-product-cat-manage'),
  119. $row_number,
  120. $process_result['message']
  121. );
  122. // Add to failed items
  123. $result['failed_items'][] = $data;
  124. }
  125. }
  126. $result['processed'] = $offset + count($batch);
  127. return $result;
  128. }
  129. /**
  130. * Process single product row
  131. *
  132. * @param array $data Product data
  133. * @param int $row_number Row number for error reporting
  134. * @return array Result with status and message
  135. */
  136. private function process_product_row($data, $row_number) {
  137. // Validate required fields
  138. if (!isset($data['Typ']) || empty($data['Typ'])) {
  139. return array(
  140. 'status' => 'skipped',
  141. 'message' => __('Missing product type', 'studiou-wc-product-cat-manage')
  142. );
  143. }
  144. // Rule 3: Only allow "variable" or "variation" types
  145. $type = trim($data['Typ']);
  146. if (!in_array($type, array('variable', 'variation'))) {
  147. return array(
  148. 'status' => 'skipped',
  149. 'message' => sprintf(
  150. __('Invalid product type: %s (only "variable" or "variation" allowed)', 'studiou-wc-product-cat-manage'),
  151. $type
  152. )
  153. );
  154. }
  155. // Get product ID
  156. $product_id = isset($data['ID']) ? intval($data['ID']) : 0;
  157. $sku = isset($data['Katalogové číslo']) ? trim($data['Katalogové číslo']) : '';
  158. // Rule 4: SKU must be unique
  159. if (empty($sku)) {
  160. return array(
  161. 'status' => 'skipped',
  162. 'message' => __('Missing SKU (Katalogové číslo)', 'studiou-wc-product-cat-manage')
  163. );
  164. }
  165. if (!$this->db->is_sku_unique($sku, $product_id)) {
  166. return array(
  167. 'status' => 'skipped',
  168. 'message' => sprintf(
  169. __('SKU "%s" already exists', 'studiou-wc-product-cat-manage'),
  170. $sku
  171. )
  172. );
  173. }
  174. // Rule 5: Validate image URL if provided
  175. $image_url = isset($data['Obrázky']) ? trim($data['Obrázky']) : '';
  176. if (!empty($image_url) && !$this->db->validate_image_url($image_url)) {
  177. return array(
  178. 'status' => 'skipped',
  179. 'message' => sprintf(
  180. __('Invalid image URL: %s', 'studiou-wc-product-cat-manage'),
  181. $image_url
  182. )
  183. );
  184. }
  185. // Process based on product type
  186. if ($type === 'variation') {
  187. return $this->process_variation($data, $product_id);
  188. } else {
  189. return $this->process_variable_product($data, $product_id);
  190. }
  191. }
  192. /**
  193. * Process variable product
  194. *
  195. * @param array $data Product data
  196. * @param int $product_id Product ID (0 for new product)
  197. * @return array Result with status and message
  198. */
  199. private function process_variable_product($data, $product_id) {
  200. try {
  201. // Create or update product
  202. if ($product_id == 0) {
  203. $product = new WC_Product_Variable();
  204. } else {
  205. $product = wc_get_product($product_id);
  206. if (!$product || !$product->is_type('variable')) {
  207. return array(
  208. 'status' => 'error',
  209. 'message' => sprintf(
  210. __('Product ID %d is not a variable product', 'studiou-wc-product-cat-manage'),
  211. $product_id
  212. )
  213. );
  214. }
  215. }
  216. // Set basic data
  217. $product->set_name(isset($data['Jméno']) ? $data['Jméno'] : '');
  218. $product->set_sku(isset($data['Katalogové číslo']) ? $data['Katalogové číslo'] : '');
  219. $product->set_short_description(isset($data['Krátký popis']) ? $data['Krátký popis'] : '');
  220. $product->set_description(isset($data['Popis']) ? $data['Popis'] : '');
  221. // Set categories
  222. if (isset($data['Kategorie']) && !empty($data['Kategorie'])) {
  223. $categories = array_map('trim', explode('|', $data['Kategorie']));
  224. $category_ids = array();
  225. error_log('STUDIOU WC IMPORT: Processing categories for product: ' . $product->get_name());
  226. error_log('STUDIOU WC IMPORT: Categories string from CSV: ' . $data['Kategorie']);
  227. foreach ($categories as $category_name) {
  228. if (empty($category_name)) {
  229. continue;
  230. }
  231. $category_id = $this->find_category_by_name($category_name);
  232. error_log('STUDIOU WC IMPORT: Looking for category "' . $category_name . '" - Result: ' . ($category_id ? 'FOUND ID=' . $category_id : 'NOT FOUND'));
  233. if ($category_id) {
  234. $category_ids[] = $category_id;
  235. }
  236. }
  237. error_log('STUDIOU WC IMPORT: Total category IDs found: ' . count($category_ids) . ' - IDs: ' . implode(',', $category_ids));
  238. if (!empty($category_ids)) {
  239. $product->set_category_ids($category_ids);
  240. error_log('STUDIOU WC IMPORT: Categories assigned to product via set_category_ids()');
  241. } else {
  242. error_log('STUDIOU WC IMPORT: WARNING - No valid category IDs found for: ' . $data['Kategorie']);
  243. }
  244. } else {
  245. error_log('STUDIOU WC IMPORT: No categories in CSV for product: ' . $product->get_name());
  246. }
  247. // Handle image
  248. if (isset($data['Obrázky']) && !empty($data['Obrázky'])) {
  249. $image_id = $this->upload_image_from_url($data['Obrázky']);
  250. if ($image_id) {
  251. $product->set_image_id($image_id);
  252. }
  253. }
  254. // Set reviews enabled
  255. $reviews_enabled = isset($data['Povolit zákaznické recenze?']) && $data['Povolit zákaznické recenze?'] === '1';
  256. $product->set_reviews_allowed($reviews_enabled);
  257. // Handle product attribute
  258. if (isset($data['Název 1 vlastnosti']) && !empty($data['Název 1 vlastnosti'])) {
  259. $attribute_name = $data['Název 1 vlastnosti'];
  260. $attribute_values = isset($data['Hodnota(y) 1 vlastnosti']) ? $data['Hodnota(y) 1 vlastnosti'] : '';
  261. $is_visible = isset($data['Vlastnost 1 viditelnost']) && $data['Vlastnost 1 viditelnost'] === '1';
  262. $is_variation = isset($data['Vlastnost 1 varianta']) && $data['Vlastnost 1 varianta'] === '1';
  263. // Create attribute taxonomy if it doesn't exist
  264. $this->db->create_attribute_if_not_exists($attribute_name);
  265. $attribute = new WC_Product_Attribute();
  266. $attribute->set_name(wc_attribute_taxonomy_name($attribute_name));
  267. $attribute->set_visible($is_visible);
  268. $attribute->set_variation($is_variation);
  269. // Set attribute options (terms)
  270. $values = array_map('trim', explode('|', $attribute_values));
  271. $attribute->set_options($values);
  272. $product->set_attributes(array($attribute));
  273. }
  274. // Handle minimum/maximum quantity
  275. if (isset($data['Minimum Quantity']) && !empty($data['Minimum Quantity'])) {
  276. $product->update_meta_data('_minimum_quantity', intval($data['Minimum Quantity']));
  277. }
  278. if (isset($data['Maximum Quantity']) && !empty($data['Maximum Quantity'])) {
  279. $product->update_meta_data('_maximum_quantity', intval($data['Maximum Quantity']));
  280. }
  281. // Save product
  282. $saved_id = $product->save();
  283. $message = $product_id == 0 ?
  284. sprintf(__('Created variable product: %s', 'studiou-wc-product-cat-manage'), $product->get_name()) :
  285. sprintf(__('Updated variable product: %s', 'studiou-wc-product-cat-manage'), $product->get_name());
  286. return array(
  287. 'status' => 'success',
  288. 'message' => $message
  289. );
  290. } catch (Exception $e) {
  291. return array(
  292. 'status' => 'error',
  293. 'message' => $e->getMessage()
  294. );
  295. }
  296. }
  297. /**
  298. * Process product variation
  299. *
  300. * @param array $data Product data
  301. * @param int $variation_id Variation ID (0 for new variation)
  302. * @return array Result with status and message
  303. */
  304. private function process_variation($data, $variation_id) {
  305. try {
  306. // Rule 6: Must have parent reference
  307. if (!isset($data['Nadřazené']) || empty($data['Nadřazené'])) {
  308. return array(
  309. 'status' => 'skipped',
  310. 'message' => __('Missing parent product reference (Nadřazené)', 'studiou-wc-product-cat-manage')
  311. );
  312. }
  313. // Rule 7: Must have regular price
  314. if (!isset($data['Běžná cena']) || empty($data['Běžná cena'])) {
  315. return array(
  316. 'status' => 'skipped',
  317. 'message' => __('Missing regular price (Běžná cena)', 'studiou-wc-product-cat-manage')
  318. );
  319. }
  320. // Find parent product by SKU
  321. $parent_sku = trim($data['Nadřazené']);
  322. $parent_product = $this->db->get_product_by_sku($parent_sku);
  323. if (!$parent_product || !$parent_product->is_type('variable')) {
  324. return array(
  325. 'status' => 'skipped',
  326. 'message' => sprintf(
  327. __('Parent product with SKU "%s" not found or not variable', 'studiou-wc-product-cat-manage'),
  328. $parent_sku
  329. )
  330. );
  331. }
  332. // Create or update variation
  333. if ($variation_id == 0) {
  334. $variation = new WC_Product_Variation();
  335. $variation->set_parent_id($parent_product->get_id());
  336. } else {
  337. $variation = wc_get_product($variation_id);
  338. if (!$variation || !$variation->is_type('variation')) {
  339. return array(
  340. 'status' => 'error',
  341. 'message' => sprintf(
  342. __('Product ID %d is not a variation', 'studiou-wc-product-cat-manage'),
  343. $variation_id
  344. )
  345. );
  346. }
  347. }
  348. // Set basic data
  349. $variation->set_name(isset($data['Jméno']) ? $data['Jméno'] : '');
  350. $variation->set_sku(isset($data['Katalogové číslo']) ? $data['Katalogové číslo'] : '');
  351. $variation->set_description(isset($data['Popis']) ? $data['Popis'] : '');
  352. $variation->set_regular_price($data['Běžná cena']);
  353. // Handle image
  354. if (isset($data['Obrázky']) && !empty($data['Obrázky'])) {
  355. $image_id = $this->upload_image_from_url($data['Obrázky']);
  356. if ($image_id) {
  357. $variation->set_image_id($image_id);
  358. }
  359. }
  360. // Set variation attributes
  361. if (isset($data['Název 1 vlastnosti']) && !empty($data['Název 1 vlastnosti']) &&
  362. isset($data['Hodnota(y) 1 vlastnosti']) && !empty($data['Hodnota(y) 1 vlastnosti'])) {
  363. $attribute_name = wc_attribute_taxonomy_name($data['Název 1 vlastnosti']);
  364. $attribute_value = $data['Hodnota(y) 1 vlastnosti'];
  365. $variation->set_attributes(array(
  366. $attribute_name => $attribute_value
  367. ));
  368. }
  369. // Handle minimum/maximum quantity
  370. if (isset($data['Minimum Quantity']) && !empty($data['Minimum Quantity'])) {
  371. $variation->update_meta_data('_minimum_quantity', intval($data['Minimum Quantity']));
  372. }
  373. if (isset($data['Maximum Quantity']) && !empty($data['Maximum Quantity'])) {
  374. $variation->update_meta_data('_maximum_quantity', intval($data['Maximum Quantity']));
  375. }
  376. // Save variation
  377. $saved_id = $variation->save();
  378. $message = $variation_id == 0 ?
  379. sprintf(__('Created variation: %s', 'studiou-wc-product-cat-manage'), $variation->get_name()) :
  380. sprintf(__('Updated variation: %s', 'studiou-wc-product-cat-manage'), $variation->get_name());
  381. return array(
  382. 'status' => 'success',
  383. 'message' => $message
  384. );
  385. } catch (Exception $e) {
  386. return array(
  387. 'status' => 'error',
  388. 'message' => $e->getMessage()
  389. );
  390. }
  391. }
  392. /**
  393. * Upload image from URL
  394. *
  395. * @param string $url Image URL
  396. * @return int|null Attachment ID or null on failure
  397. */
  398. private function upload_image_from_url($url) {
  399. require_once(ABSPATH . 'wp-admin/includes/media.php');
  400. require_once(ABSPATH . 'wp-admin/includes/file.php');
  401. require_once(ABSPATH . 'wp-admin/includes/image.php');
  402. // Set shorter timeout for image download (30 seconds)
  403. add_filter('http_request_timeout', function() { return 30; });
  404. try {
  405. $tmp = download_url($url, 30);
  406. if (is_wp_error($tmp)) {
  407. if (defined('WP_DEBUG') && WP_DEBUG) {
  408. error_log('STUDIOU WC: Image download failed for URL: ' . $url . ' - ' . $tmp->get_error_message());
  409. }
  410. return null;
  411. }
  412. $file_array = array(
  413. 'name' => basename($url),
  414. 'tmp_name' => $tmp
  415. );
  416. // Disable image processing to speed up import
  417. add_filter('intermediate_image_sizes_advanced', function() { return array(); });
  418. $id = media_handle_sideload($file_array, 0);
  419. // Re-enable image processing
  420. remove_filter('intermediate_image_sizes_advanced', function() { return array(); });
  421. if (is_wp_error($id)) {
  422. @unlink($file_array['tmp_name']);
  423. if (defined('WP_DEBUG') && WP_DEBUG) {
  424. error_log('STUDIOU WC: Image upload failed for URL: ' . $url . ' - ' . $id->get_error_message());
  425. }
  426. return null;
  427. }
  428. return $id;
  429. } catch (Exception $e) {
  430. if (defined('WP_DEBUG') && WP_DEBUG) {
  431. error_log('STUDIOU WC: Image upload exception for URL: ' . $url . ' - ' . $e->getMessage());
  432. }
  433. return null;
  434. }
  435. }
  436. /**
  437. * Find category by name (simple lookup, categories have unique names)
  438. *
  439. * @param string $category_name Category name
  440. * @return int|null Category ID or null if not found
  441. */
  442. private function find_category_by_name($category_name) {
  443. // Trim and normalize the category name
  444. $category_name = trim($category_name);
  445. if (empty($category_name)) {
  446. return null;
  447. }
  448. // Simple name lookup only (no hierarchy detection)
  449. // Categories have unique names throughout the system
  450. error_log('STUDIOU WC IMPORT: find_category_by_name() called for: "' . $category_name . '"');
  451. // Method 1: Exact name match
  452. $term = get_term_by('name', $category_name, 'product_cat');
  453. if ($term) {
  454. error_log('STUDIOU WC IMPORT: Found by exact name match - ID: ' . $term->term_id);
  455. return $term->term_id;
  456. }
  457. // Method 2: Try by slug
  458. $slug = sanitize_title($category_name);
  459. error_log('STUDIOU WC IMPORT: Exact name failed, trying slug: "' . $slug . '"');
  460. $term = get_term_by('slug', $slug, 'product_cat');
  461. if ($term) {
  462. error_log('STUDIOU WC IMPORT: Found by slug match - ID: ' . $term->term_id);
  463. return $term->term_id;
  464. }
  465. // Method 3: Search case-insensitively through all categories
  466. $all_terms = get_terms(array(
  467. 'taxonomy' => 'product_cat',
  468. 'hide_empty' => false
  469. ));
  470. error_log('STUDIOU WC IMPORT: Slug failed, searching ' . count($all_terms) . ' categories case-insensitively');
  471. foreach ($all_terms as $t) {
  472. if (strcasecmp($t->name, $category_name) === 0) {
  473. error_log('STUDIOU WC IMPORT: Found by case-insensitive match - ID: ' . $t->term_id . ', name: "' . $t->name . '"');
  474. return $t->term_id;
  475. }
  476. }
  477. // Not found - try to create it
  478. error_log('STUDIOU WC IMPORT: Category NOT FOUND: "' . $category_name . '" - Attempting to create it');
  479. $new_term = wp_insert_term(
  480. $category_name,
  481. 'product_cat',
  482. array(
  483. 'slug' => $slug
  484. )
  485. );
  486. if (is_wp_error($new_term)) {
  487. error_log('STUDIOU WC IMPORT: Failed to create category "' . $category_name . '": ' . $new_term->get_error_message());
  488. error_log('STUDIOU WC IMPORT: Total available categories: ' . count($all_terms));
  489. // Show first 20 categories for debugging
  490. $sample_cats = array_slice($all_terms, 0, 20);
  491. foreach ($sample_cats as $cat) {
  492. error_log('STUDIOU WC IMPORT: Available: "' . $cat->name . '" (ID: ' . $cat->term_id . ', slug: ' . $cat->slug . ')');
  493. }
  494. return null;
  495. } else {
  496. $term_id = $new_term['term_id'];
  497. error_log('STUDIOU WC IMPORT: Successfully created category "' . $category_name . '" with ID: ' . $term_id);
  498. return $term_id;
  499. }
  500. }
  501. /**
  502. * Generate CSV with failed items
  503. *
  504. * @param array $failed_items Array of failed product data
  505. * @return string CSV content
  506. */
  507. public function generate_failed_csv($failed_items) {
  508. if (empty($failed_items)) {
  509. return '';
  510. }
  511. // Define CSV header
  512. $header = array(
  513. 'ID', 'Typ', 'Katalogové číslo', 'Jméno', 'Nadřazené', 'Krátký popis', 'Popis',
  514. 'Název 1 vlastnosti', 'Hodnota(y) 1 vlastnosti', 'Kategorie', 'Obrázky', 'Běžná cena',
  515. 'Vlastnost 1 viditelnost', 'Vlastnost 1 globální', 'Vlastnost 1 varianta',
  516. 'Povolit zákaznické recenze?', 'Minimum Quantity', 'Maximum Quantity'
  517. );
  518. // Start CSV output
  519. $csv = '';
  520. $csv .= '"' . implode('","', $header) . '"' . "\n";
  521. // Add failed items
  522. foreach ($failed_items as $item) {
  523. $row = array();
  524. foreach ($header as $column) {
  525. $value = isset($item[$column]) ? $item[$column] : '';
  526. $row[] = '"' . str_replace('"', '""', $value) . '"';
  527. }
  528. $csv .= implode(',', $row) . "\n";
  529. }
  530. return $csv;
  531. }
  532. }