for. Valid values are view and edit. * @return string price */ public function get_sale_price( $context = 'view' ) { return $this->get_prop( 'sale_price', $context ); } /** * Get date on sale from. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_on_sale_from( $context = 'view' ) { return $this->get_prop( 'date_on_sale_from', $context ); } /** * Get date on sale to. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return WC_DateTime|NULL object if the date is set or null if there is no date. */ public function get_date_on_sale_to( $context = 'view' ) { return $this->get_prop( 'date_on_sale_to', $context ); } /** * Get number total of sales. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_total_sales( $context = 'view' ) { return $this->get_prop( 'total_sales', $context ); } /** * Returns the tax status. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_tax_status( $context = 'view' ) { return $this->get_prop( 'tax_status', $context ); } /** * Returns the tax class. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_tax_class( $context = 'view' ) { return $this->get_prop( 'tax_class', $context ); } /** * Return if product manage stock. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return boolean */ public function get_manage_stock( $context = 'view' ) { return $this->get_prop( 'manage_stock', $context ); } /** * Returns number of items available for sale. * * @param string $context What the value is for. Valid values are view and edit. * @return int|null */ public function get_stock_quantity( $context = 'view' ) { return $this->get_prop( 'stock_quantity', $context ); } /** * Return the stock status. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.0.0 * @return string */ public function get_stock_status( $context = 'view' ) { return $this->get_prop( 'stock_status', $context ); } /** * Get backorders. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.0.0 * @return string yes no or notify */ public function get_backorders( $context = 'view' ) { return $this->get_prop( 'backorders', $context ); } /** * Get low stock amount. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.5.0 * @return int|string Returns empty string if value not set */ public function get_low_stock_amount( $context = 'view' ) { return $this->get_prop( 'low_stock_amount', $context ); } /** * Return if should be sold individually. * * @param string $context What the value is for. Valid values are view and edit. * @since 3.0.0 * @return boolean */ public function get_sold_individually( $context = 'view' ) { return $this->get_prop( 'sold_individually', $context ); } /** * Returns the product's weight. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_weight( $context = 'view' ) { return $this->get_prop( 'weight', $context ); } /** * Returns the product length. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_length( $context = 'view' ) { return $this->get_prop( 'length', $context ); } /** * Returns the product width. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_width( $context = 'view' ) { return $this->get_prop( 'width', $context ); } /** * Returns the product height. * * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_height( $context = 'view' ) { return $this->get_prop( 'height', $context ); } /** * Returns formatted dimensions. * * @param bool $formatted True by default for legacy support - will be false/not set in future versions to return the array only. Use wc_format_dimensions for formatted versions instead. * @return string|array */ public function get_dimensions( $formatted = true ) { if ( $formatted ) { wc_deprecated_argument( 'WC_Product::get_dimensions', '3.0', 'By default, get_dimensions has an argument set to true so that HTML is returned. This is to support the legacy version of the method. To get HTML dimensions, instead use wc_format_dimensions() function. Pass false to this method to return an array of dimensions. This will be the new default behavior in future versions.' ); return apply_filters( 'woocommerce_product_dimensions', wc_format_dimensions( $this->get_dimensions( false ) ), $this ); } return array( 'length' => $this->get_length(), 'width' => $this->get_width(), 'height' => $this->get_height(), ); } /** * Get upsell IDs. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_upsell_ids( $context = 'view' ) { return $this->get_prop( 'upsell_ids', $context ); } /** * Get cross sell IDs. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_cross_sell_ids( $context = 'view' ) { return $this->get_prop( 'cross_sell_ids', $context ); } /** * Get parent ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_parent_id( $context = 'view' ) { return $this->get_prop( 'parent_id', $context ); } /** * Return if reviews is allowed. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_reviews_allowed( $context = 'view' ) { return $this->get_prop( 'reviews_allowed', $context ); } /** * Get purchase note. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_purchase_note( $context = 'view' ) { return $this->get_prop( 'purchase_note', $context ); } /** * Returns product attributes. * * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_attributes( $context = 'view' ) { return $this->get_prop( 'attributes', $context ); } /** * Get default attributes. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_default_attributes( $context = 'view' ) { return $this->get_prop( 'default_attributes', $context ); } /** * Get menu order. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_menu_order( $context = 'view' ) { return $this->get_prop( 'menu_order', $context ); } /** * Get post password. * * @since 3.6.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_post_password( $context = 'view' ) { return $this->get_prop( 'post_password', $context ); } /** * Get category ids. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_category_ids( $context = 'view' ) { return $this->get_prop( 'category_ids', $context ); } /** * Get tag ids. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_tag_ids( $context = 'view' ) { return $this->get_prop( 'tag_ids', $context ); } /** * Get virtual. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_virtual( $context = 'view' ) { return $this->get_prop( 'virtual', $context ); } /** * Returns the gallery attachment ids. * * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_gallery_image_ids( $context = 'view' ) { return $this->get_prop( 'gallery_image_ids', $context ); } /** * Get shipping class ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_shipping_class_id( $context = 'view' ) { return $this->get_prop( 'shipping_class_id', $context ); } /** * Get downloads. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return array */ public function get_downloads( $context = 'view' ) { return $this->get_prop( 'downloads', $context ); } /** * Get download expiry. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_download_expiry( $context = 'view' ) { return $this->get_prop( 'download_expiry', $context ); } /** * Get downloadable. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function get_downloadable( $context = 'view' ) { return $this->get_prop( 'downloadable', $context ); } /** * Get download limit. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_download_limit( $context = 'view' ) { return $this->get_prop( 'download_limit', $context ); } /** * Get main image ID. * * @since 3.0.0 * @param string $context What the value is for. Valid values are view and edit. * @return string */ public function get_image_id( $context = 'view' ) { return $this->get_prop( 'image_id', $context ); } /** * Get rating count. * * @param string $context What the value is for. Valid values are view and edit. * @return array of counts */ public function get_rating_counts( $context = 'view' ) { return $this->get_prop( 'rating_counts', $context ); } /** * Get average rating. * * @param string $context What the value is for. Valid values are view and edit. * @return float */ public function get_average_rating( $context = 'view' ) { return $this->get_prop( 'average_rating', $context ); } /** * Get review count. * * @param string $context What the value is for. Valid values are view and edit. * @return int */ public function get_review_count( $context = 'view' ) { return $this->get_prop( 'review_count', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting product data. These should not update anything in the | database itself and should only change what is stored in the class | object. */ /** * Set product name. * * @since 3.0.0 * @param string $name Product name. */ public function set_name( $name ) { $this->set_prop( 'name', $name ); } /** * Set product slug. * * @since 3.0.0 * @param string $slug Product slug. */ public function set_slug( $slug ) { $this->set_prop( 'slug', $slug ); } /** * Set product created date. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_created( $date = null ) { $this->set_date_prop( 'date_created', $date ); } /** * Set product modified date. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_modified( $date = null ) { $this->set_date_prop( 'date_modified', $date ); } /** * Set product status. * * @since 3.0.0 * @param string $status Product status. */ public function set_status( $status ) { $this->set_prop( 'status', $status ); } /** * Set if the product is featured. * * @since 3.0.0 * @param bool|string $featured Whether the product is featured or not. */ public function set_featured( $featured ) { $this->set_prop( 'featured', wc_string_to_bool( $featured ) ); } /** * Set catalog visibility. * * @since 3.0.0 * @throws WC_Data_Exception Throws exception when invalid data is found. * @param string $visibility Options: 'hidden', 'visible', 'search' and 'catalog'. */ public function set_catalog_visibility( $visibility ) { $options = array_keys( wc_get_product_visibility_options() ); $visibility = in_array( $visibility, $options, true ) ? $visibility : strtolower( $visibility ); if ( ! in_array( $visibility, $options, true ) ) { $this->error( 'product_invalid_catalog_visibility', __( 'Invalid catalog visibility option.', 'woocommerce' ) ); } $this->set_prop( 'catalog_visibility', $visibility ); } /** * Set product description. * * @since 3.0.0 * @param string $description Product description. */ public function set_description( $description ) { $this->set_prop( 'description', $description ); } /** * Set product short description. * * @since 3.0.0 * @param string $short_description Product short description. */ public function set_short_description( $short_description ) { $this->set_prop( 'short_description', $short_description ); } /** * Set SKU. * * @since 3.0.0 * @throws WC_Data_Exception Throws exception when invalid data is found. * @param string $sku Product SKU. */ public function set_sku( $sku ) { $sku = (string) $sku; if ( $this->get_object_read() && ! empty( $sku ) && ! wc_product_has_unique_sku( $this->get_id(), $sku ) ) { $sku_found = wc_get_product_id_by_sku( $sku ); $this->error( 'product_invalid_sku', __( 'Invalid or duplicated SKU.', 'woocommerce' ), 400, array( 'resource_id' => $sku_found, 'unique_sku' => wc_product_generate_unique_sku( $this->get_id(), $sku ), ) ); } $this->set_prop( 'sku', $sku ); } /** * Set global_unique_id * * @since 9.1.0 * @param string $global_unique_id Unique ID. */ public function set_global_unique_id( $global_unique_id ) { $global_unique_id = preg_replace( '/[^0-9\-]/', '', (string) $global_unique_id ); if ( $this->get_object_read() && ! empty( $global_unique_id ) && ! wc_product_has_global_unique_id( $this->get_id(), $global_unique_id ) ) { $global_unique_id_found = wc_get_product_id_by_global_unique_id( $global_unique_id ); $this->error( 'product_invalid_global_unique_id', __( 'Invalid or duplicated GTIN, UPC, EAN or ISBN.', 'woocommerce' ), 400, array( 'resource_id' => $global_unique_id_found, ) ); } $this->set_prop( 'global_unique_id', $global_unique_id ); } /** * Set the product's active price. * * @param string $price Price. */ public function set_price( $price ) { $this->set_prop( 'price', wc_format_decimal( $price ) ); } /** * Set the product's regular price. * * @since 3.0.0 * @param string $price Regular price. */ public function set_regular_price( $price ) { $this->set_prop( 'regular_price', wc_format_decimal( $price ) ); } /** * Set the product's sale price. * * @since 3.0.0 * @param string $price sale price. */ public function set_sale_price( $price ) { $this->set_prop( 'sale_price', wc_format_decimal( $price ) ); } /** * Set date on sale from. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_on_sale_from( $date = null ) { $this->set_date_prop( 'date_on_sale_from', $date ); } /** * Set date on sale to. * * @since 3.0.0 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. */ public function set_date_on_sale_to( $date = null ) { $this->set_date_prop( 'date_on_sale_to', $date ); } /** * Set number total of sales. * * @since 3.0.0 * @param int $total Total of sales. */ public function set_total_sales( $total ) { $this->set_prop( 'total_sales', absint( $total ) ); } /** * Set the tax status. * * @since 3.0.0 * @throws WC_Data_Exception Throws exception when invalid data is found. * @param string $status Tax status. */ public function set_tax_status( $status ) { $options = array( ProductTaxStatus::TAXABLE, ProductTaxStatus::SHIPPING, ProductTaxStatus::NONE, ); // Set default if empty. if ( empty( $status ) ) { $status = ProductTaxStatus::TAXABLE; } $status = strtolower( $status ); if ( ! in_array( $status, $options, true ) ) { $this->error( 'product_invalid_tax_status', __( 'Invalid product tax status.', 'woocommerce' ) ); } $this->set_prop( 'tax_status', $status ); } /** * Set the tax class. * * @since 3.0.0 * @param string $class Tax class. */ public function set_tax_class( $class ) { $class = sanitize_title( $class ); $class = 'standard' === $class ? '' : $class; $valid_classes = $this->get_valid_tax_classes(); if ( ! in_array( $class, $valid_classes, true ) ) { $class = ''; } $this->set_prop( 'tax_class', $class ); } /** * Return an array of valid tax classes * * @return array valid tax classes */ protected function get_valid_tax_classes() { return WC_Tax::get_tax_class_slugs(); } /** * Set if product manage stock. * * @since 3.0.0 * @param bool $manage_stock Whether or not manage stock is enabled. */ public function set_manage_stock( $manage_stock ) { $this->set_prop( 'manage_stock', wc_string_to_bool( $manage_stock ) ); } /** * Set number of items available for sale. * * @since 3.0.0 * @param float|null $quantity Stock quantity. */ public function set_stock_quantity( $quantity ) { $this->set_prop( 'stock_quantity', '' !== $quantity ? wc_stock_amount( $quantity ) : null ); } /** * Set stock status. * * @param string $status New status. */ public function set_stock_status( $status = ProductStockStatus::IN_STOCK ) { $valid_statuses = wc_get_product_stock_status_options(); if ( isset( $valid_statuses[ $status ] ) ) { $this->set_prop( 'stock_status', $status ); } else { $this->set_prop( 'stock_status', ProductStockStatus::IN_STOCK ); } } /** * Set backorders. * * @since 3.0.0 * @param string $backorders Options: 'yes', 'no' or 'notify'. */ public function set_backorders( $backorders ) { $this->set_prop( 'backorders', $backorders ); } /** * Set low stock amount. * * @param int|string $amount Empty string if value not set. * @since 3.5.0 */ public function set_low_stock_amount( $amount ) { $this->set_prop( 'low_stock_amount', '' === $amount ? '' : absint( $amount ) ); } /** * Set if should be sold individually. * * @since 3.0.0 * @param bool $sold_individually Whether or not product is sold individually. */ public function set_sold_individually( $sold_individually ) { $this->set_prop( 'sold_individually', wc_string_to_bool( $sold_individually ) ); } /** * Set the product's weight. * * @since 3.0.0 * @param float|string $weight Total weight. */ public function set_weight( $weight ) { $this->set_prop( 'weight', '' === $weight ? '' : wc_format_decimal( $weight ) ); } /** * Set the product length. * * @since 3.0.0 * @param float|string $length Total length. */ public function set_length( $length ) { $this->set_prop( 'length', '' === $length ? '' : wc_format_decimal( $length ) ); } /** * Set the product width. * * @since 3.0.0 * @param float|string $width Total width. */ public function set_width( $width ) { $this->set_prop( 'width', '' === $width ? '' : wc_format_decimal( $width ) ); } /** * Set the product height. * * @since 3.0.0 * @param float|string $height Total height. */ public function set_height( $height ) { $this->set_prop( 'height', '' === $height ? '' : wc_format_decimal( $height ) ); } /** * Set upsell IDs. * * @since 3.0.0 * @param array $upsell_ids IDs from the up-sell products. */ public function set_upsell_ids( $upsell_ids ) { $this->set_prop( 'upsell_ids', array_filter( (array) $upsell_ids ) ); } /** * Set crosssell IDs. * * @since 3.0.0 * @param array $cross_sell_ids IDs from the cross-sell products. */ public function set_cross_sell_ids( $cross_sell_ids ) { $this->set_prop( 'cross_sell_ids', array_filter( (array) $cross_sell_ids ) ); } /** * Set parent ID. * * @since 3.0.0 * @param int $parent_id Product parent ID. */ public function set_parent_id( $parent_id ) { $this->set_prop( 'parent_id', absint( $parent_id ) ); } /** * Set if reviews is allowed. * * @since 3.0.0 * @param bool $reviews_allowed Reviews allowed or not. */ public function set_reviews_allowed( $reviews_allowed ) { $this->set_prop( 'reviews_allowed', wc_string_to_bool( $reviews_allowed ) ); } /** * Set purchase note. * * @since 3.0.0 * @param string $purchase_note Purchase note. */ public function set_purchase_note( $purchase_note ) { $this->set_prop( 'purchase_note', $purchase_note ); } /** * Set product attributes. * * Attributes are made up of: * id - 0 for product level attributes. ID for global attributes. * name - Attribute name. * options - attribute value or array of term ids/names. * position - integer sort order. * visible - If visible on frontend. * variation - If used for variations. * Indexed by unique key to allow clearing old ones after a set. * * @since 3.0.0 * @param array $raw_attributes Array of WC_Product_Attribute objects. */ public function set_attributes( $raw_attributes ) { $attributes = array_fill_keys( array_keys( $this->get_attributes( 'edit' ) ), null ); foreach ( $raw_attributes as $attribute ) { if ( is_a( $attribute, 'WC_Product_Attribute' ) ) { $attributes[ sanitize_title( $attribute->get_name() ) ] = $attribute; } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); $this->set_prop( 'attributes', $attributes ); } /** * Set default attributes. These will be saved as strings and should map to attribute values. * * @since 3.0.0 * @param array $default_attributes List of default attributes. */ public function set_default_attributes( $default_attributes ) { $this->set_prop( 'default_attributes', array_map( 'strval', array_filter( (array) $default_attributes, 'wc_array_filter_default_attributes' ) ) ); } /** * Set menu order. * * @since 3.0.0 * @param int $menu_order Menu order. */ public function set_menu_order( $menu_order ) { $this->set_prop( 'menu_order', intval( $menu_order ) ); } /** * Set post password. * * @since 3.6.0 * @param int $post_password Post password. */ public function set_post_password( $post_password ) { $this->set_prop( 'post_password', $post_password ); } /** * Set the product categories. * * @since 3.0.0 * @param array $term_ids List of terms IDs. */ public function set_category_ids( $term_ids ) { $this->set_prop( 'category_ids', array_unique( array_map( 'intval', $term_ids ) ) ); } /** * Set the product tags. * * @since 3.0.0 * @param array $term_ids List of terms IDs. */ public function set_tag_ids( $term_ids ) { $this->set_prop( 'tag_ids', array_unique( array_map( 'intval', $term_ids ) ) ); } /** * Set if the product is virtual. * * @since 3.0.0 * @param bool|string $virtual Whether product is virtual or not. */ public function set_virtual( $virtual ) { $this->set_prop( 'virtual', wc_string_to_bool( $virtual ) ); } /** * Set shipping class ID. * * @since 3.0.0 * @param int $id Product shipping class id. */ public function set_shipping_class_id( $id ) { $this->set_prop( 'shipping_class_id', absint( $id ) ); } /** * Set if the product is downloadable. * * @since 3.0.0 * @param bool|string $downloadable Whether product is downloadable or not. */ public function set_downloadable( $downloadable ) { $this->set_prop( 'downloadable', wc_string_to_bool( $downloadable ) ); } /** * Set downloads. * * @throws WC_Data_Exception If an error relating to one of the downloads is encountered. * * @param array $downloads_array Array of WC_Product_Download objects or arrays. * * @since 3.0.0 */ public function set_downloads( $downloads_array ) { // When the object is first hydrated, only the previously persisted downloads will be passed in. $existing_downloads = $this->get_object_read() ? (array) $this->get_prop( 'downloads' ) : $downloads_array; $downloads = array(); $errors = array(); $downloads_array = $this->build_downloads_map( $downloads_array ); $existing_downloads = $this->build_downloads_map( $existing_downloads ); foreach ( $downloads_array as $download ) { $download_id = $download->get_id(); $is_new = ! isset( $existing_downloads[ $download_id ] ); $has_changed = ! $is_new && $existing_downloads[ $download_id ]->get_file() !== $downloads_array[ $download_id ]->get_file(); try { $download->check_is_valid( $this->get_object_read() ); $downloads[ $download_id ] = $download; } catch ( Exception $e ) { // We only add error messages for newly added downloads (let's not overwhelm the user if there are // multiple existing files which are problematic). if ( $is_new || $has_changed ) { $errors[] = $e->getMessage(); } // If the problem is with an existing download, disable it. if ( ! $is_new ) { $download->set_enabled( false ); $downloads[ $download_id ] = $download; } } } $this->set_prop( 'downloads', $downloads ); if ( $errors && $this->get_object_read() ) { $this->error( 'product_invalid_download', $errors[0] ); } } /** * Takes an array of downloadable file representations and converts it into an array of * WC_Product_Download objects, indexed by download ID. * * @param array[]|WC_Product_Download[] $downloads Download data to be re-mapped. * * @return WC_Product_Download[] */ private function build_downloads_map( array $downloads ): array { $downloads_map = array(); foreach ( $downloads as $download_data ) { // If the item is already a WC_Product_Download we can add it to the map and move on. if ( is_a( $download_data, 'WC_Product_Download' ) ) { $downloads_map[ $download_data->get_id() ] = $download_data; continue; } // If the item is not an array, there is nothing else we can do (bad data). if ( ! is_array( $download_data ) ) { continue; } // Otherwise, transform the array to a WC_Product_Download and add to the map. $download_object = new WC_Product_Download(); // If we don't have a previous hash, generate UUID for download. if ( empty( $download_data['download_id'] ) ) { $download_data['download_id'] = wp_generate_uuid4(); } $download_object->set_id( $download_data['download_id'] ); $download_object->set_name( $download_data['name'] ); $download_object->set_file( $download_data['file'] ); $download_object->set_enabled( isset( $download_data['enabled'] ) ? $download_data['enabled'] : true ); $downloads_map[ $download_object->get_id() ] = $download_object; } return $downloads_map; } /** * Set download limit. * * @since 3.0.0 * @param int|string $download_limit Product download limit. */ public function set_download_limit( $download_limit ) { $this->set_prop( 'download_limit', -1 === (int) $download_limit || '' === $download_limit ? -1 : absint( $download_limit ) ); } /** * Set download expiry. * * @since 3.0.0 * @param int|string $download_expiry Product download expiry. */ public function set_download_expiry( $download_expiry ) { $this->set_prop( 'download_expiry', -1 === (int) $download_expiry || '' === $download_expiry ? -1 : absint( $download_expiry ) ); } /** * Set gallery attachment ids. * * @since 3.0.0 * @param array $image_ids List of image ids. */ public function set_gallery_image_ids( $image_ids ) { $image_ids = wp_parse_id_list( $image_ids ); $this->set_prop( 'gallery_image_ids', $image_ids ); } /** * Set main image ID. * * @since 3.0.0 * @param int|string $image_id Product image id. */ public function set_image_id( $image_id = '' ) { $this->set_prop( 'image_id', $image_id ); } /** * Set rating counts. Read only. * * @param array $counts Product rating counts. */ public function set_rating_counts( $counts ) { $this->set_prop( 'rating_counts', array_filter( array_map( 'absint', (array) $counts ) ) ); } /** * Set average rating. Read only. * * @param float $average Product average rating. */ public function set_average_rating( $average ) { $this->set_prop( 'average_rating', wc_format_decimal( $average ) ); } /** * Set review count. Read only. * * @param int $count Product review count. */ public function set_review_count( $count ) { $this->set_prop( 'review_count', absint( $count ) ); } /* |-------------------------------------------------------------------------- | Other Methods |-------------------------------------------------------------------------- */ /** * Ensure properties are set correctly before save. * * @since 3.0.0 */ public function validate_props() { // Before updating, ensure stock props are all aligned. Qty, backorders and low stock amount are not needed if not stock managed. if ( ! $this->get_manage_stock() ) { $this->set_stock_quantity( '' ); $this->set_backorders( 'no' ); $this->set_low_stock_amount( '' ); return; } $stock_is_above_notification_threshold = ( (int) $this->get_stock_quantity() > absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ) ); $backorders_are_allowed = ( 'no' !== $this->get_backorders() ); if ( $stock_is_above_notification_threshold ) { $new_stock_status = ProductStockStatus::IN_STOCK; } elseif ( $backorders_are_allowed ) { $new_stock_status = ProductStockStatus::ON_BACKORDER; } else { $new_stock_status = ProductStockStatus::OUT_OF_STOCK; } $this->set_stock_status( $new_stock_status ); } /** * Save data (either create or update depending on if we are working on an existing product). * * @since 3.0.0 * @return int */ public function save() { $this->validate_props(); if ( ! $this->data_store ) { return $this->get_id(); } /** * Trigger action before saving to the DB. Allows you to adjust object props before save. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); $state = $this->before_data_store_save_or_update(); if ( $this->get_id() ) { $changeset = $this->get_changes(); $this->data_store->update( $this ); } else { $changeset = null; $this->data_store->create( $this ); } $this->after_data_store_save_or_update( $state ); // Update attributes lookup table if the product is new OR it's not but there are actually any changes. if ( is_null( $changeset ) || ! empty( $changeset ) ) { wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $this, $changeset ); } /** * Trigger action after saving to the DB. * * @param WC_Data $this The object being saved. * @param WC_Data_Store_WP $data_store THe data store persisting the data. */ do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); return $this->get_id(); } /** * Do any extra processing needed before the actual product save * (but after triggering the 'woocommerce_before_..._object_save' action) * * @return mixed A state value that will be passed to after_data_store_save_or_update. */ protected function before_data_store_save_or_update() { } /** * Do any extra processing needed after the actual product save * (but before triggering the 'woocommerce_after_..._object_save' action) * * @param mixed $state The state object that was returned by before_data_store_save_or_update. */ protected function after_data_store_save_or_update( $state ) { $this->maybe_defer_product_sync(); } /** * Delete the product, set its ID to 0, and return result. * * @param bool $force_delete Should the product be deleted permanently. * @return bool result */ public function delete( $force_delete = false ) { $product_id = $this->get_id(); $deleted = parent::delete( $force_delete ); if ( $deleted ) { $this->maybe_defer_product_sync(); wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_deleted( $product_id ); } return $deleted; } /** * If this is a child product, queue its parent for syncing at the end of the request. */ protected function maybe_defer_product_sync() { $parent_id = $this->get_parent_id(); if ( $parent_id ) { wc_deferred_product_sync( $parent_id ); } } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- */ /** * Check if a product supports a given feature. * * Product classes should override this to declare support (or lack of support) for a feature. * * @param string $feature string The name of a feature to test support for. * @return bool True if the product supports the feature, false otherwise. * @since 2.5.0 */ public function supports( $feature ) { return apply_filters( 'woocommerce_product_supports', in_array( $feature, $this->supports, true ), $feature, $this ); } /** * Returns whether or not the product post exists. * * @return bool */ public function exists() { return false !== $this->get_status(); } /** * Checks the product type. * * Backwards compatibility with downloadable/virtual. * * @param string|array $type Array or string of types. * @return bool */ public function is_type( $type ) { return ( $this->get_type() === $type || ( is_array( $type ) && in_array( $this->get_type(), $type, true ) ) ); } /** * Checks if a product is downloadable. * * @return bool */ public function is_downloadable() { return apply_filters( 'woocommerce_is_downloadable', true === $this->get_downloadable(), $this ); } /** * Checks if a product is virtual (has no shipping). * * @return bool */ public function is_virtual() { return apply_filters( 'woocommerce_is_virtual', true === $this->get_virtual(), $this ); } /** * Returns whether or not the product is featured. * * @return bool */ public function is_featured() { return true === $this->get_featured(); } /** * Check if a product is sold individually (no quantities). * * @return bool */ public function is_sold_individually() { return apply_filters( 'woocommerce_is_sold_individually', true === $this->get_sold_individually(), $this ); } /** * Returns whether or not the product is visible in the catalog. * * @return bool */ public function is_visible() { $visible = $this->is_visible_core(); return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() ); } /** * Returns whether or not the product is visible in the catalog (doesn't trigger filters). * * @return bool */ protected function is_visible_core() { $visible = CatalogVisibility::VISIBLE === $this->get_catalog_visibility() || ( is_search() && CatalogVisibility::SEARCH === $this->get_catalog_visibility() ) || ( ! is_search() && CatalogVisibility::CATALOG === $this->get_catalog_visibility() ); if ( ProductStatus::TRASH === $this->get_status() ) { $visible = false; } elseif ( ProductStatus::PUBLISH !== $this->get_status() && ! current_user_can( 'edit_post', $this->get_id() ) ) { $visible = false; } if ( $this->get_parent_id() ) { $parent_product = wc_get_product( $this->get_parent_id() ); if ( $parent_product && ProductStatus::PUBLISH !== $parent_product->get_status() && ! current_user_can( 'edit_post', $parent_product->get_id() ) ) { $visible = false; } } if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $this->is_in_stock() ) { $visible = false; } return $visible; } /** * Returns false if the product cannot be bought. * * @return bool */ public function is_purchasable() { /** * Filters whether a product is purchasable. * * @since 2.7.0 * @param bool $purchasable Whether the product is purchasable. * @param WC_Product $product Product object. */ return apply_filters( 'woocommerce_is_purchasable', $this->exists() && ( ProductStatus::PUBLISH === $this->get_status() || current_user_can( 'edit_post', $this->get_id() ) ) && '' !== $this->get_price(), $this ); } /** * Returns whether or not the product is on sale. * * @param string $context What the value is for. Valid values are view and edit. * @return bool */ public function is_on_sale( $context = 'view' ) { if ( '' !== (string) $this->get_sale_price( $context ) && $this->get_regular_price( $context ) > $this->get_sale_price( $context ) ) { $on_sale = true; if ( $this->get_date_on_sale_from( $context ) && $this->get_date_on_sale_from( $context )->getTimestamp() > time() ) { $on_sale = false; } if ( $this->get_date_on_sale_to( $context ) && $this->get_date_on_sale_to( $context )->getTimestamp() < time() ) { $on_sale = false; } } else { $on_sale = false; } return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; } /** * Returns whether or not the product has dimensions set. * * @return bool */ public function has_dimensions() { return ( $this->get_length() || $this->get_height() || $this->get_width() ) && ! $this->get_virtual(); } /** * Returns whether or not the product has weight set. * * @return bool */ public function has_weight() { return $this->get_weight() && ! $this->get_virtual(); } /** * Returns whether or not the product can be purchased. * This returns true for 'instock' and 'onbackorder' stock statuses. * * @return bool */ public function is_in_stock() { /** * Filters whether a product is in stock. * * @since 2.7.0 * @param bool $in_stock Whether the product is in stock. * @param WC_Product $product Product object. */ return apply_filters( 'woocommerce_product_is_in_stock', ProductStockStatus::OUT_OF_STOCK !== $this->get_stock_status(), $this ); } /** * Checks if a product needs shipping. * * @return bool */ public function needs_shipping() { return apply_filters( 'woocommerce_product_needs_shipping', ! $this->is_virtual(), $this ); } /** * Returns whether or not the product is taxable. * * @return bool */ public function is_taxable() { /** * Filters whether a product is taxable. * * @since 2.7.0 * @param bool $taxable Whether the product is taxable. * @param WC_Product $product Product object. */ return apply_filters( 'woocommerce_product_is_taxable', $this->get_tax_status() === ProductTaxStatus::TAXABLE && wc_tax_enabled(), $this ); } /** * Returns whether or not the product shipping is taxable. * * @return bool */ public function is_shipping_taxable() { return $this->needs_shipping() && ( $this->get_tax_status() === ProductTaxStatus::TAXABLE || $this->get_tax_status() === ProductTaxStatus::SHIPPING ); } /** * Returns whether or not the product is stock managed. * * @return bool */ public function managing_stock() { if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { return $this->get_manage_stock(); } return false; } /** * Returns whether or not the product can be backordered. * * @return bool */ public function backorders_allowed() { return apply_filters( 'woocommerce_product_backorders_allowed', ( 'yes' === $this->get_backorders() || 'notify' === $this->get_backorders() ), $this->get_id(), $this ); } /** * Returns whether or not the product needs to notify the customer on backorder. * * @return bool */ public function backorders_require_notification() { return apply_filters( 'woocommerce_product_backorders_require_notification', ( $this->managing_stock() && 'notify' === $this->get_backorders() ), $this ); } /** * Check if a product is on backorder. * * @param int $qty_in_cart (default: 0). * @return bool */ public function is_on_backorder( $qty_in_cart = 0 ) { if ( ProductStockStatus::ON_BACKORDER === $this->get_stock_status() ) { return true; } return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_stock_quantity() - $qty_in_cart ) < 0; } /** * Returns whether or not the product has enough stock for the order. * * @param mixed $quantity Quantity of a product added to an order. * @return bool */ public function has_enough_stock( $quantity ) { return ! $this->managing_stock() || $this->backorders_allowed() || $this->get_stock_quantity() >= $quantity; } /** * Returns whether or not the product has any visible attributes. * * @return boolean */ public function has_attributes() { foreach ( $this->get_attributes() as $attribute ) { if ( $attribute->get_visible() ) { return true; } } return false; } /** * Returns whether or not the product has any child product. * * @return bool */ public function has_child() { return 0 < count( $this->get_children() ); } /** * Does a child have dimensions? * * @since 3.0.0 * @return bool */ public function child_has_dimensions() { return false; } /** * Does a child have a weight? * * @since 3.0.0 * @return boolean */ public function child_has_weight() { return false; } /** * Check if downloadable product has a file attached. * * @since 1.6.2 * * @param string $download_id file identifier. * @return bool Whether downloadable product has a file attached. */ public function has_file( $download_id = '' ) { return $this->is_downloadable() && $this->get_file( $download_id ); } /** * Returns whether or not the product has additional options that need * selecting before adding to cart. * * @since 3.0.0 * @return boolean */ public function has_options() { return apply_filters( 'woocommerce_product_has_options', false, $this ); } /* |-------------------------------------------------------------------------- | Non-CRUD Getters |-------------------------------------------------------------------------- */ /** * Get the product's title. For products this is the product name. * * @return string */ public function get_title() { return apply_filters( 'woocommerce_product_title', $this->get_name(), $this ); } /** * Product permalink. * * @return string */ public function get_permalink() { return get_permalink( $this->get_id() ); } /** * Returns the children IDs if applicable. Overridden by child classes. * * @return array of IDs */ public function get_children() { return array(); } /** * If the stock level comes from another product ID, this should be modified. * * @since 3.0.0 * @return int */ public function get_stock_managed_by_id() { return $this->get_id(); } /** * Returns the price in html format. * * @param string $deprecated Deprecated param. * * @return string */ public function get_price_html( $deprecated = '' ) { if ( '' === $this->get_price() ) { $price = apply_filters( 'woocommerce_empty_price_html', '', $this ); } elseif ( $this->is_on_sale() ) { $price = wc_format_sale_price( wc_get_price_to_display( $this, array( 'price' => $this->get_regular_price() ) ), wc_get_price_to_display( $this ) ) . $this->get_price_suffix(); } else { $price = wc_price( wc_get_price_to_display( $this ) ) . $this->get_price_suffix(); } return apply_filters( 'woocommerce_get_price_html', $price, $this ); } /** * Returns the Cost of Goods Sold value in html format. * * @return string */ public function get_cogs_value_html() { $value = $this->get_cogs_total_value(); if ( 0.0 === $value ) { /** * Filter to customize how an empty Cost of Goods Sold value for a product gets rendered to HTML. * * @param string $html The rendered HTML. * @param WC_Product $product The product for which the cost is rendered. * * @since 9.8.0 */ $html = apply_filters( 'woocommerce_empty_cogs_html', '', $this ); } else { $html = wc_price( $value ) . $this->get_price_suffix(); } /** * Filter to customize how the Cost of Goods Sold value for a product gets rendered to HTML. * * @param string $html The rendered HTML. * @param float $value The cost value that is being rendered. * @param WC_Product $product The product for which the cost is rendered. * * @since 9.8.0 */ return apply_filters( 'woocommerce_get_cogs_html', $html, $value, $this ); } /** * Get product name with SKU or ID. Used within admin. * * @return string Formatted product name */ public function get_formatted_name() { if ( $this->get_sku() ) { $identifier = $this->get_sku(); } else { $identifier = '#' . $this->get_id(); } return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ); } /** * Get min quantity which can be purchased at once. * * @since 3.0.0 * @return int */ public function get_min_purchase_quantity() { return 1; } /** * Get max quantity which can be purchased at once. * * @since 3.0.0 * @return int Quantity or -1 if unlimited. */ public function get_max_purchase_quantity() { return $this->is_sold_individually() ? 1 : ( $this->backorders_allowed() || ! $this->managing_stock() ? -1 : $this->get_stock_quantity() ); } /** * Get the add to url used mainly in loops. * * @return string */ public function add_to_cart_url() { return apply_filters( 'woocommerce_product_add_to_cart_url', $this->get_permalink(), $this ); } /** * Get the add to cart button text for the single page. * * @return string */ public function single_add_to_cart_text() { return apply_filters( 'woocommerce_product_single_add_to_cart_text', __( 'Add to cart', 'woocommerce' ), $this ); } /** * Get the aria-describedby description for the add to cart button. * * @return string */ public function add_to_cart_aria_describedby() { /** * Filter the aria-describedby description for the add to cart button. * * @since 7.8.0 * * @param string $var Text for the 'aria-describedby' attribute. * @param WC_Product $this Product object. */ return apply_filters( 'woocommerce_product_add_to_cart_aria_describedby', '', $this ); } /** * Get the add to cart button text. * * @return string */ public function add_to_cart_text() { return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Read more', 'woocommerce' ), $this ); } /** * Get the add to cart button text description - used in aria tags. * * @since 3.3.0 * @return string */ public function add_to_cart_description() { /* translators: %s: Product title */ return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Read more about “%s”', 'woocommerce' ), $this->get_name() ), $this ); } /** * Returns the main product image. * * @param string $size (default: 'woocommerce_thumbnail'). * @param array $attr Image attributes. * @param bool $placeholder True to return $placeholder if no image is found, or false to return an empty string. * @return string */ public function get_image( $size = 'woocommerce_thumbnail', $attr = array(), $placeholder = true ) { $image = ''; if ( $this->get_image_id() ) { $image_alt = get_post_meta( $this->get_image_id(), '_wp_attachment_image_alt', true ); $attr = wp_parse_args( $attr, array( 'alt' => $image_alt ? $image_alt : $this->get_name(), ) ); $image = wp_get_attachment_image( $this->get_image_id(), $size, false, $attr ); } elseif ( $this->get_parent_id() ) { $parent_product = wc_get_product( $this->get_parent_id() ); if ( $parent_product ) { $image = $parent_product->get_image( $size, $attr, $placeholder ); } } if ( ! $image && $placeholder ) { $image = wc_placeholder_img( $size, $attr ); } return apply_filters( 'woocommerce_product_get_image', $image, $this, $size, $attr, $placeholder, $image ); } /** * Returns the product shipping class SLUG. * * @return string */ public function get_shipping_class() { $class_id = $this->get_shipping_class_id(); if ( $class_id ) { $term = get_term_by( 'id', $class_id, 'product_shipping_class' ); if ( $term && ! is_wp_error( $term ) ) { return $term->slug; } } return ''; } /** * Returns a single product attribute as a string. * * @param string $attribute to get. * @return string */ public function get_attribute( $attribute ) { $attributes = $this->get_attributes(); $attribute = sanitize_title( $attribute ); if ( isset( $attributes[ $attribute ] ) ) { $attribute_object = $attributes[ $attribute ]; } elseif ( isset( $attributes[ 'pa_' . $attribute ] ) ) { $attribute_object = $attributes[ 'pa_' . $attribute ]; } else { return ''; } return $attribute_object->is_taxonomy() ? implode( ', ', wc_get_product_terms( $this->get_id(), $attribute_object->get_name(), array( 'fields' => 'names' ) ) ) : wc_implode_text_attributes( $attribute_object->get_options() ); } /** * Get the total amount (COUNT) of ratings, or just the count for one rating e.g. number of 5 star ratings. * * @param int $value Optional. Rating value to get the count for. By default returns the count of all rating values. * @return int */ public function get_rating_count( $value = null ) { $counts = $this->get_rating_counts(); if ( is_null( $value ) ) { return array_sum( $counts ); } elseif ( isset( $counts[ $value ] ) ) { return absint( $counts[ $value ] ); } else { return 0; } } /** * Get a file by $download_id. * * @param string $download_id file identifier. * @return array|false if not found */ public function get_file( $download_id = '' ) { $files = $this->get_downloads(); if ( '' === $download_id ) { $file = count( $files ) ? current( $files ) : false; } elseif ( isset( $files[ $download_id ] ) ) { $file = $files[ $download_id ]; } else { $file = false; } return apply_filters( 'woocommerce_product_file', $file, $this, $download_id ); } /** * Get file download path identified by $download_id. * * @param string $download_id file identifier. * @return string */ public function get_file_download_path( $download_id ) { $files = $this->get_downloads(); $file_path = isset( $files[ $download_id ] ) ? $files[ $download_id ]->get_file() : ''; // allow overriding based on the particular file being requested. return apply_filters( 'woocommerce_product_file_download_path', $file_path, $this, $download_id ); } /** * Get the suffix to display after prices > 0. * * @param string $price to calculate, left blank to just use get_price(). * @param integer $qty passed on to get_price_including_tax() or get_price_excluding_tax(). * @return string */ public function get_price_suffix( $price = '', $qty = 1 ) { $html = ''; $suffix = get_option( 'woocommerce_price_display_suffix' ); if ( $suffix && wc_tax_enabled() && ProductTaxStatus::TAXABLE === $this->get_tax_status() ) { if ( '' === $price ) { $price = $this->get_price(); } $replacements = array( '{price_including_tax}' => wc_price( wc_get_price_including_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine, WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound '{price_excluding_tax}' => wc_price( wc_get_price_excluding_tax( $this, array( 'qty' => $qty, 'price' => $price ) ) ), // @phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound ); $html = str_replace( array_keys( $replacements ), array_values( $replacements ), ' ' . wp_kses_post( $suffix ) . '' ); } return apply_filters( 'woocommerce_get_price_suffix', $html, $this, $price, $qty ); } /** * Returns the availability of the product. * * @return string[] */ public function get_availability() { return apply_filters( 'woocommerce_get_availability', array( 'availability' => $this->get_availability_text(), 'class' => $this->get_availability_class(), ), $this ); } /** * Get availability text based on stock status. * * @return string */ protected function get_availability_text() { if ( ! $this->is_in_stock() ) { $availability = __( 'Out of stock', 'woocommerce' ); } elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) { $availability = $this->backorders_require_notification() ? __( 'Available on backorder', 'woocommerce' ) : ''; } elseif ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) { $availability = __( 'Available on backorder', 'woocommerce' ); } elseif ( $this->managing_stock() ) { $availability = wc_format_stock_for_display( $this ); } else { $availability = ''; } return apply_filters( 'woocommerce_get_availability_text', $availability, $this ); } /** * Get availability classname based on stock status. * * @return string */ protected function get_availability_class() { if ( ! $this->is_in_stock() ) { $class = 'out-of-stock'; } elseif ( ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) || ( ! $this->managing_stock() && $this->is_on_backorder( 1 ) ) ) { $class = 'available-on-backorder'; } else { $class = 'in-stock'; } return apply_filters( 'woocommerce_get_availability_class', $class, $this ); } /** * Set the defined value of the Cost of Goods Sold for this product. * * In this implementation the defined value is a monetary value, but in the future * (or in derived classes) it could be something different like e.g. a percent of the price; * see also get_cogs_effective_value and get_cogs_total_value. * * The defined value can be null. By default this is equivalent to a value of zero, * but again: in the future, or in derived classes, it can mean something different. * See also adjust_cogs_value_before_set. * * WARNING! If the Cost of Goods Sold feature is disabled this method will have no effect. * * @param float|null $value The value to set for this product. */ public function set_cogs_value( ?float $value ): void { if ( $this->cogs_is_enabled( __METHOD__ ) ) { $value = $this->adjust_cogs_value_before_set( $value ); $this->set_prop( 'cogs_value', $value ); } } /** * Adjust the value of the Cost of Goods Sold before actually setting it. * * To disable the conversion of zero into null in a derived class, * override this method with just "return $value;" in the body. * * @param float|null $value Cost value passed to the set_cogs_value method. * @return float|null The actual value that will be set for the cost property. */ protected function adjust_cogs_value_before_set( ?float $value ): ?float { return 0.0 === $value ? null : $value; } /** * Get the defined value of the Cost of Goods Sold for this product. * See set_cogs_value. * * WARNING! If the Cost of Goods Sold feature is disabled this method will always return null. * * @return float The current value for this product. */ public function get_cogs_value(): ?float { if ( ! $this->cogs_is_enabled( __METHOD__ ) ) { return null; } $value = $this->get_prop( 'cogs_value' ); return is_null( $value ) ? null : (float) $value; } /** * Get the effective value of the Cost of Goods Sold for this product. * * The effective value is the defined value once converted to a monetary value; * in the current implementation both values are always equal, but this could change * in the future (or in derived classes). See also get_cogs_effective_value_core * and get_cogs_total_value. * * WARNING! If the Cost of Goods Sold feature is disabled this method will always return zero. * * @return float The effective value for this product. */ public function get_cogs_effective_value(): float { return $this->cogs_is_enabled( __METHOD__ ) ? $this->get_cogs_effective_value_core() : 0; } /** * Core method to get the effective value of the Cost of Goods Sold for this product. * (the final, actual monetary value). * * Derived classes can override this method to provide an alternative way * of calculating the effective value from the defined value, * see for example the WC_Product_Variation class. * * @return float The effective value for this product. */ protected function get_cogs_effective_value_core(): float { return $this->get_cogs_value() ?? 0; } /** * Get the effective total value of the Cost of Goods Sold for this product. * This is the monetary value that will be applied to orders and used for analytics purposes, * see also get_cogs_total_value_core. * * WARNING! If the Cost of Goods Sold feature is disabled this method will always return zero. * * @return float The effective total value for this product. */ public function get_cogs_total_value(): float { if ( ! $this->cogs_is_enabled( __METHOD__ ) ) { return 0; } /** * Filter to customize the total Cost of Goods Sold value that get_cogs_total_value returns for a given product. * * @since 9.5.0 * * @param float $total_value The effective total value of the product. * @param WC_Product $product The product for which the total value is being retrieved. */ return apply_filters( 'woocommerce_get_product_cogs_total_value', $this->get_cogs_total_value_core(), $this ); } /** * Core function to get the effective total value of the Cost of Goods Sold for this product. * * Derived classes can override this method to provide an alternative way * of calculating the total effective value from the single effective value * and/or the defined value. * * @return float The effective total value for this product. */ protected function get_cogs_total_value_core(): float { return $this->get_cogs_effective_value(); } } text. * @return string */ public function get_shipping_total( $context = 'view' ) { return $this->get_prop( 'shipping_total', $context ); } /** * Get shipping_tax. * * @param string $context View or edit context. * @return string */ public function get_shipping_tax( $context = 'view' ) { return $this->get_prop( 'shipping_tax', $context ); } /** * Gets cart tax amount. * * @param string $context View or edit context. * @return float */ public function get_cart_tax( $context = 'view' ) { return $this->get_prop( 'cart_tax', $context ); } /** * Gets order grand total including taxes, shipping cost, fees, and coupon discounts. Used in gateways. * * @param string $context View or edit context. * @return float */ public function get_total( $context = 'view' ) { return $this->get_prop( 'total', $context ); } /** * Get total tax amount. Alias for get_order_tax(). * * @param string $context View or edit context. * @return float */ public function get_total_tax( $context = 'view' ) { return $this->get_prop( 'total_tax', $context ); } /* |-------------------------------------------------------------------------- | Non-CRUD Getters |-------------------------------------------------------------------------- */ /** * Gets the total discount amount. * * @param bool $ex_tax Show discount excl any tax. * @return float */ public function get_total_discount( $ex_tax = true ) { if ( $ex_tax ) { $total_discount = (float) $this->get_discount_total(); } else { $total_discount = (float) $this->get_discount_total() + (float) $this->get_discount_tax(); } return apply_filters( 'woocommerce_order_get_total_discount', NumberUtil::round( $total_discount, WC_ROUNDING_PRECISION ), $this ); } /** * Gets order subtotal. Order subtotal is the price of all items excluding taxes, fees, shipping cost, and coupon discounts. * If sale price is set on an item, the subtotal will include this sale discount. E.g. a product with a regular * price of $100 bought at a 50% discount will represent $50 of the subtotal for the order. * * @return float */ public function get_subtotal() { $subtotal = NumberUtil::round( $this->get_cart_subtotal_for_order(), wc_get_price_decimals() ); return apply_filters( 'woocommerce_order_get_subtotal', (float) $subtotal, $this ); } /** * Get taxes, merged by code, formatted ready for output. * * @return array */ public function get_tax_totals() { $tax_totals = array(); foreach ( $this->get_items( 'tax' ) as $key => $tax ) { $code = $tax->get_rate_code(); if ( ! isset( $tax_totals[ $code ] ) ) { $tax_totals[ $code ] = new stdClass(); $tax_totals[ $code ]->amount = 0; } $tax_totals[ $code ]->id = $key; $tax_totals[ $code ]->rate_id = $tax->get_rate_id(); $tax_totals[ $code ]->is_compound = $tax->is_compound(); $tax_totals[ $code ]->label = $tax->get_label(); $tax_totals[ $code ]->amount += (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total(); $tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount, array( 'currency' => $this->get_currency() ) ); } if ( apply_filters( 'woocommerce_order_hide_zero_taxes', true ) ) { $amounts = array_filter( wp_list_pluck( $tax_totals, 'amount' ) ); $tax_totals = array_intersect_key( $tax_totals, $amounts ); } return apply_filters( 'woocommerce_order_get_tax_totals', $tax_totals, $this ); } /** * Get all valid statuses for this order * * @since 3.0.0 * @return array Internal status keys e.g. 'wc-processing' */ protected function get_valid_statuses() { return array_keys( wc_get_order_statuses() ); } /** * Get user ID. Used by orders, not other order types like refunds. * * @param string $context View or edit context. * @return int */ public function get_user_id( $context = 'view' ) { return 0; } /** * Get user. Used by orders, not other order types like refunds. * * @return WP_User|false */ public function get_user() { return false; } /** * Gets information about whether coupon counts were updated. * * @param string $context What the value is for. Valid values are view and edit. * * @return bool True if coupon counts were updated, false otherwise. */ public function get_recorded_coupon_usage_counts( $context = 'view' ) { return wc_string_to_bool( $this->get_prop( 'recorded_coupon_usage_counts', $context ) ); } /** * Get basic order data in array format. * * @return array */ public function get_base_data() { return array_merge( array( 'id' => $this->get_id() ), $this->data ); } /** * Get info about the card used for payment in the order. * * @return array */ public function get_payment_card_info() { return PaymentInfo::get_card_info( $this ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting order data. These should not update anything in the | database itself and should only change what is stored in the class | object. However, for backwards compatibility pre 3.0.0 some of these | setters may handle both. */ /** * Set parent order ID. * * @since 3.0.0 * @param int $value Value to set. * @throws WC_Data_Exception Exception thrown if parent ID does not exist or is invalid. */ public function set_parent_id( $value ) { if ( $value && ( $value === $this->get_id() || ! wc_get_order( $value ) ) ) { $this->error( 'order_invalid_parent_id', __( 'Invalid parent ID', 'woocommerce' ) ); } $this->set_prop( 'parent_id', absint( $value ) ); } /** * Set order status. * * @since 3.0.0 * @param string $new_status Status to change the order to. No internal wc- prefix is required. * @return array details of change */ public function set_status( $new_status ) { $old_status = $this->get_status(); $new_status = OrderUtil::remove_status_prefix( (string) $new_status ); $status_exceptions = array( OrderStatus::AUTO_DRAFT, OrderStatus::TRASH ); // If setting the status, ensure it's set to a valid status. if ( true === $this->object_read ) { // Only allow valid new status. if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && ! in_array( $new_status, $status_exceptions, true ) ) { $new_status = OrderStatus::PENDING; } // If the old status is set but unknown (e.g. draft) assume its pending for action usage. if ( $old_status && ( OrderStatus::AUTO_DRAFT === $old_status || ( ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && ! in_array( $old_status, $status_exceptions, true ) ) ) ) { $old_status = OrderStatus::PENDING; } } $this->set_prop( 'status', $new_status ); return array( 'from' => $old_status, 'to' => $new_status, ); } /** * Set order_version. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_version( $value ) { $this->set_prop( 'version', $value ); } /** * Set order_currency. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_currency( $value ) { if ( $value && ! in_array( $value, array_keys( get_woocommerce_currencies() ), true ) ) { $this->error( 'order_invalid_currency', __( 'Invalid currency code', 'woocommerce' ) ); } $this->set_prop( 'currency', $value ? $value : get_woocommerce_currency() ); } /** * Set prices_include_tax. * * @param bool $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_prices_include_tax( $value ) { $this->set_prop( 'prices_include_tax', (bool) $value ); } /** * Set date_created. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_date_created( $date = null ) { $this->set_date_prop( 'date_created', $date ); } /** * Set date_modified. * * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_date_modified( $date = null ) { $this->set_date_prop( 'date_modified', $date ); } /** * Set discount_total. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_discount_total( $value ) { $this->set_prop( 'discount_total', wc_format_decimal( $value, false, true ) ); } /** * Set discount_tax. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_discount_tax( $value ) { $this->set_prop( 'discount_tax', wc_format_decimal( $value, false, true ) ); } /** * Set shipping_total. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_shipping_total( $value ) { $this->set_prop( 'shipping_total', wc_format_decimal( $value, false, true ) ); } /** * Set shipping_tax. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_shipping_tax( $value ) { $this->set_prop( 'shipping_tax', wc_format_decimal( $value, false, true ) ); $this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() ); } /** * Set cart tax. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_cart_tax( $value ) { $this->set_prop( 'cart_tax', wc_format_decimal( $value, false, true ) ); $this->set_total_tax( (float) $this->get_cart_tax() + (float) $this->get_shipping_tax() ); } /** * Sets order tax (sum of cart and shipping tax). Used internally only. * * @param string $value Value to set. * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ protected function set_total_tax( $value ) { // We round here because this is a total entry, as opposed to line items in other setters. $this->set_prop( 'total_tax', wc_format_decimal( NumberUtil::round( $value, wc_get_price_decimals() ) ) ); } /** * Set total. * * @param string $value Value to set. * @param string $deprecated Function used to set different totals based on this. * * @return bool|void * @throws WC_Data_Exception Exception may be thrown if value is invalid. */ public function set_total( $value, $deprecated = '' ) { if ( $deprecated ) { wc_deprecated_argument( 'total_type', '3.0', 'Use dedicated total setter methods instead.' ); return $this->legacy_set_total( $value, $deprecated ); } $this->set_prop( 'total', wc_format_decimal( $value, wc_get_price_decimals() ) ); } /** * Stores information about whether the coupon usage were counted. * * @param bool|string $value True if counted, false if not. * * @return void */ public function set_recorded_coupon_usage_counts( $value ) { $this->set_prop( 'recorded_coupon_usage_counts', wc_string_to_bool( $value ) ); } /* |-------------------------------------------------------------------------- | Order Item Handling |-------------------------------------------------------------------------- | | Order items are used for products, taxes, shipping, and fees within | each order. */ /** * Remove all line items (products, coupons, shipping, taxes) from the order. * * @param string $type Order item type. Default null. */ public function remove_order_items( $type = null ) { /** * Trigger action before removing all order line items. Allows you to track order items. * * @param WC_Order $this The current order object. * @param string $type Order item type. Default null. * * @since 7.8.0 */ do_action( 'woocommerce_remove_order_items', $this, $type ); if ( ! empty( $type ) ) { $this->data_store->delete_items( $this, $type ); $group = $this->type_to_group( $type ); if ( $group ) { unset( $this->items[ $group ] ); } } else { $this->data_store->delete_items( $this ); $this->items = array(); } /** * Trigger action after removing all order line items. * * @param WC_Order $this The current order object. * @param string $type Order item type. Default null. * * @since 7.8.0 */ do_action( 'woocommerce_removed_order_items', $this, $type ); } /** * Convert a type to a types group. * * @param string $type type to lookup. * @return string */ protected function type_to_group( $type ) { $type_to_group = apply_filters( 'woocommerce_order_type_to_group', $this->item_types_to_group ); return $type_to_group[ $type ] ?? ''; } /** * Mappings of order item types to groups. * * @var array */ protected array $item_types_to_group = array( 'line_item' => 'line_items', 'tax' => 'tax_lines', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); /** * Return an array of items/products within this order. * * @param string|array $types Types of line items to get (array or string). * @return WC_Order_Item[] */ public function get_items( $types = 'line_item' ) { $items = array(); $types = array_filter( (array) $types ); foreach ( $types as $type ) { $group = $this->type_to_group( $type ); if ( $group ) { if ( ! isset( $this->items[ $group ] ) ) { $this->items[ $group ] = array_filter( $this->data_store->read_items( $this, $type ) ); } // Don't use array_merge here because keys are numeric. $items = $items + $this->items[ $group ]; } } return apply_filters( 'woocommerce_order_get_items', $items, $this, $types ); } /** * Return array of values for calculations. * * @param string $field Field name to return. * * @return array Array of values. */ protected function get_values_for_total( $field ) { $items = array_map( function ( $item ) use ( $field ) { return wc_add_number_precision( $item[ $field ], false ); }, array_values( $this->get_items() ) ); return $items; } /** * Return an array of coupons within this order. * * @since 3.7.0 * @return WC_Order_Item_Coupon[] */ public function get_coupons() { return $this->get_items( 'coupon' ); } /** * Return an array of fees within this order. * * @return WC_Order_item_Fee[] */ public function get_fees() { return $this->get_items( 'fee' ); } /** * Return an array of taxes within this order. * * @return WC_Order_Item_Tax[] */ public function get_taxes() { return $this->get_items( 'tax' ); } /** * Return an array of shipping costs within this order. * * @return WC_Order_Item_Shipping[] */ public function get_shipping_methods() { return $this->get_items( 'shipping' ); } /** * Gets formatted shipping method title. * * @return string */ public function get_shipping_method() { $names = array(); foreach ( $this->get_shipping_methods() as $shipping_method ) { $names[] = $shipping_method->get_name(); } return apply_filters( 'woocommerce_order_shipping_method', implode( ', ', $names ), $this ); } /** * Get used coupon codes only. * * @since 3.7.0 * @return array */ public function get_coupon_codes() { $coupon_codes = array(); $coupons = $this->get_items( 'coupon' ); if ( $coupons ) { foreach ( $coupons as $coupon ) { $coupon_codes[] = $coupon->get_code(); } } return $coupon_codes; } /** * Gets the count of order items of a certain type. * * @param string $item_type Item type to lookup. * @return int|string */ public function get_item_count( $item_type = '' ) { $items = $this->get_items( empty( $item_type ) ? 'line_item' : $item_type ); $count = 0; foreach ( $items as $item ) { $count += $item->get_quantity(); } return apply_filters( 'woocommerce_get_item_count', $count, $item_type, $this ); } /** * Get an order item object, based on its type. * * @since 3.0.0 * @param int $item_id ID of item to get. * @param bool $load_from_db Prior to 3.2 this item was loaded direct from WC_Order_Factory, not this object. This param is here for backwards compatibility with that. If false, uses the local items variable instead. * @return WC_Order_Item|false */ public function get_item( $item_id, $load_from_db = true ) { if ( $load_from_db ) { return WC_Order_Factory::get_order_item( $item_id ); } // Search for item id. if ( $this->items ) { foreach ( $this->items as $group => $items ) { if ( isset( $items[ $item_id ] ) ) { return $items[ $item_id ]; } } } // Load all items of type and cache. $type = $this->data_store->get_order_item_type( $this, $item_id ); if ( ! $type ) { return false; } $items = $this->get_items( $type ); return ! empty( $items[ $item_id ] ) ? $items[ $item_id ] : false; } /** * Get key for where a certain item type is stored in _items. * * @since 3.0.0 * @param string $item object Order item (product, shipping, fee, coupon, tax). * @return string */ protected function get_items_key( $item ) { if ( is_a( $item, 'WC_Order_Item_Product' ) ) { return 'line_items'; } elseif ( is_a( $item, 'WC_Order_Item_Fee' ) ) { return 'fee_lines'; } elseif ( is_a( $item, 'WC_Order_Item_Shipping' ) ) { return 'shipping_lines'; } elseif ( is_a( $item, 'WC_Order_Item_Tax' ) ) { return 'tax_lines'; } elseif ( is_a( $item, 'WC_Order_Item_Coupon' ) ) { return 'coupon_lines'; } return apply_filters( 'woocommerce_get_items_key', '', $item ); } /** * Remove item from the order. * * @param int $item_id Item ID to delete. * @return false|void */ public function remove_item( $item_id ) { $item = $this->get_item( $item_id, false ); $items_key = $item ? $this->get_items_key( $item ) : false; if ( ! $items_key ) { return false; } // Unset and remove later. $this->items_to_delete[] = $item; unset( $this->items[ $items_key ][ $item->get_id() ] ); } /** * Adds an order item to this order. The order item will not persist until save. * * @since 3.0.0 * @param WC_Order_Item $item Order item object (product, shipping, fee, coupon, tax). * @return false|void */ public function add_item( $item ) { $items_key = $this->get_items_key( $item ); if ( ! $items_key ) { return false; } // Make sure existing items are loaded so we can append this new one. if ( ! isset( $this->items[ $items_key ] ) ) { $this->items[ $items_key ] = $this->get_items( $item->get_type() ); } // Set parent. $item->set_order_id( $this->get_id() ); // Append new row with generated temporary ID. $item_id = $item->get_id(); if ( $item_id ) { $this->items[ $items_key ][ $item_id ] = $item; } else { $this->items[ $items_key ][ 'new:' . $items_key . count( $this->items[ $items_key ] ) ] = $item; } } /** * Check and records coupon usage tentatively so that counts validation is correct. Display an error if coupon usage limit has been reached. * * If you are using this method, make sure to `release_held_coupons` in case an Exception is thrown. * * @throws Exception When not able to apply coupon. * * @param string $billing_email Billing email of order. */ public function hold_applied_coupons( $billing_email ) { $held_keys = array(); $held_keys_for_user = array(); $error = null; try { foreach ( WC()->cart->get_applied_coupons() as $code ) { $coupon = new WC_Coupon( $code ); if ( ! $coupon->get_data_store() ) { continue; } // Hold coupon for when global coupon usage limit is present. if ( 0 < $coupon->get_usage_limit() ) { $held_key = $this->hold_coupon( $coupon ); if ( $held_key ) { $held_keys[ $coupon->get_id() ] = $held_key; } } // Hold coupon for when usage limit per customer is enabled. if ( 0 < $coupon->get_usage_limit_per_user() ) { if ( ! isset( $user_ids_and_emails ) ) { $user_alias = get_current_user_id() ? wp_get_current_user()->ID : sanitize_email( $billing_email ); $user_ids_and_emails = $this->get_billing_and_current_user_aliases( $billing_email ); } $held_key_for_user = $this->hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ); if ( $held_key_for_user ) { $held_keys_for_user[ $coupon->get_id() ] = $held_key_for_user; } } } } catch ( Exception $e ) { $error = $e; } finally { // Even in case of error, we will save keys for whatever coupons that were held so our data remains accurate. // We save them in bulk instead of one by one for performance reasons. if ( 0 < count( $held_keys_for_user ) || 0 < count( $held_keys ) ) { $this->get_data_store()->set_coupon_held_keys( $this, $held_keys, $held_keys_for_user ); } if ( $error instanceof Exception ) { throw $error; } } } /** * Hold coupon if a global usage limit is defined. * * @param WC_Coupon $coupon Coupon object. * * @return string Meta key which indicates held coupon. * @throws Exception When can't be held. */ private function hold_coupon( $coupon ) { $result = $coupon->get_data_store()->check_and_hold_coupon( $coupon ); if ( false === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } elseif ( 0 === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'Coupon %s was used in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } return $result; } /** * Hold coupon if usage limit per customer is defined. * * @param WC_Coupon $coupon Coupon object. * @param array $user_ids_and_emails Array of user Id and emails to check for usage limit. * @param string $user_alias User ID or email to use to record current usage. * * @return string Meta key which indicates held coupon. * @throws Exception When coupon can't be held. */ private function hold_coupon_for_users( $coupon, $user_ids_and_emails, $user_alias ) { $result = $coupon->get_data_store()->check_and_hold_coupon_for_user( $coupon, $user_ids_and_emails, $user_alias ); if ( false === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'An unexpected error happened while applying the Coupon %s.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } elseif ( 0 === $result ) { // translators: Actual coupon code. throw new Exception( sprintf( __( 'You have used this coupon %s in another transaction during this checkout, and coupon usage limit is reached. Please remove the coupon and try again.', 'woocommerce' ), esc_html( $coupon->get_code() ) ) ); } return $result; } /** * Helper method to get all aliases for current user and provide billing email. * * @param string $billing_email Billing email provided in form. * * @return array Array of all aliases. * @throws Exception When validation fails. */ private function get_billing_and_current_user_aliases( $billing_email ) { $emails = array( $billing_email ); if ( get_current_user_id() ) { $emails[] = wp_get_current_user()->user_email; } $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); $customer_data_store = WC_Data_Store::load( 'customer' ); $user_ids = $customer_data_store->get_user_ids_for_billing_email( $emails ); return array_merge( $user_ids, $emails ); } /** * Apply a coupon to the order and recalculate totals. * * @since 3.2.0 * @param string|WC_Coupon $raw_coupon Coupon code or object. * @return true|WP_Error True if applied, error if not. */ public function apply_coupon( $raw_coupon ) { if ( is_a( $raw_coupon, 'WC_Coupon' ) ) { $coupon = $raw_coupon; } elseif ( is_string( $raw_coupon ) ) { $code = wc_format_coupon_code( $raw_coupon ); $coupon = new WC_Coupon( $code ); if ( $coupon->get_code() !== $code ) { return new WP_Error( 'invalid_coupon', __( 'Invalid coupon code', 'woocommerce' ) ); } } else { return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); } // Check to make sure coupon is not already applied. $applied_coupons = $this->get_items( 'coupon' ); foreach ( $applied_coupons as $applied_coupon ) { if ( $applied_coupon->get_code() === $coupon->get_code() ) { return new WP_Error( 'invalid_coupon', __( 'Coupon code already applied!', 'woocommerce' ) ); } } $discounts = new WC_Discounts( $this ); $applied = $discounts->apply_coupon( $coupon ); if ( is_wp_error( $applied ) ) { return $applied; } $data_store = $coupon->get_data_store(); // Check specific for guest checkouts here as well since WC_Cart handles that separately in check_customer_coupons. if ( $data_store && 0 === $this->get_customer_id() ) { $usage_count = $data_store->get_usage_by_email( $coupon, $this->get_billing_email() ); if ( 0 < $coupon->get_usage_limit_per_user() && $usage_count >= $coupon->get_usage_limit_per_user() ) { return new WP_Error( 'invalid_coupon', $coupon->get_coupon_error( 106 ), array( 'status' => 400, ) ); } } /** * Action to signal that a coupon has been applied to an order. * * @param WC_Coupon $coupon The applied coupon object. * @param WC_Order $order The current order object. * * @since 7.3 */ do_action( 'woocommerce_order_applied_coupon', $coupon, $this ); $this->set_coupon_discount_amounts( $discounts ); $this->save(); // Recalculate totals and taxes. $this->recalculate_coupons(); // Record usage so counts and validation is correct. $used_by = $this->get_user_id(); if ( ! $used_by ) { $used_by = $this->get_billing_email(); } $order_data_store = $this->get_data_store(); if ( $order_data_store->get_recorded_coupon_usage_counts( $this ) ) { $coupon->increase_usage_count( $used_by ); } wc_update_coupon_usage_counts( $this->get_id() ); return true; } /** * Remove a coupon from the order and recalculate totals. * * Coupons affect line item totals, but there is no relationship between * coupon and line total, so to remove a coupon we need to work from the * line subtotal (price before discount) and re-apply all coupons in this * order. * * Manual discounts are not affected; those are separate and do not affect * stored line totals. * * @since 3.2.0 * @since 7.6.0 Returns a boolean indicating success. * * @param string $code Coupon code. * @return bool TRUE if coupon was removed, FALSE otherwise. */ public function remove_coupon( $code ) { $coupons = $this->get_items( 'coupon' ); // Remove the coupon line. foreach ( $coupons as $item_id => $coupon ) { if ( $coupon->get_code() === $code ) { $this->remove_item( $item_id ); $coupon_object = new WC_Coupon( $code ); $coupon_object->decrease_usage_count( $this->get_user_id() ); $this->recalculate_coupons(); return true; } } return false; } /** * Apply all coupons in this order again to all line items. * This method is public since WooCommerce 3.8.0. * * @since 3.2.0 */ public function recalculate_coupons() { // Reset line item totals. foreach ( $this->get_items() as $item ) { $item->set_total( $item->get_subtotal() ); $item->set_total_tax( $item->get_subtotal_tax() ); } $discounts = new WC_Discounts( $this ); foreach ( $this->get_items( 'coupon' ) as $coupon_item ) { $coupon_code = $coupon_item->get_code(); $coupon_id = wc_get_coupon_id_by_code( $coupon_code ); // If we have a coupon ID (loaded via wc_get_coupon_id_by_code) we can simply load the new coupon object using the ID. if ( $coupon_id ) { $coupon_object = new WC_Coupon( $coupon_id ); } else { // If we do not have a coupon ID (was it virtual? has it been deleted?) we must create a temporary coupon using what data we have stored during checkout. $coupon_object = $this->get_temporary_coupon( $coupon_item ); $coupon_object->set_code( $coupon_code ); $coupon_object->set_virtual( true ); // If there is no coupon amount (maybe dynamic?), set it to the given **discount** amount so the coupon's same value is applied. if ( ! $coupon_object->get_amount() ) { // If the order originally had prices including tax, remove the discount + discount tax. if ( $this->get_prices_include_tax() ) { $coupon_object->set_amount( (float) $coupon_item->get_discount() + (float) $coupon_item->get_discount_tax() ); } else { $coupon_object->set_amount( $coupon_item->get_discount() ); } $coupon_object->set_discount_type( 'fixed_cart' ); } } /** * Allow developers to filter this coupon before it gets re-applied to the order. * * @since 3.2.0 */ $coupon_object = apply_filters( 'woocommerce_order_recalculate_coupons_coupon_object', $coupon_object, $coupon_code, $coupon_item, $this ); if ( $coupon_object ) { $discounts->apply_coupon( $coupon_object, false ); } } $this->set_coupon_discount_amounts( $discounts ); $this->set_item_discount_amounts( $discounts ); // Recalculate totals and taxes. $this->calculate_totals( true ); } /** * Get a coupon object populated from order line item metadata, to be used when reapplying coupons * if the original coupon no longer exists. * * @since 8.7.0 * * @param WC_Order_Item_Coupon $coupon_item The order item corresponding to the coupon to reapply. * @returns WC_Coupon Coupon object populated from order line item metadata, or empty if no such metadata exists (should never happen). */ private function get_temporary_coupon( WC_Order_Item_Coupon $coupon_item ): WC_Coupon { $coupon_object = new WC_Coupon(); // Since WooCommerce 8.7 a succinct 'coupon_info' line item meta entry is created // whenever a coupon is applied to an order. Previously a more verbose 'coupon_data' was created. $coupon_info = $coupon_item->get_meta( 'coupon_info', true ); if ( $coupon_info ) { $coupon_object->set_short_info( $coupon_info ); return $coupon_object; } $coupon_data = $coupon_item->get_meta( 'coupon_data', true ); if ( $coupon_data ) { $coupon_object->set_props( (array) $coupon_data ); } return $coupon_object; } /** * After applying coupons via the WC_Discounts class, update line items. * * @since 3.2.0 * @param WC_Discounts $discounts Discounts class. */ protected function set_item_discount_amounts( $discounts ) { $item_discounts = $discounts->get_discounts_by_item(); $tax_location = $this->get_tax_location(); $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] ); if ( $item_discounts ) { foreach ( $item_discounts as $item_id => $amount ) { $item = $this->get_item( $item_id, false ); // If the prices include tax, discounts should be taken off the tax inclusive prices like in the cart. if ( $this->get_prices_include_tax() && wc_tax_enabled() && ProductTaxStatus::TAXABLE === $item->get_tax_status() ) { $taxes = WC_Tax::calc_tax( $amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), true ); // Use unrounded taxes so totals will be re-calculated accurately, like in cart. $amount = $amount - array_sum( $taxes ); } $item->set_total( max( 0, $item->get_total() - $amount ) ); } } } /** * After applying coupons via the WC_Discounts class, update or create coupon items. * * @since 3.2.0 * @param WC_Discounts $discounts Discounts class. */ protected function set_coupon_discount_amounts( $discounts ) { $coupons = $this->get_items( 'coupon' ); $coupon_code_to_id = wc_list_pluck( $coupons, 'get_id', 'get_code' ); $all_discounts = $discounts->get_discounts(); $coupon_discounts = $discounts->get_discounts_by_coupon(); $tax_location = $this->get_tax_location(); $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'], ); if ( $coupon_discounts ) { foreach ( $coupon_discounts as $coupon_code => $amount ) { $item_id = isset( $coupon_code_to_id[ $coupon_code ] ) ? $coupon_code_to_id[ $coupon_code ] : 0; if ( ! $item_id ) { $coupon_item = new WC_Order_Item_Coupon(); $coupon_item->set_code( $coupon_code ); // Add coupon data. $coupon_id = wc_get_coupon_id_by_code( $coupon_code ); $coupon = new WC_Coupon( $coupon_id ); $coupon_info = $coupon->get_short_info(); $coupon_item->add_meta_data( 'coupon_info', $coupon_info ); } else { $coupon_item = $this->get_item( $item_id, false ); } $discount_tax = 0; // Work out how much tax has been removed as a result of the discount from this coupon. foreach ( $all_discounts[ $coupon_code ] as $item_id => $item_discount_amount ) { $item = $this->get_item( $item_id, false ); if ( ProductTaxStatus::TAXABLE !== $item->get_tax_status() || ! wc_tax_enabled() ) { continue; } $taxes = array_sum( WC_Tax::calc_tax( $item_discount_amount, $this->get_tax_rates( $item->get_tax_class(), $tax_location ), $this->get_prices_include_tax() ) ); if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $taxes = wc_round_tax_total( $taxes ); } $discount_tax += $taxes; if ( $this->get_prices_include_tax() ) { $amount = $amount - $taxes; } } $coupon_item->set_discount( $amount ); $coupon_item->set_discount_tax( $discount_tax ); $this->add_item( $coupon_item ); } } } /** * Add a product line item to the order. This is the only line item type with * its own method because it saves looking up order amounts (costs are added up for you). * * @param WC_Product $product Product object. * @param int $qty Quantity to add. * @param array $args Args for the added product. * @return int */ public function add_product( $product, $qty = 1, $args = array() ) { if ( $product ) { $order = ArrayUtil::get_value_or_default( $args, 'order' ); $total = wc_get_price_excluding_tax( $product, array( 'qty' => $qty, 'order' => $order, ) ); $default_args = array( 'name' => $product->get_name(), 'tax_class' => $product->get_tax_class(), 'product_id' => $product->is_type( ProductType::VARIATION ) ? $product->get_parent_id() : $product->get_id(), 'variation_id' => $product->is_type( ProductType::VARIATION ) ? $product->get_id() : 0, 'variation' => $product->is_type( ProductType::VARIATION ) ? $product->get_attributes() : array(), 'subtotal' => $total, 'total' => $total, 'quantity' => $qty, ); } else { $default_args = array( 'quantity' => $qty, ); } $args = wp_parse_args( $args, $default_args ); // BW compatibility with old args. if ( isset( $args['totals'] ) ) { foreach ( $args['totals'] as $key => $value ) { if ( 'tax' === $key ) { $args['total_tax'] = $value; } elseif ( 'tax_data' === $key ) { $args['taxes'] = $value; } else { $args[ $key ] = $value; } } } $item = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Order_Item_Product::class ); $item->set_props( $args ); $item->set_backorder_meta(); $item->set_order_id( $this->get_id() ); $item->save(); $this->add_item( $item ); wc_do_deprecated_action( 'woocommerce_order_add_product', array( $this->get_id(), $item->get_id(), $product, $qty, $args ), '3.0', 'woocommerce_new_order_item action instead' ); delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' ); return $item->get_id(); } /* |-------------------------------------------------------------------------- | Payment Token Handling |-------------------------------------------------------------------------- | | Payment tokens are hashes used to take payments by certain gateways. | */ /** * Add a payment token to an order * * @since 2.6 * @param WC_Payment_Token $token Payment token object. * @return boolean|int The new token ID or false if it failed. */ public function add_payment_token( $token ) { if ( empty( $token ) || ! ( $token instanceof WC_Payment_Token ) ) { return false; } $token_ids = $this->data_store->get_payment_token_ids( $this ); $token_ids[] = $token->get_id(); $this->data_store->update_payment_token_ids( $this, $token_ids ); do_action( 'woocommerce_payment_token_added_to_order', $this->get_id(), $token->get_id(), $token, $token_ids ); return $token->get_id(); } /** * Returns a list of all payment tokens associated with the current order * * @since 2.6 * @return array An array of payment token objects */ public function get_payment_tokens() { return $this->data_store->get_payment_token_ids( $this ); } /* |-------------------------------------------------------------------------- | Calculations. |-------------------------------------------------------------------------- | | These methods calculate order totals and taxes based on the current data. | */ /** * Calculate shipping total. * * @since 2.2 * @return float */ public function calculate_shipping() { $shipping_total = 0; foreach ( $this->get_shipping_methods() as $shipping ) { $shipping_total += (float) $shipping->get_total(); } $this->set_shipping_total( $shipping_total ); $this->save(); return $this->get_shipping_total(); } /** * Get all tax classes for items in the order. * * @since 2.6.3 * @return array */ public function get_items_tax_classes() { $found_tax_classes = array(); foreach ( $this->get_items() as $item ) { if ( is_callable( array( $item, 'get_tax_status' ) ) && in_array( $item->get_tax_status(), array( ProductTaxStatus::TAXABLE, ProductTaxStatus::SHIPPING ), true ) ) { $found_tax_classes[] = $item->get_tax_class(); } } return array_unique( $found_tax_classes ); } /** * Get tax location for this order. * * @since 3.2.0 * @param array $args array Override the location. * @return array */ protected function get_tax_location( $args = array() ) { $tax_based_on = get_option( 'woocommerce_tax_based_on' ); if ( 'shipping' === $tax_based_on && ! $this->get_shipping_country() ) { $tax_based_on = 'billing'; } $args = wp_parse_args( $args, array( 'country' => 'billing' === $tax_based_on ? $this->get_billing_country() : $this->get_shipping_country(), 'state' => 'billing' === $tax_based_on ? $this->get_billing_state() : $this->get_shipping_state(), 'postcode' => 'billing' === $tax_based_on ? $this->get_billing_postcode() : $this->get_shipping_postcode(), 'city' => 'billing' === $tax_based_on ? $this->get_billing_city() : $this->get_shipping_city(), ) ); /** * Filters whether apply base tax for local pickup shipping method or not. * * @since 6.8.0 * @param boolean apply_base_tax Whether apply base tax for local pickup. Default true. */ $apply_base_tax = true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ); /** * Filters local pickup shipping methods. * * @since 6.8.0 * @param string[] $local_pickup_methods Local pickup shipping method IDs. */ $local_pickup_methods = apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ); $shipping_method_ids = ArrayUtil::select( $this->get_shipping_methods(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD ); // Set shop base address as a tax location if order has local pickup shipping method. if ( $apply_base_tax && count( array_intersect( $shipping_method_ids, $local_pickup_methods ) ) > 0 ) { $tax_based_on = 'base'; } // Default to base. if ( 'base' === $tax_based_on || empty( $args['country'] ) ) { $args['country'] = WC()->countries->get_base_country(); $args['state'] = WC()->countries->get_base_state(); $args['postcode'] = WC()->countries->get_base_postcode(); $args['city'] = WC()->countries->get_base_city(); } return apply_filters( 'woocommerce_order_get_tax_location', $args, $this ); } /** * Public wrapper for exposing get_tax_location() method, enabling 3rd parties to get the tax location for an order. * * @since 7.6.0 * @param array $args array Override the location. * @return array */ public function get_taxable_location( $args = array() ) { return $this->get_tax_location( $args ); } /** * Get tax rates for an order. Use order's shipping or billing address, defaults to base location. * * @param string $tax_class Tax class to get rates for. * @param array $location_args Location to compute rates for. Should be in form: array( country, state, postcode, city). * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`. * * @return mixed|void Tax rates. */ protected function get_tax_rates( $tax_class, $location_args = array(), $customer = null ) { $tax_location = $this->get_tax_location( $location_args ); $tax_location = array( $tax_location['country'], $tax_location['state'], $tax_location['postcode'], $tax_location['city'] ); return WC_Tax::get_rates_from_location( $tax_class, $tax_location, $customer ); } /** * Calculate taxes for all line items and shipping, and store the totals and tax rows. * * If by default the taxes are based on the shipping address and the current order doesn't * have any, it would use the billing address rather than using the Shopping base location. * * Will use the base country unless customer addresses are set. * * @param array $args Added in 3.0.0 to pass things like location. */ public function calculate_taxes( $args = array() ) { do_action( 'woocommerce_order_before_calculate_taxes', $args, $this ); $calculate_tax_for = $this->get_tax_location( $args ); $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); if ( 'inherit' === $shipping_tax_class ) { $found_classes = array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), $this->get_items_tax_classes() ); $shipping_tax_class = count( $found_classes ) ? current( $found_classes ) : false; } $is_vat_exempt = apply_filters( 'woocommerce_order_is_vat_exempt', 'yes' === $this->get_meta( 'is_vat_exempt' ), $this ); // Trigger tax recalculation for all items. foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) { if ( ! $is_vat_exempt ) { $item->calculate_taxes( $calculate_tax_for ); } else { $item->set_taxes( false ); } } foreach ( $this->get_shipping_methods() as $item_id => $item ) { if ( false !== $shipping_tax_class && ! $is_vat_exempt ) { $item->calculate_taxes( array_merge( $calculate_tax_for, array( 'tax_class' => $shipping_tax_class ) ) ); } else { $item->set_taxes( false ); } } $this->update_taxes(); } /** * Calculate fees for all line items. * * @return float Fee total. */ public function get_total_fees() { return array_reduce( $this->get_fees(), function ( $carry, $item ) { return $carry + (float) $item->get_total(); }, 0.0 ); } /** * Update tax lines for the order based on the line item taxes themselves. */ public function update_taxes() { $cart_taxes = array(); $shipping_taxes = array(); $existing_taxes = $this->get_taxes(); $saved_rate_ids = array(); foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) { $taxes = $item->get_taxes(); foreach ( $taxes['total'] as $tax_rate_id => $tax ) { $tax_amount = (float) $this->round_line_tax( $tax, false ); $cart_taxes[ $tax_rate_id ] = isset( $cart_taxes[ $tax_rate_id ] ) ? (float) $cart_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount; } } foreach ( $this->get_shipping_methods() as $item_id => $item ) { $taxes = $item->get_taxes(); foreach ( $taxes['total'] as $tax_rate_id => $tax ) { $tax_amount = (float) $tax; if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $tax_amount = wc_round_tax_total( $tax_amount ); } $shipping_taxes[ $tax_rate_id ] = isset( $shipping_taxes[ $tax_rate_id ] ) ? (float) $shipping_taxes[ $tax_rate_id ] + $tax_amount : $tax_amount; } } foreach ( $existing_taxes as $tax ) { // Remove taxes which no longer exist for cart/shipping. if ( ( ! array_key_exists( $tax->get_rate_id(), $cart_taxes ) && ! array_key_exists( $tax->get_rate_id(), $shipping_taxes ) ) || in_array( $tax->get_rate_id(), $saved_rate_ids, true ) ) { $this->remove_item( $tax->get_id() ); continue; } $saved_rate_ids[] = $tax->get_rate_id(); $tax->set_rate( $tax->get_rate_id() ); $tax->set_tax_total( isset( $cart_taxes[ $tax->get_rate_id() ] ) ? $cart_taxes[ $tax->get_rate_id() ] : 0 ); $tax->set_label( WC_Tax::get_rate_label( $tax->get_rate_id() ) ); $tax->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax->get_rate_id() ] ) ? $shipping_taxes[ $tax->get_rate_id() ] : 0 ); $tax->save(); } $new_rate_ids = wp_parse_id_list( array_diff( array_keys( $cart_taxes + $shipping_taxes ), $saved_rate_ids ) ); // New taxes. foreach ( $new_rate_ids as $tax_rate_id ) { $item = new WC_Order_Item_Tax(); $item->set_rate( $tax_rate_id ); $item->set_tax_total( isset( $cart_taxes[ $tax_rate_id ] ) ? $cart_taxes[ $tax_rate_id ] : 0 ); $item->set_shipping_tax_total( ! empty( $shipping_taxes[ $tax_rate_id ] ) ? $shipping_taxes[ $tax_rate_id ] : 0 ); $this->add_item( $item ); } $this->set_shipping_tax( array_sum( $shipping_taxes ) ); $this->set_cart_tax( array_sum( $cart_taxes ) ); $this->save(); } /** * Helper function. * If you add all items in this order in cart again, this would be the cart subtotal (assuming all other settings are same). * * @return float Cart subtotal. */ protected function get_cart_subtotal_for_order() { return wc_remove_number_precision( $this->get_rounded_items_total( $this->get_values_for_total( 'subtotal' ) ) ); } /** * Helper function. * If you add all items in this order in cart again, this would be the cart total (assuming all other settings are same). * * @return float Cart total. */ protected function get_cart_total_for_order() { return wc_remove_number_precision( $this->get_rounded_items_total( $this->get_values_for_total( 'total' ) ) ); } /** * Calculate totals by looking at the contents of the order. Stores the totals and returns the orders final total. * * @since 2.2 * @param bool $and_taxes Calc taxes if true. * @return float calculated grand total. */ public function calculate_totals( $and_taxes = true ) { do_action( 'woocommerce_order_before_calculate_totals', $and_taxes, $this ); $fees_total = 0; $shipping_total = 0; $cart_subtotal_tax = 0; $cart_total_tax = 0; $cart_subtotal = $this->get_cart_subtotal_for_order(); $cart_total = (float) $this->get_cart_total_for_order(); // Sum shipping costs. foreach ( $this->get_shipping_methods() as $shipping ) { $shipping_total += NumberUtil::round( $shipping->get_total(), wc_get_price_decimals() ); } $this->set_shipping_total( $shipping_total ); // Sum fee costs. foreach ( $this->get_fees() as $item ) { $fee_total = (float) $item->get_total(); if ( 0 > $fee_total ) { $max_discount = NumberUtil::round( $cart_total + $fees_total + $shipping_total, wc_get_price_decimals() ) * -1; if ( $fee_total < $max_discount && 0 > $max_discount ) { $item->set_total( $max_discount ); } } $fees_total += (float) $item->get_total(); } // Calculate taxes for items, shipping, discounts. Note; this also triggers save(). if ( $and_taxes ) { $this->calculate_taxes(); } // Sum taxes again so we can work out how much tax was discounted. This uses original values, not those possibly rounded to 2dp. foreach ( $this->get_items() as $item ) { $taxes = $item->get_taxes(); foreach ( $taxes['total'] as $tax_rate_id => $tax ) { $cart_total_tax += (float) $tax; } foreach ( $taxes['subtotal'] as $tax_rate_id => $tax ) { $cart_subtotal_tax += (float) $tax; } } $this->set_discount_total( NumberUtil::round( $cart_subtotal - $cart_total, wc_get_price_decimals() ) ); $this->set_discount_tax( wc_round_tax_total( $cart_subtotal_tax - $cart_total_tax ) ); $this->set_total( NumberUtil::round( $cart_total + $fees_total + (float) $this->get_shipping_total() + (float) $this->get_cart_tax() + (float) $this->get_shipping_tax(), wc_get_price_decimals() ) ); if ( $this->has_cogs() && $this->cogs_is_enabled() ) { $this->calculate_cogs_total_value(); } do_action( 'woocommerce_order_after_calculate_totals', $and_taxes, $this ); $this->save(); return $this->get_total(); } /** * Get item subtotal - this is the cost before discount. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_item_subtotal( $item, $inc_tax = false, $round = true ) { $subtotal = 0; if ( is_callable( array( $item, 'get_subtotal' ) ) && $item->get_quantity() ) { if ( $inc_tax ) { $subtotal = ( (float) $item->get_subtotal() + (float) $item->get_subtotal_tax() ) / $item->get_quantity(); } else { $subtotal = ( (float) $item->get_subtotal() ) / $item->get_quantity(); } $subtotal = $round ? NumberUtil::round( $subtotal, wc_get_price_decimals() ) : $subtotal; } return apply_filters( 'woocommerce_order_amount_item_subtotal', $subtotal, $this, $item, $inc_tax, $round ); } /** * Get line subtotal - this is the cost before discount. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_line_subtotal( $item, $inc_tax = false, $round = true ) { $subtotal = 0; if ( is_callable( array( $item, 'get_subtotal' ) ) ) { if ( $inc_tax ) { $subtotal = (float) $item->get_subtotal() + (float) $item->get_subtotal_tax(); } else { $subtotal = (float) $item->get_subtotal(); } $subtotal = $round ? NumberUtil::round( $subtotal, wc_get_price_decimals() ) : $subtotal; } return apply_filters( 'woocommerce_order_amount_line_subtotal', $subtotal, $this, $item, $inc_tax, $round ); } /** * Calculate item cost - useful for gateways. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_item_total( $item, $inc_tax = false, $round = true ) { $total = 0; if ( is_callable( array( $item, 'get_total' ) ) && $item->get_quantity() ) { if ( $inc_tax ) { $total = ( (float) $item->get_total() + (float) $item->get_total_tax() ) / $item->get_quantity(); } else { $total = ( (float) $item->get_total() ) / $item->get_quantity(); } $total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total; } return apply_filters( 'woocommerce_order_amount_item_total', $total, $this, $item, $inc_tax, $round ); } /** * Calculate line total - useful for gateways. * * @param object $item Item to get total from. * @param bool $inc_tax (default: false). * @param bool $round (default: true). * @return float */ public function get_line_total( $item, $inc_tax = false, $round = true ) { $total = 0; if ( is_callable( array( $item, 'get_total' ) ) ) { // Check if we need to add line tax to the line total. $total = $inc_tax ? (float) $item->get_total() + (float) $item->get_total_tax() : (float) $item->get_total(); // Check if we need to round. $total = $round ? NumberUtil::round( $total, wc_get_price_decimals() ) : $total; } return apply_filters( 'woocommerce_order_amount_line_total', $total, $this, $item, $inc_tax, $round ); } /** * Get item tax - useful for gateways. * * @param mixed $item Item to get total from. * @param bool $round (default: true). * @return float */ public function get_item_tax( $item, $round = true ) { $tax = 0; if ( is_callable( array( $item, 'get_total_tax' ) ) && $item->get_quantity() ) { $tax = $item->get_total_tax() / $item->get_quantity(); $tax = $round ? wc_round_tax_total( $tax ) : $tax; } return apply_filters( 'woocommerce_order_amount_item_tax', $tax, $item, $round, $this ); } /** * Get line tax - useful for gateways. * * @param mixed $item Item to get total from. * @return float */ public function get_line_tax( $item ) { return apply_filters( 'woocommerce_order_amount_line_tax', is_callable( array( $item, 'get_total_tax' ) ) ? wc_round_tax_total( $item->get_total_tax() ) : 0, $item, $this ); } /** * Gets line subtotal - formatted for display. * * @param object $item Item to get total from. * @param string $tax_display Incl or excl tax display mode. * @return string */ public function get_formatted_line_subtotal( $item, $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); if ( 'excl' === $tax_display ) { $ex_tax_label = $this->get_prices_include_tax() ? 1 : 0; $subtotal = wc_price( $this->get_line_subtotal( $item ), array( 'ex_tax_label' => $ex_tax_label, 'currency' => $this->get_currency(), ) ); } else { $subtotal = wc_price( $this->get_line_subtotal( $item, true ), array( 'currency' => $this->get_currency() ) ); } return apply_filters( 'woocommerce_order_formatted_line_subtotal', $subtotal, $item, $this ); } /** * Gets order total - formatted for display. * * @return string */ public function get_formatted_order_total() { $formatted_total = wc_price( $this->get_total(), array( 'currency' => $this->get_currency() ) ); return apply_filters( 'woocommerce_get_formatted_order_total', $formatted_total, $this ); } /** * Gets subtotal - subtotal is shown before discounts, but with localised taxes. * * @param bool $compound (default: false). * @param string $tax_display (default: the tax_display_cart value). * @return string */ public function get_subtotal_to_display( $compound = false, $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); $subtotal = (float) $this->get_cart_subtotal_for_order(); if ( ! $compound ) { if ( 'incl' === $tax_display ) { $subtotal_taxes = 0; foreach ( $this->get_items() as $item ) { $subtotal_taxes += self::round_line_tax( (float) $item->get_subtotal_tax(), false ); } $subtotal += wc_round_tax_total( $subtotal_taxes ); } $subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) ); if ( 'excl' === $tax_display && $this->get_prices_include_tax() && wc_tax_enabled() ) { $subtotal .= ' ' . WC()->countries->ex_tax_or_vat() . ''; } } else { if ( 'incl' === $tax_display ) { return ''; } // Add Shipping Costs. $subtotal += (float) $this->get_shipping_total(); // Remove non-compound taxes. foreach ( $this->get_taxes() as $tax ) { if ( $tax->is_compound() ) { continue; } $subtotal = $subtotal + (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total(); } // Remove discounts. $subtotal = $subtotal - (float) $this->get_total_discount(); $subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) ); } return apply_filters( 'woocommerce_order_subtotal_to_display', $subtotal, $compound, $this ); } /** * Gets shipping (formatted). * * @param string $tax_display Excl or incl tax display mode. * @return string */ public function get_shipping_to_display( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); if ( 0 < abs( (float) $this->get_shipping_total() ) ) { if ( 'excl' === $tax_display ) { // Show shipping excluding tax. $shipping = wc_price( $this->get_shipping_total(), array( 'currency' => $this->get_currency() ) ); if ( (float) $this->get_shipping_tax() > 0 && $this->get_prices_include_tax() ) { $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', ' ' . WC()->countries->ex_tax_or_vat() . '', $this, $tax_display ); } } else { // Show shipping including tax. $shipping = wc_price( (float) $this->get_shipping_total() + (float) $this->get_shipping_tax(), array( 'currency' => $this->get_currency() ) ); if ( (float) $this->get_shipping_tax() > 0 && ! $this->get_prices_include_tax() ) { $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_tax_label', ' ' . WC()->countries->inc_tax_or_vat() . '', $this, $tax_display ); } } /* translators: %s: method */ $shipping .= apply_filters( 'woocommerce_order_shipping_to_display_shipped_via', ' ' . sprintf( __( 'via %s', 'woocommerce' ), $this->get_shipping_method() ) . '', $this ); } elseif ( $this->get_shipping_method() ) { $shipping = $this->get_shipping_method(); } else { $shipping = __( 'Free!', 'woocommerce' ); } return apply_filters( 'woocommerce_order_shipping_to_display', $shipping, $this, $tax_display ); } /** * Get the discount amount (formatted). * * @since 2.3.0 * @param string $tax_display Excl or incl tax display mode. * @return string */ public function get_discount_to_display( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); /** * Filter the discount amount to display. * * @since 2.7.0. */ return apply_filters( 'woocommerce_order_discount_to_display', wc_price( $this->get_total_discount( 'excl' === $tax_display ), array( 'currency' => $this->get_currency() ) ), $this ); } /** * Add total row for subtotal. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_subtotal_row( &$total_rows, $tax_display ) { $subtotal = $this->get_subtotal_to_display( false, $tax_display ); if ( $subtotal ) { $total_rows['cart_subtotal'] = array( 'type' => 'subtotal', 'label' => __( 'Subtotal:', 'woocommerce' ), 'value' => $subtotal, ); } } /** * Add total row for discounts. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_discount_row( &$total_rows, $tax_display ) { if ( $this->get_total_discount() > 0 ) { $total_rows['discount'] = array( 'type' => 'discount', 'label' => __( 'Discount:', 'woocommerce' ), 'value' => '-' . $this->get_discount_to_display( $tax_display ), ); } } /** * Add total row for shipping. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_shipping_row( &$total_rows, $tax_display ) { if ( $this->get_shipping_method() ) { $total_rows['shipping'] = array( 'type' => 'shipping', 'label' => __( 'Shipping:', 'woocommerce' ), 'value' => $this->get_shipping_to_display( $tax_display ), 'meta' => $this->get_shipping_method(), ); } } /** * Add total row for fees. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_fee_rows( &$total_rows, $tax_display ) { $fees = $this->get_fees(); if ( $fees ) { foreach ( $fees as $id => $fee ) { if ( apply_filters( 'woocommerce_get_order_item_totals_excl_free_fees', empty( $fee['line_total'] ) && empty( $fee['line_tax'] ), $id ) ) { continue; } $total_rows[ 'fee_' . $fee->get_id() ] = array( 'type' => 'fee', 'label' => $fee->get_name() . ':', 'value' => wc_price( 'excl' === $tax_display ? (float) $fee->get_total() : (float) $fee->get_total() + (float) $fee->get_total_tax(), array( 'currency' => $this->get_currency() ) ), ); } } } /** * Add total row for taxes. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_tax_rows( &$total_rows, $tax_display ) { // Tax for tax exclusive prices. if ( 'excl' === $tax_display && wc_tax_enabled() ) { if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { foreach ( $this->get_tax_totals() as $code => $tax ) { $total_rows[ sanitize_title( $code ) ] = array( 'type' => 'tax', 'label' => $tax->label . ':', 'value' => $tax->formatted_amount, ); } } else { $total_rows['tax'] = array( 'type' => 'tax', 'label' => WC()->countries->tax_or_vat() . ':', 'value' => wc_price( $this->get_total_tax(), array( 'currency' => $this->get_currency() ) ), ); } } } /** * Add total row for grand total. * * @param array $total_rows Reference to total rows array. * @param string $tax_display Excl or incl tax display mode. */ protected function add_order_item_totals_total_row( &$total_rows, $tax_display ) { $total_rows['order_total'] = array( 'type' => 'total', 'label' => __( 'Total:', 'woocommerce' ), 'value' => $this->get_formatted_order_total( $tax_display ), ); } /** * Get totals for display on pages and in emails. * * @param mixed $tax_display Excl or incl tax display mode. * @return array */ public function get_order_item_totals( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' ); $total_rows = array(); $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); $this->add_order_item_totals_total_row( $total_rows, $tax_display ); return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- | | Checks if a condition is true or false. | */ /** * Checks the order status against a passed in status. * * @param array|string $status Status to check. * @return bool */ public function has_status( $status ) { return apply_filters( 'woocommerce_order_has_status', ( is_array( $status ) && in_array( $this->get_status(), $status, true ) ) || $this->get_status() === $status, $this, $status ); } /** * Check whether this order has a specific shipping method or not. * * @param string $method_id Method ID to check. * @return bool */ public function has_shipping_method( $method_id ) { foreach ( $this->get_shipping_methods() as $shipping_method ) { if ( strpos( $shipping_method->get_method_id(), $method_id ) === 0 ) { return true; } } return false; } /** * Returns true if the order contains a free product. * * @since 2.5.0 * @return bool */ public function has_free_item() { foreach ( $this->get_items() as $item ) { if ( ! $item->get_total() ) { return true; } } return false; } /** * Get order title. * * @return string Order title. */ public function get_title(): string { if ( method_exists( $this->data_store, 'get_title' ) ) { return $this->data_store->get_title( $this ); } else { return __( 'Order', 'woocommerce' ); } } /** * Indicates if the current order has an associated Cost of Goods Sold value. * * Derived classes representing orders that have a COGS value should override this method to return "true". * * @since 9.5.0 * * @return bool True if this order has an associated Cost of Goods Sold value. */ public function has_cogs() { return false; } /** * Calculate the Cost of Goods Sold value and set it as the actual value for this order. * * @since 9.5.0 * * @return float The calculated value. */ public function calculate_cogs_total_value(): float { if ( ! $this->has_cogs() || ! $this->cogs_is_enabled( __METHOD__ ) ) { return 0; } $cogs_value = $this->calculate_cogs_total_value_core(); /** * Filter to modify the Cost of Goods Sold value that gets calculated for a given order. * * @since 9.5.0 * * @param float $value The value originally calculated. * @param WC_Abstract_Order $order The order for which the value is calculated. */ $cogs_value = apply_filters( 'woocommerce_calculated_order_cogs_value', $cogs_value, $this ); $this->set_cogs_total_value( $cogs_value ); return $cogs_value; } /** * Core method to calculate the Cost of Goods Sold value for this order: * it doesn't check if COGS is enabled at class or system level, doesn't fire hooks, and doesn't set the value as the current one for the order. * * @return float The calculated value. */ protected function calculate_cogs_total_value_core(): float { if ( ! $this->has_cogs() || ! $this->cogs_is_enabled( __METHOD__ ) ) { return 0; } $value = 0; foreach ( array_keys( $this->item_types_to_group ) as $item_type ) { $order_items = $this->get_items( $item_type ); foreach ( $order_items as $item ) { if ( $item->has_cogs() ) { $item->calculate_cogs_value(); $value += $item->get_cogs_value(); } } } return $value; } /** * Get the value of the Cost of Goods Sold for this order. * * WARNING! If the Cost of Goods Sold feature is disabled this method will always return zero. * * @return float The current value for this order. */ public function get_cogs_total_value(): float { return (float) ( $this->has_cogs() && $this->cogs_is_enabled( __METHOD__ ) ? $this->get_prop( 'cogs_total_value' ) : 0 ); } /** * Set the value of the Cost of Goods Sold for this order. * * WARNING! If the Cost of Goods Sold feature is disabled this method will have no effect. * * @param float $value The value to set for this order. * * @internal This method is intended for data store usage only, the value set here will be overridden by calculate_cogs_total_value. */ public function set_cogs_total_value( float $value ) { if ( $this->has_cogs() && $this->cogs_is_enabled( __METHOD__ ) ) { $this->set_prop( 'cogs_total_value', $value ); } } }