Răsfoiți Sursa

Phase B — product-import attribute round-trip — v1.6.3

Implements review-00-plan-00.md Phase B. Items addressed:

* H2 — the importer now reads the `Vlastnost 1 globální` CSV column
  (previously written by the exporter but never consumed) and branches
  attribute handling on it. Default when the column is absent or 0 is
  the custom (non-taxonomy) path.

* H3 — for the taxonomy branch (globální = 1), the importer:
  - registers the pa_ taxonomy if needed,
  - resolves each `Hodnota(y) 1 vlastnosti` value to its term, creating
    the term via wp_insert_term when missing,
  - passes the term IDs (not names) to WC_Product_Attribute::set_options,
  - calls wp_set_object_terms after the existing $product->save() to
    link the terms to the freshly-saved product (belt-and-braces; modern
    WC CRUD usually does this on save() when set_options gets term IDs,
    but the call is cheap and survives WC-internals changes).

  For variations under the taxonomy branch, the importer now stores the
  term **slug** under the attribute_pa_* meta key (previously the raw
  name, which WC couldn't match against the parent's slugs → "No
  matching variation" on the storefront).

  For the custom branch (globální = 0 or absent), the importer now
  builds a non-taxonomy WC_Product_Attribute with set_id(0) and a plain
  display name, stores the raw values, and writes variation meta under
  attribute_<sanitized name>. Previously every attribute was force-
  converted to a pa_ taxonomy.

LIVE-TEST GATE — per master plan §5.B.0, verify on a real WC install
before deploying: create a variable product with pa_color (red, green,
blue) and 3 variations, export, delete, re-import, then storefront-test
that each variation is selectable. The fix is shaped from the review's
analysis; the live test is what proves it. If the test shows
wp_set_object_terms is redundant (WC CRUD already linked), it can be
removed in a follow-up — it's only there for safety.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dalibor Votruba 1 lună în urmă
părinte
comite
b95d9554a5

+ 1 - 1
studiou-wc-product-cat-manage/CLAUDE.md

@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 **QDR - Studiou WC Export/Import Product Categories** is a WordPress plugin that extends WooCommerce to provide batch export/import functionality for both product categories and products, along with category manipulation tools.
 
-**Current Version:** 1.6.2
+**Current Version:** 1.6.3
 
 ## Requirements
 

+ 1 - 1
studiou-wc-product-cat-manage/assets/js/admin.js

@@ -19,7 +19,7 @@
     // Initialize admin scripts
     $(document).ready(function() {
         // Debug: Log that script is loaded
-        console.log('STUDIOU WC: Admin script loaded v1.6.2');
+        console.log('STUDIOU WC: Admin script loaded v1.6.3');
 
         if (typeof studiouWcpcm === 'undefined') {
             console.error('STUDIOU WC: studiouWcpcm object is NOT defined - AJAX will not work');

+ 117 - 19
studiou-wc-product-cat-manage/includes/class-studiou-wc-product-manage-product-import.php

@@ -375,26 +375,88 @@ class Studiou_WC_Product_Manage_Product_Import {
             $reviews_enabled = isset($data['Povolit zákaznické recenze?']) && $data['Povolit zákaznické recenze?'] === '1';
             $product->set_reviews_allowed($reviews_enabled);
 
-            // Handle product attribute
+            // v1.6.3 — Handle product attribute (taxonomy vs. custom).
+            //
+            // Pre-1.6.3 behaviour ignored the `Vlastnost 1 globální` column and
+            // forced every attribute into a global pa_ taxonomy: it called
+            // create_attribute_if_not_exists() unconditionally, set the
+            // attribute name via wc_attribute_taxonomy_name(), and passed raw
+            // name strings to set_options(). Result: custom (product-level)
+            // attributes silently became global taxonomies; for variations,
+            // raw names landed where slugs were required, so the storefront
+            // couldn't match the customer's selection ("No matching variation").
+            //
+            // Branch on the global flag and, for the taxonomy path, resolve
+            // each value to a term (creating it if missing), pass term IDs to
+            // set_options(), remember the slugs so process_variation() can
+            // store the correct value, and link the terms via
+            // wp_set_object_terms after the product save.
+            //
+            // Hand-built CSVs that omit `Vlastnost 1 globální` default to the
+            // custom branch.
+            //
+            // LIVE-TEST GATE: §5.B.0 of review-00-plan-00.md asks for a real
+            // storefront round-trip before deploy. Verify both the taxonomy
+            // and custom paths there.
+            $term_ids_for_link = array();
+            $taxonomy_for_link = '';
             if (isset($data['Název 1 vlastnosti']) && !empty($data['Název 1 vlastnosti'])) {
-                $attribute_name = $data['Název 1 vlastnosti'];
+                $attribute_name = trim($data['Název 1 vlastnosti']);
                 $attribute_values = isset($data['Hodnota(y) 1 vlastnosti']) ? $data['Hodnota(y) 1 vlastnosti'] : '';
                 $is_visible = isset($data['Vlastnost 1 viditelnost']) && $data['Vlastnost 1 viditelnost'] === '1';
                 $is_variation = isset($data['Vlastnost 1 varianta']) && $data['Vlastnost 1 varianta'] === '1';
-
-                // Create attribute taxonomy if it doesn't exist
-                $this->db->create_attribute_if_not_exists($attribute_name);
+                $is_global    = isset($data['Vlastnost 1 globální']) && $data['Vlastnost 1 globální'] === '1';
+                $values = array_map('trim', explode('|', $attribute_values));
+                $values = array_values(array_filter($values, function($v) { return $v !== ''; }));
 
                 $attribute = new WC_Product_Attribute();
-                $attribute->set_name(wc_attribute_taxonomy_name($attribute_name));
-                $attribute->set_visible($is_visible);
-                $attribute->set_variation($is_variation);
 
-                // Set attribute options (terms)
-                $values = array_map('trim', explode('|', $attribute_values));
-                $attribute->set_options($values);
+                if ($is_global) {
+                    // Taxonomy attribute — register, resolve/create terms,
+                    // pass term IDs (not names) to set_options.
+                    $this->db->create_attribute_if_not_exists($attribute_name);
+                    $taxonomy = wc_attribute_taxonomy_name($attribute_name);
+                    if (!taxonomy_exists($taxonomy)) {
+                        register_taxonomy($taxonomy, 'product');
+                    }
 
-                $product->set_attributes(array($attribute));
+                    $term_ids = array();
+                    foreach ($values as $value) {
+                        $term = get_term_by('name', $value, $taxonomy);
+                        if (!$term) {
+                            $inserted = wp_insert_term($value, $taxonomy);
+                            if (is_wp_error($inserted)) {
+                                error_log('STUDIOU WC IMPORT: failed to insert term "' . $value . '" into ' . $taxonomy . ': ' . $inserted->get_error_message());
+                                continue;
+                            }
+                            $term = get_term($inserted['term_id'], $taxonomy);
+                        }
+                        if ($term && !is_wp_error($term)) {
+                            $term_ids[] = (int) $term->term_id;
+                        }
+                    }
+
+                    $attribute->set_id(wc_attribute_taxonomy_id_by_name($attribute_name));
+                    $attribute->set_name($taxonomy);
+                    $attribute->set_options($term_ids);
+                    $attribute->set_visible($is_visible);
+                    $attribute->set_variation($is_variation);
+                    $product->set_attributes(array($attribute));
+
+                    // Defer term-relationship linkage until after save() so we
+                    // have a real product ID.
+                    $term_ids_for_link = $term_ids;
+                    $taxonomy_for_link = $taxonomy;
+                } else {
+                    // Custom (product-level) attribute — no taxonomy, store
+                    // the raw display name and the raw values.
+                    $attribute->set_id(0);
+                    $attribute->set_name($attribute_name);
+                    $attribute->set_options($values);
+                    $attribute->set_visible($is_visible);
+                    $attribute->set_variation($is_variation);
+                    $product->set_attributes(array($attribute));
+                }
             }
 
             // Handle minimum/maximum quantity
@@ -408,6 +470,14 @@ class Studiou_WC_Product_Manage_Product_Import {
             // Save product
             $saved_id = $product->save();
 
+            // Belt-and-braces: link the attribute terms to the freshly-saved
+            // product. Modern WC CRUD typically does this on save() when
+            // set_options() receives term IDs, but the link is cheap and
+            // makes the import robust against WC internals changes.
+            if (!empty($term_ids_for_link) && $taxonomy_for_link !== '' && $saved_id) {
+                wp_set_object_terms($saved_id, $term_ids_for_link, $taxonomy_for_link, false);
+            }
+
             $message = $product_id == 0 ?
                 sprintf(__('Created variable product: %s', 'studiou-wc-product-cat-manage'), $product->get_name()) :
                 sprintf(__('Updated variable product: %s', 'studiou-wc-product-cat-manage'), $product->get_name());
@@ -495,16 +565,44 @@ class Studiou_WC_Product_Manage_Product_Import {
                 }
             }
 
-            // Set variation attributes
+            // v1.6.3 — Variation attribute (taxonomy vs. custom).
+            //
+            // Pre-1.6.3 stored the raw value (e.g. "Red") under the taxonomy
+            // meta key (e.g. attribute_pa_color). WC matches variations by
+            // term slug ("red"), so the variation was unreachable on the
+            // storefront. Now: for taxonomy attributes resolve the value to
+            // its term slug; for custom attributes store the literal value
+            // under attribute_<sanitized name>.
             if (isset($data['Název 1 vlastnosti']) && !empty($data['Název 1 vlastnosti']) &&
                 isset($data['Hodnota(y) 1 vlastnosti']) && !empty($data['Hodnota(y) 1 vlastnosti'])) {
 
-                $attribute_name = wc_attribute_taxonomy_name($data['Název 1 vlastnosti']);
-                $attribute_value = $data['Hodnota(y) 1 vlastnosti'];
-
-                $variation->set_attributes(array(
-                    $attribute_name => $attribute_value
-                ));
+                $attribute_name = trim($data['Název 1 vlastnosti']);
+                $attribute_value = trim($data['Hodnota(y) 1 vlastnosti']);
+                $is_global = isset($data['Vlastnost 1 globální']) && $data['Vlastnost 1 globální'] === '1';
+
+                if ($is_global) {
+                    $taxonomy = wc_attribute_taxonomy_name($attribute_name);
+                    $term = get_term_by('name', $attribute_value, $taxonomy);
+                    if (!$term) {
+                        // The variation row may arrive before its parent's
+                        // taxonomy terms exist (rare but possible on hand-built
+                        // CSVs). Create it so the variation is selectable.
+                        if (!taxonomy_exists($taxonomy)) {
+                            $this->db->create_attribute_if_not_exists($attribute_name);
+                            register_taxonomy($taxonomy, 'product');
+                        }
+                        $inserted = wp_insert_term($attribute_value, $taxonomy);
+                        if (!is_wp_error($inserted)) {
+                            $term = get_term($inserted['term_id'], $taxonomy);
+                        }
+                    }
+                    $slug = ($term && !is_wp_error($term)) ? $term->slug : sanitize_title($attribute_value);
+                    $variation->set_attributes(array($taxonomy => $slug));
+                } else {
+                    $variation->set_attributes(array(
+                        sanitize_title($attribute_name) => $attribute_value,
+                    ));
+                }
             }
 
             // Handle minimum/maximum quantity

+ 7 - 1
studiou-wc-product-cat-manage/readme.md

@@ -1,6 +1,6 @@
 # QDR - Studiou WC Export/Import Product Categories
 
-**Version: 1.6.2**
+**Version: 1.6.3**
 
 A WordPress plugin that extends WooCommerce to provide batch export and import functionality for both product categories and products, along with category manipulation tools.
 
@@ -352,6 +352,12 @@ For support, please visit [https://www.quadarax.com/plugins/studiou-wc-product-c
 
 ## Changelog
 
+### Version 1.6.3
+- **Product import: correct round-trip for taxonomy and custom attributes.** The importer now reads the `Vlastnost 1 globální` column (previously written by the exporter but never consumed) and branches on it:
+  - **Global / taxonomy attribute** (`globální = 1`): the importer registers the taxonomy, resolves each `Hodnota(y) 1 vlastnosti` value to its term (creating the term via `wp_insert_term` if it doesn't exist), passes the term IDs to `WC_Product_Attribute::set_options()`, and links the terms to the product via `wp_set_object_terms` after save. For variations the importer stores the term **slug** (not the raw name) under the `attribute_pa_*` meta key, so WooCommerce can match the customer's selection on the storefront. Previously the importer passed raw names to `set_options()` and stored raw names in variation meta, which meant taxonomy variations were unreachable ("No matching variation").
+  - **Custom (product-level) attribute** (`globální = 0` or column absent): the importer now correctly creates a non-taxonomy `WC_Product_Attribute` with `set_id(0)` and a plain display name, stores the raw values, and writes variation meta under `attribute_<sanitized name>`. Previously every attribute was force-converted to a `pa_*` taxonomy, polluting the store with global taxonomies for what was meant to be a product-level attribute.
+- **Live-test gate before deployment.** Per the master plan §5.B.0, verify on a real WooCommerce install before shipping: create a variable product with a global `pa_*` attribute and 2-3 variations, export, delete, re-import the export, then storefront-test that each variation is selectable. The fix is shaped from the review's analysis; the live test is what proves it.
+
 ### Version 1.6.2
 - **Safety: "Remove Not Assigned Media" no longer destroys WooCommerce product-category images.** The "unused" attachment query now also excludes any attachment referenced from termmeta `thumbnail_id` (the WC category-image convention) and the customizer (custom logo, site icon, header image). NOTE: this *narrows* the blast radius; it does not make the operation safe in general. Images embedded only in post / page / product content HTML, in ACF/meta fields, in custom termmeta keys other than `thumbnail_id`, and in WooCommerce downloadable-product files are still NOT detected and will still be deleted if you click the button. The tab warning has been rewritten to say so explicitly. The preview-list UI that actually closes the gap is a separate change tracked under review-00-plan-02.
 - **Product import: ragged CSV rows no longer crash the entire import.** On PHP 8+ `array_combine` throws `ValueError` (which extends `Error`, not `Exception`) when a row's column count doesn't match the header. The surrounding `catch (Exception)` missed it and the whole import died as a bare 500. Now: short rows are padded with empty cells (Excel trailing-comma artifact, recoverable); over-long rows are skipped with a clear "column count mismatch" reason and surface in the failed-CSV download. All product import / export handlers' catches widened from `Exception` to `\Throwable`.

+ 2 - 2
studiou-wc-product-cat-manage/studiou-wc-product-cat-manage.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Export/Import Product Categories
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-product-cat-manage
  * Description: Allows batch import and export WooCommerce product categories and products
- * Version: 1.6.2
+ * Version: 1.6.3
  * Requires at least: 6.8.1
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
@@ -23,7 +23,7 @@ if (!defined('WPINC')) {
 }
 
 // Define plugin constants
-if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.6.2');
+if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.6.3');
 if (!defined('STUDIOU_WCPCM_PLUGIN_DIR')) define('STUDIOU_WCPCM_PLUGIN_DIR', plugin_dir_path(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_URL')) define('STUDIOU_WCPCM_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_BASENAME')) define('STUDIOU_WCPCM_PLUGIN_BASENAME', plugin_basename(__FILE__));