Plugin Directory

Changeset 3408329


Ignore:
Timestamp:
12/02/2025 05:50:28 PM (4 months ago)
Author:
malakontask
Message:

v2.8.5 - Add support for Perfect Brands and YITH WooCommerce Brands plugins

Location:
transfer-brands-for-woocommerce
Files:
12 edited
1 copied

Legend:

Unmodified
Added
Removed
  • transfer-brands-for-woocommerce/tags/2.8.5/includes/class-admin.php

    r3408293 r3408329  
    303303     *
    304304     * @since 2.3.0
     305     * @since 2.8.5 Added support for Perfect Brands for WooCommerce (pwb-brand)
    305306     */
    306307    public function source_taxonomy_callback() {
    307308        $attribute_taxonomies = wc_get_attribute_taxonomies();
    308309        $source = $this->core->get_option('source_taxonomy', 'pa_brand');
    309        
     310
    310311        echo '<select name="tbfw_transfer_brands_options[source_taxonomy]">';
    311        
    312         foreach ($attribute_taxonomies as $tax) {
    313             $tax_name = 'pa_' . $tax->attribute_name;
    314             echo '<option value="' . esc_attr($tax_name) . '" ' . selected($tax_name, $source, false) . '>';
    315             echo esc_html($tax->attribute_label) . ' (' . esc_html($tax_name) . ')';
    316             echo '</option>';
     312
     313        // WooCommerce Product Attributes section
     314        if (!empty($attribute_taxonomies)) {
     315            echo '<optgroup label="' . esc_attr__('WooCommerce Attributes', 'transfer-brands-for-woocommerce') . '">';
     316            foreach ($attribute_taxonomies as $tax) {
     317                $tax_name = 'pa_' . $tax->attribute_name;
     318                echo '<option value="' . esc_attr($tax_name) . '" ' . selected($tax_name, $source, false) . '>';
     319                echo esc_html($tax->attribute_label) . ' (' . esc_html($tax_name) . ')';
     320                echo '</option>';
     321            }
     322            echo '</optgroup>';
    317323        }
    318        
     324
     325        // Brand Plugins section - check for supported brand plugin taxonomies
     326        $brand_plugins = $this->get_supported_brand_plugins();
     327        if (!empty($brand_plugins)) {
     328            echo '<optgroup label="' . esc_attr__('Brand Plugins', 'transfer-brands-for-woocommerce') . '">';
     329            foreach ($brand_plugins as $plugin) {
     330                echo '<option value="' . esc_attr($plugin['taxonomy']) . '" ' . selected($plugin['taxonomy'], $source, false) . '>';
     331                echo esc_html($plugin['label']);
     332                echo '</option>';
     333            }
     334            echo '</optgroup>';
     335        }
     336
    319337        echo '</select>';
    320         echo '<p class="description">' . esc_html__('Select the source attribute that contains your brands.', 'transfer-brands-for-woocommerce') . '</p>';
     338        echo '<p class="description">' . esc_html__('Select the source attribute or taxonomy that contains your brands.', 'transfer-brands-for-woocommerce') . '</p>';
     339
     340        // Show info about detected brand plugins
     341        if (!empty($brand_plugins)) {
     342            echo '<p class="description" style="color: #2271b1;"><span class="dashicons dashicons-info" style="font-size: 14px; width: 14px; height: 14px;"></span> ';
     343            echo esc_html__('Detected brand plugin(s):', 'transfer-brands-for-woocommerce') . ' ';
     344            $plugin_names = array_column($brand_plugins, 'name');
     345            echo '<strong>' . esc_html(implode(', ', $plugin_names)) . '</strong>';
     346            echo '</p>';
     347        }
     348    }
     349
     350    /**
     351     * Get list of supported brand plugins that are active
     352     *
     353     * @since 2.8.5
     354     * @return array Array of supported brand plugins with their taxonomies
     355     */
     356    private function get_supported_brand_plugins() {
     357        $plugins = [];
     358
     359        // Perfect Brands for WooCommerce
     360        if (taxonomy_exists('pwb-brand')) {
     361            $plugins[] = [
     362                'taxonomy' => 'pwb-brand',
     363                'name' => 'Perfect Brands for WooCommerce',
     364                'label' => __('Perfect Brands (pwb-brand)', 'transfer-brands-for-woocommerce'),
     365                'image_meta_key' => 'pwb_brand_image'
     366            ];
     367        }
     368
     369        // YITH WooCommerce Brands (uses yith_product_brand taxonomy)
     370        if (taxonomy_exists('yith_product_brand')) {
     371            $plugins[] = [
     372                'taxonomy' => 'yith_product_brand',
     373                'name' => 'YITH WooCommerce Brands',
     374                'label' => __('YITH Brands (yith_product_brand)', 'transfer-brands-for-woocommerce'),
     375                'image_meta_key' => 'yith_woocommerce_brand_thumbnail_id'
     376            ];
     377        }
     378
     379        return $plugins;
    321380    }
    322381   
  • transfer-brands-for-woocommerce/tags/2.8.5/includes/class-ajax.php

    r3408293 r3408329  
    134134    /**
    135135     * AJAX handler for checking brands
     136     *
     137     * @since 2.8.5 Added support for brand plugin taxonomies (pwb-brand, etc.)
    136138     */
    137139    public function ajax_check_brands() {
    138140        check_ajax_referer('tbfw_transfer_brands_nonce', 'nonce');
    139        
    140         if (!current_user_can('manage_woocommerce')) {
    141             wp_die(__('You do not have permission to perform this action.', 'transfer-brands-for-woocommerce'));
    142         }
    143        
     141
     142        if (!current_user_can('manage_woocommerce')) {
     143            wp_die(__('You do not have permission to perform this action.', 'transfer-brands-for-woocommerce'));
     144        }
     145
     146        $source_taxonomy = $this->core->get_option('source_taxonomy');
     147        $is_brand_plugin = $this->core->get_utils()->is_brand_plugin_taxonomy($source_taxonomy);
     148
    144149        $source_terms = get_terms([
    145             'taxonomy' => $this->core->get_option('source_taxonomy'),
     150            'taxonomy' => $source_taxonomy,
    146151            'hide_empty' => false
    147152        ]);
    148        
     153
    149154        if (is_wp_error($source_terms)) {
    150155            wp_send_json_error(['message' => 'Error: ' . $source_terms->get_error_message()]);
    151156            return;
    152157        }
    153        
     158
    154159        global $wpdb;
    155        
    156         // Get info about custom attributes
    157         $custom_attribute_count = $wpdb->get_var(
    158             $wpdb->prepare(
    159                 "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta}
    160                 WHERE meta_key = '_product_attributes'
    161                 AND meta_value LIKE %s
    162                 AND meta_value LIKE %s
    163                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
    164                 '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%',
    165                 '%"is_taxonomy";i:0;%'
    166             )
    167         );
    168        
    169         // Sample of products with custom attributes
    170         $custom_products = $wpdb->get_results(
    171             $wpdb->prepare(
    172                 "SELECT post_id, meta_value FROM {$wpdb->postmeta}
    173                 WHERE meta_key = '_product_attributes'
    174                 AND meta_value LIKE %s
    175                 AND meta_value LIKE %s
    176                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
    177                 LIMIT 10",
    178                 '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%',
    179                 '%"is_taxonomy";i:0;%'
    180             )
    181         );
    182        
     160
     161        $custom_attribute_count = 0;
    183162        $custom_samples = [];
    184         foreach ($custom_products as $item) {
    185             $attributes = maybe_unserialize($item->meta_value);
    186             if (is_array($attributes) && isset($attributes[$this->core->get_option('source_taxonomy')])) {
    187                 $product = wc_get_product($item->post_id);
    188                 if ($product) {
    189                     $custom_samples[] = [
    190                         'id' => $item->post_id,
    191                         'name' => $product->get_name(),
    192                         'value' => $attributes[$this->core->get_option('source_taxonomy')]['value'] ?? 'N/A'
    193                     ];
     163        $sample_products = [];
     164
     165        // For brand plugin taxonomies, skip custom attribute checks (they don't use _product_attributes)
     166        if (!$is_brand_plugin) {
     167            // Get info about custom attributes (only for WooCommerce attributes)
     168            $custom_attribute_count = $wpdb->get_var(
     169                $wpdb->prepare(
     170                    "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta}
     171                    WHERE meta_key = '_product_attributes'
     172                    AND meta_value LIKE %s
     173                    AND meta_value LIKE %s
     174                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
     175                    '%' . $wpdb->esc_like($source_taxonomy) . '%',
     176                    '%"is_taxonomy";i:0;%'
     177                )
     178            );
     179
     180            // Sample of products with custom attributes
     181            $custom_products = $wpdb->get_results(
     182                $wpdb->prepare(
     183                    "SELECT post_id, meta_value FROM {$wpdb->postmeta}
     184                    WHERE meta_key = '_product_attributes'
     185                    AND meta_value LIKE %s
     186                    AND meta_value LIKE %s
     187                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
     188                    LIMIT 10",
     189                    '%' . $wpdb->esc_like($source_taxonomy) . '%',
     190                    '%"is_taxonomy";i:0;%'
     191                )
     192            );
     193
     194            foreach ($custom_products as $item) {
     195                $attributes = maybe_unserialize($item->meta_value);
     196                if (is_array($attributes) && isset($attributes[$source_taxonomy])) {
     197                    $product = wc_get_product($item->post_id);
     198                    if ($product) {
     199                        $custom_samples[] = [
     200                            'id' => $item->post_id,
     201                            'name' => $product->get_name(),
     202                            'value' => $attributes[$source_taxonomy]['value'] ?? 'N/A'
     203                        ];
     204                    }
    194205                }
    195206            }
    196207        }
    197        
     208
    198209        // Check if any terms already exist in destination
    199210        $conflicting_terms = [];
    200211        $terms_with_images = 0;
    201        
     212
    202213        foreach ($source_terms as $term) {
    203214            $exists = term_exists($term->name, $this->core->get_option('destination_taxonomy'));
     
    205216                $conflicting_terms[] = $term->name;
    206217            }
    207            
     218
    208219            // Check if term has image using the comprehensive detection method
    209220            $transfer_instance = $this->core->get_transfer();
     
    213224            $method->setAccessible(true);
    214225            $image_id = $method->invoke($transfer_instance, $term->term_id);
    215            
     226
    216227            if ($image_id) {
    217228                $terms_with_images++;
    218229            }
    219230        }
    220        
    221         // Get some sample products with taxonomy attributes
    222         $products_query = new WP_Query([
    223             'post_type' => 'product',
    224             'posts_per_page' => 5,
    225             'tax_query' => [
    226                 [
    227                     'taxonomy' => $this->core->get_option('source_taxonomy'),
    228                     'operator' => 'EXISTS',
     231
     232        // Get sample products - different logic for brand plugins vs WooCommerce attributes
     233        if ($is_brand_plugin) {
     234            // For brand plugins, query products via taxonomy relationship
     235            $products_query = new WP_Query([
     236                'post_type' => 'product',
     237                'posts_per_page' => 5,
     238                'post_status' => 'publish',
     239                'tax_query' => [
     240                    [
     241                        'taxonomy' => $source_taxonomy,
     242                        'operator' => 'EXISTS',
     243                    ]
    229244                ]
    230             ]
    231         ]);
    232        
    233         $sample_products = [];
    234        
    235         if ($products_query->have_posts()) {
    236             foreach ($products_query->posts as $post) {
    237                 $product = wc_get_product($post->ID);
    238                 $attrs = $product->get_attributes();
    239                
    240                 if (isset($attrs[$this->core->get_option('source_taxonomy')])) {
    241                     $terms = [];
    242                     foreach ($attrs[$this->core->get_option('source_taxonomy')]->get_options() as $term_id) {
    243                         $term = get_term($term_id, $this->core->get_option('source_taxonomy'));
    244                         if ($term && !is_wp_error($term)) {
    245                             $terms[] = $term->name;
    246                         }
     245            ]);
     246
     247            if ($products_query->have_posts()) {
     248                foreach ($products_query->posts as $post) {
     249                    $product = wc_get_product($post->ID);
     250                    if (!$product) continue;
     251
     252                    // Get terms directly from taxonomy
     253                    $product_terms = get_the_terms($post->ID, $source_taxonomy);
     254                    $term_names = [];
     255
     256                    if ($product_terms && !is_wp_error($product_terms)) {
     257                        $term_names = wp_list_pluck($product_terms, 'name');
    247258                    }
    248                    
     259
    249260                    $sample_products[] = [
    250261                        'id' => $post->ID,
    251262                        'name' => $product->get_name(),
    252                         'brands' => $terms
     263                        'brands' => $term_names
    253264                    ];
    254265                }
    255266            }
    256         }
    257        
     267        } else {
     268            // For WooCommerce attributes, use the original query
     269            $products_query = new WP_Query([
     270                'post_type' => 'product',
     271                'posts_per_page' => 5,
     272                'tax_query' => [
     273                    [
     274                        'taxonomy' => $source_taxonomy,
     275                        'operator' => 'EXISTS',
     276                    ]
     277                ]
     278            ]);
     279
     280            if ($products_query->have_posts()) {
     281                foreach ($products_query->posts as $post) {
     282                    $product = wc_get_product($post->ID);
     283                    $attrs = $product->get_attributes();
     284
     285                    if (isset($attrs[$source_taxonomy])) {
     286                        $terms = [];
     287                        foreach ($attrs[$source_taxonomy]->get_options() as $term_id) {
     288                            $term = get_term($term_id, $source_taxonomy);
     289                            if ($term && !is_wp_error($term)) {
     290                                $terms[] = $term->name;
     291                            }
     292                        }
     293
     294                        $sample_products[] = [
     295                            'id' => $post->ID,
     296                            'name' => $product->get_name(),
     297                            'brands' => $terms
     298                        ];
     299                    }
     300                }
     301            }
     302        }
     303
    258304        // Add debug info
    259305        $this->core->add_debug("Brand analysis performed", [
    260306            'source_terms' => count($source_terms),
     307            'is_brand_plugin' => $is_brand_plugin,
    261308            'custom_attribute_count' => $custom_attribute_count,
    262309            'taxonomy_samples' => count($sample_products),
     
    297344
    298345        $html .= '<h4>' . esc_html__('Source Brands Summary', 'transfer-brands-for-woocommerce') . '</h4>';
    299         $html .= '<ul>';
    300         $html .= '<li><strong>' . count($source_terms) . '</strong> ' . esc_html__('brands found in taxonomy', 'transfer-brands-for-woocommerce') . ' ' . esc_html($this->core->get_option('source_taxonomy')) . '</li>';
    301         $html .= '<li><strong>' . $custom_attribute_count . '</strong> ' . esc_html__('products have custom (non-taxonomy) attributes with name', 'transfer-brands-for-woocommerce') . ' ' . esc_html($this->core->get_option('source_taxonomy')) . '</li>';
    302         $html .= '<li><strong>' . $terms_with_images . '</strong> ' . esc_html__('brands have images that will be transferred', 'transfer-brands-for-woocommerce') . '</li>';
    303         $html .= '</ul>';
    304        
    305         // Warning about custom attributes
    306         if ($custom_attribute_count > 0) {
     346
     347        // Show different info for brand plugins vs WooCommerce attributes
     348        if ($is_brand_plugin) {
     349            $html .= '<div class="notice notice-info inline" style="margin: 0 0 15px 0; padding: 10px 12px;">';
     350            $html .= '<p style="margin: 0;"><span class="dashicons dashicons-info" style="color: #2271b1;"></span> ';
     351            $html .= '<strong>' . esc_html__('Brand Plugin Detected:', 'transfer-brands-for-woocommerce') . '</strong> ';
     352            $html .= esc_html__('Transferring from a third-party brand plugin taxonomy.', 'transfer-brands-for-woocommerce');
     353            $html .= '</p></div>';
     354
     355            // Get product count for brand plugin
     356            $brand_plugin_product_count = $this->core->get_utils()->count_products_with_source();
     357
     358            $html .= '<ul>';
     359            $html .= '<li><strong>' . count($source_terms) . '</strong> ' . esc_html__('brands found in taxonomy', 'transfer-brands-for-woocommerce') . ' <code>' . esc_html($source_taxonomy) . '</code></li>';
     360            $html .= '<li><strong>' . $brand_plugin_product_count . '</strong> ' . esc_html__('products have brands assigned', 'transfer-brands-for-woocommerce') . '</li>';
     361            $html .= '<li><strong>' . $terms_with_images . '</strong> ' . esc_html__('brands have images that will be transferred', 'transfer-brands-for-woocommerce') . '</li>';
     362            $html .= '</ul>';
     363        } else {
     364            $html .= '<ul>';
     365            $html .= '<li><strong>' . count($source_terms) . '</strong> ' . esc_html__('brands found in taxonomy', 'transfer-brands-for-woocommerce') . ' <code>' . esc_html($source_taxonomy) . '</code></li>';
     366            $html .= '<li><strong>' . $custom_attribute_count . '</strong> ' . esc_html__('products have custom (non-taxonomy) attributes with name', 'transfer-brands-for-woocommerce') . ' <code>' . esc_html($source_taxonomy) . '</code></li>';
     367            $html .= '<li><strong>' . $terms_with_images . '</strong> ' . esc_html__('brands have images that will be transferred', 'transfer-brands-for-woocommerce') . '</li>';
     368            $html .= '</ul>';
     369        }
     370
     371        // Warning about custom attributes (only for WooCommerce attributes)
     372        if (!$is_brand_plugin && $custom_attribute_count > 0) {
    307373            $html .= '<div class="notice notice-warning inline" style="margin-top: 15px;">';
    308374            $html .= '<p><strong>Custom Attributes Detected:</strong> Some of your products use custom (non-taxonomy) attributes for brands.</p>';
     
    349415       
    350416        if (!empty($sample_products)) {
    351             $html .= '<h4>Sample Taxonomy Products</h4>';
    352             $html .= '<p>Here are some products with taxonomy brand attributes:</p>';
     417            if ($is_brand_plugin) {
     418                $html .= '<h4>' . esc_html__('Sample Products with Brand Plugin Brands', 'transfer-brands-for-woocommerce') . '</h4>';
     419                $html .= '<p>' . esc_html__('Here are some products with brands from the brand plugin:', 'transfer-brands-for-woocommerce') . '</p>';
     420            } else {
     421                $html .= '<h4>' . esc_html__('Sample Products with Brand Attributes', 'transfer-brands-for-woocommerce') . '</h4>';
     422                $html .= '<p>' . esc_html__('Here are some products with taxonomy brand attributes:', 'transfer-brands-for-woocommerce') . '</p>';
     423            }
    353424            $html .= '<table class="widefat" style="margin-top: 10px;">';
    354             $html .= '<thead><tr><th>ID</th><th>Product</th><th>Current Brands</th></tr></thead>';
     425            $html .= '<thead><tr><th>ID</th><th>' . esc_html__('Product', 'transfer-brands-for-woocommerce') . '</th><th>' . esc_html__('Current Brands', 'transfer-brands-for-woocommerce') . '</th></tr></thead>';
    355426            $html .= '<tbody>';
    356            
     427
    357428            foreach ($sample_products as $product) {
    358429                $html .= '<tr>';
    359                 $html .= '<td>' . $product['id'] . '</td>';
     430                $html .= '<td>' . esc_html($product['id']) . '</td>';
    360431                $html .= '<td>' . esc_html($product['name']) . '</td>';
    361432                $html .= '<td>' . esc_html(implode(', ', $product['brands'])) . '</td>';
    362433                $html .= '</tr>';
    363434            }
    364            
     435
    365436            $html .= '</tbody></table>';
    366437        }
  • transfer-brands-for-woocommerce/tags/2.8.5/includes/class-transfer.php

    r3408293 r3408329  
    120120    /**
    121121     * Process a batch of products
    122      *
     122     *
     123     * @since 2.8.5 Added support for brand plugin taxonomies (pwb-brand, etc.)
    123124     * @return array Result data
    124125     */
    125126    public function process_products_batch() {
    126127        global $wpdb;
    127        
     128
     129        $source_taxonomy = $this->core->get_option('source_taxonomy');
     130        $is_brand_plugin = $this->core->get_utils()->is_brand_plugin_taxonomy($source_taxonomy);
     131
    128132        // Get products that have already been processed
    129133        $processed_products = get_option('tbfw_brands_processed_ids', []);
    130        
    131         // Create exclusion placeholders for already processed products
    132         $exclude_condition = '';
    133         $query_args = [
    134             '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%'
    135         ];
    136        
    137         if (!empty($processed_products)) {
    138             $placeholders = implode(',', array_fill(0, count($processed_products), '%d'));
    139             $exclude_condition = " AND post_id NOT IN ($placeholders)";
    140             $query_args = array_merge($query_args, $processed_products);
    141         }
    142        
    143         $query_args[] = $this->core->get_batch_size();
    144        
    145         // Find products with brand attribute that haven't been processed yet
    146         $query = "SELECT DISTINCT post_id
    147                 FROM {$wpdb->postmeta}
    148                 WHERE meta_key = '_product_attributes'
    149                 AND meta_value LIKE %s
    150                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
    151                 {$exclude_condition}
    152                 LIMIT %d";
    153        
    154         $product_ids = $wpdb->get_col(
    155             $wpdb->prepare($query, $query_args)
    156         );
    157        
    158         // Count total products for progress calculation
    159         $total = $wpdb->get_var(
    160             $wpdb->prepare(
    161                 "SELECT COUNT(DISTINCT post_id)
    162                 FROM {$wpdb->postmeta}
    163                 WHERE meta_key = '_product_attributes'
    164                 AND meta_value LIKE %s
    165                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
    166                 '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%'
    167             )
    168         );
    169         $this->core->add_debug("Total products with source brand", ['total' => $total]);
     134
     135        // Different query logic for brand plugin taxonomies vs WooCommerce attributes
     136        if ($is_brand_plugin) {
     137            // For brand plugin taxonomies, query products via taxonomy relationship
     138            $product_ids = $this->get_brand_plugin_products($source_taxonomy, $processed_products);
     139            $total = $this->count_brand_plugin_products($source_taxonomy);
     140        } else {
     141            // For WooCommerce attributes, use the original _product_attributes meta query
     142            $exclude_condition = '';
     143            $query_args = [
     144                '%' . $wpdb->esc_like($source_taxonomy) . '%'
     145            ];
     146
     147            if (!empty($processed_products)) {
     148                $placeholders = implode(',', array_fill(0, count($processed_products), '%d'));
     149                $exclude_condition = " AND post_id NOT IN ($placeholders)";
     150                $query_args = array_merge($query_args, $processed_products);
     151            }
     152
     153            $query_args[] = $this->core->get_batch_size();
     154
     155            // Find products with brand attribute that haven't been processed yet
     156            $query = "SELECT DISTINCT post_id
     157                    FROM {$wpdb->postmeta}
     158                    WHERE meta_key = '_product_attributes'
     159                    AND meta_value LIKE %s
     160                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
     161                    {$exclude_condition}
     162                    LIMIT %d";
     163
     164            $product_ids = $wpdb->get_col(
     165                $wpdb->prepare($query, $query_args)
     166            );
     167
     168            // Count total products for progress calculation
     169            $total = $wpdb->get_var(
     170                $wpdb->prepare(
     171                    "SELECT COUNT(DISTINCT post_id)
     172                    FROM {$wpdb->postmeta}
     173                    WHERE meta_key = '_product_attributes'
     174                    AND meta_value LIKE %s
     175                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
     176                    '%' . $wpdb->esc_like($source_taxonomy) . '%'
     177                )
     178            );
     179        }
     180
     181        $this->core->add_debug("Total products with source brand", [
     182            'total' => $total,
     183            'is_brand_plugin' => $is_brand_plugin
     184        ]);
    170185       
    171186        // If there are no more products to process, we complete
     
    185200        $processed_count = 0;
    186201        $custom_processed = 0;
     202        $brand_plugin_processed = 0;
    187203        $log_message = '';
    188        
     204
    189205        foreach ($product_ids as $product_id) {
    190206            $product = wc_get_product($product_id);
    191207            if (!$product) continue;
    192            
     208
    193209            $newly_processed[] = $product_id;
    194210            $processed_count++;
    195            
    196             $attrs = $product->get_attributes();
    197            
    198             // Get the raw product attributes
    199             $raw_attributes = get_post_meta($product_id, '_product_attributes', true);
    200            
    201             // First try to process it as a taxonomy attribute
    202             if (isset($attrs[$this->core->get_option('source_taxonomy')])) {
    203                 $brand_ids = [];
    204                
    205                 // Check if this is a taxonomy attribute
    206                 if ($attrs[$this->core->get_option('source_taxonomy')]->is_taxonomy()) {
    207                     // Get term IDs from the attribute
    208                     $brand_ids = $attrs[$this->core->get_option('source_taxonomy')]->get_options();
    209                    
     211
     212            // Handle brand plugin taxonomies differently
     213            if ($is_brand_plugin) {
     214                // For brand plugins like Perfect Brands, get terms directly from taxonomy
     215                $source_terms = get_the_terms($product_id, $source_taxonomy);
     216
     217                if ($source_terms && !is_wp_error($source_terms)) {
    210218                    $new_brand_ids = [];
    211                     foreach ($brand_ids as $old_id) {
    212                         $term = get_term($old_id, $this->core->get_option('source_taxonomy'));
    213                         if ($term && !is_wp_error($term)) {
    214                             $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
    215                             if ($new_term) {
    216                                 $new_brand_ids[] = (int)$new_term->term_id;
    217                             }
     219
     220                    foreach ($source_terms as $term) {
     221                        // Find corresponding term in destination taxonomy
     222                        $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
     223                        if ($new_term) {
     224                            $new_brand_ids[] = (int)$new_term->term_id;
    218225                        }
    219226                    }
    220                    
     227
    221228                    if (!empty($new_brand_ids)) {
    222229                        // Store previous assignments for potential rollback
    223230                        $this->core->get_backup()->backup_product_terms($product_id);
    224                        
     231
    225232                        // Assign new terms
    226233                        wp_set_object_terms($product_id, $new_brand_ids, $this->core->get_option('destination_taxonomy'));
     234                        $brand_plugin_processed++;
     235
     236                        $this->core->add_debug("Brand plugin product processed", [
     237                            'product_id' => $product_id,
     238                            'source_taxonomy' => $source_taxonomy,
     239                            'source_terms' => wp_list_pluck($source_terms, 'name'),
     240                            'new_term_ids' => $new_brand_ids
     241                        ]);
    227242                    }
    228                 } else {
    229                     // This is a custom attribute, not a taxonomy
    230                     $custom_processed++;
    231                    
    232                     // Get the value from the attribute
    233                     if (isset($raw_attributes[$this->core->get_option('source_taxonomy')]) &&
    234                         isset($raw_attributes[$this->core->get_option('source_taxonomy')]['value'])) {
    235                        
    236                         $brand_value = $raw_attributes[$this->core->get_option('source_taxonomy')]['value'];
    237                        
    238                         // Try to find a term with this ID first (common case)
    239                         $term = get_term($brand_value, $this->core->get_option('source_taxonomy'));
    240                        
    241                         if ($term && !is_wp_error($term)) {
    242                             // We found a matching term by ID
    243                             $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
    244                             if ($new_term) {
    245                                 // Store previous assignments for potential rollback
    246                                 $this->core->get_backup()->backup_product_terms($product_id);
    247                                
    248                                 // Assign new term
    249                                 wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
    250                                
    251                                 $this->core->add_debug("Custom attribute processed using term ID", [
    252                                     'product_id' => $product_id,
    253                                     'brand_value' => $brand_value,
    254                                     'term_id' => $term->term_id,
    255                                     'term_name' => $term->name,
    256                                     'new_term_id' => $new_term->term_id
    257                                 ]);
     243                }
     244            } else {
     245                // Original logic for WooCommerce attributes
     246                $attrs = $product->get_attributes();
     247
     248                // Get the raw product attributes
     249                $raw_attributes = get_post_meta($product_id, '_product_attributes', true);
     250
     251                // First try to process it as a taxonomy attribute
     252                if (isset($attrs[$source_taxonomy])) {
     253                    $brand_ids = [];
     254
     255                    // Check if this is a taxonomy attribute
     256                    if ($attrs[$source_taxonomy]->is_taxonomy()) {
     257                        // Get term IDs from the attribute
     258                        $brand_ids = $attrs[$source_taxonomy]->get_options();
     259
     260                        $new_brand_ids = [];
     261                        foreach ($brand_ids as $old_id) {
     262                            $term = get_term($old_id, $source_taxonomy);
     263                            if ($term && !is_wp_error($term)) {
     264                                $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
     265                                if ($new_term) {
     266                                    $new_brand_ids[] = (int)$new_term->term_id;
     267                                }
    258268                            }
    259                         } else {
    260                             // If we couldn't find by ID, treat it as a name or slug
    261                             $new_term = get_term_by('name', $brand_value, $this->core->get_option('destination_taxonomy'));
    262                            
    263                             if (!$new_term) {
    264                                 // Try creating the term
    265                                 $result = wp_insert_term($brand_value, $this->core->get_option('destination_taxonomy'));
    266                                 if (!is_wp_error($result)) {
    267                                     $new_term_id = $result['term_id'];
    268                                    
     269                        }
     270
     271                        if (!empty($new_brand_ids)) {
     272                            // Store previous assignments for potential rollback
     273                            $this->core->get_backup()->backup_product_terms($product_id);
     274
     275                            // Assign new terms
     276                            wp_set_object_terms($product_id, $new_brand_ids, $this->core->get_option('destination_taxonomy'));
     277                        }
     278                    } else {
     279                        // This is a custom attribute, not a taxonomy
     280                        $custom_processed++;
     281
     282                        // Get the value from the attribute
     283                        if (isset($raw_attributes[$source_taxonomy]) &&
     284                            isset($raw_attributes[$source_taxonomy]['value'])) {
     285
     286                            $brand_value = $raw_attributes[$source_taxonomy]['value'];
     287
     288                            // Try to find a term with this ID first (common case)
     289                            $term = get_term($brand_value, $source_taxonomy);
     290
     291                            if ($term && !is_wp_error($term)) {
     292                                // We found a matching term by ID
     293                                $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
     294                                if ($new_term) {
    269295                                    // Store previous assignments for potential rollback
    270296                                    $this->core->get_backup()->backup_product_terms($product_id);
    271                                    
     297
    272298                                    // Assign new term
    273                                     wp_set_object_terms($product_id, [$new_term_id], $this->core->get_option('destination_taxonomy'));
    274                                    
    275                                     $this->core->add_debug("Custom attribute processed by creating new term", [
     299                                    wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
     300
     301                                    $this->core->add_debug("Custom attribute processed using term ID", [
    276302                                        'product_id' => $product_id,
    277303                                        'brand_value' => $brand_value,
    278                                         'new_term_id' => $new_term_id
    279                                     ]);
    280                                 } else {
    281                                     $this->core->add_debug("Error creating term from custom attribute", [
    282                                         'product_id' => $product_id,
    283                                         'brand_value' => $brand_value,
    284                                         'error' => $result->get_error_message()
     304                                        'term_id' => $term->term_id,
     305                                        'term_name' => $term->name,
     306                                        'new_term_id' => $new_term->term_id
    285307                                    ]);
    286308                                }
    287309                            } else {
    288                                 // Store previous assignments for potential rollback
    289                                 $this->core->get_backup()->backup_product_terms($product_id);
    290                                
    291                                 // Assign existing term
    292                                 wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
    293                                
    294                                 $this->core->add_debug("Custom attribute processed using existing term", [
    295                                     'product_id' => $product_id,
    296                                     'brand_value' => $brand_value,
    297                                     'new_term_id' => $new_term->term_id,
    298                                     'new_term_name' => $new_term->name
    299                                 ]);
     310                                // If we couldn't find by ID, treat it as a name or slug
     311                                $new_term = get_term_by('name', $brand_value, $this->core->get_option('destination_taxonomy'));
     312
     313                                if (!$new_term) {
     314                                    // Try creating the term
     315                                    $result = wp_insert_term($brand_value, $this->core->get_option('destination_taxonomy'));
     316                                    if (!is_wp_error($result)) {
     317                                        $new_term_id = $result['term_id'];
     318
     319                                        // Store previous assignments for potential rollback
     320                                        $this->core->get_backup()->backup_product_terms($product_id);
     321
     322                                        // Assign new term
     323                                        wp_set_object_terms($product_id, [$new_term_id], $this->core->get_option('destination_taxonomy'));
     324
     325                                        $this->core->add_debug("Custom attribute processed by creating new term", [
     326                                            'product_id' => $product_id,
     327                                            'brand_value' => $brand_value,
     328                                            'new_term_id' => $new_term_id
     329                                        ]);
     330                                    } else {
     331                                        $this->core->add_debug("Error creating term from custom attribute", [
     332                                            'product_id' => $product_id,
     333                                            'brand_value' => $brand_value,
     334                                            'error' => $result->get_error_message()
     335                                        ]);
     336                                    }
     337                                } else {
     338                                    // Store previous assignments for potential rollback
     339                                    $this->core->get_backup()->backup_product_terms($product_id);
     340
     341                                    // Assign existing term
     342                                    wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
     343
     344                                    $this->core->add_debug("Custom attribute processed using existing term", [
     345                                        'product_id' => $product_id,
     346                                        'brand_value' => $brand_value,
     347                                        'new_term_id' => $new_term->term_id,
     348                                        'new_term_name' => $new_term->name
     349                                    ]);
     350                                }
    300351                            }
    301352                        }
     
    304355            }
    305356        }
    306        
     357
    307358        // Update the list of processed products
    308359        $processed_products = array_merge($processed_products, $newly_processed);
    309360        update_option('tbfw_brands_processed_ids', $processed_products);
    310        
     361
    311362        // Calculate overall progress
    312363        $processed_total = count($processed_products);
    313364        $percent = min(95, 45 + round(($processed_total / ($total + 1)) * 50));
    314        
    315         $log_message = "Processed {$processed_count} products in this batch ({$custom_processed} with custom attributes). Total processed: {$processed_total} of {$total}";
     365
     366        // Build log message based on processing type
     367        if ($is_brand_plugin) {
     368            $log_message = "Processed {$processed_count} products from brand plugin ({$brand_plugin_processed} transferred). Total processed: {$processed_total} of {$total}";
     369        } else {
     370            $log_message = "Processed {$processed_count} products in this batch ({$custom_processed} with custom attributes). Total processed: {$processed_total} of {$total}";
     371        }
    316372       
    317373        return [
     
    632688                )
    633689            );
    634            
     690
    635691            if ($attachment_id) {
    636692                return (int)$attachment_id;
    637693            }
    638694        }
    639        
     695
    640696        return false;
    641697    }
     698
     699    /**
     700     * Get products from a brand plugin taxonomy
     701     *
     702     * @since 2.8.5
     703     * @param string $taxonomy The brand plugin taxonomy (e.g., pwb-brand)
     704     * @param array $exclude_ids Product IDs to exclude (already processed)
     705     * @return array Array of product IDs
     706     */
     707    private function get_brand_plugin_products($taxonomy, $exclude_ids = []) {
     708        global $wpdb;
     709
     710        $batch_size = $this->core->get_batch_size();
     711
     712        // Build query to get products with this taxonomy
     713        $query = "SELECT DISTINCT tr.object_id
     714                  FROM {$wpdb->term_relationships} tr
     715                  INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
     716                  INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
     717                  WHERE tt.taxonomy = %s
     718                  AND p.post_type = 'product'
     719                  AND p.post_status = 'publish'";
     720
     721        $query_args = [$taxonomy];
     722
     723        // Exclude already processed products
     724        if (!empty($exclude_ids)) {
     725            $placeholders = implode(',', array_fill(0, count($exclude_ids), '%d'));
     726            $query .= " AND tr.object_id NOT IN ($placeholders)";
     727            $query_args = array_merge($query_args, $exclude_ids);
     728        }
     729
     730        $query .= " ORDER BY tr.object_id ASC LIMIT %d";
     731        $query_args[] = $batch_size;
     732
     733        return $wpdb->get_col($wpdb->prepare($query, $query_args));
     734    }
     735
     736    /**
     737     * Count total products with a brand plugin taxonomy
     738     *
     739     * @since 2.8.5
     740     * @param string $taxonomy The brand plugin taxonomy
     741     * @return int Total number of products
     742     */
     743    private function count_brand_plugin_products($taxonomy) {
     744        global $wpdb;
     745
     746        return (int) $wpdb->get_var(
     747            $wpdb->prepare(
     748                "SELECT COUNT(DISTINCT tr.object_id)
     749                FROM {$wpdb->term_relationships} tr
     750                INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
     751                INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
     752                WHERE tt.taxonomy = %s
     753                AND p.post_type = 'product'
     754                AND p.post_status = 'publish'",
     755                $taxonomy
     756            )
     757        );
     758    }
    642759}
  • transfer-brands-for-woocommerce/tags/2.8.5/includes/class-utils.php

    r3408293 r3408329  
    5757    /**
    5858     * Count products with source brand - Improved version for both taxonomy and custom attributes
    59      *
     59     *
     60     * @since 2.8.5 Added support for brand plugin taxonomies (pwb-brand, etc.)
    6061     * @return int Number of products
    6162     */
    6263    public function count_products_with_source() {
    6364        global $wpdb;
    64        
     65
    6566        $source_taxonomy = $this->core->get_option('source_taxonomy');
    66        
    67         // Get all products with this attribute, regardless of whether it's a taxonomy or custom attribute
    68         $count = $wpdb->get_var(
    69             $wpdb->prepare(
    70                 "SELECT COUNT(DISTINCT post_id)
    71                 FROM {$wpdb->postmeta}
    72                 WHERE meta_key = '_product_attributes'
    73                 AND meta_value LIKE %s
    74                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
    75                 '%' . $wpdb->esc_like($source_taxonomy) . '%'
    76             )
    77         );
    78        
     67
     68        // Check if this is a brand plugin taxonomy (not a WooCommerce attribute)
     69        if ($this->is_brand_plugin_taxonomy($source_taxonomy)) {
     70            // For brand plugin taxonomies, count products using the taxonomy relationship
     71            $count = $wpdb->get_var(
     72                $wpdb->prepare(
     73                    "SELECT COUNT(DISTINCT tr.object_id)
     74                    FROM {$wpdb->term_relationships} tr
     75                    INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
     76                    INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
     77                    WHERE tt.taxonomy = %s
     78                    AND p.post_type = 'product'
     79                    AND p.post_status = 'publish'",
     80                    $source_taxonomy
     81                )
     82            );
     83        } else {
     84            // For WooCommerce attributes, use the _product_attributes meta query
     85            $count = $wpdb->get_var(
     86                $wpdb->prepare(
     87                    "SELECT COUNT(DISTINCT post_id)
     88                    FROM {$wpdb->postmeta}
     89                    WHERE meta_key = '_product_attributes'
     90                    AND meta_value LIKE %s
     91                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
     92                    '%' . $wpdb->esc_like($source_taxonomy) . '%'
     93                )
     94            );
     95        }
     96
    7997        // Log debug info
    8098        $this->core->add_debug("Product count for {$source_taxonomy}: {$count}", [
    8199            'source_taxonomy' => $source_taxonomy,
    82             'sql' => $wpdb->last_query,
     100            'is_brand_plugin' => $this->is_brand_plugin_taxonomy($source_taxonomy),
    83101            'count' => $count
    84102        ]);
    85        
     103
    86104        return $count;
     105    }
     106
     107    /**
     108     * Check if the given taxonomy is from a brand plugin (not a WooCommerce attribute)
     109     *
     110     * @since 2.8.5
     111     * @param string $taxonomy The taxonomy to check
     112     * @return bool True if it's a brand plugin taxonomy
     113     */
     114    public function is_brand_plugin_taxonomy($taxonomy) {
     115        // List of known brand plugin taxonomies
     116        $brand_plugin_taxonomies = [
     117            'pwb-brand',           // Perfect Brands for WooCommerce
     118            'yith_product_brand',  // YITH WooCommerce Brands
     119        ];
     120
     121        return in_array($taxonomy, $brand_plugin_taxonomies, true);
     122    }
     123
     124    /**
     125     * Get the image meta key for a brand plugin taxonomy
     126     *
     127     * @since 2.8.5
     128     * @param string $taxonomy The taxonomy to get the image key for
     129     * @return string|false The meta key or false if not a brand plugin
     130     */
     131    public function get_brand_plugin_image_key($taxonomy) {
     132        $image_keys = [
     133            'pwb-brand' => 'pwb_brand_image',
     134            'yith_product_brand' => 'yith_woocommerce_brand_thumbnail_id',
     135        ];
     136
     137        return isset($image_keys[$taxonomy]) ? $image_keys[$taxonomy] : false;
    87138    }
    88139   
  • transfer-brands-for-woocommerce/tags/2.8.5/readme.txt

    r3408293 r3408329  
    44Requires at least: 6.0
    55Tested up to: 6.8.2
    6 Stable tag: 2.8.4
     6Stable tag: 2.8.5
    77Requires PHP: 7.4
    88License: GPLv2 or later
     
    119119== Changelog ==
    120120
     121= 2.8.5 =
     122* Added: Support for Perfect Brands for WooCommerce plugin (pwb-brand taxonomy)
     123* Added: Support for YITH WooCommerce Brands plugin (yith_product_brand taxonomy)
     124* Added: Automatic detection of third-party brand plugins in the source dropdown
     125* Added: Brand plugin taxonomies now shown in a separate "Brand Plugins" section in settings
     126* Improved: Analysis tool now properly displays brand plugin statistics
     127* Improved: Transfer logic handles taxonomy-to-taxonomy transfers for brand plugins
     128* Fixed: "Invalid taxonomy" error when using Perfect Brands for WooCommerce
     129
    121130= 2.8.4 =
    122131* Added: Pre-transfer validation to check if WooCommerce Brands feature is enabled
     
    236245== Upgrade Notice ==
    237246
     247= 2.8.5 =
     248**New**: Now supports Perfect Brands for WooCommerce and YITH WooCommerce Brands! If you're using these popular brand plugins and want to migrate to WooCommerce's built-in Brands, this update makes it possible. Simply select your brand plugin's taxonomy from the dropdown and transfer.
     249
    238250= 2.8.4 =
    239251**Important**: This update prevents a common issue where brands appear to transfer successfully but don't show in WooCommerce admin. The plugin now validates that WooCommerce Brands is properly enabled before allowing transfers, with clear instructions on how to enable it.
  • transfer-brands-for-woocommerce/tags/2.8.5/transfer-brands-for-woocommerce.php

    r3408293 r3408329  
    44 * Plugin URI: https://pluginatlas.com/transfer-brands-for-woocommerce
    55 * Description: Official migration tool for WooCommerce 9.6 Brands. Safely transfer your product brand attributes to the new brand taxonomy with image support, batch processing, and full backup capabilities.
    6  * Version: 2.8.4
     6 * Version: 2.8.5
    77 * Requires at least: 6.0
    88 * Requires PHP: 7.4
     
    3636
    3737// Define plugin constants
    38 define('TBFW_VERSION', '2.8.4');
     38define('TBFW_VERSION', '2.8.5');
    3939define('TBFW_PLUGIN_DIR', plugin_dir_path(__FILE__));
    4040define('TBFW_PLUGIN_URL', plugin_dir_url(__FILE__));
  • transfer-brands-for-woocommerce/trunk/includes/class-admin.php

    r3408293 r3408329  
    303303     *
    304304     * @since 2.3.0
     305     * @since 2.8.5 Added support for Perfect Brands for WooCommerce (pwb-brand)
    305306     */
    306307    public function source_taxonomy_callback() {
    307308        $attribute_taxonomies = wc_get_attribute_taxonomies();
    308309        $source = $this->core->get_option('source_taxonomy', 'pa_brand');
    309        
     310
    310311        echo '<select name="tbfw_transfer_brands_options[source_taxonomy]">';
    311        
    312         foreach ($attribute_taxonomies as $tax) {
    313             $tax_name = 'pa_' . $tax->attribute_name;
    314             echo '<option value="' . esc_attr($tax_name) . '" ' . selected($tax_name, $source, false) . '>';
    315             echo esc_html($tax->attribute_label) . ' (' . esc_html($tax_name) . ')';
    316             echo '</option>';
     312
     313        // WooCommerce Product Attributes section
     314        if (!empty($attribute_taxonomies)) {
     315            echo '<optgroup label="' . esc_attr__('WooCommerce Attributes', 'transfer-brands-for-woocommerce') . '">';
     316            foreach ($attribute_taxonomies as $tax) {
     317                $tax_name = 'pa_' . $tax->attribute_name;
     318                echo '<option value="' . esc_attr($tax_name) . '" ' . selected($tax_name, $source, false) . '>';
     319                echo esc_html($tax->attribute_label) . ' (' . esc_html($tax_name) . ')';
     320                echo '</option>';
     321            }
     322            echo '</optgroup>';
    317323        }
    318        
     324
     325        // Brand Plugins section - check for supported brand plugin taxonomies
     326        $brand_plugins = $this->get_supported_brand_plugins();
     327        if (!empty($brand_plugins)) {
     328            echo '<optgroup label="' . esc_attr__('Brand Plugins', 'transfer-brands-for-woocommerce') . '">';
     329            foreach ($brand_plugins as $plugin) {
     330                echo '<option value="' . esc_attr($plugin['taxonomy']) . '" ' . selected($plugin['taxonomy'], $source, false) . '>';
     331                echo esc_html($plugin['label']);
     332                echo '</option>';
     333            }
     334            echo '</optgroup>';
     335        }
     336
    319337        echo '</select>';
    320         echo '<p class="description">' . esc_html__('Select the source attribute that contains your brands.', 'transfer-brands-for-woocommerce') . '</p>';
     338        echo '<p class="description">' . esc_html__('Select the source attribute or taxonomy that contains your brands.', 'transfer-brands-for-woocommerce') . '</p>';
     339
     340        // Show info about detected brand plugins
     341        if (!empty($brand_plugins)) {
     342            echo '<p class="description" style="color: #2271b1;"><span class="dashicons dashicons-info" style="font-size: 14px; width: 14px; height: 14px;"></span> ';
     343            echo esc_html__('Detected brand plugin(s):', 'transfer-brands-for-woocommerce') . ' ';
     344            $plugin_names = array_column($brand_plugins, 'name');
     345            echo '<strong>' . esc_html(implode(', ', $plugin_names)) . '</strong>';
     346            echo '</p>';
     347        }
     348    }
     349
     350    /**
     351     * Get list of supported brand plugins that are active
     352     *
     353     * @since 2.8.5
     354     * @return array Array of supported brand plugins with their taxonomies
     355     */
     356    private function get_supported_brand_plugins() {
     357        $plugins = [];
     358
     359        // Perfect Brands for WooCommerce
     360        if (taxonomy_exists('pwb-brand')) {
     361            $plugins[] = [
     362                'taxonomy' => 'pwb-brand',
     363                'name' => 'Perfect Brands for WooCommerce',
     364                'label' => __('Perfect Brands (pwb-brand)', 'transfer-brands-for-woocommerce'),
     365                'image_meta_key' => 'pwb_brand_image'
     366            ];
     367        }
     368
     369        // YITH WooCommerce Brands (uses yith_product_brand taxonomy)
     370        if (taxonomy_exists('yith_product_brand')) {
     371            $plugins[] = [
     372                'taxonomy' => 'yith_product_brand',
     373                'name' => 'YITH WooCommerce Brands',
     374                'label' => __('YITH Brands (yith_product_brand)', 'transfer-brands-for-woocommerce'),
     375                'image_meta_key' => 'yith_woocommerce_brand_thumbnail_id'
     376            ];
     377        }
     378
     379        return $plugins;
    321380    }
    322381   
  • transfer-brands-for-woocommerce/trunk/includes/class-ajax.php

    r3408293 r3408329  
    134134    /**
    135135     * AJAX handler for checking brands
     136     *
     137     * @since 2.8.5 Added support for brand plugin taxonomies (pwb-brand, etc.)
    136138     */
    137139    public function ajax_check_brands() {
    138140        check_ajax_referer('tbfw_transfer_brands_nonce', 'nonce');
    139        
    140         if (!current_user_can('manage_woocommerce')) {
    141             wp_die(__('You do not have permission to perform this action.', 'transfer-brands-for-woocommerce'));
    142         }
    143        
     141
     142        if (!current_user_can('manage_woocommerce')) {
     143            wp_die(__('You do not have permission to perform this action.', 'transfer-brands-for-woocommerce'));
     144        }
     145
     146        $source_taxonomy = $this->core->get_option('source_taxonomy');
     147        $is_brand_plugin = $this->core->get_utils()->is_brand_plugin_taxonomy($source_taxonomy);
     148
    144149        $source_terms = get_terms([
    145             'taxonomy' => $this->core->get_option('source_taxonomy'),
     150            'taxonomy' => $source_taxonomy,
    146151            'hide_empty' => false
    147152        ]);
    148        
     153
    149154        if (is_wp_error($source_terms)) {
    150155            wp_send_json_error(['message' => 'Error: ' . $source_terms->get_error_message()]);
    151156            return;
    152157        }
    153        
     158
    154159        global $wpdb;
    155        
    156         // Get info about custom attributes
    157         $custom_attribute_count = $wpdb->get_var(
    158             $wpdb->prepare(
    159                 "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta}
    160                 WHERE meta_key = '_product_attributes'
    161                 AND meta_value LIKE %s
    162                 AND meta_value LIKE %s
    163                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
    164                 '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%',
    165                 '%"is_taxonomy";i:0;%'
    166             )
    167         );
    168        
    169         // Sample of products with custom attributes
    170         $custom_products = $wpdb->get_results(
    171             $wpdb->prepare(
    172                 "SELECT post_id, meta_value FROM {$wpdb->postmeta}
    173                 WHERE meta_key = '_product_attributes'
    174                 AND meta_value LIKE %s
    175                 AND meta_value LIKE %s
    176                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
    177                 LIMIT 10",
    178                 '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%',
    179                 '%"is_taxonomy";i:0;%'
    180             )
    181         );
    182        
     160
     161        $custom_attribute_count = 0;
    183162        $custom_samples = [];
    184         foreach ($custom_products as $item) {
    185             $attributes = maybe_unserialize($item->meta_value);
    186             if (is_array($attributes) && isset($attributes[$this->core->get_option('source_taxonomy')])) {
    187                 $product = wc_get_product($item->post_id);
    188                 if ($product) {
    189                     $custom_samples[] = [
    190                         'id' => $item->post_id,
    191                         'name' => $product->get_name(),
    192                         'value' => $attributes[$this->core->get_option('source_taxonomy')]['value'] ?? 'N/A'
    193                     ];
     163        $sample_products = [];
     164
     165        // For brand plugin taxonomies, skip custom attribute checks (they don't use _product_attributes)
     166        if (!$is_brand_plugin) {
     167            // Get info about custom attributes (only for WooCommerce attributes)
     168            $custom_attribute_count = $wpdb->get_var(
     169                $wpdb->prepare(
     170                    "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta}
     171                    WHERE meta_key = '_product_attributes'
     172                    AND meta_value LIKE %s
     173                    AND meta_value LIKE %s
     174                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
     175                    '%' . $wpdb->esc_like($source_taxonomy) . '%',
     176                    '%"is_taxonomy";i:0;%'
     177                )
     178            );
     179
     180            // Sample of products with custom attributes
     181            $custom_products = $wpdb->get_results(
     182                $wpdb->prepare(
     183                    "SELECT post_id, meta_value FROM {$wpdb->postmeta}
     184                    WHERE meta_key = '_product_attributes'
     185                    AND meta_value LIKE %s
     186                    AND meta_value LIKE %s
     187                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
     188                    LIMIT 10",
     189                    '%' . $wpdb->esc_like($source_taxonomy) . '%',
     190                    '%"is_taxonomy";i:0;%'
     191                )
     192            );
     193
     194            foreach ($custom_products as $item) {
     195                $attributes = maybe_unserialize($item->meta_value);
     196                if (is_array($attributes) && isset($attributes[$source_taxonomy])) {
     197                    $product = wc_get_product($item->post_id);
     198                    if ($product) {
     199                        $custom_samples[] = [
     200                            'id' => $item->post_id,
     201                            'name' => $product->get_name(),
     202                            'value' => $attributes[$source_taxonomy]['value'] ?? 'N/A'
     203                        ];
     204                    }
    194205                }
    195206            }
    196207        }
    197        
     208
    198209        // Check if any terms already exist in destination
    199210        $conflicting_terms = [];
    200211        $terms_with_images = 0;
    201        
     212
    202213        foreach ($source_terms as $term) {
    203214            $exists = term_exists($term->name, $this->core->get_option('destination_taxonomy'));
     
    205216                $conflicting_terms[] = $term->name;
    206217            }
    207            
     218
    208219            // Check if term has image using the comprehensive detection method
    209220            $transfer_instance = $this->core->get_transfer();
     
    213224            $method->setAccessible(true);
    214225            $image_id = $method->invoke($transfer_instance, $term->term_id);
    215            
     226
    216227            if ($image_id) {
    217228                $terms_with_images++;
    218229            }
    219230        }
    220        
    221         // Get some sample products with taxonomy attributes
    222         $products_query = new WP_Query([
    223             'post_type' => 'product',
    224             'posts_per_page' => 5,
    225             'tax_query' => [
    226                 [
    227                     'taxonomy' => $this->core->get_option('source_taxonomy'),
    228                     'operator' => 'EXISTS',
     231
     232        // Get sample products - different logic for brand plugins vs WooCommerce attributes
     233        if ($is_brand_plugin) {
     234            // For brand plugins, query products via taxonomy relationship
     235            $products_query = new WP_Query([
     236                'post_type' => 'product',
     237                'posts_per_page' => 5,
     238                'post_status' => 'publish',
     239                'tax_query' => [
     240                    [
     241                        'taxonomy' => $source_taxonomy,
     242                        'operator' => 'EXISTS',
     243                    ]
    229244                ]
    230             ]
    231         ]);
    232        
    233         $sample_products = [];
    234        
    235         if ($products_query->have_posts()) {
    236             foreach ($products_query->posts as $post) {
    237                 $product = wc_get_product($post->ID);
    238                 $attrs = $product->get_attributes();
    239                
    240                 if (isset($attrs[$this->core->get_option('source_taxonomy')])) {
    241                     $terms = [];
    242                     foreach ($attrs[$this->core->get_option('source_taxonomy')]->get_options() as $term_id) {
    243                         $term = get_term($term_id, $this->core->get_option('source_taxonomy'));
    244                         if ($term && !is_wp_error($term)) {
    245                             $terms[] = $term->name;
    246                         }
     245            ]);
     246
     247            if ($products_query->have_posts()) {
     248                foreach ($products_query->posts as $post) {
     249                    $product = wc_get_product($post->ID);
     250                    if (!$product) continue;
     251
     252                    // Get terms directly from taxonomy
     253                    $product_terms = get_the_terms($post->ID, $source_taxonomy);
     254                    $term_names = [];
     255
     256                    if ($product_terms && !is_wp_error($product_terms)) {
     257                        $term_names = wp_list_pluck($product_terms, 'name');
    247258                    }
    248                    
     259
    249260                    $sample_products[] = [
    250261                        'id' => $post->ID,
    251262                        'name' => $product->get_name(),
    252                         'brands' => $terms
     263                        'brands' => $term_names
    253264                    ];
    254265                }
    255266            }
    256         }
    257        
     267        } else {
     268            // For WooCommerce attributes, use the original query
     269            $products_query = new WP_Query([
     270                'post_type' => 'product',
     271                'posts_per_page' => 5,
     272                'tax_query' => [
     273                    [
     274                        'taxonomy' => $source_taxonomy,
     275                        'operator' => 'EXISTS',
     276                    ]
     277                ]
     278            ]);
     279
     280            if ($products_query->have_posts()) {
     281                foreach ($products_query->posts as $post) {
     282                    $product = wc_get_product($post->ID);
     283                    $attrs = $product->get_attributes();
     284
     285                    if (isset($attrs[$source_taxonomy])) {
     286                        $terms = [];
     287                        foreach ($attrs[$source_taxonomy]->get_options() as $term_id) {
     288                            $term = get_term($term_id, $source_taxonomy);
     289                            if ($term && !is_wp_error($term)) {
     290                                $terms[] = $term->name;
     291                            }
     292                        }
     293
     294                        $sample_products[] = [
     295                            'id' => $post->ID,
     296                            'name' => $product->get_name(),
     297                            'brands' => $terms
     298                        ];
     299                    }
     300                }
     301            }
     302        }
     303
    258304        // Add debug info
    259305        $this->core->add_debug("Brand analysis performed", [
    260306            'source_terms' => count($source_terms),
     307            'is_brand_plugin' => $is_brand_plugin,
    261308            'custom_attribute_count' => $custom_attribute_count,
    262309            'taxonomy_samples' => count($sample_products),
     
    297344
    298345        $html .= '<h4>' . esc_html__('Source Brands Summary', 'transfer-brands-for-woocommerce') . '</h4>';
    299         $html .= '<ul>';
    300         $html .= '<li><strong>' . count($source_terms) . '</strong> ' . esc_html__('brands found in taxonomy', 'transfer-brands-for-woocommerce') . ' ' . esc_html($this->core->get_option('source_taxonomy')) . '</li>';
    301         $html .= '<li><strong>' . $custom_attribute_count . '</strong> ' . esc_html__('products have custom (non-taxonomy) attributes with name', 'transfer-brands-for-woocommerce') . ' ' . esc_html($this->core->get_option('source_taxonomy')) . '</li>';
    302         $html .= '<li><strong>' . $terms_with_images . '</strong> ' . esc_html__('brands have images that will be transferred', 'transfer-brands-for-woocommerce') . '</li>';
    303         $html .= '</ul>';
    304        
    305         // Warning about custom attributes
    306         if ($custom_attribute_count > 0) {
     346
     347        // Show different info for brand plugins vs WooCommerce attributes
     348        if ($is_brand_plugin) {
     349            $html .= '<div class="notice notice-info inline" style="margin: 0 0 15px 0; padding: 10px 12px;">';
     350            $html .= '<p style="margin: 0;"><span class="dashicons dashicons-info" style="color: #2271b1;"></span> ';
     351            $html .= '<strong>' . esc_html__('Brand Plugin Detected:', 'transfer-brands-for-woocommerce') . '</strong> ';
     352            $html .= esc_html__('Transferring from a third-party brand plugin taxonomy.', 'transfer-brands-for-woocommerce');
     353            $html .= '</p></div>';
     354
     355            // Get product count for brand plugin
     356            $brand_plugin_product_count = $this->core->get_utils()->count_products_with_source();
     357
     358            $html .= '<ul>';
     359            $html .= '<li><strong>' . count($source_terms) . '</strong> ' . esc_html__('brands found in taxonomy', 'transfer-brands-for-woocommerce') . ' <code>' . esc_html($source_taxonomy) . '</code></li>';
     360            $html .= '<li><strong>' . $brand_plugin_product_count . '</strong> ' . esc_html__('products have brands assigned', 'transfer-brands-for-woocommerce') . '</li>';
     361            $html .= '<li><strong>' . $terms_with_images . '</strong> ' . esc_html__('brands have images that will be transferred', 'transfer-brands-for-woocommerce') . '</li>';
     362            $html .= '</ul>';
     363        } else {
     364            $html .= '<ul>';
     365            $html .= '<li><strong>' . count($source_terms) . '</strong> ' . esc_html__('brands found in taxonomy', 'transfer-brands-for-woocommerce') . ' <code>' . esc_html($source_taxonomy) . '</code></li>';
     366            $html .= '<li><strong>' . $custom_attribute_count . '</strong> ' . esc_html__('products have custom (non-taxonomy) attributes with name', 'transfer-brands-for-woocommerce') . ' <code>' . esc_html($source_taxonomy) . '</code></li>';
     367            $html .= '<li><strong>' . $terms_with_images . '</strong> ' . esc_html__('brands have images that will be transferred', 'transfer-brands-for-woocommerce') . '</li>';
     368            $html .= '</ul>';
     369        }
     370
     371        // Warning about custom attributes (only for WooCommerce attributes)
     372        if (!$is_brand_plugin && $custom_attribute_count > 0) {
    307373            $html .= '<div class="notice notice-warning inline" style="margin-top: 15px;">';
    308374            $html .= '<p><strong>Custom Attributes Detected:</strong> Some of your products use custom (non-taxonomy) attributes for brands.</p>';
     
    349415       
    350416        if (!empty($sample_products)) {
    351             $html .= '<h4>Sample Taxonomy Products</h4>';
    352             $html .= '<p>Here are some products with taxonomy brand attributes:</p>';
     417            if ($is_brand_plugin) {
     418                $html .= '<h4>' . esc_html__('Sample Products with Brand Plugin Brands', 'transfer-brands-for-woocommerce') . '</h4>';
     419                $html .= '<p>' . esc_html__('Here are some products with brands from the brand plugin:', 'transfer-brands-for-woocommerce') . '</p>';
     420            } else {
     421                $html .= '<h4>' . esc_html__('Sample Products with Brand Attributes', 'transfer-brands-for-woocommerce') . '</h4>';
     422                $html .= '<p>' . esc_html__('Here are some products with taxonomy brand attributes:', 'transfer-brands-for-woocommerce') . '</p>';
     423            }
    353424            $html .= '<table class="widefat" style="margin-top: 10px;">';
    354             $html .= '<thead><tr><th>ID</th><th>Product</th><th>Current Brands</th></tr></thead>';
     425            $html .= '<thead><tr><th>ID</th><th>' . esc_html__('Product', 'transfer-brands-for-woocommerce') . '</th><th>' . esc_html__('Current Brands', 'transfer-brands-for-woocommerce') . '</th></tr></thead>';
    355426            $html .= '<tbody>';
    356            
     427
    357428            foreach ($sample_products as $product) {
    358429                $html .= '<tr>';
    359                 $html .= '<td>' . $product['id'] . '</td>';
     430                $html .= '<td>' . esc_html($product['id']) . '</td>';
    360431                $html .= '<td>' . esc_html($product['name']) . '</td>';
    361432                $html .= '<td>' . esc_html(implode(', ', $product['brands'])) . '</td>';
    362433                $html .= '</tr>';
    363434            }
    364            
     435
    365436            $html .= '</tbody></table>';
    366437        }
  • transfer-brands-for-woocommerce/trunk/includes/class-transfer.php

    r3408293 r3408329  
    120120    /**
    121121     * Process a batch of products
    122      *
     122     *
     123     * @since 2.8.5 Added support for brand plugin taxonomies (pwb-brand, etc.)
    123124     * @return array Result data
    124125     */
    125126    public function process_products_batch() {
    126127        global $wpdb;
    127        
     128
     129        $source_taxonomy = $this->core->get_option('source_taxonomy');
     130        $is_brand_plugin = $this->core->get_utils()->is_brand_plugin_taxonomy($source_taxonomy);
     131
    128132        // Get products that have already been processed
    129133        $processed_products = get_option('tbfw_brands_processed_ids', []);
    130        
    131         // Create exclusion placeholders for already processed products
    132         $exclude_condition = '';
    133         $query_args = [
    134             '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%'
    135         ];
    136        
    137         if (!empty($processed_products)) {
    138             $placeholders = implode(',', array_fill(0, count($processed_products), '%d'));
    139             $exclude_condition = " AND post_id NOT IN ($placeholders)";
    140             $query_args = array_merge($query_args, $processed_products);
    141         }
    142        
    143         $query_args[] = $this->core->get_batch_size();
    144        
    145         // Find products with brand attribute that haven't been processed yet
    146         $query = "SELECT DISTINCT post_id
    147                 FROM {$wpdb->postmeta}
    148                 WHERE meta_key = '_product_attributes'
    149                 AND meta_value LIKE %s
    150                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
    151                 {$exclude_condition}
    152                 LIMIT %d";
    153        
    154         $product_ids = $wpdb->get_col(
    155             $wpdb->prepare($query, $query_args)
    156         );
    157        
    158         // Count total products for progress calculation
    159         $total = $wpdb->get_var(
    160             $wpdb->prepare(
    161                 "SELECT COUNT(DISTINCT post_id)
    162                 FROM {$wpdb->postmeta}
    163                 WHERE meta_key = '_product_attributes'
    164                 AND meta_value LIKE %s
    165                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
    166                 '%' . $wpdb->esc_like($this->core->get_option('source_taxonomy')) . '%'
    167             )
    168         );
    169         $this->core->add_debug("Total products with source brand", ['total' => $total]);
     134
     135        // Different query logic for brand plugin taxonomies vs WooCommerce attributes
     136        if ($is_brand_plugin) {
     137            // For brand plugin taxonomies, query products via taxonomy relationship
     138            $product_ids = $this->get_brand_plugin_products($source_taxonomy, $processed_products);
     139            $total = $this->count_brand_plugin_products($source_taxonomy);
     140        } else {
     141            // For WooCommerce attributes, use the original _product_attributes meta query
     142            $exclude_condition = '';
     143            $query_args = [
     144                '%' . $wpdb->esc_like($source_taxonomy) . '%'
     145            ];
     146
     147            if (!empty($processed_products)) {
     148                $placeholders = implode(',', array_fill(0, count($processed_products), '%d'));
     149                $exclude_condition = " AND post_id NOT IN ($placeholders)";
     150                $query_args = array_merge($query_args, $processed_products);
     151            }
     152
     153            $query_args[] = $this->core->get_batch_size();
     154
     155            // Find products with brand attribute that haven't been processed yet
     156            $query = "SELECT DISTINCT post_id
     157                    FROM {$wpdb->postmeta}
     158                    WHERE meta_key = '_product_attributes'
     159                    AND meta_value LIKE %s
     160                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')
     161                    {$exclude_condition}
     162                    LIMIT %d";
     163
     164            $product_ids = $wpdb->get_col(
     165                $wpdb->prepare($query, $query_args)
     166            );
     167
     168            // Count total products for progress calculation
     169            $total = $wpdb->get_var(
     170                $wpdb->prepare(
     171                    "SELECT COUNT(DISTINCT post_id)
     172                    FROM {$wpdb->postmeta}
     173                    WHERE meta_key = '_product_attributes'
     174                    AND meta_value LIKE %s
     175                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
     176                    '%' . $wpdb->esc_like($source_taxonomy) . '%'
     177                )
     178            );
     179        }
     180
     181        $this->core->add_debug("Total products with source brand", [
     182            'total' => $total,
     183            'is_brand_plugin' => $is_brand_plugin
     184        ]);
    170185       
    171186        // If there are no more products to process, we complete
     
    185200        $processed_count = 0;
    186201        $custom_processed = 0;
     202        $brand_plugin_processed = 0;
    187203        $log_message = '';
    188        
     204
    189205        foreach ($product_ids as $product_id) {
    190206            $product = wc_get_product($product_id);
    191207            if (!$product) continue;
    192            
     208
    193209            $newly_processed[] = $product_id;
    194210            $processed_count++;
    195            
    196             $attrs = $product->get_attributes();
    197            
    198             // Get the raw product attributes
    199             $raw_attributes = get_post_meta($product_id, '_product_attributes', true);
    200            
    201             // First try to process it as a taxonomy attribute
    202             if (isset($attrs[$this->core->get_option('source_taxonomy')])) {
    203                 $brand_ids = [];
    204                
    205                 // Check if this is a taxonomy attribute
    206                 if ($attrs[$this->core->get_option('source_taxonomy')]->is_taxonomy()) {
    207                     // Get term IDs from the attribute
    208                     $brand_ids = $attrs[$this->core->get_option('source_taxonomy')]->get_options();
    209                    
     211
     212            // Handle brand plugin taxonomies differently
     213            if ($is_brand_plugin) {
     214                // For brand plugins like Perfect Brands, get terms directly from taxonomy
     215                $source_terms = get_the_terms($product_id, $source_taxonomy);
     216
     217                if ($source_terms && !is_wp_error($source_terms)) {
    210218                    $new_brand_ids = [];
    211                     foreach ($brand_ids as $old_id) {
    212                         $term = get_term($old_id, $this->core->get_option('source_taxonomy'));
    213                         if ($term && !is_wp_error($term)) {
    214                             $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
    215                             if ($new_term) {
    216                                 $new_brand_ids[] = (int)$new_term->term_id;
    217                             }
     219
     220                    foreach ($source_terms as $term) {
     221                        // Find corresponding term in destination taxonomy
     222                        $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
     223                        if ($new_term) {
     224                            $new_brand_ids[] = (int)$new_term->term_id;
    218225                        }
    219226                    }
    220                    
     227
    221228                    if (!empty($new_brand_ids)) {
    222229                        // Store previous assignments for potential rollback
    223230                        $this->core->get_backup()->backup_product_terms($product_id);
    224                        
     231
    225232                        // Assign new terms
    226233                        wp_set_object_terms($product_id, $new_brand_ids, $this->core->get_option('destination_taxonomy'));
     234                        $brand_plugin_processed++;
     235
     236                        $this->core->add_debug("Brand plugin product processed", [
     237                            'product_id' => $product_id,
     238                            'source_taxonomy' => $source_taxonomy,
     239                            'source_terms' => wp_list_pluck($source_terms, 'name'),
     240                            'new_term_ids' => $new_brand_ids
     241                        ]);
    227242                    }
    228                 } else {
    229                     // This is a custom attribute, not a taxonomy
    230                     $custom_processed++;
    231                    
    232                     // Get the value from the attribute
    233                     if (isset($raw_attributes[$this->core->get_option('source_taxonomy')]) &&
    234                         isset($raw_attributes[$this->core->get_option('source_taxonomy')]['value'])) {
    235                        
    236                         $brand_value = $raw_attributes[$this->core->get_option('source_taxonomy')]['value'];
    237                        
    238                         // Try to find a term with this ID first (common case)
    239                         $term = get_term($brand_value, $this->core->get_option('source_taxonomy'));
    240                        
    241                         if ($term && !is_wp_error($term)) {
    242                             // We found a matching term by ID
    243                             $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
    244                             if ($new_term) {
    245                                 // Store previous assignments for potential rollback
    246                                 $this->core->get_backup()->backup_product_terms($product_id);
    247                                
    248                                 // Assign new term
    249                                 wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
    250                                
    251                                 $this->core->add_debug("Custom attribute processed using term ID", [
    252                                     'product_id' => $product_id,
    253                                     'brand_value' => $brand_value,
    254                                     'term_id' => $term->term_id,
    255                                     'term_name' => $term->name,
    256                                     'new_term_id' => $new_term->term_id
    257                                 ]);
     243                }
     244            } else {
     245                // Original logic for WooCommerce attributes
     246                $attrs = $product->get_attributes();
     247
     248                // Get the raw product attributes
     249                $raw_attributes = get_post_meta($product_id, '_product_attributes', true);
     250
     251                // First try to process it as a taxonomy attribute
     252                if (isset($attrs[$source_taxonomy])) {
     253                    $brand_ids = [];
     254
     255                    // Check if this is a taxonomy attribute
     256                    if ($attrs[$source_taxonomy]->is_taxonomy()) {
     257                        // Get term IDs from the attribute
     258                        $brand_ids = $attrs[$source_taxonomy]->get_options();
     259
     260                        $new_brand_ids = [];
     261                        foreach ($brand_ids as $old_id) {
     262                            $term = get_term($old_id, $source_taxonomy);
     263                            if ($term && !is_wp_error($term)) {
     264                                $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
     265                                if ($new_term) {
     266                                    $new_brand_ids[] = (int)$new_term->term_id;
     267                                }
    258268                            }
    259                         } else {
    260                             // If we couldn't find by ID, treat it as a name or slug
    261                             $new_term = get_term_by('name', $brand_value, $this->core->get_option('destination_taxonomy'));
    262                            
    263                             if (!$new_term) {
    264                                 // Try creating the term
    265                                 $result = wp_insert_term($brand_value, $this->core->get_option('destination_taxonomy'));
    266                                 if (!is_wp_error($result)) {
    267                                     $new_term_id = $result['term_id'];
    268                                    
     269                        }
     270
     271                        if (!empty($new_brand_ids)) {
     272                            // Store previous assignments for potential rollback
     273                            $this->core->get_backup()->backup_product_terms($product_id);
     274
     275                            // Assign new terms
     276                            wp_set_object_terms($product_id, $new_brand_ids, $this->core->get_option('destination_taxonomy'));
     277                        }
     278                    } else {
     279                        // This is a custom attribute, not a taxonomy
     280                        $custom_processed++;
     281
     282                        // Get the value from the attribute
     283                        if (isset($raw_attributes[$source_taxonomy]) &&
     284                            isset($raw_attributes[$source_taxonomy]['value'])) {
     285
     286                            $brand_value = $raw_attributes[$source_taxonomy]['value'];
     287
     288                            // Try to find a term with this ID first (common case)
     289                            $term = get_term($brand_value, $source_taxonomy);
     290
     291                            if ($term && !is_wp_error($term)) {
     292                                // We found a matching term by ID
     293                                $new_term = get_term_by('name', $term->name, $this->core->get_option('destination_taxonomy'));
     294                                if ($new_term) {
    269295                                    // Store previous assignments for potential rollback
    270296                                    $this->core->get_backup()->backup_product_terms($product_id);
    271                                    
     297
    272298                                    // Assign new term
    273                                     wp_set_object_terms($product_id, [$new_term_id], $this->core->get_option('destination_taxonomy'));
    274                                    
    275                                     $this->core->add_debug("Custom attribute processed by creating new term", [
     299                                    wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
     300
     301                                    $this->core->add_debug("Custom attribute processed using term ID", [
    276302                                        'product_id' => $product_id,
    277303                                        'brand_value' => $brand_value,
    278                                         'new_term_id' => $new_term_id
    279                                     ]);
    280                                 } else {
    281                                     $this->core->add_debug("Error creating term from custom attribute", [
    282                                         'product_id' => $product_id,
    283                                         'brand_value' => $brand_value,
    284                                         'error' => $result->get_error_message()
     304                                        'term_id' => $term->term_id,
     305                                        'term_name' => $term->name,
     306                                        'new_term_id' => $new_term->term_id
    285307                                    ]);
    286308                                }
    287309                            } else {
    288                                 // Store previous assignments for potential rollback
    289                                 $this->core->get_backup()->backup_product_terms($product_id);
    290                                
    291                                 // Assign existing term
    292                                 wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
    293                                
    294                                 $this->core->add_debug("Custom attribute processed using existing term", [
    295                                     'product_id' => $product_id,
    296                                     'brand_value' => $brand_value,
    297                                     'new_term_id' => $new_term->term_id,
    298                                     'new_term_name' => $new_term->name
    299                                 ]);
     310                                // If we couldn't find by ID, treat it as a name or slug
     311                                $new_term = get_term_by('name', $brand_value, $this->core->get_option('destination_taxonomy'));
     312
     313                                if (!$new_term) {
     314                                    // Try creating the term
     315                                    $result = wp_insert_term($brand_value, $this->core->get_option('destination_taxonomy'));
     316                                    if (!is_wp_error($result)) {
     317                                        $new_term_id = $result['term_id'];
     318
     319                                        // Store previous assignments for potential rollback
     320                                        $this->core->get_backup()->backup_product_terms($product_id);
     321
     322                                        // Assign new term
     323                                        wp_set_object_terms($product_id, [$new_term_id], $this->core->get_option('destination_taxonomy'));
     324
     325                                        $this->core->add_debug("Custom attribute processed by creating new term", [
     326                                            'product_id' => $product_id,
     327                                            'brand_value' => $brand_value,
     328                                            'new_term_id' => $new_term_id
     329                                        ]);
     330                                    } else {
     331                                        $this->core->add_debug("Error creating term from custom attribute", [
     332                                            'product_id' => $product_id,
     333                                            'brand_value' => $brand_value,
     334                                            'error' => $result->get_error_message()
     335                                        ]);
     336                                    }
     337                                } else {
     338                                    // Store previous assignments for potential rollback
     339                                    $this->core->get_backup()->backup_product_terms($product_id);
     340
     341                                    // Assign existing term
     342                                    wp_set_object_terms($product_id, [(int)$new_term->term_id], $this->core->get_option('destination_taxonomy'));
     343
     344                                    $this->core->add_debug("Custom attribute processed using existing term", [
     345                                        'product_id' => $product_id,
     346                                        'brand_value' => $brand_value,
     347                                        'new_term_id' => $new_term->term_id,
     348                                        'new_term_name' => $new_term->name
     349                                    ]);
     350                                }
    300351                            }
    301352                        }
     
    304355            }
    305356        }
    306        
     357
    307358        // Update the list of processed products
    308359        $processed_products = array_merge($processed_products, $newly_processed);
    309360        update_option('tbfw_brands_processed_ids', $processed_products);
    310        
     361
    311362        // Calculate overall progress
    312363        $processed_total = count($processed_products);
    313364        $percent = min(95, 45 + round(($processed_total / ($total + 1)) * 50));
    314        
    315         $log_message = "Processed {$processed_count} products in this batch ({$custom_processed} with custom attributes). Total processed: {$processed_total} of {$total}";
     365
     366        // Build log message based on processing type
     367        if ($is_brand_plugin) {
     368            $log_message = "Processed {$processed_count} products from brand plugin ({$brand_plugin_processed} transferred). Total processed: {$processed_total} of {$total}";
     369        } else {
     370            $log_message = "Processed {$processed_count} products in this batch ({$custom_processed} with custom attributes). Total processed: {$processed_total} of {$total}";
     371        }
    316372       
    317373        return [
     
    632688                )
    633689            );
    634            
     690
    635691            if ($attachment_id) {
    636692                return (int)$attachment_id;
    637693            }
    638694        }
    639        
     695
    640696        return false;
    641697    }
     698
     699    /**
     700     * Get products from a brand plugin taxonomy
     701     *
     702     * @since 2.8.5
     703     * @param string $taxonomy The brand plugin taxonomy (e.g., pwb-brand)
     704     * @param array $exclude_ids Product IDs to exclude (already processed)
     705     * @return array Array of product IDs
     706     */
     707    private function get_brand_plugin_products($taxonomy, $exclude_ids = []) {
     708        global $wpdb;
     709
     710        $batch_size = $this->core->get_batch_size();
     711
     712        // Build query to get products with this taxonomy
     713        $query = "SELECT DISTINCT tr.object_id
     714                  FROM {$wpdb->term_relationships} tr
     715                  INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
     716                  INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
     717                  WHERE tt.taxonomy = %s
     718                  AND p.post_type = 'product'
     719                  AND p.post_status = 'publish'";
     720
     721        $query_args = [$taxonomy];
     722
     723        // Exclude already processed products
     724        if (!empty($exclude_ids)) {
     725            $placeholders = implode(',', array_fill(0, count($exclude_ids), '%d'));
     726            $query .= " AND tr.object_id NOT IN ($placeholders)";
     727            $query_args = array_merge($query_args, $exclude_ids);
     728        }
     729
     730        $query .= " ORDER BY tr.object_id ASC LIMIT %d";
     731        $query_args[] = $batch_size;
     732
     733        return $wpdb->get_col($wpdb->prepare($query, $query_args));
     734    }
     735
     736    /**
     737     * Count total products with a brand plugin taxonomy
     738     *
     739     * @since 2.8.5
     740     * @param string $taxonomy The brand plugin taxonomy
     741     * @return int Total number of products
     742     */
     743    private function count_brand_plugin_products($taxonomy) {
     744        global $wpdb;
     745
     746        return (int) $wpdb->get_var(
     747            $wpdb->prepare(
     748                "SELECT COUNT(DISTINCT tr.object_id)
     749                FROM {$wpdb->term_relationships} tr
     750                INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
     751                INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
     752                WHERE tt.taxonomy = %s
     753                AND p.post_type = 'product'
     754                AND p.post_status = 'publish'",
     755                $taxonomy
     756            )
     757        );
     758    }
    642759}
  • transfer-brands-for-woocommerce/trunk/includes/class-utils.php

    r3408293 r3408329  
    5757    /**
    5858     * Count products with source brand - Improved version for both taxonomy and custom attributes
    59      *
     59     *
     60     * @since 2.8.5 Added support for brand plugin taxonomies (pwb-brand, etc.)
    6061     * @return int Number of products
    6162     */
    6263    public function count_products_with_source() {
    6364        global $wpdb;
    64        
     65
    6566        $source_taxonomy = $this->core->get_option('source_taxonomy');
    66        
    67         // Get all products with this attribute, regardless of whether it's a taxonomy or custom attribute
    68         $count = $wpdb->get_var(
    69             $wpdb->prepare(
    70                 "SELECT COUNT(DISTINCT post_id)
    71                 FROM {$wpdb->postmeta}
    72                 WHERE meta_key = '_product_attributes'
    73                 AND meta_value LIKE %s
    74                 AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
    75                 '%' . $wpdb->esc_like($source_taxonomy) . '%'
    76             )
    77         );
    78        
     67
     68        // Check if this is a brand plugin taxonomy (not a WooCommerce attribute)
     69        if ($this->is_brand_plugin_taxonomy($source_taxonomy)) {
     70            // For brand plugin taxonomies, count products using the taxonomy relationship
     71            $count = $wpdb->get_var(
     72                $wpdb->prepare(
     73                    "SELECT COUNT(DISTINCT tr.object_id)
     74                    FROM {$wpdb->term_relationships} tr
     75                    INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
     76                    INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
     77                    WHERE tt.taxonomy = %s
     78                    AND p.post_type = 'product'
     79                    AND p.post_status = 'publish'",
     80                    $source_taxonomy
     81                )
     82            );
     83        } else {
     84            // For WooCommerce attributes, use the _product_attributes meta query
     85            $count = $wpdb->get_var(
     86                $wpdb->prepare(
     87                    "SELECT COUNT(DISTINCT post_id)
     88                    FROM {$wpdb->postmeta}
     89                    WHERE meta_key = '_product_attributes'
     90                    AND meta_value LIKE %s
     91                    AND post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish')",
     92                    '%' . $wpdb->esc_like($source_taxonomy) . '%'
     93                )
     94            );
     95        }
     96
    7997        // Log debug info
    8098        $this->core->add_debug("Product count for {$source_taxonomy}: {$count}", [
    8199            'source_taxonomy' => $source_taxonomy,
    82             'sql' => $wpdb->last_query,
     100            'is_brand_plugin' => $this->is_brand_plugin_taxonomy($source_taxonomy),
    83101            'count' => $count
    84102        ]);
    85        
     103
    86104        return $count;
     105    }
     106
     107    /**
     108     * Check if the given taxonomy is from a brand plugin (not a WooCommerce attribute)
     109     *
     110     * @since 2.8.5
     111     * @param string $taxonomy The taxonomy to check
     112     * @return bool True if it's a brand plugin taxonomy
     113     */
     114    public function is_brand_plugin_taxonomy($taxonomy) {
     115        // List of known brand plugin taxonomies
     116        $brand_plugin_taxonomies = [
     117            'pwb-brand',           // Perfect Brands for WooCommerce
     118            'yith_product_brand',  // YITH WooCommerce Brands
     119        ];
     120
     121        return in_array($taxonomy, $brand_plugin_taxonomies, true);
     122    }
     123
     124    /**
     125     * Get the image meta key for a brand plugin taxonomy
     126     *
     127     * @since 2.8.5
     128     * @param string $taxonomy The taxonomy to get the image key for
     129     * @return string|false The meta key or false if not a brand plugin
     130     */
     131    public function get_brand_plugin_image_key($taxonomy) {
     132        $image_keys = [
     133            'pwb-brand' => 'pwb_brand_image',
     134            'yith_product_brand' => 'yith_woocommerce_brand_thumbnail_id',
     135        ];
     136
     137        return isset($image_keys[$taxonomy]) ? $image_keys[$taxonomy] : false;
    87138    }
    88139   
  • transfer-brands-for-woocommerce/trunk/readme.txt

    r3408293 r3408329  
    44Requires at least: 6.0
    55Tested up to: 6.8.2
    6 Stable tag: 2.8.4
     6Stable tag: 2.8.5
    77Requires PHP: 7.4
    88License: GPLv2 or later
     
    119119== Changelog ==
    120120
     121= 2.8.5 =
     122* Added: Support for Perfect Brands for WooCommerce plugin (pwb-brand taxonomy)
     123* Added: Support for YITH WooCommerce Brands plugin (yith_product_brand taxonomy)
     124* Added: Automatic detection of third-party brand plugins in the source dropdown
     125* Added: Brand plugin taxonomies now shown in a separate "Brand Plugins" section in settings
     126* Improved: Analysis tool now properly displays brand plugin statistics
     127* Improved: Transfer logic handles taxonomy-to-taxonomy transfers for brand plugins
     128* Fixed: "Invalid taxonomy" error when using Perfect Brands for WooCommerce
     129
    121130= 2.8.4 =
    122131* Added: Pre-transfer validation to check if WooCommerce Brands feature is enabled
     
    236245== Upgrade Notice ==
    237246
     247= 2.8.5 =
     248**New**: Now supports Perfect Brands for WooCommerce and YITH WooCommerce Brands! If you're using these popular brand plugins and want to migrate to WooCommerce's built-in Brands, this update makes it possible. Simply select your brand plugin's taxonomy from the dropdown and transfer.
     249
    238250= 2.8.4 =
    239251**Important**: This update prevents a common issue where brands appear to transfer successfully but don't show in WooCommerce admin. The plugin now validates that WooCommerce Brands is properly enabled before allowing transfers, with clear instructions on how to enable it.
  • transfer-brands-for-woocommerce/trunk/transfer-brands-for-woocommerce.php

    r3408293 r3408329  
    44 * Plugin URI: https://pluginatlas.com/transfer-brands-for-woocommerce
    55 * Description: Official migration tool for WooCommerce 9.6 Brands. Safely transfer your product brand attributes to the new brand taxonomy with image support, batch processing, and full backup capabilities.
    6  * Version: 2.8.4
     6 * Version: 2.8.5
    77 * Requires at least: 6.0
    88 * Requires PHP: 7.4
     
    3636
    3737// Define plugin constants
    38 define('TBFW_VERSION', '2.8.4');
     38define('TBFW_VERSION', '2.8.5');
    3939define('TBFW_PLUGIN_DIR', plugin_dir_path(__FILE__));
    4040define('TBFW_PLUGIN_URL', plugin_dir_url(__FILE__));
Note: See TracChangeset for help on using the changeset viewer.