Commit f5459b9f110 for woocommerce
commit f5459b9f11070e9ee5a0cc6d45eb8b8da6b848d3
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date: Tue Oct 6 11:24:14 2026 +0200
Product Export: enable category filtering when exporting variable products and product variations together (#69264)
* Add changelog
* Product Export: enable category filtering when exporting variable products and product variations together
* Make sure export percentages are correct
* Harden test
diff --git a/plugins/woocommerce/changelog/update-allow-filtering-by-category-when-exporting-variations b/plugins/woocommerce/changelog/update-allow-filtering-by-category-when-exporting-variations
new file mode 100644
index 00000000000..aee8311875f
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-allow-filtering-by-category-when-exporting-variations
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Product Export: enable category filtering when exporting variable products and product variations together
diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-product-export.js b/plugins/woocommerce/client/legacy/js/admin/wc-product-export.js
index 69331654bd8..9740235f558 100644
--- a/plugins/woocommerce/client/legacy/js/admin/wc-product-export.js
+++ b/plugins/woocommerce/client/legacy/js/admin/wc-product-export.js
@@ -91,9 +91,11 @@
* Handle fields per export type.
*/
productExportForm.prototype.exportTypeFields = function() {
- var exportCategory = $( '.woocommerce-exporter-category' );
+ var exportCategory = $( '.woocommerce-exporter-category' );
+ var exportTypes = $( this ).val() || [];
+ var exportingVariationsOnly = -1 !== $.inArray( 'variation', exportTypes ) && -1 === $.inArray( 'variable', exportTypes );
- if ( -1 !== $.inArray( 'variation', $( this ).val() ) ) {
+ if ( exportingVariationsOnly ) {
exportCategory.closest( 'tr' ).hide();
exportCategory.val( '' ).trigger( 'change' ); // Reset WooSelect selected value.
} else {
diff --git a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php
index 3622df651cb..240d105e52c 100644
--- a/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php
+++ b/plugins/woocommerce/includes/export/class-wc-product-csv-exporter.php
@@ -63,6 +63,13 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter {
*/
protected $product_ids_to_export = array();
+ /**
+ * Rows on this page that the paginated query did not return.
+ *
+ * @var int
+ */
+ protected $rows_outside_query = 0;
+
/**
* Constructor.
*/
@@ -225,10 +232,11 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter {
$products = wc_get_products( $args );
- $this->total_rows = $products->total;
- $this->row_data = array();
- $variable_products = array();
- $include_variations = ! isset( $args['type'] ) || in_array( ProductType::VARIATION, (array) $args['type'], true );
+ $this->total_rows = $products->total;
+ $this->row_data = array();
+ $this->rows_outside_query = 0;
+ $variable_products = array();
+ $include_variations = ! isset( $args['type'] ) || in_array( ProductType::VARIATION, (array) $args['type'], true );
foreach ( $products->products as $product ) {
// Check if the product is variable and if either the include or category filter is active.
@@ -261,11 +269,30 @@ class WC_Product_CSV_Exporter extends WC_CSV_Batch_Exporter {
foreach ( $products as $product ) {
$this->row_data[] = $this->generate_row_data( $product );
+ ++$this->rows_outside_query;
}
}
}
}
+ /**
+ * Get total percentage complete.
+ *
+ * Variations appended after the paginated query are written on this page, but total_rows does not include them. Leave them out of this calculation so the export finishes at 100 instead of stopping early or requesting pages forever.
+ *
+ * @since 11.3.0
+ * @return int
+ */
+ public function get_percent_complete() {
+ if ( 0 === $this->total_rows ) {
+ return 100;
+ }
+
+ $exported = parent::get_total_exported() - $this->rows_outside_query;
+
+ return (int) floor( ( max( 0, $exported ) / $this->total_rows ) * 100 );
+ }
+
/**
* Take a product and generate row data from it for export.
*
diff --git a/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php b/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php
index e9ac1e7d7e3..926bc0163e0 100644
--- a/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php
+++ b/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php
@@ -58,10 +58,11 @@ class WC_Product_CSV_Exporter_Test extends \WC_Unit_Test_Case {
/**
* Create a variable product assigned to a product category.
*
- * @return array{product: WC_Product_Variable, category_slug: string}
+ * @param string $category_name Category name.
+ * @return array{product: WC_Product_Variable, category_slug: string, category_id: int}
*/
- private function create_categorized_variation_product(): array {
- $term = wp_insert_term( 'Export Test Category', 'product_cat' );
+ private function create_categorized_variation_product( string $category_name = 'Export Test Category' ): array {
+ $term = wp_insert_term( $category_name, 'product_cat' );
$this->assertIsArray( $term, 'Failed to create product category for export test.' );
$product = WC_Helper_Product::create_variation_product();
@@ -73,9 +74,23 @@ class WC_Product_CSV_Exporter_Test extends \WC_Unit_Test_Case {
return array(
'product' => $product,
'category_slug' => $category->slug,
+ 'category_id' => (int) $category->term_id,
);
}
+ /**
+ * Write the current page the way file generation does, so the completion percentage includes its rows.
+ *
+ * @param WC_Product_CSV_Exporter $exporter Exporter instance.
+ */
+ private function export_current_page( WC_Product_CSV_Exporter $exporter ): void {
+ $exporter->prepare_data_to_export();
+
+ $export_rows = ( new ReflectionClass( WC_Product_CSV_Exporter::class ) )->getMethod( 'export_rows' );
+ $export_rows->setAccessible( true );
+ $export_rows->invoke( $exporter );
+ }
+
/**
* @testdox variations should use draft status from parent product
*/
@@ -162,6 +177,50 @@ class WC_Product_CSV_Exporter_Test extends \WC_Unit_Test_Case {
}
}
+ /**
+ * @testdox Category export includes variable products and their variations without changing the completion percentage.
+ */
+ public function test_appended_variations_do_not_change_category_export_completion(): void {
+ $first = $this->create_categorized_variation_product( 'Export Paged Progress Category' );
+ $second = WC_Helper_Product::create_variation_product();
+ $second->set_category_ids( array( $first['category_id'] ) );
+ $second->save();
+ $this->create_categorized_variation_product( 'Export Other Progress Category' );
+
+ $products = array(
+ 1 => $first['product'],
+ 2 => $second,
+ );
+ $percentages = array(
+ 1 => 50,
+ 2 => 100,
+ );
+
+ foreach ( $products as $page => $product ) {
+ $exporter = new WC_Product_CSV_Exporter();
+ $exporter->set_limit( 1 );
+ $exporter->set_page( $page );
+ $exporter->set_product_types_to_export( array( ProductType::VARIABLE, ProductType::VARIATION ) );
+ $exporter->set_product_category_to_export( array( $first['category_slug'] ) );
+ $this->export_current_page( $exporter );
+
+ $exported_ids = array_map( 'intval', wp_list_pluck( $this->get_exported_data( $exporter ), 'id' ) );
+ $expected_ids = array_map(
+ 'intval',
+ array_merge(
+ array( $product->get_id() ),
+ $product->get_children( 'edit' )
+ )
+ );
+ sort( $exported_ids );
+ sort( $expected_ids );
+
+ $this->assertGreaterThan( $exporter->get_limit(), count( $exported_ids ), 'The page should include the parent and its appended variations.' );
+ $this->assertSame( $expected_ids, $exported_ids, 'The page should include that variable product and its variations, and exclude the other category.' );
+ $this->assertSame( $percentages[ $page ], $exporter->get_percent_complete() );
+ }
+ }
+
/**
* @testdox Selected product IDs restrict the export to those products and their variations.
*/