Bläddra i källkod

Merge branch '1.5.0.studiou-wc-product-cat-manage'

Dalibor Votruba 2 månader sedan
förälder
incheckning
a663ef0136

+ 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);

+ 605 - 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,605 @@ 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: existing variation postmeta (attribute_*).
+        $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;
+                }
+                $seen[$slug] = true;
+                $heads[] = array(
+                    'key'         => $slug,
+                    'label'       => wc_attribute_label($slug),
+                    'is_taxonomy' => (strpos($slug, 'pa_') === 0) || taxonomy_exists($slug),
+                );
+            }
+        }
+
+        // Source 3: variable parent products' _product_attributes postmeta.
+        // Catches attributes flagged "Used for variations" on the product's
+        // Attributes tab, even when (a) the attribute is a custom non-global
+        // entry, or (b) no variations have been generated yet.
+        $sql_parents = "
+            SELECT pm.meta_value
+              FROM {$wpdb->postmeta} pm
+              INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
+             WHERE p.post_type = 'product'
+               AND p.post_status IN ('publish','private','draft')
+               AND pm.meta_key = '_product_attributes'
+        ";
+        $serialized_rows = $wpdb->get_col($sql_parents);
+        if (!empty($serialized_rows)) {
+            foreach ($serialized_rows as $serialized) {
+                $attrs = maybe_unserialize($serialized);
+                if (!is_array($attrs)) {
+                    continue;
+                }
+                foreach ($attrs as $attr_key => $attr_data) {
+                    if (!is_array($attr_data) || empty($attr_data['is_variation'])) {
+                        continue;
+                    }
+                    $is_taxonomy = !empty($attr_data['is_taxonomy']);
+                    $name = isset($attr_data['name']) ? $attr_data['name'] : $attr_key;
+                    $key  = $is_taxonomy ? $name : sanitize_title($name);
+                    if ($key === '' || isset($seen[$key])) {
+                        continue;
+                    }
+                    $seen[$key] = true;
+                    $heads[] = array(
+                        'key'         => $key,
+                        'label'       => $is_taxonomy ? wc_attribute_label($key) : $name,
+                        'is_taxonomy' => $is_taxonomy,
+                    );
+                }
+            }
+        }
+
+        // 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));
+
+        $is_taxonomy = (strpos($attribute_key, 'pa_') === 0) || taxonomy_exists($attribute_key);
+
+        // Fallback: if SQL returned no values but the attribute exists on a
+        // variable parent's _product_attributes (e.g. variations not generated
+        // yet, or custom attribute), parse pipe-separated values from there.
+        if (empty($values)) {
+            $sanitized_key = $is_taxonomy
+                ? (strpos($attribute_key, 'pa_') === 0 ? $attribute_key : 'pa_' . $attribute_key)
+                : sanitize_title($attribute_key);
+
+            $sql_parents = "
+                SELECT pm.meta_value
+                  FROM {$wpdb->postmeta} pm
+                  INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
+                 WHERE p.post_type = 'product'
+                   AND p.post_status IN ('publish','private','draft')
+                   AND pm.meta_key = '_product_attributes'
+            ";
+            $rows = $wpdb->get_col($sql_parents);
+            $values = array();
+            if (!empty($rows)) {
+                foreach ($rows as $serialized) {
+                    $attrs = maybe_unserialize($serialized);
+                    if (!is_array($attrs) || !isset($attrs[$sanitized_key])) {
+                        continue;
+                    }
+                    $entry = $attrs[$sanitized_key];
+                    if (empty($entry['is_variation'])) {
+                        continue;
+                    }
+                    if ($is_taxonomy) {
+                        // Taxonomy custom values are stored as term IDs in 'value'.
+                        $term_ids = !empty($entry['value']) ? wp_parse_id_list($entry['value']) : array();
+                        if (empty($term_ids)) {
+                            // Or the parent uses all terms — pull them from the taxonomy.
+                            $tax_name = $sanitized_key;
+                            $terms = get_terms(array('taxonomy' => $tax_name, 'hide_empty' => false));
+                            if (!is_wp_error($terms)) {
+                                foreach ($terms as $t) {
+                                    $values[] = $t->slug;
+                                }
+                            }
+                        } else {
+                            foreach ($term_ids as $tid) {
+                                $term = get_term($tid, $sanitized_key);
+                                if ($term && !is_wp_error($term)) {
+                                    $values[] = $term->slug;
+                                }
+                            }
+                        }
+                    } else {
+                        $raw = isset($entry['value']) ? (string) $entry['value'] : '';
+                        if ($raw !== '') {
+                            $parts = array_map('trim', explode('|', $raw));
+                            foreach ($parts as $p) {
+                                if ($p !== '') {
+                                    $values[] = $p;
+                                }
+                            }
+                        }
+                    }
+                }
+                $values = array_values(array_unique($values));
+            }
+        }
+
+        if (empty($values)) {
+            return array();
+        }
+
+        $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();
+        }
+    }
 }
 }

