Browse Source

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 2 months ago
parent
commit
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.
 **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
 ## Requirements
 
 
@@ -53,6 +53,7 @@ The plugin follows a modular class-based architecture:
   - Delete products in category (permanently deletes products and variants)
   - Delete products in category (permanently deletes products and variants)
   - Clear Media Categories (permanently deletes media files from media categories) (NEW in v1.5)
   - 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)
   - 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
   - Uses direct database queries for reliable product detection
   - Batch processing for delete operations (10 items per batch)
   - Batch processing for delete operations (10 items per batch)
   - Auto-detects media category taxonomy (any custom taxonomy registered for attachments)
   - 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)
   - Delete products in category (checkbox list + "Delete" button + progress bar)
   - Clear Media Categories (checkbox list + "Delete" button + progress bar) (NEW in v1.5)
   - 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)
   - 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)
 - **product-import.php** - Product import page UI (NEW in v1.4)
   - File upload for CSV import
   - File upload for CSV import
   - Progress bar for import operations
   - Progress bar for import operations
@@ -210,6 +212,7 @@ All pages are added under **Products** menu:
   - Delete products in category
   - Delete products in category
   - Clear Media Categories (NEW in v1.5)
   - Clear Media Categories (NEW in v1.5)
   - Remove Not Assigned Media (NEW in v1.5.2)
   - 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 Import (slug: `studiou-product-import`)** - NEW in v1.4
 - **Product Export (slug: `studiou-product-export`)** - 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 physical files and attachment posts are both deleted
 - Verify button count updates after page reload
 - 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
 ### General
 - Verify translations are properly loaded (English/Czech)
 - Verify translations are properly loaded (English/Czech)
 - Verify progress bars update correctly
 - 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)
 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)
 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)
 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)
 ### 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:
 All AJAX handlers include:
 - Nonce verification (`studiou-wcpcm-nonce`)
 - 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-clear-media-results ul li:last-child,
 .studiou-wcpcm-remove-unassigned-media-results ul li:last-child {
 .studiou-wcpcm-remove-unassigned-media-results ul li:last-child {
     border-bottom: none;
     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
     // Initialize admin scripts
     $(document).ready(function() {
     $(document).ready(function() {
         // Debug: Log that script is loaded
         // 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') {
         if (typeof studiouWcpcm === 'undefined') {
             console.error('STUDIOU WC: studiouWcpcm object is NOT defined - AJAX will not work');
             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('Sample download', '#studiou-wcpcm-download-sample', initSampleDownload);
         safeInit('Product import form', '#studiou-wcpcm-product-import-form', initProductImportForm);
         safeInit('Product import form', '#studiou-wcpcm-product-import-form', initProductImportForm);
         safeInit('Product export form', '#studiou-wcpcm-product-export-form', initProductExportForm);
         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 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: 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: 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);
 })(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_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', 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_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 a test AJAX handler to verify AJAX is working
         add_action('wp_ajax_studiou_wcpcm_test', array($this, 'handle_test_ajax'));
         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();
             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
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.
 # This file is distributed under the same license as the QDR - Studiou WC Export/Import Product Categories package.
 msgid ""
 msgid ""
 msgstr ""
 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-"
 "Report-Msgid-Bugs-To: https://www.quadarax.com/plugins/studiou-wc-product-"
 "cat-manage\n"
 "cat-manage\n"
 "POT-Creation-Date: 2026-04-01 12:00+0000\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
 #: includes/class-studiou-wc-product-cat-manage-manipulator.php
 msgid "Found %d unused uncategorized media files to delete"
 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.
 # This file is distributed under the same license as the QDR - Studiou WC Export/Import Product Categories package.
 msgid ""
 msgid ""
 msgstr ""
 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"
 "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"
 "POT-Creation-Date: 2026-04-01 12:00+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\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
 #: includes/class-studiou-wc-product-cat-manage-manipulator.php
 msgid "Found %d unused uncategorized media files to delete"
 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 ""
 msgstr ""

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

@@ -1,6 +1,6 @@
 # QDR - Studiou WC Export/Import Product Categories
 # 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.
 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
 - **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)
 - **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)
 - **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)
 ### Product Management (NEW in v1.4)
 - **Export products to CSV** - Export variable products and variations by category selection
 - **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
 - A real-time progress bar shows the deletion progress
 - The page will automatically reload after successful deletion to update the count
 - 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 CSVs
 
 
 ### Sample Product Category CSV
 ### Sample Product Category CSV
@@ -305,6 +329,8 @@ After activation, the plugin adds the following menu items under **Products** in
   - Delete products in category
   - Delete products in category
   - Clear Media Categories (NEW in v1.5)
   - Clear Media Categories (NEW in v1.5)
   - Remove Not Assigned Media (NEW in v1.5.2)
   - 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 Management (NEW in v1.4)
 - **Product Import** - Batch import variable products and variations
 - **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
 ## 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
 ### Version 1.5.2
 - **NEW: Remove Not Assigned Media** - permanently delete all unused uncategorized media files
 - **NEW: Remove Not Assigned Media** - permanently delete all unused uncategorized media files
   - Button with count of unused uncategorized media
   - 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 Name: QDR - Studiou WC Export/Import Product Categories
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-product-cat-manage
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-product-cat-manage
  * Description: Allows batch import and export WooCommerce product categories and products
  * 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 at least: 6.8.1
  * Requires PHP: 8.2
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
  * Author: Dalibor Votruba
@@ -23,7 +23,7 @@ if (!defined('WPINC')) {
 }
 }
 
 
 // Define plugin constants
 // 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_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_URL')) define('STUDIOU_WCPCM_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_BASENAME')) define('STUDIOU_WCPCM_PLUGIN_BASENAME', plugin_basename(__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'),
                     'removeUnassignedSuccess' => __('Remove unassigned media operation successful', 'studiou-wc-product-cat-manage'),
                     'removeUnassignedError' => __('Remove unassigned media operation error', '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'),
                     '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>
     </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 -->
     <!-- Important Notes -->
     <div class="studiou-wcpcm-card">
     <div class="studiou-wcpcm-card">
         <h2><?php echo esc_html__('Important Notes', 'studiou-wc-product-cat-manage'); ?></h2>
         <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 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 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><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>
         </ul>
     </div>
     </div>
 </div>
 </div>