et_product_term_ids( $product_id, 'product_cat' ); foreach ( $product_cats as $product_cat ) { $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) ); } return $product_cats; } /** * Gets data about an attachment, such as alt text and captions. * * @since 2.6.0 * * @param int|null $attachment_id Attachment ID. * @param WC_Product|bool $product WC_Product object. * * @return array */ function wc_get_product_attachment_props( $attachment_id = null, $product = false ) { $props = array( 'title' => '', 'caption' => '', 'url' => '', 'alt' => '', 'src' => '', 'srcset' => false, 'sizes' => false, ); $attachment = get_post( $attachment_id ); if ( $attachment && 'attachment' === $attachment->post_type ) { $props['title'] = wp_strip_all_tags( $attachment->post_title ); $props['caption'] = wp_strip_all_tags( $attachment->post_excerpt ); $props['url'] = wp_get_attachment_url( $attachment_id ); // Alt text. $alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) ); if ( $product && $product instanceof WC_Product ) { $alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) ); } $alt_text = array_filter( $alt_text ); $props['alt'] = $alt_text ? reset( $alt_text ) : ''; /** * Filters the size for the full gallery image. * * @param string $size Image size name. * * @since 2.6.0 */ $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); $src = wp_get_attachment_image_src( $attachment_id, $full_size ); $props['full_src'] = $src[0] ?? null; $props['full_src_w'] = $src[1] ?? null; $props['full_src_h'] = $src[2] ?? null; $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); /** * Filters the size for the gallery thumbnail. * * @param array $size Array containing width and height dimensions. * * @since 2.6.0 */ $gallery_thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); $src = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size ); $props['gallery_thumbnail_src'] = $src[0] ?? null; $props['gallery_thumbnail_src_w'] = $src[1] ?? null; $props['gallery_thumbnail_src_h'] = $src[2] ?? null; /** * Filters the thumbnail size. * * @param string $size Image size name. * * @since 2.6.0 */ $thumbnail_size = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' ); $src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); $props['thumb_src'] = $src[0] ?? null; $props['thumb_src_w'] = $src[1] ?? null; $props['thumb_src_h'] = $src[2] ?? null; /** * Filters the size for the gallery image. * * @param string $size Image size name. * * @since 2.6.0 */ $image_size = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' ); $src = wp_get_attachment_image_src( $attachment_id, $image_size ); $props['src'] = $src[0] ?? null; $props['src_w'] = $src[1] ?? null; $props['src_h'] = $src[2] ?? null; $props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false; $props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false; } return $props; } /** * Get product visibility options. * * @since 3.0.0 * @return array */ function wc_get_product_visibility_options() { return apply_filters( 'woocommerce_product_visibility_options', array( CatalogVisibility::VISIBLE => __( 'Shop and search results', 'woocommerce' ), CatalogVisibility::CATALOG => __( 'Shop only', 'woocommerce' ), CatalogVisibility::SEARCH => __( 'Search results only', 'woocommerce' ), CatalogVisibility::HIDDEN => __( 'Hidden', 'woocommerce' ), ) ); } /** * Get product tax class options. * * @since 3.0.0 * @return array */ function wc_get_product_tax_class_options() { $tax_classes = WC_Tax::get_tax_classes(); $tax_class_options = array(); $tax_class_options[''] = __( 'Standard', 'woocommerce' ); if ( ! empty( $tax_classes ) ) { foreach ( $tax_classes as $class ) { $tax_class_options[ sanitize_title( $class ) ] = $class; } } return $tax_class_options; } /** * Get stock status options. * * @since 3.0.0 * @return array */ function wc_get_product_stock_status_options() { return apply_filters( 'woocommerce_product_stock_status_options', array( ProductStockStatus::IN_STOCK => __( 'In stock', 'woocommerce' ), ProductStockStatus::OUT_OF_STOCK => __( 'Out of stock', 'woocommerce' ), ProductStockStatus::ON_BACKORDER => __( 'On backorder', 'woocommerce' ), ) ); } /** * Get backorder options. * * @since 3.0.0 * @return array */ function wc_get_product_backorder_options() { return array( 'no' => __( 'Do not allow', 'woocommerce' ), 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), 'yes' => __( 'Allow', 'woocommerce' ), ); } /** * Get related products based on product category and tags. * * @since 3.0.0 * @param int $product_id Product ID. * @param int $limit Limit of results. * @param array $exclude_ids Exclude IDs from the results. * @param array $related_by Related by category and tags boolean flags. * @return array */ function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(), $related_by = array() ) { // Log an error if the limit is not an integer since this is what we expect. // However this is not a problem and we can continue. if ( ! is_int( $limit ) ) { wc_get_logger()->error( sprintf( 'Invalid limit type passed to wc_get_related_products. Expected integer, got %s with value: %s', gettype( $limit ), wp_json_encode( $limit ) ), array( 'source' => 'wc_get_related_products' ) ); } // If the limit is not numeric, set it to null. $limit = is_numeric( $limit ) ? (int) $limit : null; if ( null === $limit ) { return array(); } $product_id = absint( $product_id ); $limit = $limit >= -1 ? $limit : 5; $exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids ); $transient_name = 'wc_related_' . $product_id; $query_args = http_build_query( array( 'limit' => $limit, 'exclude_ids' => $exclude_ids, 'related_by' => $related_by, ) ); $transient = get_transient( $transient_name ); $related_posts = $transient && is_array( $transient ) && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false; // We want to query related posts if they are not cached, or we don't have enough. if ( false === $related_posts || count( $related_posts ) < $limit ) { $cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array(); $tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array(); // Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related. if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) { $related_posts = array(); } else { $data_store = WC_Data_Store::load( 'product' ); $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id ); } if ( $transient && is_array( $transient ) ) { $transient[ $query_args ] = $related_posts; } else { $transient = array( $query_args => $related_posts ); } set_transient( $transient_name, $transient, DAY_IN_SECONDS ); } $related_posts = apply_filters( 'woocommerce_related_products', $related_posts, $product_id, array( 'limit' => $limit, 'excluded_ids' => $exclude_ids, ) ); $related_posts = is_array( $related_posts ) ? $related_posts : array(); if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) { shuffle( $related_posts ); } return array_slice( $related_posts, 0, $limit ); } /** * Retrieves product term ids for a taxonomy. * * @since 3.0.0 * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @return array */ function wc_get_product_term_ids( $product_id, $taxonomy ) { $terms = get_the_terms( $product_id, $taxonomy ); return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' ); } /** * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax included, or an empty string if price calculation failed. */ function wc_get_price_including_tax( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : (float) $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; $return_price = $line_price; if ( $product->is_taxable() ) { if ( ! wc_prices_include_tax() ) { // If the customer is exempt from VAT, set tax total to 0. if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { $taxes_total = 0.00; } else { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $taxes_total = array_sum( $taxes ); } else { $taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) ); } } $return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() ); } else { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); /** * If the customer is exempt from VAT, remove the taxes here. * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. */ if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine. $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $remove_taxes_total = array_sum( $remove_taxes ); } else { $remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) ); } $return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() ); /** * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. */ } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $base_taxes_total = array_sum( $base_taxes ); $modded_taxes_total = array_sum( $modded_taxes ); } else { $base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) ); $modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) ); } $return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() ); } } } return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); } /** * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax excluded, or an empty string if price calculation failed. */ function wc_get_price_excluding_tax( $product, $args = array() ) { if ( ! ( $product instanceof WC_Product ) ) { return ''; } $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : (float) $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; if ( $product->is_taxable() && wc_prices_include_tax() ) { $order = ArrayUtil::get_value_or_default( $args, 'order' ); $customer_id = $order ? $order->get_customer_id() : 0; $tax_rates = false; if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); } elseif ( $customer_id ) { $customer = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id ); $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer ); } elseif ( is_object( $order ) && method_exists( $order, 'get_taxable_location' ) ) { $tax_location = $order->get_taxable_location(); if ( is_array( $tax_location ) && isset( $tax_location['country'] ) ) { $tax_rates = WC_Tax::find_rates( array( 'country' => $tax_location['country'], 'state' => $tax_location['state'] ?? '', 'postcode' => $tax_location['postcode'] ?? '', 'city' => $tax_location['city'] ?? '', 'tax_class' => $product->get_tax_class(), ) ); } } // Fallback if no tax rates were determined. if ( false === $tax_rates ) { $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), null ); } $remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true ); $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. } else { $return_price = $line_price; } return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product ); } /** * Returns the price including or excluding tax. * * By default it's based on the 'woocommerce_tax_display_shop' setting. * Set `$arg['display_context']` to 'cart' to base on the 'woocommerce_tax_display_cart' setting instead. * * @since 3.0.0 * @since 7.6.0 Added `display_context` argument. * * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float */ function wc_get_price_to_display( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => 1, 'price' => $product->get_price(), 'display_context' => 'shop', ) ); $price = $args['price']; $qty = $args['qty']; $tax_display = get_option( 'cart' === $args['display_context'] ? 'woocommerce_tax_display_cart' : 'woocommerce_tax_display_shop' ); return TaxDisplayMode::INCLUSIVE === $tax_display ? wc_get_price_including_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ) : wc_get_price_excluding_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ); } /** * Returns the product categories in a list. * * @since 11.2.0 Added the `$orderby` argument. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @param string $orderby Optional ordering mode. Accepts 'name' to naturally order assigned terms by name, * ignoring the category order termmeta that the default path honors, or 'breadcrumb' * to order assigned terms by ancestry path, comparing category order, natural name, * and term ID at each level of the rendered hierarchy. Any other value preserves * WordPress term-list order; a non-empty unsupported value also triggers a * `_doing_it_wrong()` notice. Default empty string. * @return string|false|WP_Error */ function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '', $orderby = '' ) { if ( ! in_array( $orderby, array( 'name', 'breadcrumb' ), true ) ) { if ( '' !== $orderby ) { wc_doing_it_wrong( __FUNCTION__, sprintf( /* translators: 1: the unsupported ordering mode that was passed. */ __( 'Unsupported $orderby value "%1$s". Use "name", "breadcrumb", or an empty string for WordPress term-list order.', 'woocommerce' ), is_scalar( $orderby ) ? (string) $orderby : gettype( $orderby ) ), '11.2.0' ); } return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); } $terms = get_the_terms( $product_id, 'product_cat' ); if ( is_wp_error( $terms ) ) { return $terms; } if ( empty( $terms ) || ! is_array( $terms ) ) { return false; } $terms = array_values( $terms ); $should_sort = 1 < count( $terms ); if ( 'name' === $orderby && $should_sort ) { usort( $terms, function ( $a, $b ) { $name_comparison = strnatcasecmp( (string) $a->name, (string) $b->name ); return 0 !== $name_comparison ? $name_comparison : $a->term_id <=> $b->term_id; } ); } elseif ( 'breadcrumb' === $orderby && $should_sort ) { $rendered_terms = array(); foreach ( $terms as $term ) { $rendered_terms[ (int) $term->term_id ] = $term; } /* * Resolve ancestors one batched query per hierarchy level rather than one query per * ancestor. The batch is a cache prime, not the ordering data: get_ancestors() below reads * it, and so does get_term_link() further down, because product_cat is registered with a * hierarchical rewrite and therefore resolves each category's ancestry to build its * permalink. Priming here makes the deep leaf-only case cheaper than not ordering at all. */ $known_ids = array_fill_keys( array_keys( $rendered_terms ), true ); $frontier = array(); foreach ( $terms as $term ) { if ( $term->parent ) { $frontier[ (int) $term->parent ] = true; } } // Ancestry is filterable, so bound the walk rather than trusting it to terminate. $remaining_levels = 100; while ( $frontier && $remaining_levels-- > 0 ) { $frontier_ids = array_values( array_diff( array_keys( $frontier ), array_keys( $known_ids ) ) ); if ( ! $frontier_ids ) { break; } $frontier_parents = get_terms( array( 'taxonomy' => 'product_cat', 'include' => $frontier_ids, 'hide_empty' => false, 'fields' => 'id=>parent', 'orderby' => 'include', 'update_term_meta_cache' => false, ) ); /* * A filtered get_terms() can return any shape. Failing to prime is harmless; the * get_ancestors() calls below just query for themselves. */ if ( is_wp_error( $frontier_parents ) || ! is_array( $frontier_parents ) ) { break; } $frontier = array(); foreach ( $frontier_parents as $term_id => $parent_id ) { if ( ! is_numeric( $term_id ) || ! is_numeric( $parent_id ) ) { continue; } $known_ids[ (int) $term_id ] = true; if ( (int) $parent_id ) { $frontier[ (int) $parent_id ] = true; } } } update_termmeta_cache( array_keys( $rendered_terms ) ); $term_orders = array(); $term_paths = array(); foreach ( $terms as $term ) { $term_id = (int) $term->term_id; $term_order = get_term_meta( $term_id, 'order', true ); $term_orders[ $term_id ] = is_numeric( $term_order ) ? (int) $term_order : 0; $ancestor_ids = array_filter( (array) get_ancestors( $term_id, 'product_cat', 'taxonomy' ), 'is_numeric' ); $ancestor_ids = array_map( 'intval', $ancestor_ids ); // Cyclic ancestry can list the term itself among its own ancestors. $ancestor_ids = array_diff( array_unique( $ancestor_ids ), array( $term_id ) ); $path = array_reverse( array_values( $ancestor_ids ) ); $path[] = $term_id; /* * Rank on rendered terms only. Ancestors establish which term sits under which, but a * category that is not in the output never contributes a sort key of its own, so the * reason for the rendered order is always visible on the page. */ $term_paths[ $term_id ] = array_values( array_filter( $path, static function ( $step ) use ( $rendered_terms ) { return isset( $rendered_terms[ $step ] ); } ) ); } usort( $terms, static function ( $a, $b ) use ( $rendered_terms, $term_orders, $term_paths ) { $a_path = $term_paths[ (int) $a->term_id ]; $b_path = $term_paths[ (int) $b->term_id ]; $shared_depth = min( count( $a_path ), count( $b_path ) ); for ( $index = 0; $index < $shared_depth; $index++ ) { $a_term_id = $a_path[ $index ]; $b_term_id = $b_path[ $index ]; if ( $a_term_id === $b_term_id ) { continue; } if ( $term_orders[ $a_term_id ] !== $term_orders[ $b_term_id ] ) { return $term_orders[ $a_term_id ] <=> $term_orders[ $b_term_id ]; } $path_comparison = strnatcasecmp( (string) $rendered_terms[ $a_term_id ]->name, (string) $rendered_terms[ $b_term_id ]->name ); return 0 !== $path_comparison ? $path_comparison : $a_term_id <=> $b_term_id; } return count( $a_path ) <=> count( $b_path ); } ); } $links = array(); foreach ( $terms as $term ) { $link = get_term_link( $term, 'product_cat' ); if ( is_wp_error( $link ) ) { return $link; } $links[] = ''; } /* * Fire core's own term_links-{$taxonomy} hook, so that ordered output stays filterable by exactly * what already filters get_the_term_list(). */ /** This filter is documented in wp-includes/category-template.php */ $term_links = apply_filters( 'term_links-product_cat', $links ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores, WooCommerce.Commenting.CommentHooks.MissingSinceComment /* * Validate what the filter handed back. Core's own path does not, and passing a non-array to * implode() there raises a TypeError. Returning false keeps this path within the documented * string|false|WP_Error contract instead; core's behaviour is deliberately left alone, since * changing it would be a separate compatibility decision. */ if ( is_wp_error( $term_links ) ) { return $term_links; } if ( ! is_array( $term_links ) ) { return false; } return $before . implode( $sep, $term_links ) . $after; } /** * Returns the product tags in a list. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return string */ function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); } /** * Callback for array filter to get visible only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible( $product ) { return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); } /** * Callback for array filter to get visible grouped products only. * * @since 3.1.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible_grouped( $product ) { return $product && is_a( $product, 'WC_Product' ) && $product->is_viewable(); } /** * Callback for array filter to get products the user can edit only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_editable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); } /** * Callback for array filter to get products the user can view only. * * @since 3.4.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_readable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() ); } /** * Sort an array of products by a value. * * @since 3.0.0 * * @param array $products List of products to be ordered. * @param string $orderby Optional order criteria. * @param string $order Ascending or descending order. * * @return array */ function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { $orderby = strtolower( $orderby ); $order = strtolower( $order ); switch ( $orderby ) { case 'title': case 'id': case 'date': case 'modified': case 'menu_order': case 'price': usort( $products, 'wc_products_array_orderby_' . $orderby ); break; case 'none': break; default: shuffle( $products ); break; } if ( 'desc' === $order ) { $products = array_reverse( $products ); } return $products; } /** * Sort by title. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_title( $a, $b ) { return strcasecmp( $a->get_name(), $b->get_name() ); } /** * Sort by id. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_id( $a, $b ) { if ( $a->get_id() === $b->get_id() ) { return 0; } return ( $a->get_id() < $b->get_id() ) ? -1 : 1; } /** * Sort by date. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_date( $a, $b ) { if ( $a->get_date_created() === $b->get_date_created() ) { return 0; } return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; } /** * Sort by modified. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_modified( $a, $b ) { if ( $a->get_date_modified() === $b->get_date_modified() ) { return 0; } return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; } /** * Sort by menu order. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_menu_order( $a, $b ) { if ( $a->get_menu_order() === $b->get_menu_order() ) { return 0; } return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; } /** * Sort by price low to high. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_price( $a, $b ) { if ( $a->get_price() === $b->get_price() ) { return 0; } return ( $a->get_price() < $b->get_price() ) ? -1 : 1; } /** * Queue a product for syncing at the end of the request. * * @param int $product_id Product ID. */ function wc_deferred_product_sync( $product_id ) { global $wc_deferred_product_sync; if ( empty( $wc_deferred_product_sync ) ) { $wc_deferred_product_sync = array(); } $wc_deferred_product_sync[] = $product_id; } /** * See if the lookup table is being generated already. * * @since 3.6.0 * @return bool */ function wc_update_product_lookup_tables_is_running() { $table_updates_pending = WC()->queue()->search( array( 'status' => 'pending', 'group' => 'wc_update_product_lookup_tables', 'per_page' => 1, ) ); return (bool) count( $table_updates_pending ); } /** * Populate lookup table data for products. * * @since 3.6.0 */ function wc_update_product_lookup_tables() { global $wpdb; $is_cli = Constants::is_true( 'WP_CLI' ); $reviews_enabled = wc_reviews_enabled(); // Note that the table is not yet generated. update_option( 'woocommerce_product_lookup_table_is_generating', true ); // Make a row per product in lookup table. $wpdb->query( " INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`) SELECT posts.ID FROM {$wpdb->posts} posts WHERE posts.post_type IN ('product', 'product_variation') " ); // List of column names in the lookup table we need to populate. $columns = array( 'min_max_price', 'stock_quantity', 'sku', 'global_unique_id', 'stock_status', 'average_rating', 'total_sales', 'downloadable', 'virtual', 'onsale', 'tax_class', 'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated. ); foreach ( $columns as $index => $column ) { if ( 'average_rating' === $column && ! $reviews_enabled ) { continue; } if ( $is_cli ) { wc_update_product_lookup_tables_column( $column ); } else { WC()->queue()->schedule_single( time() + $index, 'wc_update_product_lookup_tables_column', array( 'column' => $column, ), 'wc_update_product_lookup_tables' ); } } if ( ! $reviews_enabled ) { return; } // Rating counts are serialised so they have to be unserialised before populating the lookup table. if ( $is_cli ) { $rating_count_rows = $wpdb->get_results( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ", ARRAY_A ); wc_update_product_lookup_tables_rating_count( $rating_count_rows ); } else { WC()->queue()->schedule_single( time() + 10, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => 0, 'limit' => 50, ), 'wc_update_product_lookup_tables' ); } } /** * Populate lookup table column data. * * @since 3.6.0 * @param string $column Column name to set. */ function wc_update_product_lookup_tables_column( $column ) { if ( empty( $column ) ) { return; } if ( 'average_rating' === $column && ! wc_reviews_enabled() ) { return; } global $wpdb; switch ( $column ) { case 'min_max_price': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table INNER JOIN ( SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price FROM {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' WHERE meta1.meta_value <> '' GROUP BY lookup_table.product_id ) as source on source.product_id = lookup_table.product_id SET lookup_table.min_price = source.min_price, lookup_table.max_price = source.max_price " ); break; case 'stock_quantity': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock' SET lookup_table.stock_quantity = meta2.meta_value WHERE meta1.meta_value = 'yes' " ); break; case 'sku': case 'global_unique_id': case 'stock_status': case 'average_rating': case 'total_sales': case 'tax_class': case 'tax_status': if ( 'total_sales' === $column ) { $meta_key = 'total_sales'; } elseif ( 'average_rating' === $column ) { $meta_key = '_wc_average_rating'; } else { $meta_key = '_' . $column; } $column = esc_sql( $column ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s SET lookup_table.`{$column}` = meta.meta_value ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'downloadable': case 'virtual': $column = esc_sql( $column ); $meta_key = '_' . $column; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s SET lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 ) ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'onsale': $column = esc_sql( $column ); $decimals = absint( wc_get_price_decimals() ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price' LEFT JOIN {$wpdb->postmeta} meta3 ON lookup_table.product_id = meta3.post_id AND meta3.meta_key = '_regular_price' SET lookup_table.`{$column}` = IF ( CAST( meta1.meta_value AS DECIMAL ) >= 0 AND CAST( meta2.meta_value AS CHAR ) != '' AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) AND CAST( meta3.meta_value AS DECIMAL( 10, %d ) ) > CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) , 1, 0 ) ", $decimals, $decimals, $decimals, $decimals ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; } // Final column - mark complete. if ( 'tax_status' === $column ) { delete_option( 'woocommerce_product_lookup_table_is_generating' ); } } add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' ); /** * Populate rating count lookup table data for products. * * @since 3.6.0 * @param array $rows Rows of rating counts to update in lookup table. */ function wc_update_product_lookup_tables_rating_count( $rows ) { if ( ! $rows || ! is_array( $rows ) ) { return; } global $wpdb; foreach ( $rows as $row ) { $count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) ); $wpdb->update( $wpdb->wc_product_meta_lookup, array( 'rating_count' => absint( $count ), ), array( 'product_id' => absint( $row['post_id'] ), ) ); } } /** * Populate a batch of rating count lookup table data for products. * * @since 3.6.2 * @param array $offset Offset to query. * @param array $limit Limit to query. */ function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) { global $wpdb; if ( ! $limit || ! wc_reviews_enabled() ) { return; } $rating_count_rows = $wpdb->get_results( $wpdb->prepare( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ORDER BY post_id ASC LIMIT %d, %d ", $offset, $limit ), ARRAY_A ); if ( $rating_count_rows ) { wc_update_product_lookup_tables_rating_count( $rating_count_rows ); WC()->queue()->schedule_single( time() + 1, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => $offset + $limit, 'limit' => $limit, ), 'wc_update_product_lookup_tables' ); } } add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 ); /** * Attach product featured image. Use image filename to match a product sku when product is not provided. * * @since 8.5.0 * @param int $attachment_id Media attachment ID. * @param WC_Product $product Optional product object. * @param bool $save_product If true, the changes in the product will be saved before the method returns. * @return void */ function wc_product_attach_featured_image( $attachment_id, $product = null, $save_product = true ) { $attachment_post = get_post( $attachment_id ); if ( ! $attachment_post ) { return; } if ( null === $product && wc_get_container()->get( MatchImageBySKU::class )->is_enabled() ) { // On upload the attachment post title is the uploaded file's filename. $file_name = pathinfo( $attachment_post->post_title, PATHINFO_FILENAME ); if ( ! $file_name ) { return; } $product_id = wc_get_product_id_by_sku( $file_name ); $product = wc_get_product( $product_id ); } if ( ! $product || ! current_user_can( 'edit_product', $product->get_id() ) ) { return; } $product->set_image_id( $attachment_id ); if ( $save_product ) { $product->save(); } if ( 0 === $attachment_post->post_parent ) { wp_update_post( array( 'ID' => $attachment_id, 'post_parent' => $product->get_id(), ) ); } } add_action( 'add_attachment', 'wc_product_attach_featured_image' ); r_id ); $trigger_reduce = apply_filters( 'woocommerce_payment_complete_reduce_order_stock', ! $stock_reduced, $order_id ); // Only continue if we're reducing stock. if ( ! $trigger_reduce ) { return; } wc_reduce_stock_levels( $order ); // Ensure stock is marked as "reduced" in case payment complete or other stock actions are called. $order->get_data_store()->set_stock_reduced( $order_id, true ); } add_action( 'woocommerce_payment_complete', 'wc_maybe_reduce_stock_levels' ); add_action( 'woocommerce_order_status_completed', 'wc_maybe_reduce_stock_levels' ); add_action( 'woocommerce_order_status_processing', 'wc_maybe_reduce_stock_levels' ); add_action( 'woocommerce_order_status_on-hold', 'wc_maybe_reduce_stock_levels' ); /** * Restore stock for an order that reduced it, when it moves to a status that releases stock * (cancelled, pending, or failed). * * @since 3.0.0 * @param int $order_id Order ID. */ function wc_maybe_increase_stock_levels( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); $trigger_increase = (bool) $stock_reduced; // Only continue if we're increasing stock. if ( ! $trigger_increase ) { return; } wc_increase_stock_levels( $order ); // Ensure stock is not marked as "reduced" anymore. $order->get_data_store()->set_stock_reduced( $order_id, false ); } add_action( 'woocommerce_order_status_cancelled', 'wc_maybe_increase_stock_levels' ); add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); // Restore stock when a stock-reduced order fails. wc_maybe_increase_stock_levels() only acts when // the order carries the `_order_stock_reduced` flag, so it restores an order that reduced stock // (in practice one left on-hold by an async payment such as SEPA/ACH) and does nothing for a // pending -> failed decline that never reduced. Added in 11.0.0. add_action( 'woocommerce_order_status_failed', 'wc_maybe_increase_stock_levels' ); /** * Reduce stock levels for items within an order, if stock has not already been reduced for the items. * * @since 3.0.0 * @param int|WC_Order $order_id Order ID or order instance. */ function wc_reduce_stock_levels( $order_id ) { if ( is_a( $order_id, 'WC_Order' ) ) { $order = $order_id; $order_id = $order->get_id(); } else { $order = wc_get_order( $order_id ); } // We need an order, and a store with stock management to continue. if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_reduce_order_stock', true, $order ) ) { return; } $changes = array(); // Loop over all items. foreach ( $order->get_items() as $item ) { if ( ! $item->is_type( 'line_item' ) ) { continue; } // Only reduce stock once for each item. $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) { continue; } /** * Filter order item quantity. * * @param int|float $quantity Quantity. * @param WC_Order $order Order data. * @param WC_Order_Item_Product $item Order item data. */ $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); $item_name = $product->get_name(); $new_stock = wc_update_product_stock( $product, $qty, 'decrease' ); if ( is_wp_error( $new_stock ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Unable to reduce stock for item %s.', 'woocommerce' ), $item_name ), false, false, array( 'note_group' => OrderNoteGroup::ERROR ) ); continue; } $item->add_meta_data( '_reduced_stock', $qty, true ); $item->save(); $change = array( 'product' => $product, 'from' => $new_stock + $qty, 'to' => $new_stock, ); $changes[] = $change; /** * Fires when stock reduced to a specific line item * * @param WC_Order_Item_Product $item Order item data. * @param array $change Change Details. * @param WC_Order $order Order data. * @since 7.6.0 */ do_action( 'woocommerce_reduce_order_item_stock', $item, $change, $order ); } wc_trigger_stock_change_notifications( $order, $changes ); do_action( 'woocommerce_reduce_order_stock', $order ); } /** * After stock change events, triggers emails and adds order notes. * * @since 3.5.0 * @param WC_Order $order order object. * @param array $changes Array of changes. */ function wc_trigger_stock_change_notifications( $order, $changes ) { if ( empty( $changes ) ) { return; } $order_notes = array(); $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); foreach ( $changes as $change ) { $order_notes[] = $change['product']->get_name() . ' (' . $change['from'] . '→' . $change['to'] . ')'; $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) ); if ( $change['to'] <= $no_stock_amount ) { /** * Action to signal that the value of 'stock_quantity' for a variation is about to change. * * @since 4.9 * * @param int $product The variation whose stock is about to change. */ do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) ); } elseif ( $change['to'] <= $low_stock_amount ) { /** * Action to signal that the value of 'stock_quantity' for a product is about to change. * * @since 4.9 * * @param int $product The product whose stock is about to change. */ do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) ); } if ( $change['to'] < 0 ) { /** * Action fires when an item in an order is backordered. * * @since 3.0 * * @param array $args { * @type WC_Product $product The product that is on backorder. * @type int $order_id The ID of the order. * @type int|float $quantity The amount of product on backorder. * } */ do_action( 'woocommerce_product_on_backorder', array( 'product' => wc_get_product( $change['product']->get_id() ), 'order_id' => $order->get_id(), 'quantity' => abs( $change['from'] - $change['to'] ), ) ); } } $order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . implode( ', ', $order_notes ), false, false, array( 'note_group' => OrderNoteGroup::PRODUCT_STOCK ) ); } /** * Check if a product's stock quantity has reached certain thresholds and trigger appropriate actions. * * This functionality was moved out of `wc_trigger_stock_change_notifications` in order to decouple it from orders, * since stock quantity can also be updated in other ways. * * @param WC_Product $product The product whose stock level has changed. * * @return void */ function wc_trigger_stock_change_actions( $product ) { if ( true !== $product->get_manage_stock() ) { return; } $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); $low_stock_amount = absint( wc_get_low_stock_amount( $product ) ); $stock_quantity = $product->get_stock_quantity(); if ( $stock_quantity <= $no_stock_amount ) { /** * Action fires when a product's stock quantity reaches the "no stock" threshold. * * @since 3.0 * * @param WC_Product $product The product whose stock quantity has changed. */ do_action( 'woocommerce_no_stock', $product ); } elseif ( $stock_quantity <= $low_stock_amount ) { /** * Action fires when a product's stock quantity reaches the "low stock" threshold. * * @since 3.0 * * @param WC_Product $product The product whose stock quantity has changed. */ do_action( 'woocommerce_low_stock', $product ); } } /** * Increase stock levels for items within an order. * * @since 3.0.0 * @param int|WC_Order $order_id Order ID or order instance. */ function wc_increase_stock_levels( $order_id ) { if ( is_a( $order_id, 'WC_Order' ) ) { $order = $order_id; $order_id = $order->get_id(); } else { $order = wc_get_order( $order_id ); } // We need an order, and a store with stock management to continue. if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_restore_order_stock', true, $order ) ) { return; } $changes = array(); // Loop over all items. foreach ( $order->get_items() as $item ) { if ( ! $item->is_type( 'line_item' ) ) { continue; } // Only increase stock once for each item. $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) { continue; } $item_name = $product->get_name(); $new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' ); $old_stock = $new_stock - $item_stock_reduced; if ( is_wp_error( $new_stock ) ) { /* translators: %s item name. */ $order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'woocommerce' ), $item_name ), false, false, array( 'note_group' => OrderNoteGroup::ERROR ) ); continue; } $item->delete_meta_data( '_reduced_stock' ); $item->save(); $changes[] = $item_name . ' (' . $old_stock . '→' . $new_stock . ')'; /** * Fires when stock restored to a specific line item * * @since 9.1.0 * @param WC_Order_Item_Product $item Order item data. * @param int $new_stock New stock. * @param int $old_stock Old stock. * @param WC_Order $order Order data. */ do_action( 'woocommerce_restore_order_item_stock', $item, $new_stock, $old_stock, $order ); } if ( $changes ) { $order->add_order_note( __( 'Stock levels increased:', 'woocommerce' ) . ' ' . implode( ', ', $changes ), false, false, array( 'note_group' => OrderNoteGroup::PRODUCT_STOCK ) ); } do_action( 'woocommerce_restore_order_stock', $order ); } /** * See how much stock is being held in pending orders. * * @since 3.5.0 * @param WC_Product $product Product to check. * @param integer $exclude_order_id Order ID to exclude. * @return int */ function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since 4.3.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return 0; } $reserve_stock = new ReserveStock(); return $reserve_stock->get_reserved_stock( $product, $exclude_order_id ); } /** * Hold stock for an order. * * @throws ReserveStockException If reserve stock fails. * * @since 4.1.0 * @param \WC_Order|int $order Order ID or instance. */ function wc_reserve_stock_for_order( $order ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since @since 4.1.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return; } $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { $reserve_stock = new ReserveStock(); $reserve_stock->reserve_stock_for_order( $order, (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ); } } add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' ); /** * Release held stock for an order. * * @since 4.3.0 * @param \WC_Order|int $order Order ID or instance. */ function wc_release_stock_for_order( $order ) { /** * Filter: woocommerce_hold_stock_for_checkout * Allows enable/disable hold stock functionality on checkout. * * @since 4.3.0 * @param bool $enabled Default to true if managing stock globally. */ if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { return; } $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { $reserve_stock = new ReserveStock(); $reserve_stock->release_stock_for_order( $order ); } } add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' ); add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 ); add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 ); /** * Release coupons used for another order. * * @since 9.5.2 * @param \WC_Order|int $order Order ID or instance. * @param bool $save Save the order after releasing coupons. */ function wc_release_coupons_for_order( $order, bool $save = true ) { $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); if ( $order ) { $order->get_data_store()->release_held_coupons( $order, $save ); } } /** * Return low stock amount to determine if notification needs to be sent * * Since 5.2.0, this function no longer redirects from variation to its parent product. * Low stock amount can now be attached to the variation itself and if it isn't, only * then we check the parent product, and if it's not there, then we take the default * from the store-wide setting. * * @param WC_Product $product Product to get data from. * @since 3.5.0 * @return int */ function wc_get_low_stock_amount( WC_Product $product ) { $low_stock_amount = $product->get_low_stock_amount(); if ( '' === $low_stock_amount && $product->is_type( ProductType::VARIATION ) ) { $parent = wc_get_product( $product->get_parent_id() ); if ( $parent ) { $low_stock_amount = $parent->get_low_stock_amount(); } } if ( '' === $low_stock_amount ) { $low_stock_amount = get_option( 'woocommerce_notify_low_stock_amount', 2 ); } return (int) $low_stock_amount; }