* * @see https://wcpos.com * @package WCPOS\WooCommercePOS */ namespace WCPOS\WooCommercePOS; use Automattic\WooCommerce\StoreApi\Exceptions\NotPurchasableException; use WC_Product; use WC_Product_Variation; use WCPOS\WooCommercePOS\Services\Settings; use WP_Query; /** * Products class. */ class Products { /** * Constructor. */ public function __construct() { add_action( 'woocommerce_product_set_stock', array( $this, 'product_set_stock' ) ); add_action( 'woocommerce_variation_set_stock', array( $this, 'product_set_stock' ) ); $pos_only_products = woocommerce_pos_get_settings( 'general', 'pos_only_products' ); if ( $pos_only_products ) { add_action( 'pre_get_posts', array( $this, 'hide_pos_only_products' ) ); add_filter( 'woocommerce_variation_is_visible', array( $this, 'hide_pos_only_variations' ), 10, 4 ); add_action( 'woocommerce_store_api_validate_add_to_cart', array( $this, 'store_api_prevent_pos_only_add_to_cart' ) ); // NOTE: this hook is marked as deprecated. add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'prevent_pos_only_add_to_cart' ), 10, 2 ); // add_filter( 'woocommerce_get_product_subcategories_args', array( $this, 'filter_category_count_exclude_pos_only' ) );. } /** * Allow decimal quantities for products and variations. * * @TODO - this will affect the online store as well, should I make it POS only? */ $allow_decimal_quantities = woocommerce_pos_get_settings( 'general', 'decimal_qty' ); if ( \is_bool( $allow_decimal_quantities ) && $allow_decimal_quantities ) { remove_filter( 'woocommerce_stock_amount', 'intval' ); add_filter( 'woocommerce_stock_amount', 'floatval' ); add_action( 'woocommerce_before_product_object_save', array( $this, 'save_decimal_quantities' ) ); } } /** * Bump modified date on stock change. * - variation->id = parent id. * * @param WC_Product $product The product object. */ public function product_set_stock( WC_Product $product ): void { $post_modified = current_time( 'mysql' ); $post_modified_gmt = current_time( 'mysql', true ); wp_update_post( array( 'ID' => $product->get_id(), 'post_modified' => $post_modified, 'post_modified_gmt' => $post_modified_gmt, ) ); } /** * Hide POS only products and variations from front-end queries. * * @param \WP_Query $query The WP_Query instance (passed by reference). * * @return void */ public function hide_pos_only_products( \WP_Query $query ): void { // Only run for front-end queries, not admin or POS requests. if ( is_admin() || woocommerce_pos_request() ) { return; } // WP_Query post_type can be string or array. $post_types = (array) $query->get( 'post_type' ); $settings_instance = Settings::instance(); $exclude_ids = array(); // Handle product queries. if ( \in_array( 'product', $post_types, true ) ) { $settings = $settings_instance->get_pos_only_product_visibility_settings(); if ( isset( $settings['ids'] ) && ! empty( $settings['ids'] ) ) { $exclude_ids = array_merge( $exclude_ids, array_map( 'intval', (array) $settings['ids'] ) ); } } // Handle product_variation queries. if ( \in_array( 'product_variation', $post_types, true ) ) { $settings = $settings_instance->get_pos_only_variations_visibility_settings(); if ( isset( $settings['ids'] ) && ! empty( $settings['ids'] ) ) { $exclude_ids = array_merge( $exclude_ids, array_map( 'intval', (array) $settings['ids'] ) ); } } // Apply exclusions if any. if ( ! empty( $exclude_ids ) ) { $existing_excludes = $query->get( 'post__not_in' ); if ( ! \is_array( $existing_excludes ) ) { $existing_excludes = array(); } $query->set( 'post__not_in', array_merge( $existing_excludes, $exclude_ids ) ); } } /** * Remove POS Only variations from the storefront. * * @param bool $visible Whether the variation is visible. * @param int $variation_id The variation ID. * @param int $product_id The product ID. * @param WC_Product_Variation $variation The variation object. */ public function hide_pos_only_variations( $visible, $variation_id, $product_id, $variation ) { if ( is_shop() || is_product_category() || is_product() ) { $settings_instance = Settings::instance(); $pos_only = $settings_instance->is_variation_pos_only( $variation_id ); if ( $pos_only ) { return false; } } return $visible; } /** * Filter category count to exclude pos_only products. * * @param array $args The arguments for getting product subcategories. * * @return array The modified arguments. */ public function filter_category_count_exclude_pos_only( $args ) { // @TODO: Do we need this? return $args; } /** * Prevent POS Only products from being added to the cart. * * NOTE: this hook is marked as deprecated. * * @param bool $passed Whether the product can be added to the cart. * @param int $product_id The product ID. * * @return bool */ public function prevent_pos_only_add_to_cart( $passed, $product_id ) { $settings_instance = Settings::instance(); $product_pos_only = $settings_instance->is_product_pos_only( $product_id ); $variation_pos_only = $settings_instance->is_variation_pos_only( $product_id ); if ( $product_pos_only || $variation_pos_only ) { return false; } return $passed; } /** * Prevent POS Only products from being added to the cart via the Store API. * * @param WC_Product $product Product. * * @throws NotPurchasableException If the product is POS only. * * @return void */ public function store_api_prevent_pos_only_add_to_cart( WC_Product $product ): void { $settings_instance = Settings::instance(); if ( $product->is_type( 'variation' ) ) { $pos_only = $settings_instance->is_variation_pos_only( $product->get_id() ); } else { $pos_only = $settings_instance->is_product_pos_only( $product->get_id() ); } if ( $pos_only ) { // @phpstan-ignore-next-line. throw new NotPurchasableException( 'woocommerce_pos_product_not_purchasable', esc_html( $product->get_name() ) ); } } /** * Save decimal quantities for products and variations. * * @param WC_Product $product Product. */ public function save_decimal_quantities( WC_Product $product ): void { if ( ! $product->get_manage_stock() ) { $product->set_stock_status( 'instock' ); return; } $stock_quantity = $product->get_stock_quantity(); $stock_notification_threshold = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); // Adjust the condition to consider stock quantities between 0 and 1 as instock if greater than 0. $stock_is_above_notification_threshold = ( $stock_quantity > 0 && $stock_quantity > $stock_notification_threshold ); $backorders_are_allowed = ( 'no' !== $product->get_backorders() ); if ( $stock_is_above_notification_threshold ) { $new_stock_status = 'instock'; } elseif ( $backorders_are_allowed ) { $new_stock_status = 'onbackorder'; } else { $new_stock_status = 'outofstock'; } $product->set_stock_status( $new_stock_status ); } }