+ 430 - 0
studiou-wc-product-cat-manage/inplementation-plan-upvp.md

@@ -0,0 +1,430 @@
+# Implementation Plan — Update Product Variant Prices (UPVP)
+
+**Target version:** 1.5.3 (bump from current 1.5.2)
+**Scope:** Add a new section "Update product variant prices" to the existing **Category Manipulations** admin page.
+**Goal:** Allow admins to bulk-update the regular price of product *variations* by filtering on (a) one or more product categories and (b) a single variation attribute name + value pair. Provide a Dry Run preview, an Apply commit, and a live affected-count label. Operation must touch only `product_variation` records — never order items, carts, or order history.
+
+---
+
+## 1. Feature summary
+
+On the page **Products → Category Manipulations**, append a new card titled **"Update product variant prices"** placed **before** the *Important Notes* card and **after** the *Remove Not Assigned Media* card. It contains:
+
+1. **Categories checkbox list** — product-category tree (re-using the existing `studiou-wcpcm-categories-list-container` markup and styles). With `Check All` / `Uncheck All` buttons.
+2. **Variant attribute combobox** — populated with the list of attribute *names* (heads) that are used for variations across the WooCommerce store (global `pa_*` taxonomies + product-level custom attributes flagged "used for variations").
+3. **Variant value combobox** — populated dynamically based on the selected attribute (and scoped to the currently checked categories). Lists distinct values that exist on real variations.
+4. **New Price** textbox — numeric input (`type=number`, `step=0.01`, `min=0`); server normalises with `wc_format_decimal`.
+5. **Dry Run** button — counts and lists the variations that would be updated, without writing.
+6. **Apply** button — saves `set_regular_price()` to all matching variations via the WooCommerce API.
+7. **Affected-count label** — live counter; recalculated when filters change, after Dry Run, and after Apply.
+
+Operation only modifies `regular_price` on `product_variation` posts. Sale prices, order line items, cart sessions and historical order data are not touched.
+
+---
+
+## 2. Files to create / modify
+
+### 2.1 New files
+- *(none)* — all new logic fits into existing classes / views.
+
+### 2.2 Modified files
+
+| File | Change |
+| --- | --- |
+| `studiou-wc-product-cat-manage.php` | Bump `Version:` header and `STUDIOU_WCPCM_VERSION` to `1.5.3`. Extend the `i18n` map in `wp_localize_script` with new UPVP strings. |
+| `includes/class-studiou-wc-product-cat-manage-manipulator.php` | Add 4 new public AJAX handlers + 4 helper methods (see §3). Register the 4 actions in the constructor's `add_action(...)` block. |
+| `views/manipulations.php` | Append the new "Update product variant prices" card before the *Important Notes* card. |
+| `assets/css/admin.css` | Minor styling for the new section (results panel, affected-count label, preview table). |
+| `assets/js/admin.js` | Add `initUpdateVariantPricesForm()` wrapped in the existing `safeInit()` pattern. |
+| `languages/studiou-wc-product-cat-manage.pot` | Add new translation strings. |
+| `languages/studiou-wc-product-cat-manage-cs_CZ.po` | Add Czech translations. |
+| `languages/studiou-wc-product-cat-manage-cs_CZ.mo` | Regenerate (`msgfmt`). |
+| `readme.md` | Bump version, new section in *Features*, new usage section, **Version 1.5.3** changelog entry. |
+| `CLAUDE.md` | Bump *Current Version* to 1.5.3, add 4 new methods to manipulator bullets, add 4 new AJAX endpoints, add bullet under *Admin Menu Structure → Category Manipulations*. |
+| `directory-structure.txt` | No structural change — verify it still matches reality. |
+
+---
+
+## 3. Backend implementation
+
+All backend work goes into `class-studiou-wc-product-cat-manage-manipulator.php`. We extend the existing manipulator because UPVP is conceptually a category manipulation and shares helpers (`get_categories_with_counts`, AJAX boilerplate) with the existing manipulator. No new class.
+
+### 3.1 New AJAX action registrations
+
+Add inside `Studiou_WC_Product_Cat_Manage_Manipulator::__construct()`, alongside the existing `add_action('wp_ajax_studiou_wcpcm_*', ...)` lines:
+
+```php
+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'));
+```
+
+Each handler follows the established pattern in the file:
+- `while (ob_get_level()) ob_end_clean(); ob_start();`
+- `error_reporting(0)` save/restore.
+- `wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')`.
+- `current_user_can('manage_woocommerce')`.
+- `try { ... } catch(Exception $e) { ... } finally { restore + ob_end_clean(); }`.
+- Always-on error logging consistent with `STUDIOU WC MEDIA:` style — prefix `STUDIOU WC UPVP:`.
+
+### 3.2 New methods on `Studiou_WC_Product_Cat_Manage_Manipulator`
+
+```php
+/** Return list of attribute "heads" (names) usable for variations. */
+public function get_variation_attribute_heads(): array
+// Returns: [ ['key' => 'pa_color', 'label' => 'Color', 'is_taxonomy' => true], ... ]
+// Source 1: every wc_get_attribute_taxonomies() entry (global attributes) -> key = 'pa_<slug>'
+// Source 2: distinct meta_key starting with 'attribute_' on post_type='product_variation'
+//           (covers custom non-taxonomy attributes flagged for variations)
+//           -> key = substring after 'attribute_' (NOT prefixed with 'pa_')
+// De-duplicate by key, sort by human label asc.
+
+/** Return distinct values for a given attribute key, scoped to existing variations. */
+public function get_variation_attribute_values(string $attribute_key, array $category_ids = []): array
+// Returns: [ ['value' => 'red', 'label' => 'Red'], ... ] sorted by label.
+// SQL strategy:
+//   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'
+//     [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 tt.taxonomy = 'product_cat' AND tt.term_id IN (...)]
+//    AND pm.meta_key = 'attribute_<sanitized key>'
+//    AND pm.meta_value <> ''
+// For taxonomy attributes (pa_*): meta_value is the term slug -> resolve labels via get_term_by('slug', ..., 'pa_<key>').
+// For non-taxonomy: meta_value is already the label.
+// All input goes through wc_sanitize_taxonomy_name() / sanitize_text_field. Use $wpdb->prepare with %d/%s placeholders.
+
+/** Count + list variations that match the filter. */
+public function find_matching_variations(array $category_ids, string $attribute_key, string $attribute_value): array
+// Returns:
+//   [
+//     'count'  => int,
+//     'ids'    => int[],          // all matching variation IDs
+//     'sample' => array[]         // up to 20 entries:
+//                                 //   [ 'id', 'sku', 'name', 'parent_name', 'current_price' ]
+//   ]
+// Same JOIN as above but selecting v.ID. Reads sample with WC API (wc_get_product) capped at 20.
+// Uses $wpdb->prepare with implode(',', array_fill(0, count($category_ids), '%d')) for the IN list.
+
+/** Apply new price to a list of variation IDs. */
+public function apply_variant_price(array $variation_ids, string $new_price): array
+// Returns ['updated'=>int, 'failed'=>int, 'failed_ids'=>int[], 'message'=>string].
+// Steps for each ID:
+//   $variation = wc_get_product($id);
+//   if (!$variation || !$variation->is_type('variation')) { failed[] = id; continue; }
+//   $variation->set_regular_price(wc_format_decimal($new_price));
+//   $variation->save();
+// After loop: foreach (unique parent_id) WC_Product_Variable::sync($parent_id);
+//             wc_delete_product_transients($parent_id);
+// Validation guard at entry: must be numeric, >= 0; otherwise return error.
+```
+
+### 3.3 Handler responsibilities (inputs → outputs)
+
+| Handler | Input ($_POST) | `data` on success |
+| --- | --- | --- |
+| `handle_upvp_get_attributes_ajax` | `nonce` | `attributes: [{key,label,is_taxonomy}]` |
+| `handle_upvp_get_attribute_values_ajax` | `nonce`, `attribute_key`, `category_ids[]` (optional) | `values: [{value,label}]` |
+| `handle_upvp_count_ajax` | `nonce`, `category_ids[]`, `attribute_key`, `attribute_value` | `count, sample[]` |
+| `handle_upvp_apply_ajax` | `nonce`, `category_ids[]`, `attribute_key`, `attribute_value`, `new_price` | `updated, failed, failed_ids, message` |
+
+All array inputs run through `array_map('intval', ...)`; strings through `sanitize_text_field`; `attribute_key` additionally through `wc_sanitize_taxonomy_name`. The price stays raw, normalised in `apply_variant_price`.
+
+### 3.4 Edge cases handled
+
+- Empty category list → error "Please select at least one category".
+- Empty attribute key → error.
+- Empty attribute value → error.
+- Non-numeric / negative price → error before any DB write.
+- Variation belongs to a non-variable parent (orphan) → counted as `failed`.
+- Attribute key collision between a taxonomy attribute and a same-labelled custom attribute — disambiguated by storing the full key (`pa_color` vs `color`) in the combobox `value`, not the label.
+- Order-line price preservation — operation never touches `woocommerce_order_itemmeta`. Document this explicitly in the *Important Notes* card and changelog.
+- Parent variable product price-range cache — re-synced via `WC_Product_Variable::sync($parent_id)` after the batch so the storefront price range stays correct.
+
+---
+
+## 4. Frontend implementation
+
+### 4.1 View — `views/manipulations.php`
+
+Insert a new card immediately **before** the existing `<!-- Important Notes -->` card:
+
+```php
+<!-- 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">
+            <!-- 1. Category checkbox list -->
+            <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>
+
+            <!-- 2. Variant attribute combobox -->
+            <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>
+
+            <!-- 3. Variant value combobox -->
+            <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>
+
+            <!-- 4. New price -->
+            <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>
+
+            <!-- 6. Affected-count label -->
+            <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>
+
+        <!-- 5. Buttons -->
+        <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>
+```
+
+`$categories` is already populated above via `$manipulator->get_categories_with_counts()`.
+
+### 4.2 JS — `assets/js/admin.js`
+
+Add a single `initUpdateVariantPricesForm()` and register it inside the existing `$(document).ready()` block via the existing `safeInit()` wrapper:
+
+```js
+safeInit('upvpForm', function() {
+    if ($('#studiou-wcpcm-upvp-form').length) initUpdateVariantPricesForm();
+});
+```
+
+Logic:
+
+1. **On init** — request `studiou_wcpcm_upvp_get_attributes`; populate `#upvp_attribute` with `<option value="key" data-istax="0|1">label</option>`.
+2. **Check All / Uncheck All** — same pattern as the existing batch-description and delete-products handlers.
+3. **On `#upvp_attribute change`** — clear `#upvp_attribute_value`; if non-empty attribute, call `studiou_wcpcm_upvp_get_attribute_values` (passing currently-checked categories so the value list is scoped); enable the value combobox after population. Trigger affected-count refresh.
+4. **On any filter change** (categories checkboxes, attribute, value) — debounced (300 ms) call to `studiou_wcpcm_upvp_count`; update `#studiou-wcpcm-upvp-affected-count`. Disable both Dry Run and Apply buttons while a count request is in-flight or while the count is `0`.
+5. **Dry Run click** — call count endpoint with full filter; render a results panel containing a small `<table class="studiou-wcpcm-upvp-preview-table">` listing the sample variations: ID, SKU, Name, Parent, Current → New Price.
+6. **Apply click** — JS `confirm()` ("This will update X variations. Continue?"); on accept, POST `studiou_wcpcm_upvp_apply` with the same filter + new price; on success render summary (updated, failed, failed_ids[]) and refresh the affected-count.
+
+All XHR calls follow the existing AJAX boilerplate: spinner, notice area, `console.log` debug lines, `showNotice()` helper.
+
+### 4.3 CSS — `assets/css/admin.css`
+
+Append:
+
+```css
+.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;
+}
+```
+
+---
+
+## 5. Translations
+
+Add the following new English → Czech strings; register in the `.pot`, the `cs_CZ.po`, and recompile `cs_CZ.mo` with `msgfmt`.
+
+| English | Czech |
+| --- | --- |
+| Update product variant prices | Aktualizace cen variant produktů |
+| 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. | 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. |
+| Variant attribute | Vlastnost varianty |
+| Variant attribute value | Hodnota vlastnosti varianty |
+| -- Select attribute -- | -- Vyberte vlastnost -- |
+| -- Select value -- | -- Vyberte hodnotu -- |
+| New Price | Nová cena |
+| Dry Run | Zkušební běh |
+| Apply | Provést |
+| Affected variations | Dotčené varianty |
+| Results | Výsledky |
+| No variations match the current filter. | Žádné varianty neodpovídají vybranému filtru. |
+| Successfully updated %d variations. | Úspěšně aktualizováno %d variant. |
+| Failed to update %d variations. | Nepodařilo se aktualizovat %d variant. |
+| Are you sure you want to update %d variations? | Opravdu chcete aktualizovat %d variant? |
+| Please select an attribute | Vyberte prosím vlastnost |
+| Please select an attribute value | Vyberte prosím hodnotu vlastnosti |
+| Please enter a valid new price | Zadejte prosím platnou novou cenu |
+| Loading attributes... | Načítání vlastností... |
+| Loading values... | Načítání hodnot... |
+| Dry run complete: %d variations would be updated to %s. | Zkušební běh dokončen: %d variant bude aktualizováno na %s. |
+| Variant prices updated successfully | Ceny variant úspěšně aktualizovány |
+| Variant prices update error | Chyba aktualizace cen variant |
+| 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. | 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. |
+
+Add matching keys to the `i18n` array in `wp_localize_script(... 'studiouWcpcm', ...)` in `studiou-wc-product-cat-manage.php`:
+`upvpConfirmApply`, `upvpSelectAttribute`, `upvpSelectValue`, `upvpInvalidPrice`, `upvpLoadingAttributes`, `upvpLoadingValues`, `upvpDryRunComplete`, `upvpApplySuccess`, `upvpApplyError`, `upvpNoMatches`.
+
+---
+
+## 6. Documentation updates
+
+### 6.1 `readme.md`
+- Bump `**Version: 1.5.2**` → `**Version: 1.5.3**`.
+- Under **Features → Category Management**, add: `**Update product variant prices** - bulk-update regular price of variations by category + attribute filter (NEW in v1.5.3)`.
+- Under **Menu Structure → Category Management → Category Manipulations** sub-list, add: `Update product variant prices (NEW in v1.5.3)`.
+- Add a new usage section **"Updating Product Variant Prices"** mirroring the structure of the existing Manipulation usage sections, with the 6 controls listed and notes:
+  - Operation modifies only `regular_price` of `product_variation` records.
+  - Order line items, carts and historical data are **not** affected.
+  - Always run **Dry Run** first to preview the affected count and sample list.
+  - Parent variable products are re-synced (`WC_Product_Variable::sync`) after the batch so the storefront price range cache stays correct.
+- Append changelog entry **Version 1.5.3** above 1.5.2:
+  ```
+  ### 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)
+  ```
+
+### 6.2 `CLAUDE.md`
+- Bump *Current Version* `1.5.2` → `1.5.3`.
+- Under `class-studiou-wc-product-cat-manage-manipulator.php` bullet list, add: `Update product variant prices (regular_price only, by category + attribute filter) (NEW in v1.5.3)`.
+- Under *Views (views/) → manipulations.php*, add: `Update product variant prices (checkbox list + 2 comboboxes + price input + Dry Run/Apply buttons + live affected-count label) (NEW in v1.5.3)`.
+- Under *Admin Menu Structure → Category Manipulations*, add bullet: `Update product variant prices (NEW in v1.5.3)`.
+- Under *AJAX Handlers → Category Operations*, append entries 11–14:
+  - `studiou_wcpcm_upvp_get_attributes` — list variation attribute heads (NEW in v1.5.3)
+  - `studiou_wcpcm_upvp_get_attribute_values` — list values for selected attribute (NEW in v1.5.3)
+  - `studiou_wcpcm_upvp_count` — count + sample of variations matching current filter (NEW in v1.5.3)
+  - `studiou_wcpcm_upvp_apply` — commit new regular price to matching variations (NEW in v1.5.3)
+- Renumber the *Product Operations* section (currently starts at 11) accordingly.
+- Add *Testing Notes → Update Product Variant Prices Operations (NEW in v1.5.3)* section listing the test cases from §7 below.
+
+### 6.3 `directory-structure.txt`
+- No structural change. Verify it is still accurate (existing classes get methods added; no new files).
+
+---
+
+## 7. Step-by-step execution order
+
+1. Bump version constant + plugin header to `1.5.3`.
+2. Implement `get_variation_attribute_heads`, `get_variation_attribute_values`, `find_matching_variations`, `apply_variant_price` in `Studiou_WC_Product_Cat_Manage_Manipulator` (no AJAX yet).
+3. Add the 4 AJAX handler methods + register them in the constructor.
+4. Append the UPVP card to `views/manipulations.php` (before *Important Notes*).
+5. Add CSS rules.
+6. Add `initUpdateVariantPricesForm()` to `admin.js`, hook through existing `safeInit()`.
+7. Extend `wp_localize_script` `i18n` map with new keys.
+8. Update `.pot`, `cs_CZ.po`; regenerate `cs_CZ.mo` with `msgfmt`.
+9. Update `readme.md`, `CLAUDE.md`. Verify `directory-structure.txt`.
+10. Manual smoke test — see §8.
+11. Hard-refresh in browser (studiou.cz strips `?ver=` query strings — note in commit message).
+
+---
+
+## 8. Test plan
+
+Manual UI tests on a dev WC store with at least one variable product per fixture below:
+
+1. **Happy path (taxonomy attribute)** — Pick 1 category containing variable products with `pa_color = red`. Pick attribute *Color*, value *Red*, price `19.99`. Affected-count updates as filters change. Dry Run shows correct count + sample table. Apply → updated count matches; spot-check a variation in WP admin shows new regular price; parent variable product price range reflects the update.
+2. **Happy path (custom non-taxonomy attribute)** — Same flow on a product whose Size attribute is product-level (not global).
+3. **Multi-category** — Select two sibling categories; variations counted only once each.
+4. **No matches** — Filter that yields zero variations: Apply button stays disabled, label shows `0`.
+5. **Invalid price** — Empty / negative / non-numeric → error notice, no DB write.
+6. **Permission** — User without `manage_woocommerce` cannot trigger AJAX.
+7. **Nonce** — Tampered nonce → `Security check failed`.
+8. **Order data integrity** — Place a test order with a variation, run UPVP, confirm the order line item retains the original price.
+9. **Translations** — Switch site to `cs_CZ`, confirm UPVP labels render in Czech.
+10. **Cache busting** — Asset version query string updates from `1.5.2` to `1.5.3`. Hard-refresh on studiou.cz.
+11. **Parent re-sync** — After Apply, variable-product price range on storefront reflects the new regular price (sync transient cleared via `wc_delete_product_transients`).
+
+---
+
+## 9. Rollback
+
+Purely additive change — no schema migrations, no destructive operations, no changes to existing handlers. To roll back: revert the listed files to 1.5.2 and re-enqueue assets. No data cleanup required because UPVP only writes to existing `product_variation` records' `_regular_price` postmeta via the WC API.

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>