Procházet zdrojové kódy

Add Update Product Variant Prices feature — v1.5.2 -> v1.5.3

New "Update product variant prices" section on Category Manipulations:
category checkbox tree, variant attribute combobox, dependent value
combobox, New Price input, Dry Run + Apply buttons, and a live
affected-count label.

- 4 new manipulator methods (heads/values/find/apply) + 4 AJAX handlers
  (studiou_wcpcm_upvp_get_attributes/get_attribute_values/count/apply)
- Operation modifies only product_variation regular_price via WC API
  (set_regular_price + WC_Product_Variable::sync); order line items,
  carts, and historical order data are never touched
- Logging prefix STUDIOU WC UPVP:
- 23 new English/Czech translation strings
- Updated readme.md, CLAUDE.md, version bumps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dalibor Votruba před 2 měsíci
rodič
revize
f8c17020e0

+ 24 - 4
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.5.2
+**Current Version:** 1.5.3
 
 ## Requirements
 
@@ -53,6 +53,7 @@ The plugin follows a modular class-based architecture:
   - Delete products in category (permanently deletes products and variants)
   - Clear Media Categories (permanently deletes media files from media categories) (NEW in v1.5)
   - Remove Not Assigned Media (deletes unused uncategorized media) (NEW in v1.5.2)
+  - Update product variant prices (regular_price only, by category + attribute filter) (NEW in v1.5.3)
   - Uses direct database queries for reliable product detection
   - Batch processing for delete operations (10 items per batch)
   - Auto-detects media category taxonomy (any custom taxonomy registered for attachments)
@@ -98,6 +99,7 @@ The plugin follows a modular class-based architecture:
   - Delete products in category (checkbox list + "Delete" button + progress bar)
   - Clear Media Categories (checkbox list + "Delete" button + progress bar) (NEW in v1.5)
   - Remove Not Assigned Media ("Delete unused uncategorized media (N)" button + progress bar) (NEW in v1.5.2)
+  - Update product variant prices (checkbox list + 2 comboboxes + price input + Dry Run/Apply buttons + live affected-count label) (NEW in v1.5.3)
 - **product-import.php** - Product import page UI (NEW in v1.4)
   - File upload for CSV import
   - Progress bar for import operations
@@ -210,6 +212,7 @@ All pages are added under **Products** menu:
   - Delete products in category
   - Clear Media Categories (NEW in v1.5)
   - Remove Not Assigned Media (NEW in v1.5.2)
+  - Update product variant prices (NEW in v1.5.3)
 - **Product Import (slug: `studiou-product-import`)** - NEW in v1.4
 - **Product Export (slug: `studiou-product-export`)** - NEW in v1.4
 
@@ -302,6 +305,19 @@ All pages are added under **Products** menu:
 - Verify physical files and attachment posts are both deleted
 - Verify button count updates after page reload
 
+### Update Product Variant Prices Operations (NEW in v1.5.3)
+- Test happy path with a global taxonomy attribute (e.g. `pa_color = red`) — Dry Run shows correct count + sample, Apply commits new regular price
+- Test happy path with a custom non-taxonomy attribute (product-level)
+- Test multi-category selection — variations counted only once each
+- Test no-match filter — Apply button stays disabled, label shows 0
+- Test invalid price input (empty / negative / non-numeric) — server-side error, no DB write
+- Verify a user without `manage_woocommerce` cannot trigger AJAX
+- Verify nonce tampering returns "Security check failed"
+- **Order data integrity**: place a test order with a variation, run UPVP, verify the order line item retains the original price (only product-side price changes)
+- Verify parent variable product price-range cache is refreshed (`WC_Product_Variable::sync` + `wc_delete_product_transients`) — storefront price range reflects new price
+- Verify Czech translations render when site locale is `cs_CZ`
+- Verify asset version query string updates from `1.5.2` to `1.5.3` (note: studiou.cz strips `?ver=` — instruct user to hard-refresh)
+
 ### General
 - Verify translations are properly loaded (English/Czech)
 - Verify progress bars update correctly
@@ -323,11 +339,15 @@ The plugin registers the following AJAX actions:
 8. **studiou_wcpcm_clear_media_batch** - Processes media deletion in batches (NEW in v1.5)
 9. **studiou_wcpcm_remove_unassigned_media** - Gets IDs of unused uncategorized media (NEW in v1.5.2)
 10. **studiou_wcpcm_remove_unassigned_media_batch** - Processes unassigned media deletion in batches (NEW in v1.5.2)
+11. **studiou_wcpcm_upvp_get_attributes** - Lists variation attribute heads (NEW in v1.5.3)
+12. **studiou_wcpcm_upvp_get_attribute_values** - Lists values for the selected attribute, scoped to selected categories (NEW in v1.5.3)
+13. **studiou_wcpcm_upvp_count** - Counts and samples variations matching the current filter (NEW in v1.5.3)
+14. **studiou_wcpcm_upvp_apply** - Commits new regular price to matching variations via WC API (NEW in v1.5.3)
 
 ### Product Operations (NEW in v1.4)
-11. **studiou_wcpcm_product_import_parse** - Parses CSV file and stores in transient (Step 1)
-12. **studiou_wcpcm_product_import_batch** - Processes batch of 5 products (Step 2, called recursively)
-13. **studiou_wcpcm_product_export** - Generates product CSV export by category selection
+15. **studiou_wcpcm_product_import_parse** - Parses CSV file and stores in transient (Step 1)
+16. **studiou_wcpcm_product_import_batch** - Processes batch of 5 products (Step 2, called recursively)
+17. **studiou_wcpcm_product_export** - Generates product CSV export by category selection
 
 All AJAX handlers include:
 - Nonce verification (`studiou-wcpcm-nonce`)

+ 27 - 0
studiou-wc-product-cat-manage/assets/css/admin.css

@@ -281,4 +281,31 @@
 .studiou-wcpcm-clear-media-results ul li:last-child,
 .studiou-wcpcm-remove-unassigned-media-results ul li:last-child {
     border-bottom: none;
+}
+
+/* Update Product Variant Prices (UPVP) */
+.studiou-wcpcm-upvp-results {
+    background: #f9f9f9;
+    padding: 15px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    margin-top: 30px;
+}
+
+#studiou-wcpcm-upvp-affected-count {
+    font-size: 1.2em;
+    color: #0073aa;
+}
+
+.studiou-wcpcm-upvp-preview-table {
+    width: 100%;
+    border-collapse: collapse;
+    margin-top: 10px;
+}
+
+.studiou-wcpcm-upvp-preview-table th,
+.studiou-wcpcm-upvp-preview-table td {
+    padding: 6px 10px;
+    border-bottom: 1px solid #eee;
+    text-align: left;
 }

