Commit 5c7a4fc915 for woocommerce

commit 5c7a4fc91593c58761b2f11c54fa55f3b2e8ef1f
Author: malinajirka <malinajirka@gmail.com>
Date:   Fri Jan 9 13:57:23 2026 +0100

    Show 'not supported' message for unsupported product types in POS visibility (#62667)

    * Add taxonomy indicating whether product should be hidden in POS

    * Add pos visibility to product detail

    * Move pos visibility from product overview to advanced

    * Filter out products hidden in pos from generated catalog

    * Remove todo

    * Hide POS Taxonomy settings when POS flag disabled

    * Remove support for variations in POS product visibility taxonomy

    * Make pos product visibility taxonomy private

    * Add tests for changes in /products endpoint

    * Rename pos_visible to visible_in_pos for clarity

    * Fix linting errors

    * Add changelog entry - add ability to hide products in POS

    * Suppress lint warning about slow query

    * Use product_object->get_id() to get product id

    * Update phpstan baseline

    * Escape visible_in_pos query param

    * Add note for unsupported products in the 'show in POS' section

    * Add tests

    * Cleanup pos visibility product section

    * Update text for visible in POS toggle

    * Update text for unsupported product type in POS

    * Hide label description behind the tip icon

    * Do not change pos visibility when field not present

    * Update baseline regarding undefined var

    * Fix version in @since comment

    * Update query_var for pos product visibility taxonomy

    * Fix version in @since comment

    * Update query_var for pos product visibility taxonomy

    * Filter out variations with hidden parent from catalog feed

    * Rename visible_in_pos to pos_products_only and simplify behavior

    The visible_in_pos parameter was confusing because setting it to false
    returned only hidden products rather than all products. The new
    pos_products_only parameter has clearer semantics:

    - pos_products_only=true: return only POS-visible products
    - pos_products_only=false or omitted: return all products (no filtering)

    This makes the API more intuitive since false now means "don't apply
    this filter" rather than "show hidden products only".

    * Update plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php

    Co-authored-by: Radoslav Georgiev <rageorgiev@gmail.com>

    * Remove misleading outdated comment

    * Fix lint/formatting in class-wc-meta-box-product-data.php

    * Sync pos-hidden taxonomy to variations for accurate catalog totals

    When a variable product is hidden from POS, its variations should also be
      excluded from the catalog feed and counted correctly in the total.

      Previously, the pos_product_visibility taxonomy was only applied to parent
      products, and variations were filtered out during feed generation via
      FeedValidator. This caused the catalog endpoint to return incorrect totals
      since variations were counted in the query but filtered out later.

      This change:
      - Registers pos_product_visibility taxonomy for product_variation post type
      - Introduces POSProductVisibilitySync class to centralize visibility logic
      - Syncs pos-hidden term to all variations when parent visibility changes
      - Inherits pos-hidden term when new variations are created
      - Removes FeedValidator variation check (no longer needed)

      Now the tax_query correctly filters both products and variations at query time,
      ensuring accurate totals in the catalog API response.

    * Add pos_products_only filter to variations REST API endpoint

    Add the pos_products_only parameter to the /variations and /products/<id>/variations endpoints to filter out variations with the pos-hidden term, matching the existing implementation in the products endpoint.

    Changes:
    - Add pos_products_only boolean parameter to get_collection_params()
    - Add tax_query filter in prepare_objects_query() to exclude pos-hidden variations
    - Add tests for the new filter behavior

    This enables POS clients to fetch only variations visible in Point of Sale when querying the variations endpoint with pos_products_only=true.

    * Guard POS visibility save logic with feature flag check

    When the POS feature is disabled, the _visible_in_pos checkbox is not rendered in the admin product edit UI. However, the save logic was executing unconditionally. This caused $_POST['_visible_in_pos'] to be unset, resulting in $visible_in_pos becoming false and unintentionally hiding products from POS.

    This fix wraps the POS visibility save logic with a feature flag check, matching the UI logic that also checks feature_is_enabled('point_of_sale') before rendering the checkbox. This prevents data loss when editing products while the POS feature is disabled.

    * Fix lint issues in class-wc-rest-product-variations-controller-tests.php

    * Add tests for POSProductVisibilitySync.php

    * Fix variable.undefined static analysis errors

    ---------

    Co-authored-by: Radoslav Georgiev <rageorgiev@gmail.com>

diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
index d5b46d367e..ed2da6febe 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
@@ -257,6 +257,16 @@ jQuery( function ( $ ) {
 		}

 		$( '.hide_if_' + product_type, context ).hide();
+
+		// POS visibility - requires combination of type AND downloadable status.
+		var is_pos_supported = ( product_type === 'simple' || product_type === 'variable' ) && ! is_downloadable;
+		if ( is_pos_supported ) {
+			$( '#pos_visibility_supported', context ).show();
+			$( '#pos_visibility_unsupported', context ).hide();
+		} else {
+			$( '#pos_visibility_supported', context ).hide();
+			$( '#pos_visibility_unsupported', context ).show();
+		}
 	}

 	function show_and_hide_panels() {
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php
index 08e98bbc05..ded7b062bf 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php
@@ -1,4 +1,11 @@
 <?php
+/**
+ * Product advanced data panel.
+ *
+ * @package WooCommerce\Admin
+ * @var WC_Product $product_object
+ */
+
 use Automattic\WooCommerce\Utilities\FeaturesUtil;

 if ( ! defined( 'ABSPATH' ) ) {
@@ -55,20 +62,32 @@ if ( ! defined( 'ABSPATH' ) ) {
 		</div>
 	<?php endif; ?>
 	<?php if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) : ?>
-	<div class="options_group">
-		<?php
-		$visible_in_pos = ! has_term( 'pos-hidden', 'pos_product_visibility', $product_object->get_id() );
-		woocommerce_wp_checkbox(
-			array(
-				'id'          => '_visible_in_pos',
-				'value'       => $visible_in_pos ? 'yes' : 'no',
-				'label'       => __( 'Available for POS', 'woocommerce' ),
-				'desc_tip'    => true,
-				'description' => __( 'Controls whether this product appears in the Point of Sale system.', 'woocommerce' ),
-			)
-		);
-		?>
-	</div>
+		<?php $is_pos_supported = $product_object->is_type( array( 'simple', 'variable' ) ) && ! $product_object->is_downloadable(); ?>
+		<div class="options_group" id="pos_visibility_supported" <?php echo $is_pos_supported ? '' : 'style="display: none;"'; ?>>
+			<?php
+			$visible_in_pos = ! has_term( 'pos-hidden', 'pos_product_visibility', $product_object->get_id() );
+			woocommerce_wp_checkbox(
+				array(
+					'id'          => '_visible_in_pos',
+					'value'       => $visible_in_pos ? 'yes' : 'no',
+					'label'       => __( 'Available for POS', 'woocommerce' ),
+					'desc_tip'    => true,
+					'description' => __( 'Controls whether this product appears in the Point of Sale system.', 'woocommerce' ),
+				)
+			);
+			?>
+		</div>
+		<div class="options_group" id="pos_visibility_unsupported" <?php echo $is_pos_supported ? 'style="display: none;"' : ''; ?>>
+			<?php
+			woocommerce_wp_note(
+				array(
+					'id'      => '_pos_visibility_note',
+					'label'   => __( 'Point of Sale', 'woocommerce' ),
+					'message' => __( 'This product type is not currently supported.', 'woocommerce' ),
+				)
+			);
+			?>
+		</div>
 	<?php endif; ?>

 	<?php do_action( 'woocommerce_product_options_advanced' ); ?>
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 92c61ac030..383a22b0a9 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -7548,12 +7548,6 @@ parameters:
 			count: 1
 			path: includes/admin/meta-boxes/views/html-product-attribute.php

-		-
-			message: '#^Variable \$product_object might not be defined\.$#'
-			identifier: variable.undefined
-			count: 4
-			path: includes/admin/meta-boxes/views/html-product-data-advanced.php
-
 		-
 			message: '#^Variable \$product_object might not be defined\.$#'
 			identifier: variable.undefined
@@ -82936,4 +82930,4 @@ parameters:
 			message: '#^Parameter \#1 \$string of function substr expects string, string\|false given\.$#'
 			identifier: argument.type
 			count: 1
-			path: src/Utilities/StringUtil.php
\ No newline at end of file
+			path: src/Utilities/StringUtil.php
diff --git a/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-product-data-pos-visibility-test.php b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-product-data-pos-visibility-test.php
new file mode 100644
index 0000000000..82d9855a6d
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-product-data-pos-visibility-test.php
@@ -0,0 +1,88 @@
+<?php
+declare( strict_types = 1 );
+
+/**
+ * Tests for POS visibility support based on product type.
+ *
+ * @package WooCommerce\Tests\Admin\MetaBoxes
+ */
+
+/**
+ * Class WC_Meta_Box_Product_Data_POS_Visibility_Test
+ *
+ * Tests to verify which product types are supported in Point of Sale.
+ * POS supports only Simple and Variable non-downloadable products.
+ */
+class WC_Meta_Box_Product_Data_POS_Visibility_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * Helper function to check if a product is supported in POS.
+	 * This mirrors the logic in html-product-data-advanced.php.
+	 *
+	 * @param WC_Product $product The product to check.
+	 * @return bool True if supported in POS, false otherwise.
+	 */
+	private function is_product_supported_in_pos( $product ) {
+		return $product->is_type( array( 'simple', 'variable' ) ) && ! $product->is_downloadable();
+	}
+
+	/**
+	 * Test that simple non-downloadable products are supported in POS.
+	 */
+	public function test_simple_non_downloadable_product_is_supported_in_pos() {
+		$product = WC_Helper_Product::create_simple_product( true, array( 'downloadable' => false ) );
+
+		$this->assertTrue(
+			$this->is_product_supported_in_pos( $product ),
+			'Simple non-downloadable products should be supported in POS.'
+		);
+	}
+
+	/**
+	 * Test that variable non-downloadable products are supported in POS.
+	 */
+	public function test_variable_non_downloadable_product_is_supported_in_pos() {
+		$product = WC_Helper_Product::create_variation_product();
+
+		$this->assertTrue(
+			$this->is_product_supported_in_pos( $product ),
+			'Variable non-downloadable products should be supported in POS.'
+		);
+	}
+
+	/**
+	 * Test that downloadable simple products are not supported in POS.
+	 */
+	public function test_downloadable_simple_product_is_not_supported_in_pos() {
+		$product = WC_Helper_Product::create_simple_product( true, array( 'downloadable' => true ) );
+
+		$this->assertFalse(
+			$this->is_product_supported_in_pos( $product ),
+			'Downloadable simple products should not be supported in POS.'
+		);
+	}
+
+	/**
+	 * Test that grouped products are not supported in POS.
+	 */
+	public function test_grouped_product_is_not_supported_in_pos() {
+		$product = WC_Helper_Product::create_grouped_product();
+
+		$this->assertFalse(
+			$this->is_product_supported_in_pos( $product ),
+			'Grouped products should not be supported in POS.'
+		);
+	}
+
+	/**
+	 * Test that external products are not supported in POS.
+	 */
+	public function test_external_product_is_not_supported_in_pos() {
+		$product = WC_Helper_Product::create_external_product();
+
+		$this->assertFalse(
+			$this->is_product_supported_in_pos( $product ),
+			'External products should not be supported in POS.'
+		);
+	}
+}