diff --git a/includes/abstracts/abstract-wc-order.php b/includes/abstracts/abstract-wc-order.php index a9e77d0c16e..1eba91fc7ff 100644 --- a/includes/abstracts/abstract-wc-order.php +++ b/includes/abstracts/abstract-wc-order.php @@ -1001,22 +1001,20 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } /** - * Calculate taxes for all line items and shipping, and store the totals and tax rows. + * Get tax location for this order. * - * 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 $args array Added in 3.0.0 to pass things like location. + * @since 3.2.0 + * @param $args array Override the location. + * @return array */ - public function calculate_taxes( $args = 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( + $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(), @@ -1032,75 +1030,38 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $args['city'] = ''; } - // Calc taxes for line items + return $args; + } + + /** + * 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() ) { + $calculate_tax_for = $this->get_tax_location( $args ); + $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); + + if ( 'inherit' === $shipping_tax_class ) { + $shipping_tax_class = current( array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), $this->get_items_tax_classes() ) ); + } + + // Trigger tax recalculation for all items. foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) { - $tax_class = $item->get_tax_class(); - $tax_status = $item->get_tax_status(); - - if ( '0' !== $tax_class && 'taxable' === $tax_status && wc_tax_enabled() ) { - $tax_rates = WC_Tax::find_rates( array( - 'country' => $args['country'], - 'state' => $args['state'], - 'postcode' => $args['postcode'], - 'city' => $args['city'], - 'tax_class' => $tax_class, - ) ); - - $total = $item->get_total(); - $taxes = WC_Tax::calc_tax( $total, $tax_rates, false ); - - if ( $item->is_type( 'line_item' ) ) { - $subtotal = $item->get_subtotal(); - $subtotal_taxes = WC_Tax::calc_tax( $subtotal, $tax_rates, false ); - $item->set_taxes( array( 'total' => $taxes, 'subtotal' => $subtotal_taxes ) ); - } else { - $item->set_taxes( array( 'total' => $taxes ) ); - } - } else { - $item->set_taxes( false ); - } + $item->calculate_taxes( $calculate_tax_for ); $item->save(); } - // Calc taxes for shipping foreach ( $this->get_shipping_methods() as $item_id => $item ) { - if ( wc_tax_enabled() ) { - $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); - - // Inherit tax class from items - if ( 'inherit' === $shipping_tax_class ) { - $tax_rates = array(); - $tax_classes = array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ); - $found_tax_classes = $this->get_items_tax_classes(); - - foreach ( $tax_classes as $tax_class ) { - if ( in_array( $tax_class, $found_tax_classes ) ) { - $tax_rates = WC_Tax::find_shipping_rates( array( - 'country' => $args['country'], - 'state' => $args['state'], - 'postcode' => $args['postcode'], - 'city' => $args['city'], - 'tax_class' => $tax_class, - ) ); - break; - } - } - } else { - $tax_rates = WC_Tax::find_shipping_rates( array( - 'country' => $args['country'], - 'state' => $args['state'], - 'postcode' => $args['postcode'], - 'city' => $args['city'], - 'tax_class' => $shipping_tax_class, - ) ); - } - - $item->set_taxes( array( 'total' => WC_Tax::calc_tax( $item->get_total(), $tax_rates, false ) ) ); - } else { - $item->set_taxes( false ); - } + $item->calculate_taxes( array_merge( $calculate_tax_for, array( 'tax_class' => $shipping_tax_class ) ) ); $item->save(); } + $this->update_taxes(); } @@ -1150,7 +1111,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $this->add_item( $item ); } - // Save tax totals + // Save tax totals. $this->set_shipping_tax( WC_Tax::round( array_sum( $shipping_taxes ) ) ); $this->set_cart_tax( WC_Tax::round( array_sum( $cart_taxes ) ) ); $this->save(); @@ -1174,7 +1135,6 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $this->calculate_taxes(); } - // line items foreach ( $this->get_items() as $item ) { $cart_subtotal += $item->get_subtotal(); $cart_total += $item->get_total(); diff --git a/includes/class-wc-totals.php b/includes/class-wc-cart-totals.php similarity index 55% rename from includes/class-wc-totals.php rename to includes/class-wc-cart-totals.php index 9ee7ada3ba5..a12fa3ffd45 100644 --- a/includes/class-wc-totals.php +++ b/includes/class-wc-cart-totals.php @@ -1,8 +1,8 @@ 0, 'fees_total_tax' => 0, 'items_subtotal' => 0, @@ -99,13 +109,84 @@ class WC_Totals { * Sets up the items provided, and calculate totals. * * @since 3.2.0 - * @param object $cart Cart or order object to calculate totals for. + * @param object $cart Cart object to calculate totals for. + * @param object $customer Customer who owns this cart. */ - public function __construct( &$cart = null ) { - $this->precision = pow( 10, wc_get_price_decimals() ); - $this->object = $cart; - $this->set_items(); - $this->calculate(); + public function __construct( &$cart = null, &$customer = null ) { + $this->object = $cart; + $this->calculate_tax = wc_tax_enabled() && ! $customer->get_is_vat_exempt(); + + if ( is_a( $cart, 'WC_Cart' ) ) { + $this->calculate(); + } + } + + /** + * Run all calculations methods on the given items in sequence. + * + * @since 3.2.0 + */ + protected function calculate() { + $this->calculate_item_totals(); + $this->calculate_fee_totals(); + $this->calculate_shipping_totals(); + $this->calculate_totals(); + } + + /** + * Get default blank set of props used per item. + * + * @since 3.2.0 + * @return array + */ + protected function get_default_item_props() { + return (object) array( + 'object' => null, + 'quantity' => 0, + 'product' => false, + 'price_includes_tax' => false, + 'subtotal' => 0, + 'subtotal_tax' => 0, + 'total' => 0, + 'total_tax' => 0, + 'taxes' => array(), + ); + } + + /** + * Get default blank set of props used per fee. + * + * @since 3.2.0 + * @return array + */ + protected function get_default_fee_props() { + return (object) array( + 'total_tax' => 0, + 'taxes' => array(), + ); + } + + /** + * Get default blank set of props used per shipping row. + * + * @since 3.2.0 + * @return array + */ + protected function get_default_shipping_props() { + return (object) array( + 'total' => 0, + 'total_tax' => 0, + 'taxes' => array(), + ); + } + + /** + * Should we round at subtotal level only? + * + * @return bool + */ + protected function round_at_subtotal() { + return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); } /** @@ -121,104 +202,80 @@ class WC_Totals { * * @since 3.2.0 */ - private function set_items() { - if ( is_a( $this->object, 'WC_Cart' ) ) { - foreach ( $this->object->get_cart() as $cart_item_key => $cart_item ) { - $item = $this->get_default_item_props(); - $item->key = $cart_item_key; - $item->cart_item = $cart_item; - $item->quantity = $cart_item['quantity']; - $item->price = $this->add_precision( $cart_item['data']->get_price() ) * $cart_item['quantity']; - $item->product = $cart_item['data']; - $this->items[ $cart_item_key ] = $item; - } + protected function set_items() { + $this->items = array(); + + foreach ( $this->object->get_cart() as $cart_item_key => $cart_item ) { + $item = $this->get_default_item_props(); + $item->object = $cart_item; + $item->price_includes_tax = wc_prices_include_tax(); + $item->quantity = $cart_item['quantity']; + $item->subtotal = wc_add_number_precision_deep( $cart_item['data']->get_price() ) * $cart_item['quantity']; + $item->product = $cart_item['data']; + $item->tax_rates = $this->get_item_tax_rates( $item ); + $this->items[ $cart_item_key ] = $item; } } /** - * Add precision (deep) to a price. + * Get fee objects from the cart. Normalises data + * into the same format for use by this class. * - * @since 3.2.0 - * @param int|array $value Value to remove precision from. - * @return float + * @since 3.2.0 */ - private function add_precision( $value ) { - if ( is_array( $value ) ) { - foreach ( $value as $key => $subvalue ) { - $value[ $key ] = $this->add_precision( $subvalue ); + protected function set_fees() { + $this->fees = array(); + $this->object->calculate_fees(); + + foreach ( $this->object->get_fees() as $fee_key => $fee_object ) { + $fee = $this->get_default_fee_props(); + $fee->object = $fee_object; + $fee->total = wc_add_number_precision_deep( $fee->object->amount ); + + if ( $this->calculate_tax && $fee->object->taxable ) { + $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class, $this->customer ), false ); + $fee->total_tax = array_sum( $fee->taxes ); + + if ( ! $this->round_at_subtotal() ) { + $fee->total_tax = wc_round_tax_total( $fee->total_tax, 0 ); + } } - } else { - $value = $value * $this->precision; + + $this->fees[ $fee_key ] = $fee; } - return $value; } /** - * Remove precision (deep) from a price. + * Get shipping methods from the cart and normalise. * - * @since 3.2.0 - * @param int|array $value Value to remove precision from. - * @return float + * @since 3.2.0 */ - private function remove_precision( $value ) { - if ( is_array( $value ) ) { - foreach ( $value as $key => $subvalue ) { - $value[ $key ] = $this->remove_precision( $subvalue ); + protected function set_shipping() { + $this->shipping = array(); + + foreach ( $this->object->calculate_shipping() as $key => $shipping_object ) { + $shipping_line = $this->get_default_shipping_props(); + $shipping_line->object = $shipping_object; + $shipping_line->total = wc_add_number_precision_deep( $shipping_object->cost ); + $shipping_line->taxes = wc_add_number_precision_deep( $shipping_object->taxes ); + $shipping_line->total_tax = wc_add_number_precision_deep( array_sum( $shipping_object->taxes ) ); + + if ( ! $this->round_at_subtotal() ) { + $shipping_line->total_tax = wc_round_tax_total( $shipping_line->total_tax, 0 ); } - } else { - $value = wc_format_decimal( $value / $this->precision, wc_get_price_decimals() ); + + $this->shipping[ $key ] = $shipping_line; } - return $value; } /** - * Get default blank set of props used per item. + * Return array of coupon objects from the cart. Normalises data + * into the same format for use by this class. * * @since 3.2.0 - * @return array */ - private function get_default_item_props() { - return (object) array( - 'key' => '', - 'quantity' => 0, - 'price' => 0, - 'product' => false, - 'price_includes_tax' => wc_prices_include_tax(), - 'subtotal' => 0, - 'subtotal_tax' => 0, - 'subtotal_taxes' => array(), - 'total' => 0, - 'total_tax' => 0, - 'taxes' => array(), - 'discounted_price' => 0, - ); - } - - /** - * Get default blank set of props used per fee. - * - * @since 3.2.0 - * @return array - */ - private function get_default_fee_props() { - return (object) array( - 'total_tax' => 0, - 'taxes' => array(), - ); - } - - /** - * Get default blank set of props used per shipping row. - * - * @since 3.2.0 - * @return array - */ - private function get_default_shipping_props() { - return (object) array( - 'total' => 0, - 'total_tax' => 0, - 'taxes' => array(), - ); + protected function set_coupons() { + $this->coupons = $this->object->get_coupons(); } /** @@ -231,16 +288,15 @@ class WC_Totals { * @param object $item Item to adjust the prices of. * @return object */ - private function adjust_non_base_location_price( $item ) { + protected function adjust_non_base_location_price( $item ) { $base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->tax_class ); - $item_tax_rates = $this->get_item_tax_rates( $item ); - if ( $item_tax_rates !== $base_tax_rates ) { + if ( $item->tax_rates !== $base_tax_rates ) { // Work out a new base price without the shop's base tax. - $taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true, true ); + $taxes = WC_Tax::calc_tax( $item->subtotal, $base_tax_rates, true, true ); // Now we have a new item price (excluding TAX). - $item->price = $item->price - array_sum( $taxes ); + $item->subtotal = $item->subtotal - array_sum( $taxes ); $item->price_includes_tax = false; } return $item; @@ -250,11 +306,11 @@ class WC_Totals { * Get discounted price of an item with precision (in cents). * * @since 3.2.0 - * @param object $item Item to get the price of. + * @param object $item_key Item to get the price of. * @return int */ - private function get_discounted_price_in_cents( $item ) { - return $item->price - $this->discount_totals[ $item->key ]; + protected function get_discounted_price_in_cents( $item_key ) { + return $this->items[ $item_key ]->subtotal - $this->discount_totals[ $item_key ]; } /** @@ -263,21 +319,9 @@ class WC_Totals { * @param object $item Item to get tax rates for. * @return array of taxes */ - private function get_item_tax_rates( $item ) { + protected function get_item_tax_rates( $item ) { $tax_class = $item->product->get_tax_class(); - return isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class() ); - } - - /** - * Return array of coupon objects from the cart or an order. - * - * @since 3.2.0 - * @return array - */ - private function get_coupons() { - if ( is_a( $this->object, 'WC_Cart' ) ) { - return $this->object->get_coupons(); - } + return isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class(), $this->customer ); } /** @@ -300,7 +344,7 @@ class WC_Totals { * @param string $key Total name you want to set. * @param int $total Total to set. */ - private function set_total( $key = 'total', $total ) { + protected function set_total( $key = 'total', $total ) { $this->totals[ $key ] = $total; } @@ -312,7 +356,7 @@ class WC_Totals { * @return array. */ public function get_totals( $in_cents = false ) { - return $in_cents ? $this->totals : array_map( array( $this, 'remove_precision' ), $this->totals ); + return $in_cents ? $this->totals : wc_remove_number_precision_deep( $this->totals ); } /** @@ -321,7 +365,7 @@ class WC_Totals { * @since 3.2.0 * @return array */ - private function get_merged_taxes() { + protected function get_merged_taxes() { $taxes = array(); foreach ( array_merge( $this->items, $this->fees, $this->shipping ) as $item ) { @@ -351,17 +395,53 @@ class WC_Totals { */ /** - * Run all calculations methods on the given items in sequence. + * Calculate item totals. * * @since 3.2.0 */ - private function calculate() { + protected function calculate_item_totals() { + $this->set_items(); $this->calculate_item_subtotals(); $this->calculate_discounts(); - $this->calculate_item_totals(); - $this->calculate_fee_totals(); - $this->calculate_shipping_totals(); - $this->calculate_totals(); + + foreach ( $this->items as $item_key => $item ) { + $item->total = $this->get_discounted_price_in_cents( $item_key ); + $item->total_tax = 0; + + if ( has_filter( 'woocommerce_get_discounted_price' ) ) { + /** + * Allow plugins to filter this price like in the legacy cart class. + * + * This is legacy and should probably be deprecated in the future. + * $item->object is the cart item object. + * $this->object is the cart object. + */ + $item->total = wc_add_number_precision( + apply_filters( 'woocommerce_get_discounted_price', wc_remove_number_precision( $item->total ), $item->object, $this->object ) + ); + } + + if ( $this->calculate_tax && $item->product->is_taxable() ) { + $item->taxes = WC_Tax::calc_tax( $item->total, $item->tax_rates, $item->price_includes_tax ); + $item->total_tax = array_sum( $item->taxes ); + + if ( ! $this->round_at_subtotal() ) { + $item->total_tax = wc_round_tax_total( $item->total_tax, 0 ); + } + + if ( $item->price_includes_tax ) { + $item->total = $item->total - $item->total_tax; + } else { + $item->total = $item->total; + } + } + } + + $this->set_total( 'items_total', array_sum( array_values( wp_list_pluck( $this->items, 'total' ) ) ) ); + $this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) ); + + $this->object->subtotal = $this->get_total( 'items_total' ) + $this->get_total( 'items_total_tax' ); + $this->object->subtotal_ex_tax = $this->get_total( 'items_total' ); } /** @@ -378,18 +458,18 @@ class WC_Totals { * * @since 3.2.0 */ - private function calculate_item_subtotals() { + protected function calculate_item_subtotals() { foreach ( $this->items as $item ) { if ( $item->price_includes_tax && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $item = $this->adjust_non_base_location_price( $item ); } + if ( $this->calculate_tax && $item->product->is_taxable() ) { + $subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $item->tax_rates, $item->price_includes_tax ); + $item->subtotal_tax = array_sum( $subtotal_taxes ); - $item->subtotal = $item->price; - $item->subtotal_tax = 0; - - if ( wc_tax_enabled() && $item->product->is_taxable() ) { - $item->subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $this->get_item_tax_rates( $item ), $item->price_includes_tax ); - $item->subtotal_tax = array_sum( $item->subtotal_taxes ); + if ( ! $this->round_at_subtotal() ) { + $item->subtotal_tax = wc_round_tax_total( $item->subtotal_tax, 0 ); + } if ( $item->price_includes_tax ) { $item->subtotal = $item->subtotal - $item->subtotal_tax; @@ -398,21 +478,23 @@ class WC_Totals { } $this->set_total( 'items_subtotal', array_sum( array_values( wp_list_pluck( $this->items, 'subtotal' ) ) ) ); $this->set_total( 'items_subtotal_tax', array_sum( array_values( wp_list_pluck( $this->items, 'subtotal_tax' ) ) ) ); + + $this->object->subtotal = $this->get_total( 'items_total' ) + $this->get_total( 'items_total_tax' ); + $this->object->subtotal_ex_tax = $this->get_total( 'items_total' ); } /** * Calculate all discount and coupon amounts. * - * @todo Manual discounts. - * @todo record coupon totals and counts for cart. - * * @since 3.2.0 * @uses WC_Discounts class. */ - private function calculate_discounts() { + protected function calculate_discounts() { + $this->set_coupons(); + $discounts = new WC_Discounts( $this->items ); - foreach ( $this->get_coupons() as $coupon ) { + foreach ( $this->coupons as $coupon ) { $discounts->apply_coupon( $coupon ); } @@ -420,44 +502,19 @@ class WC_Totals { $this->totals['discounts_total'] = array_sum( $this->discount_totals ); // See how much tax was 'discounted'. - if ( wc_tax_enabled() ) { + if ( $this->calculate_tax ) { foreach ( $this->discount_totals as $cart_item_key => $discount ) { $item = $this->items[ $cart_item_key ]; if ( $item->product->is_taxable() ) { - $tax_rates = $this->get_item_tax_rates( $item ); - $taxes = WC_Tax::calc_tax( $discount, $tax_rates, false ); - $this->totals['discounts_tax_total'] += array_sum( $taxes ); + $taxes = WC_Tax::calc_tax( $discount, $item->tax_rates, false ); + $this->totals['discounts_tax_total'] += $this->round_at_subtotal() ? array_sum( $taxes ) : wc_round_tax_total( array_sum( $taxes ), 0 ); } } } - } - /** - * Totals are costs after discounts. @todo move cart specific setters to subclass? - * - * @since 3.2.0 - */ - private function calculate_item_totals() { - foreach ( $this->items as $item ) { - $item->total = $this->get_discounted_price_in_cents( $item ); - $item->total_tax = 0; - - if ( wc_tax_enabled() && $item->product->is_taxable() ) { - $item->taxes = WC_Tax::calc_tax( $item->total, $this->get_item_tax_rates( $item ), $item->price_includes_tax ); - $item->total_tax = array_sum( $item->taxes ); - - if ( $item->price_includes_tax ) { - $item->total = $item->total - $item->total_tax; - } else { - $item->total = $item->total; - } - } - } - $this->set_total( 'items_total', array_sum( array_values( wp_list_pluck( $this->items, 'total' ) ) ) ); - $this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) ); - - $this->object->subtotal = $this->get_total( 'items_total' ) + $this->get_total( 'items_total_tax' ); - $this->object->subtotal_ex_tax = $this->get_total( 'items_total' ); + $applied_coupons = $discounts->get_applied_coupons(); + $this->object->coupon_discount_amounts = wp_list_pluck( $applied_coupons, 'discount' ); + $this->object->coupon_discount_tax_amounts = wp_list_pluck( $applied_coupons, 'discount_tax' ); } /** @@ -466,35 +523,17 @@ class WC_Totals { * Note: This class sets the totals for the 'object' as they are calculated. This is so that APIs like the fees API can see these totals if needed. * * @since 3.2.0 - * @todo logic is unqiue to carts. */ - private function calculate_fee_totals() { - $this->fees = array(); - $this->object->calculate_fees(); - - foreach ( $this->object->get_fees() as $fee_key => $fee_object ) { - $fee = $this->get_default_fee_props(); - $fee->object = $fee_object; - $fee->total = $this->add_precision( $fee->object->amount ); - - if ( wc_tax_enabled() && $fee->object->taxable ) { - $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class ), false ); - $fee->total_tax = array_sum( $fee->taxes ); - } - - $this->fees[ $fee_key ] = $fee; - } - - // Store totals to self. + protected function calculate_fee_totals() { + $this->set_fees(); $this->set_total( 'fees_total', array_sum( wp_list_pluck( $this->fees, 'total' ) ) ); $this->set_total( 'fees_total_tax', array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ); - // Transfer totals to the cart. foreach ( $this->fees as $fee_key => $fee ) { - $this->object->fees[ $fee_key ]->tax = $this->remove_precision( $fee->total_tax ); - $this->object->fees[ $fee_key ]->tax_data = $this->remove_precision( $fee->taxes ); + $this->object->fees[ $fee_key ]->tax = wc_remove_number_precision_deep( $fee->total_tax ); + $this->object->fees[ $fee_key ]->tax_data = wc_remove_number_precision_deep( $fee->taxes ); } - $this->object->fee_total = $this->remove_precision( array_sum( wp_list_pluck( $this->fees, 'total' ) ) ); + $this->object->fee_total = wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total' ) ) ); } /** @@ -502,19 +541,13 @@ class WC_Totals { * * @since 3.2.0 */ - private function calculate_shipping_totals() { - $this->shipping = array(); - - foreach ( $this->object->calculate_shipping() as $key => $shipping_object ) { - $shipping_line = $this->get_default_shipping_props(); - $shipping_line->total = $this->add_precision( $shipping_object->cost ); - $shipping_line->taxes = array_map( array( $this, 'add_precision' ), $shipping_object->taxes ); - $shipping_line->total_tax = array_sum( $shipping_object->taxes ); - $this->shipping[ $key ] = $shipping_line; - } - + protected function calculate_shipping_totals() { + $this->set_shipping(); $this->set_total( 'shipping_total', array_sum( wp_list_pluck( $this->shipping, 'total' ) ) ); $this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->shipping, 'total_tax' ) ) ); + + $this->object->shipping_total = $this->get_total( 'shipping_total' ); + $this->object->shipping_tax_total = $this->get_total( 'shipping_tax_total' ); } /** @@ -522,15 +555,24 @@ class WC_Totals { * * @since 3.2.0 */ - private function calculate_totals() { + protected function calculate_totals() { $this->set_total( 'taxes', $this->get_merged_taxes() ); $this->set_total( 'tax_total', array_sum( wp_list_pluck( $this->get_total( 'taxes', true ), 'tax_total' ) ) ); - $this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->get_total( 'taxes', true ), 'shipping_tax_total' ) ) ); $this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + $this->get_total( 'tax_total', true ) + $this->get_total( 'shipping_tax_total', true ) ) ); - $this->object->total = $this->get_total( 'total' ); - $this->object->tax_total = $this->get_total( 'tax_total' ); - $this->object->shipping_total = $this->get_total( 'shipping_total' ); - $this->object->shipping_tax_total = $this->get_total( 'shipping_tax_total' ); + // Add totals to cart object. + $this->object->taxes = wp_list_pluck( $this->get_total( 'taxes' ), 'shipping_tax_total' ); + $this->object->shipping_taxes = wp_list_pluck( $this->get_total( 'taxes' ), 'tax_total' ); + $this->object->tax_total = $this->get_total( 'tax_total' ); + $this->object->total = $this->get_total( 'total' ); + + // Allow plugins to hook and alter totals before final total is calculated. + if ( has_action( 'woocommerce_calculate_totals' ) ) { + do_action( 'woocommerce_calculate_totals', $this->object ); + } + + // Allow plugins to filter the grand total, and sum the cart totals in case of modifications. + $totals_to_sum = wc_add_number_precision_deep( array( $this->object->cart_contents_total, $this->object->tax_total, $this->object->shipping_tax_total, $this->object->shipping_total, $this->object->fee_total ) ); + $this->object->total = max( 0, apply_filters( 'woocommerce_calculated_total', wc_remove_number_precision( round( array_sum( $totals_to_sum ) ) ), $this->object ) ); } } diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index 4217a65d311..df1420eb2a9 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -390,29 +390,7 @@ class WC_Coupon extends WC_Legacy_Coupon { $discount = $single ? $discount : $discount * $cart_item_qty; } - $discount = (float) min( $discount, $discounting_amount ); - - // Handle the limit_usage_to_x_items option - if ( ! $this->is_type( array( 'fixed_cart' ) ) ) { - if ( $discounting_amount ) { - if ( null === $this->get_limit_usage_to_x_items() ) { - $limit_usage_qty = $cart_item_qty; - } else { - $limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty ); - - $this->set_limit_usage_to_x_items( max( 0, ( $this->get_limit_usage_to_x_items() - $limit_usage_qty ) ) ); - } - if ( $single ) { - $discount = ( $discount * $limit_usage_qty ) / $cart_item_qty; - } else { - $discount = ( $discount / $cart_item_qty ) * $limit_usage_qty; - } - } - } - - $discount = round( $discount, wc_get_rounding_precision() ); - - return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this ); + return apply_filters( 'woocommerce_coupon_get_discount_amount', round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ), $discounting_amount, $cart_item, $single, $this ); } /* diff --git a/includes/class-wc-discounts.php b/includes/class-wc-discounts.php index 5908d00200f..2139898f458 100644 --- a/includes/class-wc-discounts.php +++ b/includes/class-wc-discounts.php @@ -14,8 +14,6 @@ if ( ! defined( 'ABSPATH' ) ) { /** * Discounts class. - * - * @todo this class will need to be called instead get_discounted_price, in the cart? */ class WC_Discounts { @@ -40,20 +38,12 @@ class WC_Discounts { */ protected $applied_coupons = array(); - /** - * Precision so we can work in cents. - * - * @var int - */ - protected $precision = 1; - /** * Constructor. * * @param array $items Items to discount. */ public function __construct( $items = array() ) { - $this->precision = pow( 10, wc_get_price_decimals() ); $this->set_items( $items ); } @@ -75,7 +65,7 @@ class WC_Discounts { * @return array */ public function get_discount( $key ) { - return isset( $this->discounts[ $key ] ) ? $this->remove_precision( $this->discounts[ $key ] ) : 0; + return isset( $this->discounts[ $key ] ) ? wc_remove_number_precision_deep( $this->discounts[ $key ] ) : 0; } /** @@ -86,7 +76,7 @@ class WC_Discounts { * @return array */ public function get_discounts( $in_cents = false ) { - return $in_cents ? $this->discounts : array_map( array( $this, 'remove_precision' ), $this->discounts ); + return $in_cents ? $this->discounts : wc_remove_number_precision_deep ( $this->discounts ); } /** @@ -97,7 +87,7 @@ class WC_Discounts { * @return float */ public function get_discounted_price( $item ) { - return $this->remove_precision( $this->get_discounted_price_in_cents( $item ) ); + return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) ); } /** @@ -119,14 +109,14 @@ class WC_Discounts { * @return array */ public function get_applied_coupons() { - return array_map( array( $this, 'remove_precision' ), $this->applied_coupons ); + return wc_remove_number_precision_deep( $this->applied_coupons ); } /** * Set cart/order items which will be discounted. * * @since 3.2.0 - * @param array $items List items, normailised, by WC_Totals. + * @param array $items List items. */ public function set_items( $items ) { $this->items = array(); @@ -134,7 +124,11 @@ class WC_Discounts { $this->applied_coupons = array(); if ( ! empty( $items ) && is_array( $items ) ) { - $this->items = $items; + foreach ( $items as $key => $item ) { + $this->items[ $key ] = $item; + $this->items[ $key ]->key = $key; + $this->items[ $key ]->price = $item->subtotal; + } $this->discounts = array_fill_keys( array_keys( $items ), 0 ); } @@ -153,67 +147,57 @@ class WC_Discounts { return false; } - if ( ! isset( $this->applied_coupons[ $coupon->get_code() ] ) ) { - $this->applied_coupons[ $coupon->get_code() ] = 0; - } - $is_coupon_valid = $this->is_coupon_valid( $coupon ); + if ( is_wp_error( $is_coupon_valid ) ) { return $is_coupon_valid; } - // @todo how can we support the old woocommerce_coupon_get_discount_amount filter? - $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); + if ( ! isset( $this->applied_coupons[ $coupon->get_code() ] ) ) { + $this->applied_coupons[ $coupon->get_code() ] = array( + 'discount' => 0, + 'discount_tax' => 0, + ); + } + $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); + $coupon_type = $coupon->get_discount_type(); + + // Core discounts are handled here as of 3.2. switch ( $coupon->get_discount_type() ) { case 'percent' : - $this->applied_coupons[ $coupon->get_code() ] += $this->apply_percentage_discount( $items_to_apply, $coupon->get_amount() ); + $this->apply_percentage_discount( $items_to_apply, $coupon->get_amount(), $coupon ); break; case 'fixed_product' : - $this->applied_coupons[ $coupon->get_code() ] += $this->apply_fixed_product_discount( $items_to_apply, $coupon->get_amount() * $this->precision ); + $this->apply_fixed_product_discount( $items_to_apply, wc_add_number_precision( $coupon->get_amount() ), $coupon ); break; case 'fixed_cart' : - $this->applied_coupons[ $coupon->get_code() ] += $this->apply_fixed_cart_discount( $items_to_apply, $coupon->get_amount() * $this->precision ); + $this->apply_fixed_cart_discount( $items_to_apply, wc_add_number_precision( $coupon->get_amount() ), $coupon ); + break; + default : + if ( has_action( 'woocommerce_discounts_apply_coupon_' . $coupon_type ) ) { + // Allow custom coupon types to control this in their class per item, unless the new action is used. + do_action( 'woocommerce_discounts_apply_coupon_' . $coupon_type, $coupon, $items_to_apply, $this ); + } else { + // Fallback to old coupon-logic. + foreach ( $items_to_apply as $item ) { + $discounted_price = $this->get_discounted_price_in_cents( $item ); + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price : $discounted_price; + $discount = min( $discounted_price, $coupon->get_discount_amount( $price_to_discount, $item->object ) ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); + + // Store totals. + $this->discounts[ $item->key ] += $discount; + if ( $coupon ) { + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; + } + } + } break; } } - /** - * Add precision (deep) to a price. - * - * @since 3.2.0 - * @param int|array $value Value to remove precision from. - * @return float - */ - protected function add_precision( $value ) { - if ( is_array( $value ) ) { - foreach ( $value as $key => $subvalue ) { - $value[ $key ] = $this->add_precision( $subvalue ); - } - } else { - $value = $value * $this->precision; - } - return $value; - } - - /** - * Remove precision (deep) from a price. - * - * @since 3.2.0 - * @param int|array $value Value to remove precision from. - * @return float - */ - protected function remove_precision( $value ) { - if ( is_array( $value ) ) { - foreach ( $value as $key => $subvalue ) { - $value[ $key ] = $this->remove_precision( $subvalue ); - } - } else { - $value = wc_format_decimal( $value / $this->precision, wc_get_price_decimals() ); - } - return $value; - } - /** * Sort by price. * @@ -263,7 +247,7 @@ class WC_Discounts { if ( 0 === $this->get_discounted_price_in_cents( $item ) ) { continue; } - if ( ! $coupon->is_valid_for_product( $item->product, $item->cart_item ) && ! $coupon->is_valid_for_cart() ) { + if ( ! $coupon->is_valid_for_product( $item->product, $item->object ) && ! $coupon->is_valid_for_cart() ) { continue; } if ( $limit_usage_qty && $applied_count > $limit_usage_qty ) { @@ -284,36 +268,38 @@ class WC_Discounts { } /** - * Apply a discount amount to an item and ensure it does not go negative. + * Apply percent discount to items and return an array of discounts granted. * * @since 3.2.0 - * @param object $item Get data for this item. - * @param int $discount Amount of discount. - * @return int Amount discounted. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Amount of discount. + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @return int Total discounted. */ - protected function add_item_discount( &$item, $discount ) { - $discounted_price = $this->get_discounted_price_in_cents( $item ); - $discount = $discount > $discounted_price ? $discounted_price : $discount; - $this->discounts[ $item->key ] = $this->discounts[ $item->key ] + $discount; - return $discount; - } - - /** - * Apply percent discount to items. - * - * @since 3.2.0 - * @param array $items_to_apply Array of items to apply the coupon to. - * @param int $amount Amount of discount. - * @return int total discounted in cents - */ - protected function apply_percentage_discount( $items_to_apply, $amount ) { - $total_discounted = 0; + protected function apply_percentage_discount( $items_to_apply, $amount, $coupon = null ) { + $total_discount = 0; foreach ( $items_to_apply as $item ) { - $total_discounted += $this->add_item_discount( $item, $amount * ( $this->get_discounted_price_in_cents( $item ) / 100 ) ); - } + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); - return $total_discounted; + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price; + + // Run coupon calculations. + $discount = $amount * ( $price_to_discount / 100 ); + $discount = min( $discounted_price, apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $price_to_discount, $item->object, false, $coupon ) ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); + + // Store totals. + $total_discount += $discount; + $this->discounts[ $item->key ] += $discount; + if ( $coupon ) { + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; + } + } + return $total_discount; } /** @@ -321,17 +307,34 @@ class WC_Discounts { * * @since 3.2.0 * @param array $items_to_apply Array of items to apply the coupon to. - * @param int $discount Amount of discout. - * @return int total discounted in cents + * @param int $amount Amount of discount. + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @return int Total discounted. */ - protected function apply_fixed_product_discount( $items_to_apply, $discount ) { - $total_discounted = 0; + protected function apply_fixed_product_discount( $items_to_apply, $amount, $coupon = null ) { + $total_discount = 0; foreach ( $items_to_apply as $item ) { - $total_discounted += $this->add_item_discount( $item, $discount * $item->quantity ); - } + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); - return $total_discounted; + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price; + + // Run coupon calculations. + $discount = $amount * $item->quantity; + $discount = min( $discounted_price, apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $price_to_discount, $item->object, false, $coupon ) ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); + + // Store totals. + $total_discount += $discount; + $this->discounts[ $item->key ] += $discount; + if ( $coupon ) { + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; + } + } + return $total_discount; } /** @@ -340,49 +343,92 @@ class WC_Discounts { * @since 3.2.0 * @param array $items_to_apply Array of items to apply the coupon to. * @param int $cart_discount Fixed discount amount to apply. - * @return int total discounted in cents + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @return int Total discounted. */ - protected function apply_fixed_cart_discount( $items_to_apply, $cart_discount ) { - $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); + protected function apply_fixed_cart_discount( $items_to_apply, $cart_discount, $coupon = null ) { + $total_discount = 0; + $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); if ( ! $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ) ) { - return 0; + return $total_discount; } - $per_item_discount = floor( $cart_discount / $item_count ); // round it down to the nearest cent number. - $amount_discounted = 0; + $per_item_discount = floor( $cart_discount / $item_count ); // round it down to the nearest cent. if ( $per_item_discount > 0 ) { - foreach ( $items_to_apply as $item ) { - $amount_discounted += $this->add_item_discount( $item, $per_item_discount * $item->quantity ); - } + $total_discounted = $this->apply_fixed_product_discount( $items_to_apply, $per_item_discount, $coupon ); /** * If there is still discount remaining, repeat the process. */ - if ( $amount_discounted > 0 && $amount_discounted < $cart_discount ) { - $amount_discounted += $this->apply_fixed_cart_discount( $items_to_apply, $cart_discount - $amount_discounted ); + if ( $total_discounted > 0 && $total_discounted < $cart_discount ) { + $total_discounted = $total_discounted + $this->apply_fixed_cart_discount( $items_to_apply, $cart_discount - $total_discounted ); } - } elseif ( $cart_discount > 0 ) { - /** - * Deal with remaining fractional discounts by splitting it over items - * until the amount is expired, discounting 1 cent at a time. - */ - foreach ( $items_to_apply as $item ) { - for ( $i = 0; $i < $item->quantity; $i ++ ) { - $amount_discounted += $this->add_item_discount( $item, 1 ); - if ( $amount_discounted >= $cart_discount ) { - break 2; - } + } elseif ( $cart_discount > 0 ) { + $total_discounted = $this->apply_fixed_cart_discount_remainder( $items_to_apply, $cart_discount, $coupon ); + } + return $total_discount; + } + + /** + * Deal with remaining fractional discounts by splitting it over items + * until the amount is expired, discounting 1 cent at a time. + * + * @since 3.2.0 + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $cart_discount Fixed discount amount to apply. + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @return int Total discounted. + */ + protected function apply_fixed_cart_discount_remainder( $items_to_apply, $remaining_discount, $coupon = null ) { + $total_discount = 0; + + foreach ( $items_to_apply as $item ) { + for ( $i = 0; $i < $item->quantity; $i ++ ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price; + + // Run coupon calculations. + $discount = min( $discounted_price, 1 ); + $discount_tax = $this->get_item_discount_tax( $item, $discount ); + + // Store totals. + $total_discount += $discount; + $this->discounts[ $item->key ] += $discount; + if ( $coupon ) { + $this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount; + $this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax; } - if ( $amount_discounted >= $cart_discount ) { - break; + + if ( $total_discount >= $remaining_discount ) { + break 2; } } + if ( $total_discount >= $remaining_discount ) { + break; + } } + return $total_discount; + } - return $amount_discounted; + /** + * Return discounted tax amount for an item. + * + * @param object $item + * @param int $discount_amount + * @return int + */ + protected function get_item_discount_tax( $item, $discount_amount ) { + if ( $item->product->is_taxable() ) { + $taxes = WC_Tax::calc_tax( $discount_amount, $item->tax_rates, false ); + return array_sum( $taxes ); + } + return 0; } /* @@ -605,7 +651,7 @@ class WC_Discounts { $valid = false; foreach ( $this->items as $item ) { - if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->cart_item ) ) { + if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) { $valid = true; break; } diff --git a/includes/class-wc-order-item-shipping.php b/includes/class-wc-order-item-shipping.php index 9cd19a4b0e9..b77ef122b50 100644 --- a/includes/class-wc-order-item-shipping.php +++ b/includes/class-wc-order-item-shipping.php @@ -28,6 +28,27 @@ class WC_Order_Item_Shipping extends WC_Order_Item { ), ); + /** + * Calculate item taxes. + * + * @since 3.2.0 + * @param array $calculate_tax_for Location data to get taxes for. Required. + * @return bool True if taxes were calculated. + */ + public function calculate_taxes( $calculate_tax_for = array() ) { + if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'], $calculate_tax_for['tax_class'] ) ) { + return false; + } + if ( wc_tax_enabled() ) { + $tax_rates = WC_Tax::find_shipping_rates( $calculate_tax_for ); + $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); + $this->set_taxes( array( 'total' => $taxes ) ); + } else { + $this->set_taxes( false ); + } + return true; + } + /* |-------------------------------------------------------------------------- | Setters diff --git a/includes/class-wc-order-item.php b/includes/class-wc-order-item.php index 36fe47b77cc..3ab1235dff9 100644 --- a/includes/class-wc-order-item.php +++ b/includes/class-wc-order-item.php @@ -152,7 +152,8 @@ class WC_Order_Item extends WC_Data implements ArrayAccess { */ /** - * Type checking + * Type checking. + * * @param string|array $type * @return boolean */ @@ -160,6 +161,34 @@ class WC_Order_Item extends WC_Data implements ArrayAccess { return is_array( $type ) ? in_array( $this->get_type(), $type ) : $type === $this->get_type(); } + /** + * Calculate item taxes. + * + * @since 3.2.0 + * @param array $calculate_tax_for Location data to get taxes for. Required. + * @return bool True if taxes were calculated. + */ + public function calculate_taxes( $calculate_tax_for = array() ) { + if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { + return false; + } + if ( '0' !== $this->get_tax_class() && 'taxable' === $this->get_tax_status() && wc_tax_enabled() ) { + $calculate_tax_for['tax_class'] = $this->get_tax_class(); + $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); + $taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false ); + + if ( method_exists( $this, 'get_subtotal' ) ) { + $subtotal_taxes = WC_Tax::calc_tax( $this->get_subtotal(), $tax_rates, false ); + $this->set_taxes( array( 'total' => $taxes, 'subtotal' => $subtotal_taxes ) ); + } else { + $this->set_taxes( array( 'total' => $taxes ) ); + } + } else { + $this->set_taxes( false ); + } + return true; + } + /* |-------------------------------------------------------------------------- | Meta Data Handling diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php index 7132f694eb4..f0973e1d8dc 100644 --- a/includes/class-wc-tax.php +++ b/includes/class-wc-tax.php @@ -452,13 +452,18 @@ class WC_Tax { * Used by get_rates(), get_shipping_rates(). * * @param $tax_class string Optional, passed to the filter for advanced tax setups. + * @param object $customer Override the customer object to get their location. * @return array */ - public static function get_tax_location( $tax_class = '' ) { + public static function get_tax_location( $tax_class = '', $customer = null ) { $location = array(); - if ( ! empty( WC()->customer ) ) { - $location = WC()->customer->get_taxable_address(); + if ( is_null( $customer ) && ! empty( WC()->customer ) ) { + $customer = WC()->customer; + } + + if ( ! empty( $customer ) ) { + $location = $customer->get_taxable_address(); } elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) { $location = array( WC()->countries->get_base_country(), @@ -468,17 +473,19 @@ class WC_Tax { ); } - return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class ); + return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer ); } /** * Get's an array of matching rates for a tax class. - * @param string $tax_class + * + * @param string $tax_class Tax class to get rates for. + * @param object $customer Override the customer object to get their location. * @return array */ - public static function get_rates( $tax_class = '' ) { + public static function get_rates( $tax_class = '', $customer = null ) { $tax_class = sanitize_title( $tax_class ); - $location = self::get_tax_location( $tax_class ); + $location = self::get_tax_location( $tax_class, $customer ); $matched_tax_rates = array(); if ( sizeof( $location ) === 4 ) { @@ -526,10 +533,11 @@ class WC_Tax { /** * Gets an array of matching shipping tax rates for a given class. * - * @param string Tax Class - * @return mixed + * @param string $tax_class Tax class to get rates for. + * @param object $customer Override the customer object to get their location. + * @return mixed */ - public static function get_shipping_tax_rates( $tax_class = null ) { + public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) { // See if we have an explicitly set shipping tax class $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); @@ -537,7 +545,7 @@ class WC_Tax { $tax_class = $shipping_tax_class; } - $location = self::get_tax_location( $tax_class ); + $location = self::get_tax_location( $tax_class, $customer ); $matched_tax_rates = array(); if ( sizeof( $location ) === 4 ) { diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 3667641f93e..0708a7ee6fc 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -334,7 +334,7 @@ final class WooCommerce { include_once( WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php' ); include_once( WC_ABSPATH . 'includes/class-wc-background-emailer.php' ); include_once( WC_ABSPATH . 'includes/class-wc-discounts.php' ); - include_once( WC_ABSPATH . 'includes/class-wc-totals.php' ); + include_once( WC_ABSPATH . 'includes/class-wc-cart-totals.php' ); /** * Data stores - used to store and retrieve CRUD object data from the database. diff --git a/includes/wc-core-functions.php b/includes/wc-core-functions.php index e0b52734630..05543405ed3 100644 --- a/includes/wc-core-functions.php +++ b/includes/wc-core-functions.php @@ -1443,6 +1443,66 @@ function wc_get_rounding_precision() { return $precision; } +/** + * Add precision to a number and return an int. + * + * @since 3.2.0 + * @param float $value Number to add precision to. + * @return int + */ +function wc_add_number_precision( $value ) { + $precision = pow( 10, wc_get_price_decimals() ); + return $value * $precision; +} + +/** + * Remove precision from a number and return a float. + * + * @since 3.2.0 + * @param float $value Number to add precision to. + * @return float + */ +function wc_remove_number_precision( $value ) { + $precision = pow( 10, wc_get_price_decimals() ); + return wc_format_decimal( $value / $precision, wc_get_price_decimals() ); +} + +/** + * Add precision to an array of number and return an array of int. + * + * @since 3.2.0 + * @param array $value Number to add precision to. + * @return int + */ +function wc_add_number_precision_deep( $value ) { + if ( is_array( $value ) ) { + foreach ( $value as $key => $subvalue ) { + $value[ $key ] = wc_add_number_precision_deep( $subvalue ); + } + } else { + $value = wc_add_number_precision( $value ); + } + return $value; +} + +/** + * Remove precision from an array of number and return an array of int. + * + * @since 3.2.0 + * @param array $value Number to add precision to. + * @return int + */ +function wc_remove_number_precision_deep( $value ) { + if ( is_array( $value ) ) { + foreach ( $value as $key => $subvalue ) { + $value[ $key ] = wc_remove_number_precision_deep( $subvalue ); + } + } else { + $value = wc_remove_number_precision( $value ); + } + return $value; +} + /** * Get a shared logger instance. * diff --git a/includes/wc-formatting-functions.php b/includes/wc-formatting-functions.php index c4e7bbc98e4..c0d1ba2ff70 100644 --- a/includes/wc-formatting-functions.php +++ b/includes/wc-formatting-functions.php @@ -216,11 +216,12 @@ function wc_trim_zeros( $price ) { /** * Round a tax amount. * - * @param mixed $tax + * @param double $tax Amount to round. + * @param int $dp DP to round. Defaults to wc_get_price_decimals. * @return double */ -function wc_round_tax_total( $tax ) { - $dp = wc_get_price_decimals(); +function wc_round_tax_total( $tax, $dp = null ) { + $dp = is_null( $dp ) ? wc_get_price_decimals() : absint( $dp ); // @codeCoverageIgnoreStart if ( version_compare( phpversion(), '5.3', '<' ) ) { diff --git a/tests/unit-tests/discounts/discounts.php b/tests/unit-tests/discounts/discounts.php index 1cb509fd029..cdb89017cdd 100644 --- a/tests/unit-tests/discounts/discounts.php +++ b/tests/unit-tests/discounts/discounts.php @@ -24,11 +24,11 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { 'taxes' => array(), 'discounted_price' => 0, ); - $item->key = $cart_item_key; - $item->cart_item = $cart_item; - $item->quantity = $cart_item['quantity']; - $item->price = $cart_item['data']->get_price() * $precision * $cart_item['quantity']; - $item->product = $cart_item['data']; + $item->object = $cart_item; + $item->quantity = $cart_item['quantity']; + $item->subtotal = $cart_item['data']->get_price() * $precision * $cart_item['quantity']; + $item->product = $cart_item['data']; + $item->tax_rates = WC_Tax::get_rates( $item->product->get_tax_class() ); $items[ $cart_item_key ] = $item; } return $items; @@ -89,34 +89,38 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case { $coupon = WC_Helper_Coupon::create_coupon( 'test' ); $coupon->set_amount( 50 ); $coupon->set_discount_type( 'percent' ); + $coupon->save(); $discounts->apply_coupon( $coupon ); - $this->assertEquals( array( 'test' => 5 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 5, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); $coupon2 = WC_Helper_Coupon::create_coupon( 'test2' ); $coupon2->set_code( 'test2' ); $coupon2->set_amount( 50 ); $coupon2->set_discount_type( 'percent' ); + $coupon->save(); $discounts->apply_coupon( $coupon2 ); - $this->assertEquals( array( 'test' => 5, 'test2' => 2.50 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 5, 'discount_tax' => 0 ), 'test2' => array( 'discount' => 2.50, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); $discounts->apply_coupon( $coupon ); - $this->assertEquals( array( 'test' => 6.25, 'test2' => 2.50 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 6.25, 'discount_tax' => 0 ), 'test2' => array( 'discount' => 2.50, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); // Test different coupon types. WC()->cart->empty_cart(); WC()->cart->add_to_cart( $product->get_id(), 2 ); $coupon->set_discount_type( 'fixed_product' ); $coupon->set_amount( 2 ); + $coupon->save(); $discounts->set_items( $this->get_items_for_discounts_class() ); $discounts->apply_coupon( $coupon ); - $this->assertEquals( array( 'test' => 4 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 4, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); $coupon->set_discount_type( 'fixed_cart' ); + $coupon->save(); $discounts->set_items( $this->get_items_for_discounts_class() ); $discounts->apply_coupon( $coupon ); - $this->assertEquals( array( 'test' => 2 ), $discounts->get_applied_coupons() ); + $this->assertEquals( array( 'test' => array( 'discount' => 2, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() ); // Cleanup. WC()->cart->empty_cart(); diff --git a/tests/unit-tests/totals/totals.php b/tests/unit-tests/totals/totals.php index b226b61c441..b3b197807cf 100644 --- a/tests/unit-tests/totals/totals.php +++ b/tests/unit-tests/totals/totals.php @@ -67,7 +67,7 @@ class WC_Tests_Totals extends WC_Unit_Test_Case { add_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_cart_fees_callback' ) ); - $this->totals = new WC_Totals( WC()->cart ); + $this->totals = new WC_Cart_Totals( WC()->cart, WC()->customer ); } /**