PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.7.0
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.7.0
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / includes / classes / zoho-inventory / class-import-items.php
commercebird / includes / classes / zoho-inventory Last commit date
class-cmbird-categories-zi.php 7 months ago class-cmbird-image-zi.php 5 months ago class-import-items.php 5 months ago class-import-price-list.php 9 months ago class-multi-currency.php 7 months ago class-order-sync.php 7 months ago class-product.php 5 months ago class-users-contact.php 9 months ago index.php 1 year ago
class-import-items.php
2267 lines
1 <?php
2
3 /**
4 * Class to import Products from Zoho to WooCommerce
5 *
6 * @package zoho_inventory_api
7 */
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
13
14 /**
15 * Class to import Products from Zoho Inventory to WooCommerce.
16 *
17 * Handles syncing of simple products, variable products, grouped items,
18 * and composite items from Zoho Inventory to WooCommerce.
19 *
20 * @package zoho_inventory_api
21 */
22 class CMBIRD_Products_ZI {
23
24 /**
25 * Configuration array for Zoho Inventory and WooCommerce settings.
26 *
27 * @var array
28 */
29 private $config;
30
31 /**
32 * Flag indicating whether WooCommerce taxes are enabled.
33 *
34 * @var bool
35 */
36 private $is_tax_enabled;
37
38 /**
39 * WooCommerce decimal separator setting.
40 *
41 * @var string
42 */
43 private $wc_decimal_separator;
44
45 /**
46 * WooCommerce thousand separator setting.
47 *
48 * @var string
49 */
50 private $wc_thousand_separator;
51
52 /**
53 * WooCommerce price decimal separator setting.
54 *
55 * @var int
56 */
57 private $wc_price_decimal_separator;
58
59 /**
60 * Constructor to initialize Zoho Inventory and WooCommerce configuration.
61 *
62 * Sets up API credentials, sync settings, and WooCommerce price formatting options.
63 * Exits early if WooCommerce is not active.
64 */
65 public function __construct() {
66 $wc_decimal_separator = function_exists( 'wc_get_price_decimal_separator' ) ? wc_get_price_decimal_separator() : '.';
67 $wc_thousand_separator = function_exists( 'wc_get_price_thousand_separator' ) ? wc_get_price_thousand_separator() : ',';
68 $wc_price_decimal_separator = function_exists( 'wc_get_price_decimals' ) ? wc_get_price_decimals() : 2;
69 $wc_tax_enabled = 'yes' === get_option( 'woocommerce_calc_taxes' );
70
71 $this->config = array(
72 'ProductZI' => array(
73 'OID' => get_option( 'cmbird_zoho_inventory_oid' ),
74 'APIURL' => get_option( 'cmbird_zoho_inventory_url' ),
75 ),
76 'Settings' => array(
77 'disable_description' => get_option( 'cmbird_zoho_disable_description_sync_status' ),
78 'disable_name' => get_option( 'cmbird_zoho_disable_name_sync_status' ),
79 'disable_price' => get_option( 'cmbird_zoho_disable_price_sync_status' ),
80 'disable_stock' => get_option( 'cmbird_zoho_disable_stock_sync_status' ),
81 'enable_accounting_stock' => get_option( 'cmbird_zoho_enable_accounting_stock_status' ),
82 'enable_location_stock' => get_option( 'cmbird_zoho_enable_locationstock_status' ),
83 'zoho_location_id' => get_option( 'cmbird_zoho_location_id_status' ),
84 'disable_image' => get_option( 'cmbird_zoho_disable_image_sync_status' ),
85 ),
86 // Get WooCommerce settings.
87 'WooCommerce' => array(
88 'decimal_separator' => $wc_decimal_separator,
89 'thousand_separator' => $wc_thousand_separator,
90 'price_decimal_separator' => $wc_price_decimal_separator,
91 'tax_enabled' => $wc_tax_enabled,
92 ),
93 );
94
95 // Check if WooCommerce taxes are enabled and store the result.
96 $this->is_tax_enabled = $this->config['WooCommerce']['tax_enabled'];
97
98 // Check the WooCommerce settings for decimal and thousand separators.
99 $this->wc_decimal_separator = $this->config['WooCommerce']['decimal_separator'];
100 $this->wc_thousand_separator = $this->config['WooCommerce']['thousand_separator'];
101 $this->wc_price_decimal_separator = $this->config['WooCommerce']['price_decimal_separator'];
102 }
103
104 /**
105 * Get the PHP memory limit in bytes.
106 *
107 * @return int Memory limit in bytes, or 0 if unlimited.
108 */
109 private function get_memory_limit() {
110 $memory_limit = ini_get( 'memory_limit' );
111 if ( '-1' === $memory_limit ) {
112 return 0; // Unlimited.
113 }
114
115 // Convert to bytes.
116 $unit = strtoupper( substr( $memory_limit, -1 ) );
117 $value = (int) $memory_limit;
118
119 switch ( $unit ) {
120 case 'G':
121 $value *= 1024 * 1024 * 1024;
122 break;
123 case 'M':
124 $value *= 1024 * 1024;
125 break;
126 case 'K':
127 $value *= 1024;
128 break;
129 }
130
131 return $value;
132 }
133
134 /**
135 * Function to retrieve item details and sync items.
136 *
137 * @param string $url - URL to get details.
138 * @return mixed return true if data false if error.
139 */
140 public function zi_item_bulk_sync( $url ) {
141 // $fd = fopen( __DIR__ . '/zi_item_bulk_sync.txt', 'a+' );
142
143 global $wpdb;
144 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
145 $json = $execute_curl_call->execute_curl_call_get( $url );
146 $code = $json->code;
147
148 $is_tax_enabled = $this->is_tax_enabled;
149
150 // Memory management: Set memory limit if needed.
151 if ( function_exists( 'wp_raise_memory_limit' ) ) {
152 wp_raise_memory_limit();
153 }
154
155 // $message = $json->message;
156 // fwrite($fd, PHP_EOL . '$json->item : ' . print_r($json, true));
157
158 if ( 0 === $code || '0' === $code ) {
159 $processed_count = 0;
160
161 foreach ( $json->items as $arr ) {
162 // Memory cleanup every 10 items (reduced from 50 for better memory management).
163 if ( $processed_count > 0 && $processed_count % 10 === 0 ) {
164 wp_cache_flush();
165 if ( function_exists( 'gc_collect_cycles' ) ) {
166 gc_collect_cycles();
167 }
168 }
169
170 // fwrite( $fd, PHP_EOL . '$arr : ' . print_r( $arr, true ) );
171 if ( ( ! empty( $arr->item_id ) ) && ! ( $arr->is_combo_product ) ) {
172 // fwrite($fd, PHP_EOL . 'Item Id found : ' . $arr->item_id);
173
174 $product_res = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key='zi_item_id' AND meta_value=%s", $arr->item_id ) );
175
176 if ( $product_res && ! empty( $product_res->post_id ) ) {
177 $pdt_id = $product_res->post_id;
178 // Load the Product Object.
179 $product = wc_get_product( $pdt_id );
180 if ( empty( $product ) || ! $product ) {
181 continue;
182 }
183
184 $zi_disable_item_description_sync = $this->config['Settings']['disable_description'];
185 if ( ! empty( $arr->description ) && ! $zi_disable_item_description_sync ) {
186 $product->set_short_description( $arr->description );
187 }
188
189 if ( ! empty( $arr->status ) ) {
190 $status = 'active' === $arr->status ? 'publish' : 'draft';
191 $product->set_status( $status );
192 }
193
194 $zi_disable_itemname_sync = $this->config['Settings']['disable_name'];
195 if ( ( ! $zi_disable_itemname_sync ) && ! empty( $arr->name ) ) {
196 $product->set_name( $arr->name );
197 // set slug but use sanitize_title to ensure it is safe.
198 $product->set_slug( sanitize_title( $arr->name ) );
199 }
200
201 if ( ! empty( $arr->sku ) ) {
202 $product->set_sku( $arr->sku );
203 }
204
205 $zi_disable_itemprice_sync = $this->config['Settings']['disable_price'];
206 if ( ! empty( $arr->rate ) && ! $zi_disable_itemprice_sync ) {
207 $product->set_regular_price( $arr->rate );
208 $sale_price = $product->get_sale_price();
209 if ( empty( $sale_price ) ) {
210 $product->set_price( $arr->rate );
211 }
212 }
213
214 if ( isset( $arr->package_details ) ) {
215 $details = $arr->package_details;
216 if ( is_object( $details ) ) {
217 $product->set_weight( floatval( $details->weight ) );
218 $product->set_length( floatval( $details->length ) );
219 $product->set_width( floatval( $details->width ) );
220 $product->set_height( floatval( $details->height ) );
221 }
222 }
223
224 // Update Purchase Rate as Cost Price.
225 $product->update_meta_data( '_cogs_total_value', $arr->purchase_rate );
226
227 // To check status of stock sync option.
228 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
229 if ( ! $zi_disable_stock_sync && isset( $arr->available_for_sale_stock ) ) {
230 $stock = '';
231 // Update stock.
232 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
233 // Sync from specific location check.
234 $zi_enable_locationstock = $this->config['Settings']['enable_location_stock'];
235 if ( $zi_enable_locationstock && isset( $arr->locations ) ) {
236 $locations = $arr->locations;
237 $location_id = $this->config['Settings']['zoho_location_id'];
238 foreach ( $locations as $location ) {
239 if ( $location->location_id === $location_id ) {
240 if ( $accounting_stock ) {
241 $stock = $location->location_available_for_sale_stock;
242 } else {
243 $stock = $location->location_actual_available_for_sale_stock;
244 }
245 }
246 }
247 } elseif ( $accounting_stock ) {
248 $stock = $arr->available_for_sale_stock;
249 } else {
250 $stock = $arr->actual_available_for_sale_stock;
251 }
252
253 if ( is_numeric( $stock ) ) {
254 $product->set_manage_stock( true );
255 $product->set_stock_quantity( $stock );
256 if ( $stock > 0 ) {
257 $product->set_stock_status( 'instock' );
258 } else {
259 $backorder_status = $product->backorders_allowed();
260 $status = ( 'yes' === $backorder_status ) ? 'onbackorder' : 'outofstock';
261 $product->set_stock_status( $status );
262 }
263 }
264 }
265
266 if ( ! empty( $arr->tax_id ) && ! $is_tax_enabled ) {
267 $zi_common_class = new CMBIRD_Common_Functions();
268 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $arr->tax_percentage );
269 $product->set_tax_status( 'taxable' );
270 $product->set_tax_class( $woo_tax_class );
271 }
272 $product->save();
273 // Sync ACF Fields.
274 $this->sync_item_custom_fields( $arr->custom_fields, $pdt_id );
275 // Clear/refresh cache.
276 wc_delete_product_transients( $pdt_id );
277 // Explicitly unset product object to free memory.
278 unset( $product );
279 }
280 }
281
282 ++$processed_count;
283 }
284
285 // Final memory cleanup after processing all items.
286 wp_cache_flush();
287 if ( function_exists( 'gc_collect_cycles' ) ) {
288 gc_collect_cycles();
289 }
290
291 // Clear any temporary variables.
292 unset( $json, $execute_curl_call );
293 } else {
294 return false;
295 }
296 // fclose( $fd );
297 // Return if synced.
298 return true;
299 }
300
301 /**
302 * Function to add items recursively by cron job.
303 *
304 * @param number $page - Page number for getting item with pagination.
305 * @param number $category - Category id to get item of specific category.
306 * @return mixed
307 */
308 public function sync_item_recursively() {
309 // $fd = fopen( __DIR__ . '/simple-items-sync.txt', 'a+' );
310
311 $args = func_get_args();
312 // fwrite( $fd, PHP_EOL . 'Args ' . print_r( $args, true ) );
313 if ( is_array( $args ) ) {
314 if ( isset( $args['page'] ) && isset( $args['category'] ) ) {
315 $page = $args['page'];
316 $category = $args['category'];
317 } elseif ( isset( $args[0] ) && isset( $args[1] ) ) {
318 $page = $args[0];
319 $category = $args[1];
320 } elseif ( isset( $args[0] ) && ! isset( $args[1] ) ) {
321 $page = $args[0]['page'];
322 $category = $args[0]['category'];
323 } else {
324 return;
325 }
326 } else {
327 return;
328 }
329
330 $zoho_inventory_oid = $this->config['ProductZI']['OID'];
331 $zoho_inventory_url = $this->config['ProductZI']['APIURL'];
332 $timeout = ini_get( 'max_execution_time' ) ? ini_get( 'max_execution_time' ) : 60;
333 // Dynamically set per_page based on timeout.
334 if ( $timeout <= 30 ) {
335 $per_page = 20;
336 } elseif ( $timeout <= 60 ) {
337 $per_page = 50;
338 } else {
339 $per_page = 100;
340 }
341
342 $url = sprintf(
343 '%sinventory/v1/items?organization_id=%s&category_id=%s&page=%d&per_page=%d&sort_column=last_modified_time',
344 $zoho_inventory_url,
345 $zoho_inventory_oid,
346 $category,
347 $page,
348 $per_page
349 );
350 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
351 $json = $execute_curl_call->execute_curl_call_get( $url );
352 $code = (int) property_exists( $json, 'code' ) ? $json->code : '0';
353
354 global $wpdb;
355
356 /* Response for item sync with sync button. For cron sync blank array will return. */
357 $response_msg = array();
358 if ( empty( $code ) && property_exists( $json, 'items' ) ) {
359 $item_ids = array();
360 // log items.
361 // fwrite( $fd, PHP_EOL . 'Items : ' . print_r( $json, true ) );
362 // before your foreach, initialize once.
363 $logger = wc_get_logger();
364 $context = array( 'source' => 'cmbird_item_sync' );
365
366 foreach ( $json->items as $arr ) {
367 $prod_id = wc_get_product_id_by_sku( $arr->sku );
368 // Flag to enable or disable sync.
369 $allow_to_import = true;
370 // Check if product exists with same sku.
371 if ( $prod_id ) {
372 $zi_item_id = get_post_meta( $prod_id, 'zi_item_id', true );
373 if ( empty( $zi_item_id ) ) {
374 // Map existing item with zoho id.
375 update_post_meta( $prod_id, 'zi_item_id', $arr->item_id );
376 $allow_to_import = true;
377 }
378 }
379 if ( isset( $arr->is_combo_product ) && isset( $arr->group_id ) ) {
380 // If product not exists normal behavior of item sync.
381 $allow_to_import = false;
382 }
383 if ( isset( $arr->group_id ) && ! empty( $arr->group_id ) ) {
384 $this->sync_variation_of_group( $arr );
385 continue;
386 }
387
388 // Remove duplicated product based on the sku, remove also postmeta.
389 // run query to check if product exists twice with same sku. Check based on SKU.
390 if ( $prod_id && ! empty( $arr->sku ) ) {
391 $duplicate_product = $wpdb->get_row( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_sku' AND meta_value = %s AND post_id != %d LIMIT 1", $arr->sku, $prod_id ) );
392 if ( $duplicate_product ) {
393 // Remove the duplicate product.
394 wp_delete_post( $duplicate_product->post_id, true );
395 $prod_id = '';
396 }
397 }
398
399 // Get the post id by doing a meta query on the postmeta table.
400 if ( empty( $prod_id ) ) {
401 $pdt = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $arr->item_id ) );
402 $pdt_id = $pdt ? $pdt->post_id : '';
403 } else {
404 $pdt_id = $prod_id;
405 }
406
407 if ( empty( $pdt_id ) && $allow_to_import && 'active' === $arr->status ) {
408 // Create the product if it does not exist.
409 try {
410 $product = new WC_Product();
411 $product->set_name( $arr->name );
412 $product->set_status( 'publish' );
413 if ( isset( $arr->sku ) && ! empty( $arr->sku ) ) {
414 $product->set_sku( $arr->sku );
415 }
416 $product->set_regular_price( $arr->rate );
417 $pdt_id = $product->save();
418 if ( $pdt_id ) {
419 update_post_meta( $pdt_id, 'zi_item_id', $arr->item_id );
420 }
421 } catch ( Exception $e ) {
422 // Log the error so you can inspect it in WooCommerce > Status > Logs.
423 $message = sprintf(
424 'Error creating product for Zoho Item # %s (SKU: %s): %s',
425 $arr->item_id,
426 isset( $arr->sku ) ? $arr->sku : '(no sku)',
427 $e->getMessage()
428 );
429 $logger->error( $message, $context );
430 continue;
431 }
432 }
433
434 if ( $pdt_id ) {
435 if ( ! empty( $arr->category_name ) ) {
436 $term = get_term_by( 'name', $arr->category_name, 'product_cat' );
437 $term_id = $term->term_id;
438 if ( empty( $term_id ) ) {
439 $term = wp_insert_term(
440 $arr->category_name,
441 'product_cat',
442 array(
443 'parent' => 0,
444 )
445 );
446 $term_id = $term['term_id'];
447 }
448 if ( $term_id ) {
449 $existing_terms = wp_get_object_terms( $pdt_id, 'product_cat' );
450 if ( $existing_terms && count( $existing_terms ) > 0 ) {
451 $is_terms_exist = $this->zi_check_terms_exists( $existing_terms, $term_id );
452 if ( ! $is_terms_exist ) {
453 update_post_meta( $pdt_id, 'zi_category_id', $category );
454 wp_add_object_terms( $pdt_id, $term_id, 'product_cat' );
455 }
456 } else {
457 update_post_meta( $pdt_id, 'zi_category_id', $category );
458 wp_set_object_terms( $pdt_id, $term_id, 'product_cat' );
459 }
460 }
461 // Remove "uncategorized" category if assigned.
462 $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' );
463 if ( $uncategorized_term && has_term( $uncategorized_term->term_id, 'product_cat', $pdt_id ) ) {
464 wp_remove_object_terms( $pdt_id, $uncategorized_term->term_id, 'product_cat' );
465 }
466 }
467
468 if ( ! empty( $arr->brand ) ) {
469 // check if the Brand taxonomy exists and then update the term.
470 if ( taxonomy_exists( 'product_brand' ) ) {
471 wp_set_object_terms( $pdt_id, $arr->brand, 'product_brand' );
472 }
473 }
474 // Sync Featured Image if not disabled.
475 $zi_disable_image_sync = $this->config['Settings']['disable_image'];
476 if ( ! empty( $arr->image_document_id ) && ! $zi_disable_image_sync ) {
477 $image_class = new CMBIRD_Image_ZI();
478 $image_class->cmbird_zi_get_image( $arr->item_id, $arr->name, $pdt_id, $arr->image_name );
479 }
480
481 $item_ids[] = $arr->item_id;
482 } // end of wpdb post_id check.
483 }
484 if ( ! empty( $item_ids ) ) {
485 // Chunk item IDs into smaller batches to prevent memory exhaustion.
486 // Process 20 items at a time instead of all at once.
487 $chunk_size = 20;
488 $item_chunks = array_chunk( $item_ids, $chunk_size );
489 $total_chunks = count( $item_chunks );
490
491 foreach ( $item_chunks as $chunk_index => $chunk ) {
492 $item_id_str = implode( ',', $chunk );
493 $item_details_url = "{$zoho_inventory_url}inventory/v1/itemdetails?item_ids={$item_id_str}&organization_id={$zoho_inventory_oid}";
494 $this->zi_item_bulk_sync( $item_details_url );
495
496 // Memory cleanup after each chunk.
497 wp_cache_flush();
498 if ( function_exists( 'gc_collect_cycles' ) ) {
499 gc_collect_cycles();
500 }
501
502 // Check memory usage and stop if approaching limit.
503 $memory_limit = $this->get_memory_limit();
504 $memory_used = memory_get_usage( true );
505 if ( $memory_limit > 0 && $memory_used > ( $memory_limit * 0.9 ) ) {
506 $logger = wc_get_logger();
507 $context = array( 'source' => 'cmbird_item_sync' );
508 $logger->warning(
509 sprintf(
510 'Memory limit approaching (%.2f%% used). Stopping sync at chunk %d of %d.',
511 ( $memory_used / $memory_limit ) * 100,
512 $chunk_index + 1,
513 $total_chunks
514 ),
515 $context
516 );
517 break;
518 }
519 }
520
521 // Safety check: Prevent infinite loops by limiting max page depth.
522 $max_pages = 1000; // Maximum pages to process (adjust based on your needs).
523 if ( isset( $json->page_context ) && $json->page_context->has_more_page && $page < $max_pages ) {
524 $next_page = $page + 1;
525 $data = array(
526 'page' => $next_page,
527 'category' => $category,
528 );
529 // fwrite( $fd, PHP_EOL . 'Data: ' . print_r( $data, true ) );
530 // Check for existing schedule with same parameters to prevent duplicates.
531 $existing_schedule = as_has_scheduled_action( 'import_simple_items_cron', array( $data ), 'commercebird' );
532 if ( ! $existing_schedule ) {
533 // Schedule with a slight delay to prevent race conditions.
534 as_schedule_single_action( time() + 5, 'import_simple_items_cron', array( $data ), 'commercebird' );
535 // fwrite( $fd, PHP_EOL . 'Scheduled' );
536 }
537 } elseif ( $page >= $max_pages ) {
538 // Log warning if max pages reached.
539 $logger = wc_get_logger();
540 $context = array( 'source' => 'cmbird_item_sync' );
541 $logger->warning( "Max page limit ($max_pages) reached for category $category. Stopping pagination to prevent infinite loop.", $context );
542 }
543 array_push( $response_msg, $this->zi_response_message( $code, $json->message ) );
544 }
545 }
546 // fclose( $fd );
547 return $response_msg;
548 }
549
550 /**
551 * Update or Create Custom Fields of Product
552 *
553 * @param array|object $custom_fields - item object coming in from simple item recursive.
554 * @param int $pdt_id - product id.
555 * @return void
556 */
557 public function sync_item_custom_fields( $custom_fields, $pdt_id ) {
558 if ( empty( $custom_fields ) || empty( $pdt_id ) ) {
559 return;
560 }
561
562 foreach ( $custom_fields as $custom_field ) {
563 // Extract data from custom field.
564 $api_name = isset( $custom_field->api_name ) ? $custom_field->api_name : $custom_field['api_name'];
565 $value = isset( $custom_field->value ) ? $custom_field->value : $custom_field['value'];
566
567 // Check if both API name and value are present.
568 if ( ! empty( $api_name ) && ! empty( $value ) ) {
569 // Check if ACF function exists.
570 if ( function_exists( 'update_field' ) ) {
571 // Update ACF field.
572 update_field( $api_name, $value, $pdt_id );
573 } else {
574 // Fall back to update post meta.
575 update_post_meta( $pdt_id, $api_name, $value );
576 }
577 }
578 }
579 }
580
581
582 /**
583 * Function to add group items recursively by manual sync
584 *
585 * Accepts arguments via func_get_args(): page number and category ID for pagination.
586 *
587 * @return mixed
588 */
589 public function sync_groupitem_recursively() {
590 // $fd = fopen( __DIR__ . '/sync_groupitem_recursively.txt', 'a+' );
591
592 $args = func_get_args();
593 if ( ! empty( $args ) ) {
594 if ( is_array( $args ) ) {
595 if ( isset( $args['page'] ) && isset( $args['category'] ) ) {
596 $page = $args['page'];
597 $category = $args['category'];
598 } elseif ( isset( $args[0] ) && isset( $args[1] ) ) {
599 $page = $args[0];
600 $category = $args[1];
601 } elseif ( isset( $args[0] ) && ! isset( $args[1] ) ) {
602 $page = $args[0]['page'];
603 $category = $args[0]['category'];
604 } else {
605 return;
606 }
607 } else {
608 return;
609 }
610
611 // Memory management: Set memory limit if needed.
612 if ( function_exists( 'wp_raise_memory_limit' ) ) {
613 wp_raise_memory_limit( 'admin' );
614 }
615
616 global $wpdb;
617 $zoho_inventory_oid = $this->config['ProductZI']['OID'];
618 $zoho_inventory_url = $this->config['ProductZI']['APIURL'];
619 $url = $zoho_inventory_url . 'inventory/v1/itemgroups/?organization_id=' . $zoho_inventory_oid . '&category_id=' . $category . '&page=' . $page . '&per_page=20&filter_by=Status.Active';
620 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
621 $json = $execute_curl_call->execute_curl_call_get( $url );
622 $code = $json->code;
623 // $message = $json->message;
624 $response_msg = array();
625
626 if ( '0' === $code || 0 === $code ) {
627 $zi_disable_description_sync = $this->config['Settings']['disable_description'];
628 $zi_disable_name_sync = $this->config['Settings']['disable_name'];
629 // Find the term_id by searching for the option where value matches the category.
630 global $wpdb;
631 $term_id = null;
632 if ( ! empty( $category ) ) {
633 // Query directly for the matching option.
634 $option_name = $wpdb->get_var( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s AND option_value = %s LIMIT 1", 'cmbird_zoho_id_for_term_id_%', $category ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Need to find category mapping.
635 if ( ! empty( $option_name ) ) {
636 // Extract term_id from option_name (e.g., 'cmbird_zoho_id_for_term_id_37' -> '37').
637 $term_id = str_replace( 'cmbird_zoho_id_for_term_id_', '', $option_name );
638 }
639 // fwrite( $fd, PHP_EOL . 'Category: ' . $category . ' -> term_id: ' . $term_id );
640 }
641 // fwrite( $fd, PHP_EOL . '$json->itemgroups : ' . print_r( $json->itemgroups, true ) );
642 $processed_count = 0;
643 foreach ( $json->itemgroups as $gp_arr ) {
644 // Memory cleanup every 10 group items.
645 if ( $processed_count > 0 && $processed_count % 10 === 0 ) {
646 wp_cache_flush();
647 if ( function_exists( 'gc_collect_cycles' ) ) {
648 gc_collect_cycles();
649 }
650 }
651 $zi_group_id = $gp_arr->group_id;
652 $zi_group_name = $gp_arr->group_name;
653 // fwrite($fd, PHP_EOL . '$itemGroup : ' . print_r($gp_arr, true));
654 // skip if there is no first attribute.
655 $zi_group_attribute1 = $gp_arr->attribute_id1;
656 if ( empty( $zi_group_attribute1 ) ) {
657 continue;
658 }
659
660 // Get Group ID.
661 $group_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $zi_group_id ) );
662 if ( empty( $group_id ) ) {
663 // search by zi_group_name if not found by zi_item_id.
664 $group_id = $wpdb->get_var( $wpdb->prepare( "SELECT p.ID FROM $wpdb->posts p WHERE p.post_title = %s AND p.post_type = 'product' AND p.post_status IN ('publish', 'draft', 'private') LIMIT 1", $zi_group_name ) );
665 }
666 // array_push( $response_msg, $this->zi_response_message( 'SUCCESS', 'Zoho Group Item Synced: ' . $zi_group_name, $group_id ) );
667 // end insert group product.
668 // variable items.
669 // fwrite($fd, PHP_EOL . '$group_id exists ' . $group_id);
670 if ( ! empty( $group_id ) ) {
671 $existing_parent_product = wc_get_product( $group_id );
672 // fwrite($fd, PHP_EOL . 'Existing group Id');
673 if ( ! empty( $gp_arr->description ) && ! $zi_disable_description_sync ) {
674 $existing_parent_product->set_short_description( $gp_arr->description );
675 }
676 if ( ! empty( $gp_arr->name ) && ! $zi_disable_name_sync ) {
677 $existing_parent_product->set_name( $gp_arr->name );
678 // santize the name for slug and save the slug.
679 $slug = sanitize_title( $gp_arr->name );
680 $existing_parent_product->set_slug( $slug );
681 }
682 // add zi_category_id as meta.
683 $existing_parent_product->update_meta_data( 'zi_category_id', $gp_arr->category_id );
684 if ( ! empty( $term_id ) ) {
685 wp_set_object_terms( $group_id, (int) $term_id, 'product_cat' );
686 // remove the uncategorized term from the product.
687 wp_remove_object_terms( $group_id, 'uncategorized', 'product_cat' );
688 }
689 // create attributes if not exists.
690 $attributes = $existing_parent_product->get_attributes();
691 if ( empty( $attributes ) ) {
692 // Create or Update the Attributes.
693 $attr_created = $this->sync_attributes_of_group( $gp_arr, $group_id );
694 // fwrite($fd, PHP_EOL . '$attr_created ' . $attr_created);
695 }
696 $variations_check = $existing_parent_product->get_children();
697 if ( empty( $variations_check ) ) {
698 // fwrite( $fd, PHP_EOL . 'No Variations found' );
699 $this->import_variable_product_variations( $gp_arr, $group_id );
700 } else {
701 // Sort the variations.
702 $data_store = $existing_parent_product->get_data_store();
703 $data_store->sort_all_product_variations( $group_id );
704 }
705 $existing_parent_product->save();
706 // ACF Fields.
707 if ( ! empty( $gp_arr->custom_fields ) ) {
708 // fwrite($fd, PHP_EOL . 'Custom Fields : ' . print_r($gp_arr->custom_fields, true));
709 $this->sync_item_custom_fields( $gp_arr->custom_fields, $group_id );
710 }
711 // update Brand.
712 if ( ! empty( $gp_arr->brand ) ) {
713 // check if the Brand or Brands taxonomy exists and then update the term.
714 if ( taxonomy_exists( 'product_brand' ) ) {
715 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
716 } elseif ( taxonomy_exists( 'product_brand' ) ) {
717 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
718 }
719 }
720 } else {
721 // Create the parent variable product.
722 $parent_product = new WC_Product_Variable();
723 $parent_product->set_name( $zi_group_name );
724 $parent_product->set_status( 'publish' );
725 $parent_product->set_short_description( $gp_arr->description );
726 $parent_product->add_meta_data( 'zi_item_id', $zi_group_id );
727 $parent_product->add_meta_data( 'zi_category_id', $category );
728 $group_id = $parent_product->save();
729 // Sync category by finding it first.
730 if ( ! empty( $term_id ) && term_exists( $term_id, 'product_cat' ) ) {
731 wp_set_object_terms( $group_id, (int) $term_id, 'product_cat' );
732 }
733 // Add category.
734 if ( ! empty( $term_id ) ) {
735 wp_set_object_terms( $group_id, (int) $term_id, 'product_cat' );
736 }
737 // update Brand.
738 if ( ! empty( $gp_arr->brand ) ) {
739 // check if the Brand or Brands taxonomy exists and then update the term.
740 if ( taxonomy_exists( 'product_brand' ) ) {
741 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
742 } elseif ( taxonomy_exists( 'product_brand' ) ) {
743 wp_set_object_terms( $group_id, $gp_arr->brand, 'product_brand' );
744 }
745 }
746 // fwrite($fd, PHP_EOL . 'New $group_id ' . $group_id);
747 // ACF Fields.
748 if ( ! empty( $gp_arr->custom_fields ) ) {
749 // fwrite($fd, PHP_EOL . 'Custom Fields : ' . print_r($gp_arr->custom_fields, true));
750 $this->sync_item_custom_fields( $gp_arr->custom_fields, $group_id );
751 }
752 // Create or Update the Attributes.
753 $attr_created = $this->sync_attributes_of_group( $gp_arr, $group_id );
754
755 if ( ! empty( $group_id ) && $attr_created ) {
756 $this->import_variable_product_variations( $gp_arr, $group_id );
757 }
758 } // end of create variable product.
759
760 ++$processed_count;
761 } // end foreach group items.
762
763 // Final memory cleanup after processing all group items.
764 wp_cache_flush();
765 if ( function_exists( 'gc_collect_cycles' ) ) {
766 gc_collect_cycles();
767 }
768
769 // Schedule next page if available
770 if ( isset( $json->page_context ) && $json->page_context->has_more_page ) {
771 $data = array(
772 'page' => $page + 1,
773 'category' => $category,
774 );
775 $existing_schedule = as_has_scheduled_action( 'import_group_items_cron', $data, 'commercebird' );
776 // Check if the scheduled action exists.
777 if ( ! $existing_schedule ) {
778 as_schedule_single_action( time(), 'import_group_items_cron', $data, 'commercebird' );
779 }
780 }
781
782 // Clear any temporary variables.
783 unset( $json, $execute_curl_call );
784 array_push( $response_msg, $this->zi_response_message( $code, isset( $json->message ) ? $json->message : '' ) );
785 }
786 // End of logging.
787 // fclose( $fd );
788 return $response_msg;
789 } else {
790 return;
791 }
792 }
793
794 /**
795 * Callback function for importing a variable product and its variations.
796 *
797 * @param object $gp_arr - Group item object.
798 * @param int $group_id - Group item id.
799 * @return void
800 */
801 public function import_variable_product_variations( $gp_arr, $group_id ): void {
802 // $fd = fopen( __DIR__ . '/import_variable_product_variations.txt', 'a+' );
803
804 if ( empty( $gp_arr ) || empty( $group_id ) ) {
805 return;
806 }
807
808 global $wpdb;
809 $product = wc_get_product( $group_id );
810
811 // separators.
812 $wc_decimal_separator = $this->wc_decimal_separator;
813 $wc_thousand_separator = $this->wc_thousand_separator;
814 $wc_price_decimal_separator = $this->wc_price_decimal_separator;
815
816 if ( $product ) {
817 $item_group = $gp_arr;
818 $items = $item_group->items;
819 $attribute_name1 = $item_group->attribute_name1;
820 $attribute_name2 = $item_group->attribute_name2;
821 $attribute_name3 = $item_group->attribute_name3;
822
823 // fwrite( $fd, PHP_EOL . 'Items : ' . print_r( $items, true ) );
824 // get the options for stock sync.
825 $zi_enable_locationstock = $this->config['Settings']['enable_location_stock'];
826 $location_id = $this->config['Settings']['zoho_location_id'];
827 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
828 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
829 $locations = array();
830
831 foreach ( $items as $item ) {
832 // reset this array.
833 $attribute_arr = array();
834 $variation_id = '';
835 $status = $item->status === 'active' ? 'publish' : 'draft';
836
837 $zi_item_id = $item->item_id;
838 $variation_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $zi_item_id ) );
839
840 if ( ! empty( $variation_id ) ) {
841 $v_product = wc_get_product( $variation_id );
842 // Check if the product object is valid.
843 if ( $v_product && is_a( $v_product, 'WC_Product' ) ) {
844 if ( $v_product->is_type( 'simple' ) ) {
845 wp_delete_post( $variation_id, true );
846 }
847 }
848 }
849 // SKU check of the variation, if exits then remove it.
850 if ( ! empty( $item->sku ) ) {
851 $sku_prod_id = wc_get_product_id_by_sku( $item->sku );
852 $v_product = wc_get_product( $sku_prod_id );
853 // Check if the product object is valid.
854 if ( $v_product && is_a( $v_product, 'WC_Product' ) ) {
855 if ( $v_product->is_type( 'simple' ) ) {
856 wp_delete_post( $variation_id, true );
857 }
858 }
859 }
860 if ( ! empty( $variation_id ) ) {
861 continue;
862 }
863 // Stock mode check.
864 $locations = $item->locations;
865
866 if ( $zi_enable_locationstock && $location_id ) {
867 foreach ( $locations as $location ) {
868 if ( $location->location_id === $location_id ) {
869 if ( $accounting_stock ) {
870 $stock = isset( $location->location_available_stock );
871 } else {
872 $stock = isset( $location->location_actual_available_stock );
873 }
874 }
875 }
876 } elseif ( $accounting_stock ) {
877 $stock = $item->available_stock;
878 } else {
879 $stock = $item->actual_available_stock;
880 }
881 // number format the rate.
882 $rate = number_format( $item->rate, $wc_price_decimal_separator, $wc_decimal_separator, $wc_thousand_separator );
883
884 $attribute_name11 = $item->attribute_option_name1;
885 $attribute_name12 = $item->attribute_option_name2;
886 $attribute_name13 = $item->attribute_option_name3;
887 // fwrite( $fd, PHP_EOL . '>>> Item: ' . $item->item_id . ' | SKU: ' . $item->sku . ' | opt1: ' . $attribute_name11 . ' | opt2: ' . $attribute_name12 . ' | opt3: ' . $attribute_name13 );
888 // Prepare the variation data.
889 if ( ! empty( $attribute_name1 ) ) {
890 $sanitized_name1 = wc_sanitize_taxonomy_name( $attribute_name1 );
891 $attribute_arr[ $sanitized_name1 ] = $attribute_name11;
892 }
893 if ( ! empty( $attribute_name2 ) ) {
894 $sanitized_name2 = wc_sanitize_taxonomy_name( $attribute_name2 );
895 $attribute_arr[ $sanitized_name2 ] = $attribute_name12;
896 }
897 if ( ! empty( $attribute_name3 ) ) {
898 $sanitized_name3 = wc_sanitize_taxonomy_name( $attribute_name3 );
899 $attribute_arr[ $sanitized_name3 ] = $attribute_name13;
900 }
901 // fwrite( $fd, PHP_EOL . '$attribute_arr : ' . print_r( $attribute_arr, true ) );
902 // Loop through the variations and create them.
903 try {
904 $variation_post = array(
905 'post_title' => $product->get_name(),
906 'post_name' => 'product-' . $group_id . '-variation',
907 'post_status' => $status,
908 'post_parent' => $group_id,
909 'post_type' => 'product_variation',
910 'guid' => $product->get_permalink(),
911 );
912 // Creating the product variation.
913 $variation_id = wp_insert_post( $variation_post );
914 $variation = new WC_Product_Variation( $variation_id );
915 $variation->set_regular_price( $rate );
916 $variation->set_sku( $item->sku );
917 if ( ! $zi_disable_stock_sync && $stock > 0 ) {
918 $variation->set_stock_quantity( $stock );
919 $variation->set_manage_stock( true );
920 $variation->set_stock_status( '' );
921 } else {
922 $variation->set_manage_stock( false );
923 }
924 $variation->add_meta_data( 'zi_item_id', $item->item_id );
925 } catch ( Exception $e ) {
926 // fwrite( $fd, PHP_EOL . 'Error : ' . $e->getMessage() );
927 continue;
928 }
929
930 // Build variation attributes array for WooCommerce API.
931 $variation_attributes = array();
932
933 // Get the variation attributes with correct attribute values.
934 foreach ( $attribute_arr as $attribute => $term_name ) {
935 $taxonomy = 'pa_' . $attribute;
936 // If taxonomy doesn't exists we create it.
937 if ( ! taxonomy_exists( $taxonomy ) ) {
938 register_taxonomy(
939 $taxonomy,
940 'product_variation',
941 array(
942 'hierarchical' => false,
943 'label' => ucfirst( $attribute ),
944 'query_var' => true,
945 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
946 ),
947 );
948 }
949
950 // fwrite( $fd, PHP_EOL . '=== Processing Attribute: ' . $attribute . ' | Term Name: ' . $term_name . ' | Taxonomy: ' . $taxonomy );
951 // fwrite( $fd, PHP_EOL . 'Term Name: ' . $term_name . ' for Taxonomy: ' . $taxonomy );
952 // Check if the Term name exist and if not we create it.
953 $term = get_term_by( 'name', $term_name, $taxonomy );
954 if ( ! $term ) {
955 // Try by slug in case the term exists but with different capitalization.
956 $term = get_term_by( 'slug', sanitize_title( $term_name ), $taxonomy );
957 if ( ! $term ) {
958 $term_name_formatted = str_replace( array( '-', '_' ), ' ', $term_name );
959 $term_name_formatted = ucwords( $term_name_formatted );
960 $term = wp_insert_term(
961 $term_name_formatted,
962 $taxonomy,
963 array(
964 'slug' => sanitize_title( $term_name ),
965 )
966 );
967 if ( is_wp_error( $term ) ) {
968 // fwrite( $fd, PHP_EOL . 'Error creating term: ' . $term->get_error_message() );
969 continue;
970 }
971 }
972 }
973 $term_slug = $term ? $term->slug : sanitize_title( $term_name );
974 $term_id = $term ? ( is_object( $term ) ? $term->term_id : ( is_array( $term ) ? $term['term_id'] : 0 ) ) : 0;
975 // fwrite( $fd, PHP_EOL . 'Final term_slug: ' . $term_slug . ' | term_id: ' . $term_id );
976 // Get the post Terms from the parent variable product.
977 $post_term_ids = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'ids' ) );
978 // Check if the post term exist and if not we set it in the parent variable product.
979 if ( $term_id && ! in_array( $term_id, $post_term_ids, true ) ) {
980 wp_set_post_terms( $group_id, $term_id, $taxonomy, true );
981 }
982 // Add to variation attributes array using WooCommerce API format.
983 $variation_attributes[ $taxonomy ] = $term_slug;
984 }
985
986 // Set all attributes at once using WooCommerce API.
987 if ( ! empty( $variation_attributes ) ) {
988 $variation->set_attributes( $variation_attributes );
989 $variation_id = $variation->save();
990 // fwrite( $fd, PHP_EOL . '[VARIATION] Set attributes for variation ' . $variation_id . ': ' . print_r( $variation_attributes, true ) );
991 }
992
993 // update purchase price as meta data.
994 if ( ! empty( $item->purchase_rate ) ) {
995 update_post_meta( $variation_id, '_cogs_total_value', $item->purchase_rate );
996 }
997
998 // Featured Image of variation.
999 if ( ! empty( $item->image_name ) ) {
1000 $image_class = new CMBIRD_Image_ZI();
1001 $variation_image_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, $variation_id );
1002 if ( ! has_post_thumbnail( $group_id ) ) {
1003 if ( $variation_image_id ) {
1004 set_post_thumbnail( $group_id, $variation_image_id );
1005 }
1006 }
1007 }
1008 }
1009 // End group item add process.
1010 // array_push($response_msg, $this->zi_response_message('SUCCESS', 'Zoho variable item created for zoho item id ' . $zi_item_id, $variation_id));
1011
1012 // After all variations are created/updated, save the parent product to trigger WooCommerce sync.
1013 if ( $product && is_a( $product, 'WC_Product_Variable' ) ) {
1014 // Delete the transient cache for this product.
1015 delete_transient( 'wc_product_children_' . $group_id );
1016 // Force WooCommerce to regenerate the available variations.
1017 WC_Product_Variable::sync( $group_id );
1018 // Save the parent product to ensure all changes are persisted.
1019 $product->save();
1020 }
1021 // End of Logging.
1022 // fclose( $fd );
1023 }
1024 }
1025
1026 /**
1027 * Update or Create the Product Attributes for the Variable Item Sync
1028 *
1029 * @param object $gp_arr - the group item object from Zoho Inventory.
1030 * @param int $group_id - the parent product id in WooCommerce.
1031 * @return bool - true if attributes were created successfully, false otherwise
1032 */
1033 public function sync_attributes_of_group( $gp_arr, $group_id ) {
1034 // $fd = fopen( __DIR__ . '/sync_attributes_of_group.txt', 'a+' );
1035 // Check if the group item has attributes.
1036 if ( empty( $gp_arr->attribute_name1 ) ) {
1037 return false;
1038 }
1039 // Create attributes.
1040 $success = true; // Track the success of attribute creation.
1041 $attributes_data = array();
1042 $attribute_count = 0;
1043
1044 // Loop through the attribute names.
1045 for ( $i = 1; $i <= 3; $i++ ) {
1046 $attribute_name_key = 'attribute_name' . $i;
1047 $attribute_option_name_key = 'attribute_option_name' . $i;
1048
1049 // Get the attribute name.
1050 $attribute_name = $gp_arr->$attribute_name_key;
1051
1052 if ( ! empty( $attribute_name ) ) {
1053 // Check if the attribute is already added to the attributes array.
1054 if ( ! isset( $attributes_data[ $attribute_name ] ) ) {
1055 // Create the attribute and add it to the attributes array.
1056 $attribute = array(
1057 'name' => $attribute_name,
1058 'position' => $attribute_count,
1059 'visible' => true,
1060 'variation' => true,
1061 'options' => array(),
1062 );
1063
1064 // Loop through the items and retrieve attribute options.
1065 $attribute_options = array();
1066 foreach ( $gp_arr->items as $item ) {
1067 $attribute_option = $item->$attribute_option_name_key;
1068 if ( ! empty( $attribute_option ) && ! in_array( $attribute_option, $attribute_options, true ) ) {
1069 $attribute_options[] = $attribute_option;
1070 }
1071 }
1072
1073 // Set the attribute options.
1074 $attribute['options'] = $attribute_options;
1075
1076 $attributes_data[] = $attribute;
1077 ++$attribute_count;
1078 }
1079 }
1080 }
1081 // fwrite( $fd, PHP_EOL . '$attributes : ' . print_r( $attributes_data, true ) );
1082
1083 // Assign the attributes to the parent product.
1084 if ( count( $attributes_data ) > 0 ) {
1085
1086 // Loop through defined attribute data.
1087 foreach ( $attributes_data as $key => $attribute_array ) {
1088 if ( isset( $attribute_array['name'] ) && isset( $attribute_array['options'] ) ) {
1089 // Clean attribute name to get the taxonomy.
1090 $taxonomy = 'pa_' . wc_sanitize_taxonomy_name( $attribute_array['name'] );
1091 $option_term_ids = array();
1092 $existing_terms = array();
1093 // Create the attribute if it doesn't exist.
1094 if ( ! taxonomy_exists( $taxonomy ) ) {
1095 // Clean attribute label for better display.
1096 $attribute_label = ucfirst( $attribute_array['name'] );
1097
1098 // Register the new attribute taxonomy.
1099 $attribute_args = array(
1100 'slug' => $taxonomy,
1101 'name' => $attribute_label,
1102 'type' => 'select',
1103 'order_by' => 'menu_order',
1104 'has_archives' => false,
1105 );
1106
1107 $result = wc_create_attribute( $attribute_args );
1108 register_taxonomy( $taxonomy, array( 'product' ), array() );
1109
1110 if ( ! is_wp_error( $result ) ) {
1111 // fwrite($fd, PHP_EOL . 'result : ' . $result);
1112 // Loop through defined attribute data options (terms values).
1113 foreach ( $attribute_array['options'] as $option ) {
1114 // Check if the term exists for the attribute taxonomy.
1115 $term = term_exists( $option, $taxonomy );
1116 // Also check by slug in case option is slug-formatted.
1117 $term_by_slug = get_term_by( 'slug', sanitize_title( $option ), $taxonomy );
1118 if ( $term_by_slug ) {
1119 $term = array( 'term_id' => $term_by_slug->term_id );
1120 }
1121 if ( ! $term ) {
1122 // Term doesn't exist, create a new one.
1123 // Convert slug-formatted names to proper names.
1124 $term_name = str_replace( array( '-', '_' ), ' ', $option );
1125 $term_name = ucwords( $term_name );
1126 $term = wp_insert_term(
1127 $term_name,
1128 $taxonomy,
1129 array(
1130 'slug' => sanitize_title( $term_name ),
1131 )
1132 );
1133 if ( is_wp_error( $term ) ) {
1134 // fwrite( $fd, PHP_EOL . 'Error creating term: ' . $term->get_error_message() );
1135 continue;
1136 }
1137 }
1138 // Add term ID to the array for assignment to parent product.
1139 $term_id = is_array( $term ) ? $term['term_id'] : $term;
1140 $option_term_ids[] = $term_id;
1141 }
1142 } else {
1143 $success = false;
1144 // return error message.
1145 $error_string = $result->get_error_message();
1146 return $success;
1147 }
1148 } else {
1149 // Taxonomy exists, get existing terms.
1150 $existing_terms = get_terms(
1151 array(
1152 'taxonomy' => $taxonomy,
1153 'hide_empty' => false,
1154 )
1155 );
1156 }
1157
1158 if ( $existing_terms ) {
1159 foreach ( $attribute_array['options'] as $option ) {
1160 $match_found = false;
1161 foreach ( $existing_terms as $existing_term ) {
1162 // Check both name and slug to handle cases where Zoho sends slug-formatted names.
1163 if ( $existing_term->name === $option || $existing_term->slug === sanitize_title( $option ) ) {
1164 $option_term_ids[] = $existing_term->term_id;
1165 $match_found = true;
1166 break;
1167 }
1168 }
1169 // fwrite( $fd, PHP_EOL . 'Option "' . $option . '" match found: ' . ( $match_found ? 'Yes' : 'No' ) );
1170 if ( ! $match_found ) {
1171 // Check if the term exists for the attribute taxonomy.
1172 $term = get_term_by( 'slug', sanitize_title( $option ), $taxonomy );
1173 // fwrite( $fd, PHP_EOL . 'Term by slug check for option "' . $option . '": ' . print_r( $term, true ) );
1174 if ( $term ) {
1175 $term = array( 'term_id' => $term->term_id );
1176 }
1177 if ( ! $term ) {
1178 // Term doesn't exist, create it.
1179 // Convert slug-formatted names to proper names.
1180 $term_name = str_replace( array( '-', '_' ), ' ', $option );
1181 $term_name = ucwords( $term_name );
1182 $term = wp_insert_term( $term_name, $taxonomy );
1183 if ( ! is_wp_error( $term ) ) {
1184 // Get the term ID.
1185 $term_id = is_array( $term ) ? $term['term_id'] : $term;
1186 $option_term_ids[] = $term_id;
1187 } else {
1188 $success = false;
1189 }
1190 } else {
1191 // Get the existing term ID.
1192 $term_id = $term['term_id'];
1193 $option_term_ids[] = $term_id;
1194 }
1195 }
1196 }
1197 }
1198 // Set the selected terms for the product.
1199 // fwrite( $fd, PHP_EOL . '[SYNC_ATTR] Setting terms for taxonomy: ' . $taxonomy . ' | Term IDs: ' . print_r( $option_term_ids, true ) );
1200 wp_set_object_terms( $group_id, $option_term_ids, $taxonomy, false );
1201 }
1202 // Build the attribute array for product metadata.
1203 $attributes_data[ $key ]['name'] = $taxonomy;
1204 $attributes_data[ $key ]['value'] = '';
1205 $attributes_data[ $key ]['is_visible'] = $attribute_array['visible'] ?? true;
1206 $attributes_data[ $key ]['is_variation'] = $attribute_array['variation'] ?? true;
1207 $attributes_data[ $key ]['is_taxonomy'] = 1;
1208 }
1209 }
1210
1211 // Save the attributes to the parent product metadata.
1212 if ( ! empty( $attributes_data ) ) {
1213 update_post_meta( $group_id, '_product_attributes', $attributes_data );
1214 }
1215 // fclose($fd);
1216 return $success;
1217 }
1218
1219 /**
1220 * Update or create variation in WooCommerce if Group-ID already exists in wpdB
1221 *
1222 * @param object $item - item object coming in from simple item recursive function.
1223 * @return: void
1224 */
1225 public function sync_variation_of_group( $item ) {
1226 // $fd = fopen( __DIR__ . '/sync_variation_of_group.txt', 'a+' );
1227
1228 // log the item.
1229 // fwrite( $fd, PHP_EOL . 'Item : ' . print_r( $item, true ) );
1230
1231 global $wpdb;
1232 // Stock mode check.
1233 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
1234 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
1235 $is_tax_enabled = $this->is_tax_enabled;
1236
1237 if ( $accounting_stock ) {
1238 $stock = $item->available_stock;
1239 } else {
1240 $stock = $item->actual_available_stock;
1241 }
1242
1243 $item_id = $item->item_id;
1244 // $item_category = $item->category_name;
1245 $groupid = property_exists( $item, 'group_id' ) ? $item->group_id : 0;
1246 // find parent variable product.
1247 $group_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $groupid ) );
1248 // fwrite($fd, PHP_EOL . 'Row Data : ' . print_r($row, true));
1249 // fwrite($fd, PHP_EOL . 'Row $group_id : ' . $group_id);
1250 $stock_quantity = $stock < 0 ? 0 : $stock;
1251 // fwrite($fd, PHP_EOL . 'Before group item sync : ' . $group_id);
1252 if ( ! empty( $group_id ) ) {
1253 // fwrite( $fd, PHP_EOL . 'Inside item sync : ' . $item->name );
1254 // Brand.
1255 if ( isset( $item->brand ) && ! empty( $group_id ) ) {
1256 if ( taxonomy_exists( 'product_brand' ) ) {
1257 wp_set_object_terms( $group_id, $item->brand, 'product_brand' );
1258 }
1259 }
1260
1261 $variation_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1", $item_id ) );
1262 if ( ! empty( $item->sku ) && empty( $variation_id ) ) {
1263 $variation_id = wc_get_product_id_by_sku( $item->sku );
1264 }
1265 if ( $variation_id ) {
1266 // fwrite( $fd, PHP_EOL . 'Variation ID exists : ' . $variation_id );
1267 // if product type is not variation then return.
1268 $v_product = wc_get_product( $variation_id );
1269 // Check if the product object is valid.
1270 if ( $v_product && is_a( $v_product, 'WC_Product' ) ) {
1271 if ( $v_product->is_type( 'simple' ) ) {
1272 wp_delete_post( $variation_id, true );
1273 $variation_id = '';
1274 // Continue to create new variation below.
1275 }
1276 }
1277 // If variation already exists then update it.
1278 $variation = new WC_Product_Variation( $variation_id );
1279 // SKU - Imported.
1280 if ( ! empty( $item->sku ) ) {
1281 $variation->set_sku( $item->sku );
1282 }
1283 // update purchase price as meta data.
1284 if ( ! empty( $item->purchase_rate ) ) {
1285 update_post_meta( $variation_id, '_cogs_total_value', $item->purchase_rate );
1286 }
1287 // Price - Imported.
1288 $zi_disable_price_sync = $this->config['Settings']['disable_price'];
1289 $variation_sale_price = $variation->get_sale_price();
1290 if ( empty( $variation_sale_price ) && ! $zi_disable_price_sync ) {
1291 $variation->set_sale_price( $item->rate );
1292 }
1293 $variation->set_regular_price( $item->rate );
1294 // Set Tax Class.
1295 if ( $item->tax_id && $is_tax_enabled ) {
1296 $zi_common_class = new CMBIRD_Common_Functions();
1297 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item->tax_percentage );
1298 $variation->set_tax_status( 'taxable' );
1299 $variation->set_tax_class( $woo_tax_class );
1300 }
1301 // Stock Imported code.
1302 if ( ! $zi_disable_stock_sync && is_numeric( $stock_quantity ) ) {
1303 // fwrite( $fd, PHP_EOL . 'Stock Quantity : ' . $stock_quantity );
1304 $variation->set_manage_stock( true );
1305 if ( $stock_quantity > 0 ) {
1306 $variation->set_manage_stock( true );
1307 $variation->set_stock_quantity( $stock_quantity );
1308 $variation->set_stock_status( 'instock' );
1309 } elseif ( $stock_quantity <= 0 ) {
1310 $variation->set_manage_stock( true );
1311 $variation->set_stock_quantity( $stock_quantity );
1312 $stock_status = $variation->backorders_allowed() ? 'onbackorder' : 'outofstock';
1313 $variation->set_stock_status( $stock_status );
1314 }
1315 }
1316 // Featured Image of variation.
1317 if ( ! empty( $item->image_document_id ) ) {
1318 $image_class = new CMBIRD_Image_ZI();
1319 $variation_image_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, $variation_id );
1320 if ( ! has_post_thumbnail( $group_id ) ) {
1321 if ( $variation_image_id ) {
1322 set_post_thumbnail( $group_id, $variation_image_id );
1323 }
1324 }
1325 }
1326 // enable or disable based on status from Zoho.
1327 $status = ( 'active' === $item->status ) ? 'publish' : 'draft';
1328 $variation->set_status( $status );
1329 $variation->save();
1330 // clear cache.
1331 wc_delete_product_transients( $variation_id );
1332 } else {
1333 // create new variation.
1334 // if status is not active then return.
1335 if ( 'active' !== $item->status ) {
1336 return;
1337 }
1338
1339 $attribute_name1 = $item->attribute_option_name1;
1340 $attribute_name2 = $item->attribute_option_name2;
1341 $attribute_name3 = $item->attribute_option_name3;
1342 // Prepare the variation data.
1343 $attribute_arr = array();
1344 if ( ! empty( $attribute_name1 ) ) {
1345 $sanitized_name1 = wc_sanitize_taxonomy_name( $item->attribute_name1 );
1346 $attribute_arr[ $sanitized_name1 ] = $attribute_name1;
1347 }
1348 if ( ! empty( $attribute_name2 ) ) {
1349 $sanitized_name2 = wc_sanitize_taxonomy_name( $item->attribute_name2 );
1350 $attribute_arr[ $sanitized_name2 ] = $attribute_name2;
1351 }
1352 if ( ! empty( $attribute_name3 ) ) {
1353 $sanitized_name3 = wc_sanitize_taxonomy_name( $item->attribute_name3 );
1354 $attribute_arr[ $sanitized_name3 ] = $attribute_name3;
1355 }
1356 // fwrite( $fd, PHP_EOL . 'Attributes_arr: ' . print_r( $attribute_arr, true ) );
1357
1358 // here actually create new variation because sku not found.
1359 $variation = new WC_Product_Variation();
1360 $variation->set_parent_id( $group_id );
1361 $variation->set_status( 'publish' );
1362 $variation->set_regular_price( $item->rate );
1363 $variation->set_sku( $item->sku );
1364 if ( ! $zi_disable_stock_sync ) {
1365 $variation->set_stock_quantity( $stock_quantity );
1366 $variation->set_manage_stock( true );
1367 $variation->set_stock_status( '' );
1368 } else {
1369 $variation->set_manage_stock( false );
1370 }
1371 $variation->add_meta_data( 'zi_item_id', $item->item_id );
1372 $variation_id = $variation->save();
1373
1374 // Get the variation attributes with correct attribute values.
1375 foreach ( $attribute_arr as $attribute => $term_name ) {
1376 $taxonomy = $attribute;
1377 // If taxonomy doesn't exists we create it.
1378 if ( ! taxonomy_exists( $taxonomy ) ) {
1379 register_taxonomy(
1380 $taxonomy,
1381 'product_variation',
1382 array(
1383 'hierarchical' => false,
1384 'label' => ucfirst( $attribute ),
1385 'query_var' => true,
1386 'rewrite' => array( 'slug' => sanitize_title( $attribute ) ),
1387 ),
1388 );
1389 }
1390
1391 // Check if the Term name exist and if not we create it.
1392 $term = get_term_by( 'name', $term_name, $taxonomy );
1393 if ( empty( $term ) ) {
1394 // Try by slug in case the term exists but with different capitalization.
1395 $term = get_term_by( 'slug', sanitize_title( $term_name ), $taxonomy );
1396 }
1397 if ( empty( $term ) ) {
1398 // Term doesn't exist, create it
1399 wp_insert_term( $term_name, $taxonomy );
1400 $term = get_term_by( 'name', $term_name, $taxonomy );
1401 }
1402 $term_slug = $term ? $term->slug : sanitize_title( $term_name );
1403 $term_id = $term ? $term->term_id : 0;
1404 // Get the post Terms from the parent variable product.
1405 $post_term_ids = wp_get_post_terms( $group_id, $taxonomy, array( 'fields' => 'ids' ) );
1406 // Check if the post term exist and if not we set it in the parent variable product.
1407 if ( $term_id && ! in_array( $term_id, $post_term_ids, true ) ) {
1408 wp_set_post_terms( $group_id, $term_id, $taxonomy, true );
1409 }
1410 // Set/save the attribute data in the product variation.
1411 update_post_meta( $variation_id, 'attribute_' . $taxonomy, $term_slug );
1412 }
1413
1414 // update purchase price as meta data.
1415 if ( ! empty( $item->purchase_rate ) ) {
1416 update_post_meta( $variation_id, '_cogs_total_value', $item->purchase_rate );
1417 }
1418 // Stock.
1419 if ( ! empty( $stock ) && ! $zi_disable_stock_sync ) {
1420 update_post_meta( $variation_id, 'manage_stock', true );
1421 if ( $stock > 0 ) {
1422 update_post_meta( $variation_id, '_stock', $stock );
1423 update_post_meta( $variation_id, '_stock_status', 'instock' );
1424 } else {
1425 $backorder_status = get_post_meta( $group_id, '_backorders', true );
1426 update_post_meta( $variation_id, '_stock', $stock );
1427 if ( 'yes' === $backorder_status ) {
1428 update_post_meta( $variation_id, '_stock_status', 'onbackorder' );
1429 } else {
1430 update_post_meta( $variation_id, '_stock_status', 'outofstock' );
1431 }
1432 }
1433 }
1434 // Featured Image of variation.
1435 if ( ! empty( $item->image_document_id ) ) {
1436 $image_class = new CMBIRD_Image_ZI();
1437 $variation_image_id = $image_class->cmbird_zi_get_image( $item->item_id, $item->name, $item->image_name, $variation_id );
1438 if ( ! has_post_thumbnail( $group_id ) ) {
1439 if ( $variation_image_id ) {
1440 set_post_thumbnail( $group_id, $variation_image_id );
1441 }
1442 }
1443 }
1444 update_post_meta( $variation_id, 'zi_item_id', $item_id );
1445 WC_Product_Variable::sync( $group_id );
1446 // Regenerate lookup table for attributes.
1447 $lookup_data_store = new LookupDataStore();
1448 $lookup_data_store->create_data_for_product( $group_id );
1449 // End group item add process.
1450 unset( $attribute_arr );
1451 }
1452 // end of grouped item updating.
1453 }
1454 }
1455
1456 /**
1457 * Helper Function to check if child of composite items already synced or not
1458 *
1459 * @param string $composite_zoho_id - zoho composite item id to check if it's child are already synced.
1460 * @param string $zi_url - zoho api url.
1461 * @param string $zi_org_id - zoho organization token.
1462 * @param string $prod_id - woocommerce product id.
1463 * @return array | bool of child id and metadata if child item already synced else will return false.
1464 */
1465 public function zi_check_if_child_synced_already( $composite_zoho_id, $zi_url, $zi_org_id, $prod_id ) {
1466 if ( $prod_id ) {
1467 $bundle_childs = WC_PB_DB::query_bundled_items(
1468 array(
1469 'return' => 'id=>product_id',
1470 'bundle_id' => array( $prod_id ),
1471 )
1472 );
1473 }
1474 global $wpdb;
1475
1476 $url = $zi_url . 'inventory/v1/compositeitems/' . $composite_zoho_id . '?organization_id=' . $zi_org_id;
1477
1478 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1479 $json = $execute_curl_call->execute_curl_call_get( $url );
1480 $code = $json->code;
1481 // Flag to allow sync of parent composite item.
1482 $allow_sync = false;
1483 // Array of child object metadata.
1484 $product_array = array(); // [{prod_id:'',metadata:{key:'',value:''}},...].
1485 if ( '0' === $code || 0 === $code ) {
1486 foreach ( $json->composite_item->mapped_items as $child_item ) {
1487 $prod_meta = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s", $child_item->item_id ) );
1488 // If any child will not have zoho id in meta field then process will return false and syncing will be skipped for given item.
1489 if ( ! empty( $prod_meta->post_id ) ) {
1490
1491 $allow_sync = true;
1492 $product = wc_get_product( $prod_meta->post_id );
1493 $stock_status = 'out_of_stock';
1494 if ( $child_item->stock_on_hand > 0 ) {
1495 $stock_status = 'in_stock';
1496 } elseif ( $product && $product->backorders_allowed() ) {
1497 $stock_status = 'onbackorder';
1498 }
1499 $prod_obj = (object) array(
1500 'prod_id' => $prod_meta->post_id,
1501 'metadata' => (object) array(
1502 'quantity_min' => max( 1, $child_item->quantity ),
1503 'quantity_max' => max( 1, $child_item->quantity ),
1504 'stock_status' => $stock_status,
1505 'max_stock' => $child_item->stock_on_hand,
1506 ),
1507 );
1508 if ( is_array( $bundle_childs ) && ! empty( $bundle_childs ) ) {
1509 $index = array_search( $prod_meta->post_id, $bundle_childs );
1510 unset( $bundle_childs[ $index ] );
1511 }
1512 array_push( $product_array, $prod_obj );
1513 } else {
1514 continue;
1515 }
1516 }
1517 }
1518 if ( is_array( $bundle_childs ) && ! empty( $bundle_childs ) ) {
1519 foreach ( $bundle_childs as $item_id => $val ) {
1520 WC_PB_DB::delete_bundled_item( $item_id );
1521 }
1522 }
1523 if ( $allow_sync ) {
1524 return $product_array;
1525 }
1526 return false;
1527 }
1528 /**
1529 * Mapping of bundled product
1530 *
1531 * @param number $product_id - Product id of child item of bundle product.
1532 * @param number $bundle_id - Bundle id of product.
1533 * @param number $menu_order - Listing order of child product ($menu_order will useful at composite product details page).
1534 * @return number - bundle item id.
1535 */
1536 public function add_bundle_product( $product_id, $bundle_id, $menu_order = 0 ) {
1537 $bundle_items = WC_PB_DB::query_bundled_items(
1538 array(
1539 'return' => 'id=>product_id',
1540 'bundle_id' => array( $bundle_id ),
1541 'product_id' => array( $product_id ),
1542 )
1543 );
1544 $data = array(
1545 'menu_order' => $menu_order,
1546 );
1547
1548 if ( count( $bundle_items ) > 0 ) {
1549 $result = WC_PB_DB::update_bundled_item( $bundle_id, $data );
1550 return $result;
1551 } else {
1552 // create data array of bundle item.
1553 $data = array(
1554 'product_id' => $product_id,
1555 'bundle_id' => $bundle_id,
1556 'menu_order' => $menu_order,
1557 );
1558 $bundle_id = WC_PB_DB::add_bundled_item( $data );
1559 return $bundle_id;
1560 }
1561 }
1562
1563 /**
1564 * Create or update bundle item metadata
1565 *
1566 * @param number $bundle_item_id bundle item id.
1567 * @param string $meta_key - metadata key.
1568 * @param string $meta_value - metadata value.
1569 * @return float|int $result - metadata id.
1570 */
1571 public function zi_update_bundle_meta( $bundle_item_id, $meta_key, $meta_value ) {
1572 // first get metadata from db.
1573 $metadata = WC_PB_DB::get_bundled_item_meta( $bundle_item_id, $meta_key );
1574 if ( $metadata ) {
1575 $result = WC_PB_DB::update_bundled_item_meta( $bundle_item_id, $meta_key, $meta_value );
1576 } else {
1577 $result = WC_PB_DB::add_bundled_item_meta( $bundle_item_id, $meta_key, $meta_value );
1578 }
1579 return $result;
1580 }
1581
1582 /**
1583 * Function to sync composite item from zoho to woocommerce
1584 *
1585 * @param integer $page - Page number of composite item data.
1586 * @param string $category - Category id of composite data.
1587 * @return mixed - mostly array of response message.
1588 */
1589 public function recursively_sync_composite_item_from_zoho( $page, $category ) {
1590 // Start logging.
1591 // $fd = fopen( __DIR__ . '/recursively_sync_composite_item_from_zoho.txt', 'a+' );
1592
1593 global $wpdb;
1594 $zi_org_id = $this->config['ProductZI']['OID'];
1595 $zi_url = $this->config['ProductZI']['APIURL'];
1596
1597 $current_user = wp_get_current_user();
1598 $admin_author_id = $current_user->ID;
1599 if ( ! $admin_author_id ) {
1600 $admin_author_id = 1;
1601 }
1602
1603 $url = $zi_url . 'inventory/v1/compositeitems/?organization_id=' . $zi_org_id . '&filter_by=Status.Active&category_id=' . $category . '&page=' . $page;
1604
1605 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1606 $json = $execute_curl_call->execute_curl_call_get( $url );
1607 $code = $json->code;
1608 // $message = $json->message;
1609 // fwrite($fd, PHP_EOL . '$json : ' . print_r($json, true));
1610 // Response for item sync with sync button. For cron sync blank array will return.
1611 $response_msg = array();
1612 if ( '0' === $code || 0 === $code ) {
1613 if ( empty( $json->composite_items ) ) {
1614 array_push( $response_msg, $this->zi_response_message( 'ERROR', 'No composite item to sync for category : ' . $category ) );
1615 return $response_msg;
1616 }
1617 // Accounting stock mode check.
1618 $accounting_stock = $this->config['Settings']['enable_accounting_stock'];
1619 foreach ( $json->composite_items as $comp_item ) {
1620 // fwrite( $fd, PHP_EOL . 'Composite Item : ' . print_r( $comp_item, true ) );
1621 // Sync stock from specific location check.
1622 $zi_enable_locationstock = $this->config['Settings']['enable_locationstock'];
1623 $location_id = $this->config['Settings']['location_id'];
1624 $locations = $comp_item->locations;
1625
1626 if ( true === $zi_enable_locationstock ) {
1627 foreach ( $locations as $location ) {
1628 if ( $location->location_id === $location_id ) {
1629 if ( $accounting_stock ) {
1630 $stock = $location->location_available_for_sale_stock;
1631 } else {
1632 $stock = $location->location_actual_available_for_sale_stock;
1633 }
1634 }
1635 }
1636 } elseif ( $accounting_stock ) {
1637 $stock = $comp_item->available_for_sale_stock;
1638 } else {
1639 $stock = $comp_item->actual_available_for_sale_stock;
1640 }
1641
1642 // ----------------- Create composite item in woocommerce--------------.
1643 // Code to skip sync with item already exists with same sku.
1644 $prod_id = wc_get_product_id_by_sku( $comp_item->sku );
1645 // Flag to enable or disable sync.
1646 $allow_to_import = false;
1647 // Check if product exists with same sku.
1648 if ( $prod_id ) {
1649 $zi_item_id = get_post_meta( $prod_id, 'zi_item_id', true );
1650 if ( $zi_item_id === $comp_item->composite_item_id ) {
1651 // If product is with same sku and zi_item_id mapped.
1652 // Do not import ...
1653 $allow_to_import = false;
1654 } else {
1655 // Map existing item with zoho id.
1656 update_post_meta( $prod_id, 'zi_item_id', $comp_item->composite_item_id );
1657 $allow_to_import = false;
1658 }
1659 } else {
1660 // If product not exists normal bahaviour of item sync.
1661 $allow_to_import = true;
1662 }
1663 $zoho_comp_item_id = $comp_item->composite_item_id;
1664 if ( $comp_item->composite_item_id ) {
1665 $child_items = $this->zi_check_if_child_synced_already( $zoho_comp_item_id, $zi_url, $zi_org_id, $prod_id );
1666 // Check if child items already synced with zoho.
1667 if ( ! $child_items ) {
1668 array_push( $response_msg, $this->zi_response_message( 'ERROR', 'Child not synced for composite item : ' . $zoho_comp_item_id ) );
1669 continue;
1670 }
1671 $product_res = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = 'zi_item_id' AND meta_value = %s", $zoho_comp_item_id ) );
1672 if ( ! empty( $product_res->post_id ) ) {
1673 $com_prod_id = $product_res->post_id;
1674 }
1675 // Check if item is allowed to import or not.
1676 if ( $allow_to_import ) {
1677 $product_class = new CMBIRD_Products_ZI_Export();
1678 $item_array = json_decode( wp_json_encode( $comp_item ), true );
1679 $com_prod_id = $product_class->cmbird_zi_product_to_woocommerce( $item_array, $stock, 'composite' );
1680 update_post_meta( $com_prod_id, 'zi_item_id', $zoho_comp_item_id );
1681 }
1682 }
1683 // Map composite items to database.
1684 if ( ! empty( $com_prod_id ) ) {
1685 wp_set_object_terms( $com_prod_id, 'bundle', 'product_type' );
1686 foreach ( $child_items as $child_prod ) {
1687 // Adding product to bundle.
1688 $child_bundle_id = $this->add_bundle_product( $child_prod->prod_id, $com_prod_id );
1689 if ( $child_bundle_id ) {
1690 foreach ( $child_prod->metadata as $bundle_meta_key => $bundle_meta_val ) {
1691 $this->zi_update_bundle_meta( $child_bundle_id, $bundle_meta_key, $bundle_meta_val );
1692 }
1693 }
1694 }
1695 }
1696 // --------------------------------------------------------------------.
1697
1698 $is_synced_flag = false; // loggin purpose only .
1699
1700 $product = wc_get_product( $com_prod_id );
1701 foreach ( $comp_item as $key => $value ) {
1702 if ( 'status' === $key ) {
1703 if ( ! empty( $com_prod_id ) ) {
1704 $status = 'active' === $value ? 'publish' : 'draft';
1705 $product->set_status( $status );
1706 }
1707 }
1708 if ( 'description' === $key ) {
1709 if ( ! empty( $com_prod_id ) && ! empty( $value ) ) {
1710 $product->set_short_description( $value );
1711 }
1712 }
1713 if ( 'name' === $key ) {
1714 if ( ! empty( $com_prod_id ) ) {
1715 $product->set_name( $value );
1716 }
1717 }
1718 if ( 'sku' === $key ) {
1719 if ( ! empty( $com_prod_id ) ) {
1720 $product->set_sku( $value );
1721 }
1722 }
1723 // Check if stock sync allowed by plugin.
1724 if ( 'available_stock' === $key || 'actual_available_stock' === $key ) {
1725 $zi_disable_stock_sync = $this->config['Settings']['disable_stock'];
1726 if ( ! $zi_disable_stock_sync ) {
1727 if ( $stock ) {
1728 if ( ! empty( $com_prod_id ) ) {
1729 // If value is less than 0 default 1.
1730 $stock_quantity = $stock < 0 ? 0 : $stock;
1731 $product->set_manage_stock( true );
1732 $product->set_stock_quantity( $stock_quantity );
1733 if ( $stock_quantity > 0 ) {
1734 $status = 'instock';
1735 } else {
1736 $backorder_status = $product->backorders_allowed();
1737 $status = $backorder_status ? 'onbackorder' : 'outofstock';
1738 }
1739 $product->set_stock_status( $status );
1740 update_post_meta( $com_prod_id, '_wc_pb_bundled_items_stock_status', $status );
1741 }
1742 }
1743 }
1744 }
1745 if ( 'rate' === $key ) {
1746 if ( ! empty( $com_prod_id ) ) {
1747 $sale_price = $product->get_sale_price();
1748 if ( empty( $sale_price ) ) {
1749 $product->set_regular_price( $value );
1750 $product->set_price( $value );
1751 update_post_meta( $com_prod_id, '_wc_pb_base_price', $value );
1752 update_post_meta( $com_prod_id, '_wc_pb_base_regular_price', $value );
1753 update_post_meta( $com_prod_id, '_wc_sw_max_regular_price', $value );
1754 } else {
1755 $product->set_regular_price( $value );
1756 update_post_meta( $com_prod_id, '_wc_pb_base_price', $value );
1757 update_post_meta( $com_prod_id, '_wc_pb_base_regular_price', $value );
1758 update_post_meta( $com_prod_id, '_wc_sw_max_regular_price', $value );
1759 }
1760 }
1761 }
1762 $product->save();
1763
1764 if ( 'image_document_id' === $key ) {
1765 if ( ! empty( $com_prod_id ) && ! empty( $value ) ) {
1766 $image_class = new CMBIRD_Image_ZI();
1767 $image_class->cmbird_zi_get_image( $zoho_comp_item_id, $comp_item->name, $comp_item->image_name, $com_prod_id );
1768 }
1769 }
1770 if ( 'category_name' === $key ) {
1771 if ( ! empty( $com_prod_id ) && $comp_item->category_name != '' ) {
1772 $term = get_term_by( 'name', $comp_item->category_name, 'product_cat' );
1773 $term_id = $term->term_id;
1774 if ( empty( $term_id ) ) {
1775 $term = wp_insert_term(
1776 $comp_item->category_name,
1777 'product_cat',
1778 array(
1779 'parent' => 0,
1780 )
1781 );
1782 $term_id = $term['term_id'];
1783 }
1784 if ( $term_id ) {
1785 $existing_terms = wp_get_object_terms( $com_prod_id, 'product_cat' );
1786 if ( $existing_terms && count( $existing_terms ) > 0 ) {
1787 $is_terms_exist = $this->zi_check_terms_exists( $existing_terms, $term_id );
1788 if ( ! $is_terms_exist ) {
1789 update_post_meta( $com_prod_id, 'zi_category_id', $category );
1790 wp_add_object_terms( $com_prod_id, $term_id, 'product_cat' );
1791 }
1792 } else {
1793 update_post_meta( $com_prod_id, 'zi_category_id', $category );
1794 wp_set_object_terms( $com_prod_id, $term_id, 'product_cat' );
1795 }
1796 }
1797 // Remove "uncategorized" category if assigned.
1798 $uncategorized_term = get_term_by( 'slug', 'uncategorized', 'product_cat' );
1799 if ( $uncategorized_term && has_term( $uncategorized_term->term_id, 'product_cat', $com_prod_id ) ) {
1800 wp_remove_object_terms( $com_prod_id, $uncategorized_term->term_id, 'product_cat' );
1801 }
1802 }
1803 }
1804 }
1805
1806 // sync dimensions and weight.
1807 $item_url = "{$zi_url}inventory/v1/compositeitems/{$zoho_comp_item_id}?organization_id={$zi_org_id}";
1808 $this->zi_item_dimension_weight( $item_url, $com_prod_id, true );
1809
1810 // If item synced append to log : logging purpose only.
1811 if ( $is_synced_flag ) {
1812 array_push( $response_msg, $this->zi_response_message( 'SUCCESS', 'Composite item synced for id : ' . $comp_item->composite_item_id, $com_prod_id ) );
1813 }
1814 }
1815
1816 if ( $json->page_context->has_more_page ) {
1817 ++$page;
1818 $this->recursively_sync_composite_item_from_zoho( $page, $category );
1819 }
1820 } else {
1821 array_push( $response_msg, $this->zi_response_message( $code, $json->message ) );
1822 }
1823 // fclose( $fd ); // End of logging.
1824
1825 return $response_msg;
1826 }
1827
1828 /**
1829 * Function to retrieve item details, update weight and dimensions.
1830 *
1831 * @param string $url - URL to ge details.
1832 * @return mixed return true if data false if error.
1833 */
1834 public function zi_item_dimension_weight( $url, $product_id, $is_composite = false ) {
1835 // $fd = fopen(__DIR__ . '/zi_item_dimension_weight.txt', 'a+');
1836 // Check if item is for syncing purpose.
1837 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1838 $json = $execute_curl_call->execute_curl_call_get( $url );
1839 $code = $json->code;
1840 $message = $json->message;
1841 if ( 0 === $code || '0' === $code ) {
1842 if ( $is_composite ) {
1843 // fwrite($fd, PHP_EOL . '$json : ' . print_r($json, true));
1844 $details = $json->composite_item->package_details;
1845 } else {
1846 $details = $json->item->package_details;
1847 }
1848 $product = wc_get_product( $product_id );
1849 $product->set_weight( floatval( $details->weight ) );
1850 $product->set_length( floatval( $details->length ) );
1851 $product->set_width( floatval( $details->width ) );
1852 $product->set_height( floatval( $details->height ) );
1853 $product->save();
1854 } else {
1855 false;
1856 }
1857 // fclose($fd);
1858 }
1859
1860 /**
1861 * Create response object based on data.
1862 *
1863 * @param mixed $index_col - Index value error message.
1864 * @param string $message - Response message.
1865 * @return object
1866 */
1867 public function zi_response_message( $index_col, $message, $woo_id = '' ) {
1868 return (object) array(
1869 'resp_id' => $index_col,
1870 'message' => $message,
1871 'woo_prod_id' => $woo_id,
1872 );
1873 }
1874
1875 /**
1876 * Helper Function to check if terms already exists.
1877 */
1878 public function zi_check_terms_exists( $existing_terms, $term_id ) {
1879 foreach ( $existing_terms as $woo_existing_term ) {
1880 if ( $woo_existing_term->term_id === $term_id ) {
1881 return true;
1882 } else {
1883 return false;
1884 }
1885 }
1886 }
1887
1888 /**
1889 * Refactored batch sync method using WooCommerce REST API batch endpoint.
1890 *
1891 * Fetches items from Zoho API, optionally fetches itemdetails for location stock,
1892 * and processes items in batches through the WooCommerce REST API.
1893 *
1894 * @param array $args Array containing 'page' and 'category' parameters.
1895 * @return array|bool Success or failure status.
1896 */
1897 public function sync_items_batch( $args ) {
1898 $args = func_get_args();
1899 $config = $this->config['Settings'];
1900 if ( is_array( $args ) ) {
1901 if ( isset( $args['page'] ) && isset( $args['category'] ) ) {
1902 $page = $args['page'];
1903 $category = $args['category'];
1904 } elseif ( isset( $args[0] ) && isset( $args[1] ) ) {
1905 $page = $args[0];
1906 $category = $args[1];
1907 } elseif ( isset( $args[0] ) && ! isset( $args[1] ) ) {
1908 $page = $args[0]['page'];
1909 $category = $args[0]['category'];
1910 } else {
1911 return;
1912 }
1913 } else {
1914 return;
1915 }
1916
1917 // Fetch items from Zoho items endpoint.
1918 $zoho_inventory_oid = $this->config['ProductZI']['OID'];
1919 $zoho_inventory_url = $this->config['ProductZI']['APIURL'];
1920 $timeout = ini_get( 'max_execution_time' ) ? ini_get( 'max_execution_time' ) : 60;
1921
1922 // Dynamically set per_page based on timeout.
1923 if ( $timeout <= 30 ) {
1924 $per_page = 20;
1925 } elseif ( $timeout <= 60 ) {
1926 $per_page = 50;
1927 } else {
1928 $per_page = 100;
1929 }
1930
1931 $url = sprintf(
1932 '%sinventory/v1/items?organization_id=%s&category_id=%s&page=%d&per_page=%d&sort_column=last_modified_time',
1933 $zoho_inventory_url,
1934 $zoho_inventory_oid,
1935 $category,
1936 $page,
1937 $per_page
1938 );
1939 $execute_curl_call = new CMBIRD_API_Handler_Zoho();
1940 $json = $execute_curl_call->execute_curl_call_get( $url );
1941 $code = (int) property_exists( $json, 'code' ) ? $json->code : '0';
1942
1943 if ( 0 !== $code && '0' !== $code ) {
1944 return false;
1945 }
1946
1947 if ( ! isset( $json->items ) || empty( $json->items ) ) {
1948 return false;
1949 }
1950
1951 // Separate items based on location stock requirement.
1952 $items_ready = array();
1953 $item_ids = array();
1954
1955 foreach ( $json->items as $item ) {
1956 // Skip combo products.
1957 if ( isset( $item->is_combo_product ) && $item->is_combo_product ) {
1958 continue;
1959 }
1960
1961 // Collect item IDs if location stock is enabled.
1962 if ( $config['enable_location_stock'] ) {
1963 $item_ids[] = $item->item_id;
1964 } else {
1965 $items_ready[] = $item;
1966 }
1967 }
1968
1969 // Fetch itemdetails for location stock if needed.
1970 if ( ! empty( $item_ids ) ) {
1971 $item_details_url_base = "{$zoho_inventory_url}inventory/v1/itemdetails?organization_id={$zoho_inventory_oid}&item_ids=";
1972 $item_id_str = implode( ',', $item_ids );
1973 $item_details_url = $item_details_url_base . $item_id_str;
1974 $json_details = $execute_curl_call->execute_curl_call_get( $item_details_url );
1975 $details_code = (int) property_exists( $json_details, 'code' ) ? $json_details->code : '0';
1976
1977 if ( 0 === $details_code || '0' === $details_code ) {
1978 if ( isset( $json_details->items ) && is_array( $json_details->items ) ) {
1979 foreach ( $json_details->items as $item_detail ) {
1980 $items_ready[] = $item_detail;
1981 }
1982 }
1983 }
1984 }
1985
1986 // Process items through batch endpoint.
1987 if ( ! empty( $items_ready ) ) {
1988 $this->process_items_batch( $items_ready, $config );
1989 }
1990
1991 // Handle pagination.
1992 if ( isset( $json->page_context->has_more_page ) && $json->page_context->has_more_page && $page < 1000 ) {
1993 as_schedule_single_action(
1994 time() + 5,
1995 'import_simple_items_cron',
1996 array(
1997 'page' => $page + 1,
1998 'category' => $category,
1999 ),
2000 'commercebird'
2001 );
2002 }
2003
2004 return array(
2005 'success' => true,
2006 'message' => 'Batch processed successfully',
2007 );
2008 }
2009
2010 /**
2011 * Process items batch for create/update via WooCommerce REST API.
2012 *
2013 * Handles product matching (by SKU or zi_item_id), duplicate removal,
2014 * and builds create/update arrays for batch endpoint.
2015 *
2016 * @param array $items Array of Zoho items to process.
2017 * @param array $config Configuration array with sync settings.
2018 * @return void
2019 */
2020 private function process_items_batch( $items, $config ) {
2021 global $wpdb;
2022
2023 $to_create = array();
2024 $to_update = array();
2025
2026 foreach ( $items as $item ) {
2027 // Skip variations/groups.
2028 if ( isset( $item->group_id ) && ! empty( $item->group_id ) ) {
2029 $this->sync_variation_of_group( $item );
2030 continue;
2031 }
2032
2033 // 1. Try to find product by SKU.
2034 $product_id = ( isset( $item->sku ) && ! empty( $item->sku ) )
2035 ? wc_get_product_id_by_sku( $item->sku )
2036 : 0;
2037
2038 // 2. If product exists by SKU, check if mapped to Zoho.
2039 if ( $product_id ) {
2040 $zi_item_id = get_post_meta( $product_id, 'zi_item_id', true );
2041 if ( empty( $zi_item_id ) ) {
2042 // Map existing product with Zoho item ID.
2043 update_post_meta( $product_id, 'zi_item_id', $item->item_id );
2044 }
2045 }
2046
2047 // 3. Remove duplicates based on SKU.
2048 if ( $product_id && ! empty( $item->sku ) ) {
2049 $duplicate_product = $wpdb->get_row(
2050 $wpdb->prepare(
2051 "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_sku' AND meta_value = %s AND post_id != %d LIMIT 1",
2052 $item->sku,
2053 $product_id
2054 )
2055 );
2056 if ( $duplicate_product ) {
2057 wp_delete_post( $duplicate_product->post_id, true );
2058 $product_id = 0;
2059 }
2060 }
2061
2062 // 4. If no product by SKU, try to find by zi_item_id.
2063 if ( empty( $product_id ) ) {
2064 $existing = $wpdb->get_row(
2065 $wpdb->prepare(
2066 "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = 'zi_item_id' AND meta_value = %s LIMIT 1",
2067 $item->item_id
2068 )
2069 );
2070 $product_id = $existing ? $existing->post_id : 0;
2071 }
2072
2073 // 5. Create or update based on what we found.
2074 if ( empty( $product_id ) && isset( $item->status ) && 'active' === $item->status ) {
2075 // Create new product.
2076 $to_create[] = $this->map_item_to_array( $item, $config, null );
2077 } elseif ( ! empty( $product_id ) ) {
2078 // Update existing product.
2079 $to_update[] = $this->map_item_to_array( $item, $config, $product_id );
2080 }
2081 }
2082
2083 // Use batch endpoint.
2084 if ( ! empty( $to_create ) ) {
2085 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import( 'product', $to_create );
2086 }
2087
2088 if ( ! empty( $to_update ) ) {
2089 \CommerceBird\Admin\Actions\Sync\ZohoInventorySync::import( 'product', $to_update, true );
2090 }
2091 }
2092
2093 /**
2094 * Map Zoho item to WooCommerce batch endpoint format.
2095 *
2096 * Transforms Zoho item data into WooCommerce REST API product format
2097 * for batch create/update operations.
2098 *
2099 * @param object $item Zoho item object.
2100 * @param array $config Configuration array with sync settings.
2101 * @param int $product_id WooCommerce product ID (null for create).
2102 * @return array WooCommerce product data for batch endpoint.
2103 */
2104 private function map_item_to_array( $item, $config, $product_id = null ) {
2105 $data = array(
2106 'name' => isset( $item->name ) ? $item->name : '',
2107 'sku' => isset( $item->sku ) ? $item->sku : '',
2108 'status' => ( isset( $item->status ) && 'active' === $item->status ) ? 'publish' : 'draft',
2109 'regular_price' => isset( $item->rate ) ? (string) $item->rate : '0',
2110 );
2111
2112 // Add product ID for updates.
2113 if ( $product_id ) {
2114 $data['id'] = $product_id;
2115 }
2116
2117 // Add description if not disabled.
2118 if ( ! $config['disable_description'] && isset( $item->description ) ) {
2119 $data['short_description'] = $item->description;
2120 }
2121
2122 // Add images if not disabled.
2123 if ( ! $config['disable_image'] && isset( $item->image_document_id ) && ! empty( $item->image_document_id ) ) {
2124 $image_class = new CMBIRD_Image_ZI();
2125 $attachment_id = $image_class->cmbird_zi_get_image(
2126 $item->item_id,
2127 isset( $item->name ) ? $item->name : '',
2128 isset( $item->image_name ) ? $item->image_name : '',
2129 $product_id
2130 );
2131
2132 // Add image to batch data.
2133 if ( $attachment_id ) {
2134 $data['images'] = array(
2135 array( 'id' => $attachment_id ),
2136 );
2137 }
2138 }
2139
2140 // Check if Tax is enabled and set tax status.
2141 if ( ! empty( $item->tax_id ) && $this->is_tax_enabled ) {
2142 $zi_common_class = new CMBIRD_Common_Functions();
2143 $woo_tax_class = $zi_common_class->get_tax_class_by_percentage( $item->tax_percentage );
2144 $data['tax_class'] = $woo_tax_class;
2145 $data['tax_status'] = 'taxable';
2146 }
2147
2148 // Handle stock.
2149 if ( ! $config['disable_stock'] ) {
2150 $stock = null;
2151
2152 // Use location stock if available (from itemdetails).
2153 if ( isset( $item->locations ) && is_array( $item->locations ) ) {
2154 foreach ( $item->locations as $location ) {
2155 if ( $location->location_id === $config['zoho_location_id'] ) {
2156 $stock = $config['enable_accounting_stock']
2157 ? $location->location_available_for_sale_stock
2158 : $location->location_actual_available_for_sale_stock;
2159 break;
2160 }
2161 }
2162 } else {
2163 // Use global stock from items endpoint.
2164 $stock = $config['enable_accounting_stock']
2165 ? ( isset( $item->available_for_sale_stock ) ? $item->available_for_sale_stock : null )
2166 : ( isset( $item->actual_available_for_sale_stock ) ? $item->actual_available_for_sale_stock : null );
2167 }
2168 if ( null !== $stock ) {
2169 $data['manage_stock'] = true;
2170 $data['stock_quantity'] = $stock;
2171 }
2172 }
2173 // Add cost_of_goods_sold if available.
2174 if ( isset( $item->purchase_rate ) ) {
2175 $data['cost_of_goods_sold'] = $item->purchase_rate;
2176 }
2177
2178 // Add dimensions directly from items endpoint.
2179 if ( isset( $item->weight ) ) {
2180 $data['weight'] = $item->weight;
2181 }
2182 if ( isset( $item->length ) ) {
2183 $data['length'] = $item->length;
2184 }
2185 if ( isset( $item->width ) ) {
2186 $data['width'] = $item->width;
2187 }
2188 if ( isset( $item->height ) ) {
2189 $data['height'] = $item->height;
2190 }
2191
2192 // Add categories.
2193 if ( isset( $item->category_name ) && ! empty( $item->category_name ) ) {
2194 $categories = self::prepare_categories( $item->category_name );
2195 if ( ! empty( $categories ) ) {
2196 $data['categories'] = $categories;
2197 }
2198 }
2199
2200 // Add brand as category if present.
2201 if ( isset( $item->brand ) && ! empty( $item->brand ) ) {
2202 $brand_terms = self::prepare_categories( $item->brand, 'product_brand' );
2203 if ( ! empty( $brand_terms ) ) {
2204 $data['brands'] = $brand_terms;
2205 }
2206 }
2207
2208 // Add meta data.
2209 $data['meta_data'] = array(
2210 array(
2211 'key' => 'zi_item_id',
2212 'value' => isset( $item->item_id ) ? $item->item_id : '',
2213 ),
2214 array(
2215 'key' => 'zi_category_id',
2216 'value' => isset( $item->category_id ) ? $item->category_id : '',
2217 ),
2218 );
2219
2220 // Add custom fields from items endpoint (flattened format).
2221 foreach ( $item as $key => $value ) {
2222 if ( 0 === strpos( $key, 'cf_' ) && false === strpos( $key, '_unformatted' ) ) {
2223 $data['meta_data'][] = array(
2224 'key' => 'zi_' . $key,
2225 'value' => $value,
2226 );
2227 }
2228 }
2229
2230 return $data;
2231 }
2232
2233 /**
2234 * Prepare category data for WooCommerce batch endpoint.
2235 *
2236 * Creates or retrieves category term and returns in batch format.
2237 *
2238 * @param string $category_name Category name from Zoho.
2239 * @param string $taxonomy Taxonomy to use, default is 'product_cat'.
2240 * @return array Category array for WooCommerce batch endpoint.
2241 */
2242 private static function prepare_categories( $category_name, $taxonomy = 'product_cat' ) {
2243 if ( empty( $category_name ) ) {
2244 return array();
2245 }
2246
2247 $term = get_term_by( 'name', $category_name, $taxonomy );
2248 $term_id = $term ? $term->term_id : 0;
2249
2250 if ( ! $term_id ) {
2251 $term = wp_insert_term( $category_name, $taxonomy, array( 'parent' => 0 ) );
2252 $term_id = isset( $term['term_id'] ) ? $term['term_id'] : 0;
2253 }
2254
2255 return $term_id ? array( array( 'id' => $term_id ) ) : array();
2256 }
2257
2258 /**
2259 * Get sync configuration from class config or WordPress options.
2260 *
2261 * Merges class configuration with WordPress option settings.
2262 *
2263 * @return array Configuration array.
2264 */
2265 }
2266 $cmbird_products_zi = new CMBIRD_Products_ZI();
2267