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.
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:
studiou-wcpcm-categories-list-container markup and styles). With Check All / Uncheck All buttons.pa_* taxonomies + product-level custom attributes flagged "used for variations").type=number, step=0.01, min=0); server normalises with wc_format_decimal.set_regular_price() to all matching variations via the WooCommerce API.Operation only modifies regular_price on product_variation posts. Sale prices, order line items, cart sessions and historical order data are not touched.
| 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. |
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.
Add inside Studiou_WC_Product_Cat_Manage_Manipulator::__construct(), alongside the existing add_action('wp_ajax_studiou_wcpcm_*', ...) lines:
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(); }.STUDIOU WC MEDIA: style — prefix STUDIOU WC UPVP:.Studiou_WC_Product_Cat_Manage_Manipulator/** 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.
| 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.
failed.pa_color vs color) in the combobox value, not the label.woocommerce_order_itemmeta. Document this explicitly in the Important Notes card and changelog.WC_Product_Variable::sync($parent_id) after the batch so the storefront price range stays correct.views/manipulations.phpInsert a new card immediately before the existing <!-- Important Notes --> card:
<!-- 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().
assets/js/admin.jsAdd a single initUpdateVariantPricesForm() and register it inside the existing $(document).ready() block via the existing safeInit() wrapper:
safeInit('upvpForm', function() {
if ($('#studiou-wcpcm-upvp-form').length) initUpdateVariantPricesForm();
});
Logic:
studiou_wcpcm_upvp_get_attributes; populate #upvp_attribute with <option value="key" data-istax="0|1">label</option>.#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.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.<table class="studiou-wcpcm-upvp-preview-table"> listing the sample variations: ID, SKU, Name, Parent, Current → New Price.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.
assets/css/admin.cssAppend:
.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;
}
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.
readme.md**Version: 1.5.2** → **Version: 1.5.3**.**Update product variant prices** - bulk-update regular price of variations by category + attribute filter (NEW in v1.5.3).Update product variant prices (NEW in v1.5.3).regular_price of product_variation records.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)
CLAUDE.md1.5.2 → 1.5.3.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).Update product variant prices (checkbox list + 2 comboboxes + price input + Dry Run/Apply buttons + live affected-count label) (NEW in v1.5.3).Update product variant prices (NEW in v1.5.3).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)directory-structure.txt1.5.3.get_variation_attribute_heads, get_variation_attribute_values, find_matching_variations, apply_variant_price in Studiou_WC_Product_Cat_Manage_Manipulator (no AJAX yet).views/manipulations.php (before Important Notes).initUpdateVariantPricesForm() to admin.js, hook through existing safeInit().wp_localize_script i18n map with new keys..pot, cs_CZ.po; regenerate cs_CZ.mo with msgfmt.readme.md, CLAUDE.md. Verify directory-structure.txt.?ver= query strings — note in commit message).Manual UI tests on a dev WC store with at least one variable product per fixture below:
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.0.manage_woocommerce cannot trigger AJAX.Security check failed.cs_CZ, confirm UPVP labels render in Czech.1.5.2 to 1.5.3. Hard-refresh on studiou.cz.wc_delete_product_transients).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.