Commit 0278e79028e for woocommerce

commit 0278e79028e717d4bbbf87d979197d30c87bdb4b
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Wed Aug 12 14:39:50 2026 +0200

    Include product brands in term recounts (#67630)

    * Include product brands in term recounts

    * Add changelog for product brand recounts

diff --git a/plugins/woocommerce/changelog/fix-product-brand-term-recounts b/plugins/woocommerce/changelog/fix-product-brand-term-recounts
new file mode 100644
index 00000000000..7d3a52f7fc0
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-brand-term-recounts
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Include product brands in visibility-aware product term recounts.
diff --git a/plugins/woocommerce/includes/class-wc-brands.php b/plugins/woocommerce/includes/class-wc-brands.php
index 2a601715d50..4162033ac2b 100644
--- a/plugins/woocommerce/includes/class-wc-brands.php
+++ b/plugins/woocommerce/includes/class-wc-brands.php
@@ -74,10 +74,6 @@ class WC_Brands {
 		// Layered nav widget compatibility.
 		add_filter( 'woocommerce_layered_nav_term_html', array( $this, 'woocommerce_brands_update_layered_nav_link' ), 10, 4 );

-		// Filter the list of taxonomies overridden for the original term count.
-		add_action( 'woocommerce_product_set_stock_status', array( $this, 'recount_after_stock_change' ) );
-		add_action( 'woocommerce_update_options_products_inventory', array( $this, 'recount_all_brands' ) );
-
 		// Block theme integration.
 		add_filter( 'hooked_block_types', array( $this, 'hook_product_brand_block' ), 10, 4 );
 		add_filter( 'hooked_block_core/post-terms', array( $this, 'configure_product_brand_block' ), 10, 5 );
diff --git a/plugins/woocommerce/includes/wc-term-functions.php b/plugins/woocommerce/includes/wc-term-functions.php
index fc66d4da6c5..c0b24a935ec 100644
--- a/plugins/woocommerce/includes/wc-term-functions.php
+++ b/plugins/woocommerce/includes/wc-term-functions.php
@@ -569,6 +569,11 @@ function wc_recount_after_stock_change( $product_id ) {
 		if ( is_array( $product_terms ) ) {
 			wp_update_term_count( array_column( $product_terms, 'term_taxonomy_id' ), 'product_tag' );
 		}
+
+		$product_terms = get_the_terms( $product_id, 'product_brand' );
+		if ( is_array( $product_terms ) ) {
+			wp_update_term_count( array_column( $product_terms, 'term_taxonomy_id' ), 'product_brand' );
+		}
 	} else {
 		_wc_recount_terms_by_product( $product_id );
 	}
@@ -577,7 +582,7 @@ add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_chan


 /**
- * Overrides the original term count for product categories and tags with the product count.
+ * Overrides the original term count for product categories, tags, and brands with the product count
  * that takes catalog visibility into account.
  *
  * @param array        $terms      List of terms.
@@ -719,7 +724,7 @@ function wc_get_product_visibility_term_ids() {
 }

 /**
- * Recounts all terms for product categories and product tags.
+ * Recounts all terms for product categories, tags, and brands.
  *
  * @since 5.2
  *
@@ -746,10 +751,23 @@ function wc_recount_all_terms( bool $include_callback = true ) {
 		)
 	);
 	_wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), $include_callback, false );
+
+	$product_brands         = get_terms(
+		array(
+			'taxonomy'   => 'product_brand',
+			'hide_empty' => false,
+			'fields'     => 'id=>parent',
+		)
+	);
+	$product_brand_taxonomy = get_taxonomy( 'product_brand' );
+
+	if ( is_array( $product_brands ) && $product_brand_taxonomy instanceof WP_Taxonomy ) {
+		_wc_term_recount( $product_brands, $product_brand_taxonomy, $include_callback, false );
+	}
 }

 /**
- * Recounts terms by product.
+ * Recounts product category, tag, and brand terms by product.
  *
  * @since 5.2
  * @param int $product_id The ID of the product.
@@ -783,4 +801,20 @@ function _wc_recount_terms_by_product( $product_id = '' ) {

 		_wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false );
 	}
+
+	$product_terms = get_the_terms( $product_id, 'product_brand' );
+
+	if ( is_array( $product_terms ) ) {
+		$product_brands = array();
+
+		foreach ( $product_terms as $term ) {
+			$product_brands[ $term->term_id ] = $term->parent;
+		}
+
+		$product_brand_taxonomy = get_taxonomy( 'product_brand' );
+
+		if ( $product_brand_taxonomy instanceof WP_Taxonomy ) {
+			_wc_term_recount( $product_brands, $product_brand_taxonomy, false, false );
+		}
+	}
 }
diff --git a/plugins/woocommerce/tests/php/includes/wc-term-functions-tests.php b/plugins/woocommerce/tests/php/includes/wc-term-functions-tests.php
index 443bed7d650..7246e21fcb9 100644
--- a/plugins/woocommerce/tests/php/includes/wc-term-functions-tests.php
+++ b/plugins/woocommerce/tests/php/includes/wc-term-functions-tests.php
@@ -32,6 +32,13 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {
 		$this->terms['tag1'] = wp_insert_term( 'Tag 1', 'product_tag' );
 		$this->terms['tag2'] = wp_insert_term( 'Tag 2', 'product_tag' );

+		$this->terms['brand_parent'] = wp_insert_term( 'Parent brand', 'product_brand' );
+		$this->terms['brand_child']  = wp_insert_term(
+			'Child brand',
+			'product_brand',
+			array( 'parent' => $this->terms['brand_parent']['term_id'] )
+		);
+
 		$this->products['product1'] = WC_Helper_Product::create_simple_product(
 			true,
 			array(
@@ -54,6 +61,10 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {
 				'tag_ids'      => array( $this->terms['tag1']['term_id'], $this->terms['tag2']['term_id'] ),
 			)
 		);
+
+		wp_set_object_terms( $this->products['product1']->get_id(), array( $this->terms['brand_child']['term_id'] ), 'product_brand' );
+		wp_set_object_terms( $this->products['product2']->get_id(), array( $this->terms['brand_child']['term_id'] ), 'product_brand' );
+		wp_set_object_terms( $this->products['product3']->get_id(), array( $this->terms['brand_parent']['term_id'] ), 'product_brand' );
 	}

 	/**
@@ -81,7 +92,7 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {
 	public function test_term_count_baseline(): void {
 		$terms       = get_terms(
 			array(
-				'taxonomy'   => array( 'product_cat', 'product_tag' ),
+				'taxonomy'   => array( 'product_cat', 'product_tag', 'product_brand' ),
 				'hide_empty' => false,
 			)
 		);
@@ -92,6 +103,8 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {
 		$this->assertEquals( 1, $term_counts[ $this->terms['child2']['term_id'] ] );
 		$this->assertEquals( 2, $term_counts[ $this->terms['tag1']['term_id'] ] );
 		$this->assertEquals( 2, $term_counts[ $this->terms['tag2']['term_id'] ] );
+		$this->assertEquals( 3, $term_counts[ $this->terms['brand_parent']['term_id'] ] );
+		$this->assertEquals( 2, $term_counts[ $this->terms['brand_child']['term_id'] ] );
 	}

 	/**
@@ -106,7 +119,7 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {

 		$terms       = get_terms(
 			array(
-				'taxonomy'   => array( 'product_cat', 'product_tag' ),
+				'taxonomy'   => array( 'product_cat', 'product_tag', 'product_brand' ),
 				'hide_empty' => false,
 			)
 		);
@@ -117,6 +130,8 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {
 		$this->assertEquals( 1, $term_counts[ $this->terms['child2']['term_id'] ] );
 		$this->assertEquals( 1, $term_counts[ $this->terms['tag1']['term_id'] ] );
 		$this->assertEquals( 2, $term_counts[ $this->terms['tag2']['term_id'] ] );
+		$this->assertEquals( 2, $term_counts[ $this->terms['brand_parent']['term_id'] ] );
+		$this->assertEquals( 1, $term_counts[ $this->terms['brand_child']['term_id'] ] );
 	}

 	/**
@@ -130,7 +145,7 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {

 		$terms       = get_terms(
 			array(
-				'taxonomy'   => array( 'product_cat', 'product_tag' ),
+				'taxonomy'   => array( 'product_cat', 'product_tag', 'product_brand' ),
 				'hide_empty' => false,
 			)
 		);
@@ -141,6 +156,28 @@ class WC_Term_Functions_Tests extends \WC_Unit_Test_Case {
 		$this->assertEquals( 0, $term_counts[ $this->terms['child2']['term_id'] ] );
 		$this->assertEquals( 2, $term_counts[ $this->terms['tag1']['term_id'] ] );
 		$this->assertEquals( 1, $term_counts[ $this->terms['tag2']['term_id'] ] );
+		$this->assertEquals( 2, $term_counts[ $this->terms['brand_parent']['term_id'] ] );
+		$this->assertEquals( 1, $term_counts[ $this->terms['brand_child']['term_id'] ] );
+
+		delete_option( 'woocommerce_hide_out_of_stock_items' );
+	}
+
+	/**
+	 * @testdox Recounting terms for one product updates its brand and brand ancestors.
+	 */
+	public function test_recount_terms_by_product_includes_brands(): void {
+		update_option( 'woocommerce_hide_out_of_stock_items', 'yes' );
+		wp_set_object_terms(
+			$this->products['product1']->get_id(),
+			ProductStockStatus::OUT_OF_STOCK,
+			'product_visibility',
+			true
+		);
+
+		_wc_recount_terms_by_product( $this->products['product1']->get_id() );
+
+		$this->assertSame( '1', get_term_meta( $this->terms['brand_parent']['term_id'], 'product_count_product_brand', true ) );
+		$this->assertSame( '0', get_term_meta( $this->terms['brand_child']['term_id'], 'product_count_product_brand', true ) );

 		delete_option( 'woocommerce_hide_out_of_stock_items' );
 	}