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