+ 344 - 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.5.1');
+        console.log('STUDIOU WC: Admin script loaded v1.5.3');
 
         if (typeof studiouWcpcm === 'undefined') {
             console.error('STUDIOU WC: studiouWcpcm object is NOT defined - AJAX will not work');
@@ -38,6 +38,7 @@
         safeInit('Sample download', '#studiou-wcpcm-download-sample', initSampleDownload);
         safeInit('Product import form', '#studiou-wcpcm-product-import-form', initProductImportForm);
         safeInit('Product export form', '#studiou-wcpcm-product-export-form', initProductExportForm);
+        safeInit('Update variant prices form', '#studiou-wcpcm-upvp-form', initUpdateVariantPricesForm);
     });
     
     /**
@@ -1350,6 +1351,348 @@
         console.log('STUDIOU WC: Product import form exists:', $('#studiou-wcpcm-product-import-form').length > 0);
         console.log('STUDIOU WC: Product export form exists:', $('#studiou-wcpcm-product-export-form').length > 0);
         console.log('STUDIOU WC: Clear media form exists:', $('#studiou-wcpcm-clear-media-form').length > 0);
+        console.log('STUDIOU WC: UPVP form exists:', $('#studiou-wcpcm-upvp-form').length > 0);
     });
 
+    /**
+     * Initialize Update Product Variant Prices form (UPVP) — v1.5.3
+     */
+    function initUpdateVariantPricesForm() {
+        var $form          = $('#studiou-wcpcm-upvp-form');
+        var $catList       = $('#upvp_categories_list');
+        var $attrSelect    = $('#upvp_attribute');
+        var $valueSelect   = $('#upvp_attribute_value');
+        var $newPrice      = $('#upvp_new_price');
+        var $countLabel    = $('#studiou-wcpcm-upvp-affected-count');
+        var $dryRunBtn     = $('#studiou-wcpcm-upvp-dry-run-button');
+        var $applyBtn      = $('#studiou-wcpcm-upvp-apply-button');
+        var $spinner       = $form.find('.spinner');
+        var $resultsBox    = $('.studiou-wcpcm-upvp-results');
+        var $resultsMsg    = $('#studiou-wcpcm-upvp-message');
+
+        var i18n = (studiouWcpcm && studiouWcpcm.i18n) ? studiouWcpcm.i18n : {};
+        var countDebounceHandle = null;
+        var countInFlight = false;
+
+        function getSelectedCategoryIds() {
+            var ids = [];
+            $catList.find('input[type="checkbox"]:checked').each(function() {
+                ids.push($(this).val());
+            });
+            return ids;
+        }
+
+        function setApplyEnabled(count) {
+            var enabled = (count > 0) && !countInFlight;
+            $dryRunBtn.prop('disabled', !enabled);
+            $applyBtn.prop('disabled', !enabled);
+        }
+
+        function loadAttributes() {
+            $attrSelect.prop('disabled', true).html(
+                '<option value="">' + (i18n.upvpLoadingAttributes || 'Loading attributes...') + '</option>'
+            );
+
+            $.ajax({
+                url: studiouWcpcm.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcpcm_upvp_get_attributes',
+                    nonce: studiouWcpcm.nonce
+                },
+                success: function(response) {
+                    $attrSelect.empty();
+                    $attrSelect.append('<option value="">' + (i18n.upvpSelectAttribute || '-- Select attribute --') + '</option>');
+                    if (response.success && response.data && response.data.attributes) {
+                        $.each(response.data.attributes, function(_, a) {
+                            $attrSelect.append(
+                                $('<option/>')
+                                    .val(a.key)
+                                    .attr('data-istax', a.is_taxonomy ? '1' : '0')
+                                    .text(a.label)
+                            );
+                        });
+                    }
+                    $attrSelect.prop('disabled', false);
+                },
+                error: function(xhr, status, error) {
+                    console.error('STUDIOU WC UPVP: get_attributes failed', status, error);
+                    $attrSelect.prop('disabled', false);
+                }
+            });
+        }
+
+        function loadValues() {
+            var key = $attrSelect.val();
+            $valueSelect.prop('disabled', true).empty();
+            $valueSelect.append('<option value="">' + (i18n.upvpLoadingValues || 'Loading values...') + '</option>');
+
+            if (!key) {
+                $valueSelect.empty();
+                $valueSelect.append('<option value="">' + (i18n.upvpSelectValue || '-- Select value --') + '</option>');
+                $valueSelect.prop('disabled', true);
+                refreshCount();
+                return;
+            }
+
+            $.ajax({
+                url: studiouWcpcm.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcpcm_upvp_get_attribute_values',
+                    nonce: studiouWcpcm.nonce,
+                    attribute_key: key,
+                    category_ids: getSelectedCategoryIds()
+                },
+                success: function(response) {
+                    $valueSelect.empty();
+                    $valueSelect.append('<option value="">' + (i18n.upvpSelectValue || '-- Select value --') + '</option>');
+                    if (response.success && response.data && response.data.values) {
+                        $.each(response.data.values, function(_, v) {
+                            $valueSelect.append($('<option/>').val(v.value).text(v.label));
+                        });
+                    }
+                    $valueSelect.prop('disabled', false);
+                    refreshCount();
+                },
+                error: function(xhr, status, error) {
+                    console.error('STUDIOU WC UPVP: get_attribute_values failed', status, error);
+                    $valueSelect.empty();
+                    $valueSelect.append('<option value="">' + (i18n.upvpSelectValue || '-- Select value --') + '</option>');
+                    $valueSelect.prop('disabled', false);
+                }
+            });
+        }
+
+        function refreshCount() {
+            if (countDebounceHandle) {
+                clearTimeout(countDebounceHandle);
+            }
+            countDebounceHandle = setTimeout(doRefreshCount, 300);
+        }
+
+        function doRefreshCount() {
+            var cats = getSelectedCategoryIds();
+            var key  = $attrSelect.val();
+            var val  = $valueSelect.val();
+
+            if (!cats.length || !key || !val) {
+                $countLabel.text('0');
+                setApplyEnabled(0);
+                return;
+            }
+
+            countInFlight = true;
+            setApplyEnabled(0);
+
+            $.ajax({
+                url: studiouWcpcm.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcpcm_upvp_count',
+                    nonce: studiouWcpcm.nonce,
+                    category_ids: cats,
+                    attribute_key: key,
+                    attribute_value: val
+                },
+                success: function(response) {
+                    countInFlight = false;
+                    if (response.success && response.data) {
+                        var c = parseInt(response.data.count, 10) || 0;
+                        $countLabel.text(c);
+                        setApplyEnabled(c);
+                    } else {
+                        $countLabel.text('0');
+                        setApplyEnabled(0);
+                    }
+                },
+                error: function() {
+                    countInFlight = false;
+                    $countLabel.text('0');
+                    setApplyEnabled(0);
+                }
+            });
+        }
+
+        function renderSampleTable(sample, newPriceLabel) {
+            if (!sample || !sample.length) return '';
+            var html = '<table class="studiou-wcpcm-upvp-preview-table">';
+            html += '<thead><tr>';
+            html += '<th>ID</th><th>SKU</th><th>Name</th><th>Parent</th><th>Current</th>';
+            if (newPriceLabel !== undefined) html += '<th>New</th>';
+            html += '</tr></thead><tbody>';
+            $.each(sample, function(_, r) {
+                html += '<tr>';
+                html += '<td>' + r.id + '</td>';
+                html += '<td>' + (r.sku || '') + '</td>';
+                html += '<td>' + (r.name || '') + '</td>';
+                html += '<td>' + (r.parent_name || '') + '</td>';
+                html += '<td>' + (r.current_price || '') + '</td>';
+                if (newPriceLabel !== undefined) html += '<td>' + newPriceLabel + '</td>';
+                html += '</tr>';
+            });
+            html += '</tbody></table>';
+            return html;
+        }
+
+        // Wire up Check All / Uncheck All
+        $('#studiou-wcpcm-upvp-check-all').on('click', function(e) {
+            e.preventDefault();
+            $catList.find('input[type="checkbox"]').prop('checked', true);
+            refreshCount();
+        });
+        $('#studiou-wcpcm-upvp-uncheck-all').on('click', function(e) {
+            e.preventDefault();
+            $catList.find('input[type="checkbox"]').prop('checked', false);
+            refreshCount();
+        });
+
+        $catList.on('change', 'input[type="checkbox"]', refreshCount);
+        $attrSelect.on('change', loadValues);
+        $valueSelect.on('change', refreshCount);
+
+        // Dry Run
+        $dryRunBtn.on('click', function(e) {
+            e.preventDefault();
+            var cats = getSelectedCategoryIds();
+            var key  = $attrSelect.val();
+            var val  = $valueSelect.val();
+
+            if (!cats.length) {
+                showNotice('error', i18n.selectAtLeastOneCategory || 'Please select at least one category');
+                return;
+            }
+            if (!key) {
+                showNotice('error', i18n.upvpSelectAttribute || 'Please select an attribute');
+                return;
+            }
+            if (!val) {
+                showNotice('error', i18n.upvpSelectValue || 'Please select an attribute value');
+                return;
+            }
+
+            $('.studiou-wcpcm-notice-area').empty();
+            $resultsBox.hide();
+            $spinner.css('visibility', 'visible');
+            $dryRunBtn.prop('disabled', true);
+            $applyBtn.prop('disabled', true);
+
+            $.ajax({
+                url: studiouWcpcm.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcpcm_upvp_count',
+                    nonce: studiouWcpcm.nonce,
+                    category_ids: cats,
+                    attribute_key: key,
+                    attribute_value: val
+                },
+                success: function(response) {
+                    if (response.success && response.data) {
+                        var c       = parseInt(response.data.count, 10) || 0;
+                        var sample  = response.data.sample || [];
+                        var price   = $newPrice.val();
+                        var msg     = '';
+                        if (c === 0) {
+                            msg = '<p>' + (i18n.upvpNoMatches || 'No variations match the current filter.') + '</p>';
+                        } else {
+                            var tpl = i18n.upvpDryRunComplete || 'Dry run complete: %d variations would be updated to %s.';
+                            msg = '<p>' + tpl.replace('%d', c).replace('%s', price !== '' ? price : '—') + '</p>';
+                            msg += renderSampleTable(sample, price !== '' ? price : undefined);
+                        }
+                        $resultsMsg.html(msg);
+                        $resultsBox.show();
+                        $countLabel.text(c);
+                        setApplyEnabled(c);
+                    } else {
+                        showNotice('error', (response.data && response.data.message) || 'Dry run failed');
+                    }
+                },
+                error: function(xhr, status, error) {
+                    showNotice('error', 'Dry run error: ' + error);
+                },
+                complete: function() {
+                    $spinner.css('visibility', 'hidden');
+                }
+            });
+        });
+
+        // Apply
+        $applyBtn.on('click', function(e) {
+            e.preventDefault();
+            var cats  = getSelectedCategoryIds();
+            var key   = $attrSelect.val();
+            var val   = $valueSelect.val();
+            var price = $newPrice.val();
+            var count = parseInt($countLabel.text(), 10) || 0;
+
+            if (!cats.length) {
+                showNotice('error', i18n.selectAtLeastOneCategory || 'Please select at least one category');
+                return;
+            }
+            if (!key) {
+                showNotice('error', i18n.upvpSelectAttribute || 'Please select an attribute');
+                return;
+            }
+            if (!val) {
+                showNotice('error', i18n.upvpSelectValue || 'Please select an attribute value');
+                return;
+            }
+            if (price === '' || isNaN(parseFloat(price)) || parseFloat(price) < 0) {
+                showNotice('error', i18n.upvpInvalidPrice || 'Please enter a valid new price');
+                return;
+            }
+
+            var confirmTpl = i18n.upvpConfirmApply || 'Are you sure you want to update %d variations?';
+            if (!confirm(confirmTpl.replace('%d', count))) {
+                return;
+            }
+
+            $('.studiou-wcpcm-notice-area').empty();
+            $resultsBox.hide();
+            $spinner.css('visibility', 'visible');
+            $dryRunBtn.prop('disabled', true);
+            $applyBtn.prop('disabled', true);
+
+            $.ajax({
+                url: studiouWcpcm.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcpcm_upvp_apply',
+                    nonce: studiouWcpcm.nonce,
+                    category_ids: cats,
+                    attribute_key: key,
+                    attribute_value: val,
+                    new_price: price
+                },
+                success: function(response) {
+                    if (response.success && response.data) {
+                        showNotice('success', i18n.upvpApplySuccess || 'Variant prices updated successfully');
+                        var html = '<p><strong>' + (response.data.message || '') + '</strong></p>';
+                        if (response.data.failed_ids && response.data.failed_ids.length) {
+                            html += '<p>Failed IDs: ' + response.data.failed_ids.join(', ') + '</p>';
+                        }
+                        $resultsMsg.html(html);
+                        $resultsBox.show();
+                        refreshCount();
+                    } else {
+                        showNotice('error', (response.data && response.data.message) || (i18n.upvpApplyError || 'Variant prices update error'));
+                    }
+                },
+                error: function(xhr, status, error) {
+                    showNotice('error', (i18n.upvpApplyError || 'Variant prices update error') + ': ' + error);
+                },
+                complete: function() {
+                    $spinner.css('visibility', 'hidden');
+                    $dryRunBtn.prop('disabled', false);
+                    $applyBtn.prop('disabled', false);
+                }
+            });
+        });
+
+        // Initial load
+        loadAttributes();
+        setApplyEnabled(0);
+    }
+
 })(jQuery);

