Commit 7ddffb082c7 for woocommerce

commit 7ddffb082c7a5f7a97b3e8376dd51e025b7f7de3
Author: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
Date:   Fri Jul 24 12:31:39 2026 +0300

    Fix CSV re-imports deleting terms attached to existing variations (#66946)

    * Stop CSV re-imports deleting terms attached to existing variations

    The variation-placeholder cleanup added for issue #31815 ran
    unconditionally for every variation row with a valid ID, so re-imports
    with update_existing enabled stripped product_cat/product_tag terms
    attached to pre-existing variations (e.g. by an extension). Only delete
    terms when the post was just converted into a variation.

    * Add changelog entry for variation re-import term deletion fix

diff --git a/plugins/woocommerce/changelog/66940-fix-csv-import-variation-term-deletion b/plugins/woocommerce/changelog/66940-fix-csv-import-variation-term-deletion
new file mode 100644
index 00000000000..30ecb4f7f1b
--- /dev/null
+++ b/plugins/woocommerce/changelog/66940-fix-csv-import-variation-term-deletion
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Preserve category and tag terms attached to existing variations when re-importing them via CSV with "update existing" enabled, instead of unconditionally deleting them on every variation row.
diff --git a/plugins/woocommerce/includes/import/abstract-wc-product-importer.php b/plugins/woocommerce/includes/import/abstract-wc-product-importer.php
index a30d06361ab..aede47e4730 100644
--- a/plugins/woocommerce/includes/import/abstract-wc-product-importer.php
+++ b/plugins/woocommerce/includes/import/abstract-wc-product-importer.php
@@ -184,6 +184,8 @@ abstract class WC_Product_Importer implements WC_Importer_Interface {
 			try {
 				// Prevent getting "variation_invalid_id" error message from Variation Data Store.
 				if ( ProductType::VARIATION === $data['type'] ) {
+					$was_variation = $id && 'product_variation' === get_post_type( $id );
+
 					$id = wp_update_post(
 						array(
 							'ID'        => $id,
@@ -195,8 +197,11 @@ abstract class WC_Product_Importer implements WC_Importer_Interface {
 					// simple product, so the product data store may have assigned it
 					// the default product category. Variations must not carry
 					// product_cat/product_tag terms, and WordPress does not clear
-					// them when the post type changes, so remove them here.
-					if ( $id && ! is_wp_error( $id ) ) {
+					// them when the post type changes, so remove them here — but
+					// only when the post was just converted into a variation.
+					// Pre-existing variations (e.g. re-imports with "update
+					// existing" enabled) must keep any terms attached to them.
+					if ( $id && ! is_wp_error( $id ) && ! $was_variation ) {
 						wp_delete_object_term_relationships( $id, array( 'product_cat', 'product_tag' ) );
 					}
 				}
diff --git a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
index fc1ba774760..e56e4985db9 100644
--- a/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
+++ b/plugins/woocommerce/tests/php/includes/importer/class-wc-product-csv-importer-test.php
@@ -317,6 +317,67 @@ class WC_Product_CSV_Importer_Test extends \WC_Unit_Test_Case {
 		}
 	}

+	/**
+	 * @testdox Re-importing an existing variation with update_existing enabled preserves taxonomy terms attached to it.
+	 */
+	public function test_reimporting_existing_variation_preserves_attached_terms() {
+		// Term creation during import requires the manage_product_terms capability.
+		wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
+
+		$imported_ids = array();
+		$term_id      = 0;
+
+		try {
+			// Build the header-to-field mapping the way the admin import UI does.
+			$csv_file   = __DIR__ . '/variation-category-31815.csv';
+			$headers    = ( new WC_Product_CSV_Importer( $csv_file, array( 'parse' => false ) ) )->get_raw_keys();
+			$controller = new WC_Product_CSV_Importer_Controller();
+			$auto_map   = new ReflectionMethod( $controller, 'auto_map_columns' );
+			$auto_map->setAccessible( true );
+			$mapping = array_combine( $headers, $auto_map->invoke( $controller, $headers ) );
+
+			$data = ( new WC_Product_CSV_Importer(
+				$csv_file,
+				array(
+					'parse'   => true,
+					'mapping' => $mapping,
+				)
+			) )->import();
+
+			$imported_ids = array_merge( $data['imported'], $data['imported_variations'] );
+			$this->assertCount( 2, $data['imported_variations'], 'Expected 2 variations to be imported.' );
+
+			// Simulate an extension attaching a category to an existing variation.
+			$inserted     = wp_insert_term( 'Variation Extension Category', 'product_cat' );
+			$term_id      = is_wp_error( $inserted ) ? (int) $inserted->get_error_data( 'term_exists' ) : $inserted['term_id'];
+			$variation_id = $data['imported_variations'][0];
+			wp_set_object_terms( $variation_id, array( $term_id ), 'product_cat' );
+
+			$update = ( new WC_Product_CSV_Importer(
+				$csv_file,
+				array(
+					'parse'           => true,
+					'mapping'         => $mapping,
+					'update_existing' => true,
+				)
+			) )->import();
+
+			$this->assertContains( $variation_id, $update['updated'], 'Expected the existing variation to be updated by the re-import.' );
+			$this->assertSame(
+				array( $term_id ),
+				wp_get_object_terms( $variation_id, 'product_cat', array( 'fields' => 'ids' ) ),
+				'Terms attached to an existing variation must survive a re-import that updates it.'
+			);
+		} finally {
+			foreach ( $imported_ids as $id ) {
+				WC_Helper_Product::delete_product( $id );
+			}
+			if ( $term_id ) {
+				wp_delete_term( $term_id, 'product_cat' );
+			}
+		}
+	}
+
 	/**
 	 * @testdox parse_float_field should respect the store's decimal separator setting (issue #38116).
 	 * @dataProvider provider_parse_float_field_decimal_separator