+ 507 - 0
studiou-wc-product-cat-manage/includes/class-studiou-wc-product-cat-manage-manipulator.php

@@ -40,6 +40,10 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
         add_action('wp_ajax_studiou_wcpcm_clear_media_batch', array($this, 'handle_clear_media_batch_ajax'));
         add_action('wp_ajax_studiou_wcpcm_remove_unassigned_media', array($this, 'handle_remove_unassigned_media_ajax'));
         add_action('wp_ajax_studiou_wcpcm_remove_unassigned_media_batch', array($this, 'handle_remove_unassigned_media_batch_ajax'));
+        add_action('wp_ajax_studiou_wcpcm_upvp_get_attributes', array($this, 'handle_upvp_get_attributes_ajax'));
+        add_action('wp_ajax_studiou_wcpcm_upvp_get_attribute_values', array($this, 'handle_upvp_get_attribute_values_ajax'));
+        add_action('wp_ajax_studiou_wcpcm_upvp_count', array($this, 'handle_upvp_count_ajax'));
+        add_action('wp_ajax_studiou_wcpcm_upvp_apply', array($this, 'handle_upvp_apply_ajax'));
 
         // Add a test AJAX handler to verify AJAX is working
         add_action('wp_ajax_studiou_wcpcm_test', array($this, 'handle_test_ajax'));
@@ -1479,4 +1483,507 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
             ob_end_clean();
         }
     }
+
+    // ========================================================================
+    // Update Product Variant Prices (UPVP) — v1.5.3
+    // Bulk-update regular_price on product_variation records by category +
+    // attribute filter. Touches only product records; never order items, carts,
+    // or historical order data.
+    // ========================================================================
+
+    /**
+     * Get list of attribute "heads" usable for variations.
+     *
+     * Source 1: every wc_get_attribute_taxonomies() entry (global attributes)
+     *           — key = 'pa_<slug>', is_taxonomy = true.
+     * Source 2: distinct meta_key starting with 'attribute_' on product_variation
+     *           rows that are NOT global taxonomies (custom product-level
+     *           attributes flagged for variations) — key = '<slug>',
+     *           is_taxonomy = false.
+     *
+     * @return array [ ['key' => string, 'label' => string, 'is_taxonomy' => bool], ... ]
+     */
+    public function get_variation_attribute_heads() {
+        global $wpdb;
+
+        $heads = array();
+        $seen  = array();
+
+        // Source 1: global taxonomies.
+        $taxonomies = function_exists('wc_get_attribute_taxonomies') ? wc_get_attribute_taxonomies() : array();
+        if (!empty($taxonomies)) {
+            foreach ($taxonomies as $tax) {
+                $key = 'pa_' . $tax->attribute_name;
+                if (isset($seen[$key])) {
+                    continue;
+                }
+                $seen[$key] = true;
+                $heads[] = array(
+                    'key'         => $key,
+                    'label'       => !empty($tax->attribute_label) ? $tax->attribute_label : $tax->attribute_name,
+                    'is_taxonomy' => true,
+                );
+            }
+        }
+
+        // Source 2: custom (non-taxonomy) variation attributes.
+        $sql = "
+            SELECT DISTINCT pm.meta_key
+              FROM {$wpdb->postmeta} pm
+              INNER JOIN {$wpdb->posts} v ON v.ID = pm.post_id
+             WHERE v.post_type = 'product_variation'
+               AND v.post_status IN ('publish','private')
+               AND pm.meta_key LIKE %s
+        ";
+        $rows = $wpdb->get_col($wpdb->prepare($sql, $wpdb->esc_like('attribute_') . '%'));
+        if (!empty($rows)) {
+            foreach ($rows as $meta_key) {
+                $slug = substr($meta_key, strlen('attribute_'));
+                if ($slug === '' || isset($seen[$slug])) {
+                    continue;
+                }
+                // Already included as taxonomy with same slug? — prefer the
+                // taxonomy entry (already set under 'pa_<slug>'), so skip if
+                // this slug starts with 'pa_'.
+                if (strpos($slug, 'pa_') === 0 && isset($seen[$slug])) {
+                    continue;
+                }
+                $seen[$slug] = true;
+                $heads[] = array(
+                    'key'         => $slug,
+                    'label'       => wc_attribute_label($slug),
+                    'is_taxonomy' => false,
+                );
+            }
+        }
+
+        // Sort alphabetically by label.
+        usort($heads, function($a, $b) {
+            return strcasecmp($a['label'], $b['label']);
+        });
+
+        return $heads;
+    }
+
+    /**
+     * Get distinct values for the given attribute key, scoped to existing
+     * variations (and optionally to a set of parent product categories).
+     *
+     * For taxonomy attributes (pa_*) the meta_value is the term slug; we
+     * resolve labels via get_term_by('slug', ..., $taxonomy).
+     *
+     * @param string $attribute_key  E.g. 'pa_color' or 'size'.
+     * @param int[]  $category_ids   Optional list of parent product_cat term IDs.
+     * @return array [ ['value' => string, 'label' => string], ... ]
+     */
+    public function get_variation_attribute_values($attribute_key, $category_ids = array()) {
+        global $wpdb;
+
+        $attribute_key = sanitize_text_field($attribute_key);
+        if ($attribute_key === '') {
+            return array();
+        }
+
+        $meta_key = 'attribute_' . wc_sanitize_taxonomy_name($attribute_key);
+
+        $params = array($meta_key);
+
+        $cat_join  = '';
+        $cat_where = '';
+        if (!empty($category_ids)) {
+            $cat_ids = array_map('intval', $category_ids);
+            $cat_ids = array_filter($cat_ids, function($v) { return $v > 0; });
+            if (!empty($cat_ids)) {
+                $placeholders = implode(',', array_fill(0, count($cat_ids), '%d'));
+                $cat_join  = "
+                    INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
+                    INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+                ";
+                $cat_where = "
+                    AND tt.taxonomy = 'product_cat'
+                    AND tt.term_id IN ($placeholders)
+                ";
+                $params = array_merge($params, $cat_ids);
+            }
+        }
+
+        $sql = "
+            SELECT DISTINCT pm.meta_value
+              FROM {$wpdb->postmeta} pm
+              INNER JOIN {$wpdb->posts} v ON v.ID = pm.post_id
+                       AND v.post_type = 'product_variation'
+                       AND v.post_status IN ('publish','private')
+              INNER JOIN {$wpdb->posts} p ON p.ID = v.post_parent
+                       AND p.post_type = 'product'
+              $cat_join
+             WHERE pm.meta_key = %s
+               AND pm.meta_value <> ''
+               $cat_where
+        ";
+
+        $values = $wpdb->get_col($wpdb->prepare($sql, ...$params));
+        if (empty($values)) {
+            return array();
+        }
+
+        $is_taxonomy = (strpos($attribute_key, 'pa_') === 0) || taxonomy_exists($attribute_key);
+
+        $result = array();
+        foreach ($values as $value) {
+            $label = $value;
+            if ($is_taxonomy) {
+                $tax = (strpos($attribute_key, 'pa_') === 0) ? $attribute_key : 'pa_' . $attribute_key;
+                $term = get_term_by('slug', $value, $tax);
+                if ($term && !is_wp_error($term)) {
+                    $label = $term->name;
+                }
+            }
+            $result[] = array(
+                'value' => $value,
+                'label' => $label,
+            );
+        }
+
+        usort($result, function($a, $b) {
+            return strcasecmp($a['label'], $b['label']);
+        });
+
+        return $result;
+    }
+
+    /**
+     * Find variations matching the filter.
+     *
+     * @param int[]  $category_ids
+     * @param string $attribute_key
+     * @param string $attribute_value
+     * @return array ['count' => int, 'ids' => int[], 'sample' => array[]]
+     */
+    public function find_matching_variations($category_ids, $attribute_key, $attribute_value) {
+        global $wpdb;
+
+        $category_ids   = array_filter(array_map('intval', (array) $category_ids), function($v) { return $v > 0; });
+        $attribute_key  = sanitize_text_field($attribute_key);
+        $attribute_value = sanitize_text_field($attribute_value);
+
+        if (empty($category_ids) || $attribute_key === '' || $attribute_value === '') {
+            return array('count' => 0, 'ids' => array(), 'sample' => array());
+        }
+
+        $meta_key     = 'attribute_' . wc_sanitize_taxonomy_name($attribute_key);
+        $placeholders = implode(',', array_fill(0, count($category_ids), '%d'));
+
+        $sql = "
+            SELECT DISTINCT v.ID
+              FROM {$wpdb->posts} v
+              INNER JOIN {$wpdb->posts} p ON p.ID = v.post_parent
+              INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = v.ID
+              INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
+              INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+             WHERE v.post_type = 'product_variation'
+               AND v.post_status IN ('publish','private')
+               AND p.post_type = 'product'
+               AND tt.taxonomy = 'product_cat'
+               AND tt.term_id IN ($placeholders)
+               AND pm.meta_key = %s
+               AND pm.meta_value = %s
+        ";
+
+        $params = array_merge($category_ids, array($meta_key, $attribute_value));
+        $ids    = $wpdb->get_col($wpdb->prepare($sql, ...$params));
+        $ids    = $ids ? array_map('intval', $ids) : array();
+
+        // Build a sample (up to 20).
+        $sample = array();
+        $slice  = array_slice($ids, 0, 20);
+        foreach ($slice as $id) {
+            $variation = wc_get_product($id);
+            if (!$variation) {
+                continue;
+            }
+            $parent_id   = $variation->get_parent_id();
+            $parent_name = '';
+            if ($parent_id) {
+                $parent = wc_get_product($parent_id);
+                if ($parent) {
+                    $parent_name = $parent->get_name();
+                }
+            }
+            $sample[] = array(
+                'id'            => $id,
+                'sku'           => $variation->get_sku(),
+                'name'          => $variation->get_name(),
+                'parent_name'   => $parent_name,
+                'current_price' => $variation->get_regular_price(),
+            );
+        }
+
+        return array(
+            'count'  => count($ids),
+            'ids'    => $ids,
+            'sample' => $sample,
+        );
+    }
+
+    /**
+     * Apply new regular price to a list of variation IDs.
+     *
+     * @param int[]  $variation_ids
+     * @param string $new_price
+     * @return array ['updated','failed','failed_ids','message']
+     */
+    public function apply_variant_price($variation_ids, $new_price) {
+        $variation_ids = array_filter(array_map('intval', (array) $variation_ids), function($v) { return $v > 0; });
+
+        // Validate price.
+        if (!is_numeric($new_price) || (float) $new_price < 0) {
+            return array(
+                'updated'    => 0,
+                'failed'     => 0,
+                'failed_ids' => array(),
+                'message'    => __('Please enter a valid new price', 'studiou-wc-product-cat-manage'),
+                'error'      => true,
+            );
+        }
+
+        $formatted = wc_format_decimal($new_price);
+
+        $updated     = 0;
+        $failed      = 0;
+        $failed_ids  = array();
+        $parent_ids  = array();
+
+        foreach ($variation_ids as $id) {
+            try {
+                $variation = wc_get_product($id);
+                if (!$variation || !$variation->is_type('variation')) {
+                    $failed++;
+                    $failed_ids[] = $id;
+                    error_log('STUDIOU WC UPVP: Variation ' . $id . ' not found or not a variation');
+                    continue;
+                }
+                $variation->set_regular_price($formatted);
+                $variation->save();
+                $updated++;
+                $parent_id = $variation->get_parent_id();
+                if ($parent_id) {
+                    $parent_ids[$parent_id] = true;
+                }
+            } catch (Exception $e) {
+                $failed++;
+                $failed_ids[] = $id;
+                error_log('STUDIOU WC UPVP: Exception updating variation ' . $id . ' - ' . $e->getMessage());
+            }
+        }
+
+        // Re-sync parent variable products so price-range cache reflects the new prices.
+        if (!empty($parent_ids) && class_exists('WC_Product_Variable')) {
+            foreach (array_keys($parent_ids) as $parent_id) {
+                try {
+                    WC_Product_Variable::sync($parent_id);
+                    if (function_exists('wc_delete_product_transients')) {
+                        wc_delete_product_transients($parent_id);
+                    }
+                } catch (Exception $e) {
+                    error_log('STUDIOU WC UPVP: Failed to sync parent ' . $parent_id . ' - ' . $e->getMessage());
+                }
+            }
+        }
+
+        $message = sprintf(
+            __('Successfully updated %d variations.', 'studiou-wc-product-cat-manage'),
+            $updated
+        );
+        if ($failed > 0) {
+            $message .= ' ' . sprintf(
+                __('Failed to update %d variations.', 'studiou-wc-product-cat-manage'),
+                $failed
+            );
+        }
+
+        return array(
+            'updated'    => $updated,
+            'failed'     => $failed,
+            'failed_ids' => $failed_ids,
+            'message'    => $message,
+        );
+    }
+
+    /**
+     * Common AJAX preflight: nonce + capability + output buffer setup.
+     * Returns false on failure (after sending JSON error), true on success.
+     */
+    private function upvp_preflight() {
+        while (ob_get_level()) {
+            ob_end_clean();
+        }
+        ob_start();
+
+        if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')) {
+            ob_clean();
+            wp_send_json_error(array('message' => __('Security check failed', 'studiou-wc-product-cat-manage')));
+            return false;
+        }
+        if (!current_user_can('manage_woocommerce')) {
+            ob_clean();
+            wp_send_json_error(array('message' => __('You do not have sufficient permissions', 'studiou-wc-product-cat-manage')));
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * AJAX: list variation attribute heads.
+     */
+    public function handle_upvp_get_attributes_ajax() {
+        if (!$this->upvp_preflight()) return;
+
+        $original_error_reporting = error_reporting();
+        error_reporting(0);
+        try {
+            $heads = $this->get_variation_attribute_heads();
+            ob_clean();
+            wp_send_json_success(array('attributes' => $heads));
+        } catch (Exception $e) {
+            error_log('STUDIOU WC UPVP: get_attributes failed - ' . $e->getMessage());
+            ob_clean();
+            wp_send_json_error(array('message' => $e->getMessage()));
+        } finally {
+            error_reporting($original_error_reporting);
+            ob_end_clean();
+        }
+    }
+
+    /**
+     * AJAX: list values for the chosen attribute (scoped to selected categories).
+     */
+    public function handle_upvp_get_attribute_values_ajax() {
+        if (!$this->upvp_preflight()) return;
+
+        $original_error_reporting = error_reporting();
+        error_reporting(0);
+        try {
+            $attribute_key = isset($_POST['attribute_key']) ? sanitize_text_field(wp_unslash($_POST['attribute_key'])) : '';
+            $category_ids  = isset($_POST['category_ids']) ? array_map('intval', (array) $_POST['category_ids']) : array();
+
+            if ($attribute_key === '') {
+                ob_clean();
+                wp_send_json_error(array('message' => __('Please select an attribute', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+
+            $values = $this->get_variation_attribute_values($attribute_key, $category_ids);
+            ob_clean();
+            wp_send_json_success(array('values' => $values));
+        } catch (Exception $e) {
+            error_log('STUDIOU WC UPVP: get_attribute_values failed - ' . $e->getMessage());
+            ob_clean();
+            wp_send_json_error(array('message' => $e->getMessage()));
+        } finally {
+            error_reporting($original_error_reporting);
+            ob_end_clean();
+        }
+    }
+
+    /**
+     * AJAX: count + sample of variations matching the current filter.
+     */
+    public function handle_upvp_count_ajax() {
+        if (!$this->upvp_preflight()) return;
+
+        $original_error_reporting = error_reporting();
+        error_reporting(0);
+        try {
+            $category_ids    = isset($_POST['category_ids']) ? array_map('intval', (array) $_POST['category_ids']) : array();
+            $attribute_key   = isset($_POST['attribute_key']) ? sanitize_text_field(wp_unslash($_POST['attribute_key'])) : '';
+            $attribute_value = isset($_POST['attribute_value']) ? sanitize_text_field(wp_unslash($_POST['attribute_value'])) : '';
+
+            if (empty($category_ids)) {
+                ob_clean();
+                wp_send_json_error(array('message' => __('Please select at least one category', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+            if ($attribute_key === '') {
+                ob_clean();
+                wp_send_json_error(array('message' => __('Please select an attribute', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+            if ($attribute_value === '') {
+                ob_clean();
+                wp_send_json_error(array('message' => __('Please select an attribute value', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+
+            $result = $this->find_matching_variations($category_ids, $attribute_key, $attribute_value);
+
+            ob_clean();
+            wp_send_json_success(array(
+                'count'  => $result['count'],
+                'sample' => $result['sample'],
+            ));
+        } catch (Exception $e) {
+            error_log('STUDIOU WC UPVP: count failed - ' . $e->getMessage());
+            ob_clean();
+            wp_send_json_error(array('message' => $e->getMessage()));
+        } finally {
+            error_reporting($original_error_reporting);
+            ob_end_clean();
+        }
+    }
+
+    /**
+     * AJAX: apply new regular price to all matching variations.
+     */
+    public function handle_upvp_apply_ajax() {
+        if (!$this->upvp_preflight()) return;
+
+        $original_error_reporting = error_reporting();
+        error_reporting(0);
+        try {
+            $category_ids    = isset($_POST['category_ids']) ? array_map('intval', (array) $_POST['category_ids']) : array();
+            $attribute_key   = isset($_POST['attribute_key']) ? sanitize_text_field(wp_unslash($_POST['attribute_key'])) : '';
+            $attribute_value = isset($_POST['attribute_value']) ? sanitize_text_field(wp_unslash($_POST['attribute_value'])) : '';
+            $new_price       = isset($_POST['new_price']) ? sanitize_text_field(wp_unslash($_POST['new_price'])) : '';
+
+            if (empty($category_ids)) {
+                ob_clean();
+                wp_send_json_error(array('message' => __('Please select at least one category', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+            if ($attribute_key === '' || $attribute_value === '') {
+                ob_clean();
+                wp_send_json_error(array('message' => __('Please select an attribute value', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+            if ($new_price === '' || !is_numeric($new_price) || (float) $new_price < 0) {
+                ob_clean();
+                wp_send_json_error(array('message' => __('Please enter a valid new price', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+
+            $matching = $this->find_matching_variations($category_ids, $attribute_key, $attribute_value);
+            if ($matching['count'] === 0) {
+                ob_clean();
+                wp_send_json_error(array('message' => __('No variations match the current filter.', 'studiou-wc-product-cat-manage')));
+                return;
+            }
+
+            $result = $this->apply_variant_price($matching['ids'], $new_price);
+
+            ob_clean();
+            wp_send_json_success(array(
+                'updated'    => $result['updated'],
+                'failed'     => $result['failed'],
+                'failed_ids' => $result['failed_ids'],
+                'message'    => $result['message'],
+            ));
+        } catch (Exception $e) {
+            error_log('STUDIOU WC UPVP: apply failed - ' . $e->getMessage());
+            ob_clean();
+            wp_send_json_error(array('message' => $e->getMessage()));
+        } finally {
+            error_reporting($original_error_reporting);
+            ob_end_clean();
+        }
+    }
 }

binární
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage-cs_CZ.mo


+ 99 - 2
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage-cs_CZ.po

@@ -3,7 +3,7 @@
 # This file is distributed under the same license as the QDR - Studiou WC Export/Import Product Categories package.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Export/Import Product Categories 1.5.2\n"
+"Project-Id-Version: QDR - Studiou WC Export/Import Product Categories 1.5.3\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com/plugins/studiou-wc-product-"
 "cat-manage\n"
 "POT-Creation-Date: 2026-04-01 12:00+0000\n"
@@ -991,4 +991,101 @@ msgstr "Operace odstranění nepřiřazených médií trvale smaže všechny med
 
 #: includes/class-studiou-wc-product-cat-manage-manipulator.php
 msgid "Found %d unused uncategorized media files to delete"
-msgstr "Nalezeno %d nepoužívaných nekategorizovaných mediálních souborů ke smazání"
+msgstr "Nalezeno %d nepoužívaných nekategorizovaných mediálních souborů ke smazání"
+
+# UPVP — Update Product Variant Prices (v1.5.3)
+#: views/manipulations.php
+msgid "Update product variant prices"
+msgstr "Aktualizace cen variant produktů"
+
+#: views/manipulations.php
+msgid "Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected."
+msgstr "Hromadná aktualizace běžné ceny variant produktů odpovídajících vybraným kategoriím a zvolené vlastnosti varianty. Mění se pouze záznamy produktů — existující objednávky a obsah košíku nejsou dotčeny."
+
+#: views/manipulations.php
+msgid "Variant attribute"
+msgstr "Vlastnost varianty"
+
+#: views/manipulations.php
+msgid "Variant attribute value"
+msgstr "Hodnota vlastnosti varianty"
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "-- Select attribute --"
+msgstr "-- Vyberte vlastnost --"
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "-- Select value --"
+msgstr "-- Vyberte hodnotu --"
+
+#: views/manipulations.php
+msgid "New Price"
+msgstr "Nová cena"
+
+#: views/manipulations.php
+msgid "Dry Run"
+msgstr "Zkušební běh"
+
+#: views/manipulations.php
+msgid "Apply"
+msgstr "Provést"
+
+#: views/manipulations.php
+msgid "Affected variations"
+msgstr "Dotčené varianty"
+
+#: views/manipulations.php
+msgid "Results"
+msgstr "Výsledky"
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "No variations match the current filter."
+msgstr "Žádné varianty neodpovídají vybranému filtru."
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Successfully updated %d variations."
+msgstr "Úspěšně aktualizováno %d variant."
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Failed to update %d variations."
+msgstr "Nepodařilo se aktualizovat %d variant."
+
+#: studiou-wc-product-cat-manage.php
+msgid "Are you sure you want to update %d variations?"
+msgstr "Opravdu chcete aktualizovat %d variant?"
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Please select an attribute"
+msgstr "Vyberte prosím vlastnost"
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Please select an attribute value"
+msgstr "Vyberte prosím hodnotu vlastnosti"
+
+#: studiou-wc-product-cat-manage.php includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Please enter a valid new price"
+msgstr "Zadejte prosím platnou novou cenu"
+
+#: studiou-wc-product-cat-manage.php
+msgid "Loading attributes..."
+msgstr "Načítání vlastností..."
+
+#: studiou-wc-product-cat-manage.php
+msgid "Loading values..."
+msgstr "Načítání hodnot..."
+
+#: studiou-wc-product-cat-manage.php
+msgid "Dry run complete: %d variations would be updated to %s."
+msgstr "Zkušební běh dokončen: %d variant bude aktualizováno na %s."
+
+#: studiou-wc-product-cat-manage.php
+msgid "Variant prices updated successfully"
+msgstr "Ceny variant úspěšně aktualizovány"
+
+#: studiou-wc-product-cat-manage.php
+msgid "Variant prices update error"
+msgstr "Chyba aktualizace cen variant"
+
+#: views/manipulations.php
+msgid "The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified."
+msgstr "Operace aktualizace cen variant mění pouze běžnou cenu variant produktů. Položky objednávek, obsah košíku ani historická data objednávek nejsou nikdy měněny."

+ 98 - 1
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage.po

@@ -2,7 +2,7 @@
 # This file is distributed under the same license as the QDR - Studiou WC Export/Import Product Categories package.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Export/Import Product Categories 1.5.2\n"
+"Project-Id-Version: QDR - Studiou WC Export/Import Product Categories 1.5.3\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com/plugins/studiou-wc-product-cat-manage\n"
 "POT-Creation-Date: 2026-04-01 12:00+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -957,4 +957,101 @@ msgstr ""
 
 #: includes/class-studiou-wc-product-cat-manage-manipulator.php
 msgid "Found %d unused uncategorized media files to delete"
+msgstr ""
+
+# UPVP — Update Product Variant Prices (v1.5.3)
+#: views/manipulations.php
+msgid "Update product variant prices"
+msgstr ""
+
+#: views/manipulations.php
+msgid "Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected."
+msgstr ""
+
+#: views/manipulations.php
+msgid "Variant attribute"
+msgstr ""
+
+#: views/manipulations.php
+msgid "Variant attribute value"
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "-- Select attribute --"
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "-- Select value --"
+msgstr ""
+
+#: views/manipulations.php
+msgid "New Price"
+msgstr ""
+
+#: views/manipulations.php
+msgid "Dry Run"
+msgstr ""
+
+#: views/manipulations.php
+msgid "Apply"
+msgstr ""
+
+#: views/manipulations.php
+msgid "Affected variations"
+msgstr ""
+
+#: views/manipulations.php
+msgid "Results"
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "No variations match the current filter."
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Successfully updated %d variations."
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Failed to update %d variations."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Are you sure you want to update %d variations?"
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Please select an attribute"
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Please select an attribute value"
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Please enter a valid new price"
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Loading attributes..."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Loading values..."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Dry run complete: %d variations would be updated to %s."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Variant prices updated successfully"
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Variant prices update error"
+msgstr ""
+
+#: views/manipulations.php
+msgid "The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified."
 msgstr ""

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

@@ -1,6 +1,6 @@
 # QDR - Studiou WC Export/Import Product Categories
 
-**Version: 1.5.2**
+**Version: 1.5.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.
 
@@ -27,6 +27,7 @@ The plugin provides a complete solution for bulk product and category management
 - **Delete products in category** - permanently delete all products and variants from selected categories
 - **Clear Media Categories** - permanently delete all media files from selected media categories (NEW in v1.5)
 - **Remove Not Assigned Media** - permanently delete all unused uncategorized media files (NEW in v1.5.2)
+- **Update product variant prices** - bulk-update regular price of variations by category + attribute filter (NEW in v1.5.3)
 
 ### Product Management (NEW in v1.4)
 - **Export products to CSV** - Export variable products and variations by category selection
@@ -265,6 +266,29 @@ The plugin uses CSV files with the following columns for products:
 - A real-time progress bar shows the deletion progress
 - The page will automatically reload after successful deletion to update the count
 
+#### Update Product Variant Prices (NEW in v1.5.3)
+
+1. Go to Products > Category Manipulations
+2. Scroll to the "Update product variant prices" section
+3. Select one or more product categories using the checkboxes
+   - Use "Check All" to select all categories
+   - Use "Uncheck All" to deselect all categories
+4. Pick a **Variant attribute** from the dropdown (heads such as Color, Size — populated from variation-eligible attributes in the store)
+5. Pick a **Variant attribute value** from the dropdown (values are scoped to the chosen attribute and selected categories)
+6. Enter the **New Price**
+7. Watch the **Affected variations** label — it updates live as you change filters
+8. Click **Dry Run** to preview the count and a sample of variations that would be updated (no DB writes)
+9. Click **Apply** to commit the new regular price; you will be asked to confirm
+10. Review the results showing updated / failed counts and any failed variation IDs
+
+**Important Notes about Update Product Variant Prices:**
+- The operation modifies only the `regular_price` of `product_variation` records
+- **Order line items, cart contents, and historical order data are never modified** — past orders keep their original prices
+- All updates go through the WooCommerce API (`set_regular_price` + `WC_Product_Variable::sync` on the parent variable products)
+- Parent variable products are re-synced after the batch so the storefront price-range cache stays correct
+- Always run **Dry Run** first to verify the affected count
+- If a variation belongs to a non-variable parent (orphan), it is counted as failed and listed in the result
+
 ## Sample CSVs
 
 ### Sample Product Category CSV
@@ -305,6 +329,8 @@ After activation, the plugin adds the following menu items under **Products** in
   - Delete products in category
   - Clear Media Categories (NEW in v1.5)
   - Remove Not Assigned Media (NEW in v1.5.2)
+  - Update product variant prices (NEW in v1.5.3)
+  - Update product variant prices (NEW in v1.5.3)
 
 ### Product Management (NEW in v1.4)
 - **Product Import** - Batch import variable products and variations
@@ -325,6 +351,19 @@ For support, please visit [https://www.quadarax.com/plugins/studiou-wc-product-c
 
 ## Changelog
 
+### Version 1.5.3
+- **NEW: Update product variant prices** - bulk-update regular price of product variations
+  - Filter by selected product categories (with Check All / Uncheck All)
+  - Variant attribute combobox (heads), populated from variation-eligible attributes
+  - Variant value combobox, dynamically scoped to the chosen attribute and selected categories
+  - New Price textbox with numeric validation
+  - Dry Run button shows affected count + sample preview, no DB writes
+  - Apply button commits via WooCommerce API (`set_regular_price` + `WC_Product_Variable::sync`)
+  - Live affected-count label updates on every filter change
+- 4 new AJAX handlers: `studiou_wcpcm_upvp_get_attributes`, `studiou_wcpcm_upvp_get_attribute_values`, `studiou_wcpcm_upvp_count`, `studiou_wcpcm_upvp_apply`
+- Operation never touches order items, cart sessions, or historical order data
+- Added 23 new translation strings (English/Czech)
+
 ### Version 1.5.2
 - **NEW: Remove Not Assigned Media** - permanently delete all unused uncategorized media files
   - Button with count of unused uncategorized media

+ 12 - 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.5.2
+ * Version: 1.5.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.5.2');
+if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.5.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__));
@@ -281,6 +281,16 @@ class Studiou_WC_Product_Cat_Manage {
                     'removeUnassignedSuccess' => __('Remove unassigned media operation successful', 'studiou-wc-product-cat-manage'),
                     'removeUnassignedError' => __('Remove unassigned media operation error', 'studiou-wc-product-cat-manage'),
                     'noUnassignedMedia' => __('No unused uncategorized media files found', 'studiou-wc-product-cat-manage'),
+                    'upvpSelectAttribute' => __('-- Select attribute --', 'studiou-wc-product-cat-manage'),
+                    'upvpSelectValue' => __('-- Select value --', 'studiou-wc-product-cat-manage'),
+                    'upvpInvalidPrice' => __('Please enter a valid new price', 'studiou-wc-product-cat-manage'),
+                    'upvpLoadingAttributes' => __('Loading attributes...', 'studiou-wc-product-cat-manage'),
+                    'upvpLoadingValues' => __('Loading values...', 'studiou-wc-product-cat-manage'),
+                    'upvpDryRunComplete' => __('Dry run complete: %d variations would be updated to %s.', 'studiou-wc-product-cat-manage'),
+                    'upvpApplySuccess' => __('Variant prices updated successfully', 'studiou-wc-product-cat-manage'),
+                    'upvpApplyError' => __('Variant prices update error', 'studiou-wc-product-cat-manage'),
+                    'upvpNoMatches' => __('No variations match the current filter.', 'studiou-wc-product-cat-manage'),
+                    'upvpConfirmApply' => __('Are you sure you want to update %d variations?', 'studiou-wc-product-cat-manage'),
                 )
             ));
             

+ 89 - 0
studiou-wc-product-cat-manage/views/manipulations.php

@@ -301,6 +301,94 @@ $unassigned_media_count = $manipulator->get_unassigned_unused_media_count();
         </div>
     </div>
 
+    <!-- Update Product Variant Prices Operation -->
+    <div class="studiou-wcpcm-card">
+        <h2><?php echo esc_html__('Update product variant prices', 'studiou-wc-product-cat-manage'); ?></h2>
+
+        <p><?php echo esc_html__('Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected.', 'studiou-wc-product-cat-manage'); ?></p>
+
+        <form id="studiou-wcpcm-upvp-form" method="post" onsubmit="return false;">
+            <table class="form-table">
+                <tr>
+                    <th scope="row">
+                        <label for="upvp_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                    </th>
+                    <td>
+                        <div class="studiou-wcpcm-categories-list-container">
+                            <div class="studiou-wcpcm-list-actions">
+                                <button type="button" id="studiou-wcpcm-upvp-check-all" class="button">
+                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                                </button>
+                                <button type="button" id="studiou-wcpcm-upvp-uncheck-all" class="button">
+                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                                </button>
+                            </div>
+                            <div class="studiou-wcpcm-categories-list" id="upvp_categories_list">
+                                <?php foreach ($categories as $category): ?>
+                                    <label class="studiou-wcpcm-category-item">
+                                        <input type="checkbox" name="upvp_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                        <span><?php echo esc_html($category['display_name']); ?></span>
+                                    </label>
+                                <?php endforeach; ?>
+                            </div>
+                        </div>
+                    </td>
+                </tr>
+                <tr>
+                    <th scope="row">
+                        <label for="upvp_attribute"><?php echo esc_html__('Variant attribute', 'studiou-wc-product-cat-manage'); ?></label>
+                    </th>
+                    <td>
+                        <select id="upvp_attribute" name="upvp_attribute" class="regular-text">
+                            <option value=""><?php echo esc_html__('-- Select attribute --', 'studiou-wc-product-cat-manage'); ?></option>
+                        </select>
+                    </td>
+                </tr>
+                <tr>
+                    <th scope="row">
+                        <label for="upvp_attribute_value"><?php echo esc_html__('Variant attribute value', 'studiou-wc-product-cat-manage'); ?></label>
+                    </th>
+                    <td>
+                        <select id="upvp_attribute_value" name="upvp_attribute_value" class="regular-text" disabled>
+                            <option value=""><?php echo esc_html__('-- Select value --', 'studiou-wc-product-cat-manage'); ?></option>
+                        </select>
+                    </td>
+                </tr>
+                <tr>
+                    <th scope="row">
+                        <label for="upvp_new_price"><?php echo esc_html__('New Price', 'studiou-wc-product-cat-manage'); ?></label>
+                    </th>
+                    <td>
+                        <input type="number" step="0.01" min="0" id="upvp_new_price" name="upvp_new_price" class="regular-text">
+                    </td>
+                </tr>
+                <tr>
+                    <th scope="row">
+                        <?php echo esc_html__('Affected variations', 'studiou-wc-product-cat-manage'); ?>
+                    </th>
+                    <td>
+                        <strong id="studiou-wcpcm-upvp-affected-count">0</strong>
+                    </td>
+                </tr>
+            </table>
+
+            <div class="submit-buttons">
+                <button type="button" id="studiou-wcpcm-upvp-dry-run-button" class="button">
+                    <?php echo esc_html__('Dry Run', 'studiou-wc-product-cat-manage'); ?>
+                </button>
+                <button type="button" id="studiou-wcpcm-upvp-apply-button" class="button button-primary">
+                    <?php echo esc_html__('Apply', 'studiou-wc-product-cat-manage'); ?>
+                </button>
+                <span class="spinner"></span>
+            </div>
+        </form>
+
+        <div class="studiou-wcpcm-upvp-results" style="display: none;">
+            <h3><?php echo esc_html__('Results', 'studiou-wc-product-cat-manage'); ?></h3>
+            <div id="studiou-wcpcm-upvp-message"></div>
+        </div>
+    </div>
+
     <!-- Important Notes -->
     <div class="studiou-wcpcm-card">
         <h2><?php echo esc_html__('Important Notes', 'studiou-wc-product-cat-manage'); ?></h2>
@@ -313,6 +401,7 @@ $unassigned_media_count = $manipulator->get_unassigned_unused_media_count();
             <li><strong><?php echo esc_html__('The delete products operation will permanently delete all products and variants from the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
             <li><strong><?php echo esc_html__('The clear media categories operation will permanently delete all media files from the selected media categories and remove them from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
             <li><strong><?php echo esc_html__('The remove not assigned media operation will permanently delete all media files that are not categorized and not used anywhere. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
+            <li><?php echo esc_html__('The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified.', 'studiou-wc-product-cat-manage'); ?></li>
         </ul>
     </div>
 </div>