From dd3427958bdcd7216bad79e96c100246ef1ef442 Mon Sep 17 00:00:00 2001 From: Francesco Leanza Date: Thu, 21 Nov 2019 11:28:22 +0100 Subject: [PATCH] Fixed order totals calculation if it contains taxable and non-taxable products and percentage coupons Added PHPUnit tests --- includes/abstracts/abstract-wc-order.php | 61 +++++++++-------- tests/unit-tests/order/coupons.php | 87 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 28 deletions(-) diff --git a/includes/abstracts/abstract-wc-order.php b/includes/abstracts/abstract-wc-order.php index 71ea47e9481..d8ae70fd40d 100644 --- a/includes/abstracts/abstract-wc-order.php +++ b/includes/abstracts/abstract-wc-order.php @@ -439,7 +439,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $subtotal = 0; foreach ( $this->get_items() as $item ) { - $subtotal += $item->get_subtotal(); + $subtotal += $this->round_item_subtotal( $item->get_subtotal() ); } return apply_filters( 'woocommerce_order_get_subtotal', (float) $subtotal, $this ); @@ -1145,18 +1145,14 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $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() ) { + if ( $this->get_prices_include_tax() && wc_tax_enabled() && 'taxable' === $item->get_tax_status() ) { $taxes = WC_Tax::calc_tax( $amount, WC_Tax::get_rates( $item->get_tax_class() ), true ); - if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $taxes = array_map( 'wc_round_tax_total', $taxes ); - } - + // Use unrounded taxes so totals will be re-calculated accurately, like in cart. $amount = $amount - array_sum( $taxes ); - $item->set_total( max( 0, round( $item->get_total() - $amount, wc_get_price_decimals() ) ) ); - } else { - $item->set_total( max( 0, $item->get_total() - $amount ) ); } + + $item->set_total( max( 0, $item->get_total() - $amount ) ); } } } @@ -1190,23 +1186,19 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { foreach ( $all_discounts[ $coupon_code ] as $item_id => $item_discount_amount ) { $item = $this->get_item( $item_id, false ); - if ( $this->get_prices_include_tax() && wc_tax_enabled() ) { - $taxes = WC_Tax::calc_tax( $item_discount_amount, WC_Tax::get_rates( $item->get_tax_class() ), true ); + if ( 'taxable' !== $item->get_tax_status() || !wc_tax_enabled() ) { + continue; + } - if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $taxes = array_map( 'wc_round_tax_total', $taxes ); - } + $taxes = array_sum( WC_Tax::calc_tax( $item_discount_amount, WC_Tax::get_rates( $item->get_tax_class() ), $this->get_prices_include_tax() ) ); + if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $taxes = wc_round_tax_total( $taxes ); + } - $discount_tax += array_sum( $taxes ); - $amount = $amount - array_sum( $taxes ); - } else { - $taxes = WC_Tax::calc_tax( $item_discount_amount, WC_Tax::get_rates( $item->get_tax_class() ) ); + $discount_tax += $taxes; - if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { - $taxes = array_map( 'wc_round_tax_total', $taxes ); - } - - $discount_tax += array_sum( $taxes ); + if ( $this->get_prices_include_tax() ) { + $amount = $amount - $taxes; } } @@ -1517,8 +1509,8 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { // Sum line item costs. foreach ( $this->get_items() as $item ) { - $cart_subtotal += round( $item->get_subtotal(), wc_get_price_decimals() ); - $cart_total += round( $item->get_total(), wc_get_price_decimals() ); + $cart_subtotal += $item->get_subtotal(); + $cart_total += $item->get_total(); } // Sum shipping costs. @@ -1561,7 +1553,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } } - $this->set_discount_total( $cart_subtotal - $cart_total ); + $this->set_discount_total( 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( round( $cart_total + $fees_total + $this->get_shipping_total() + $this->get_cart_tax() + $this->get_shipping_tax(), wc_get_price_decimals() ) ); @@ -1744,7 +1736,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { if ( ! $compound ) { foreach ( $this->get_items() as $item ) { - $subtotal += $item->get_subtotal(); + $subtotal += $this->round_item_subtotal( $item->get_subtotal() ); if ( 'incl' === $tax_display ) { $subtotal += $item->get_subtotal_tax(); @@ -1762,7 +1754,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } foreach ( $this->get_items() as $item ) { - $subtotal += $item->get_subtotal(); + $subtotal += $this->round_item_subtotal( $item->get_subtotal() ); } // Add Shipping Costs. @@ -2012,4 +2004,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } return false; } + + /** + * Apply rounding to item subtotal before summing. + * + * @param float $value Item subtotal value. + * @return float + */ + protected function round_item_subtotal( $value ) { + if ( 'yes' !== get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $value = round( $value, wc_get_price_decimals() ); + } + return $value; + } } diff --git a/tests/unit-tests/order/coupons.php b/tests/unit-tests/order/coupons.php index 8b09f505cc5..061b95a38d7 100644 --- a/tests/unit-tests/order/coupons.php +++ b/tests/unit-tests/order/coupons.php @@ -363,4 +363,91 @@ class WC_Tests_Order_Coupons extends WC_Unit_Test_Case { $order->apply_coupon( 'test-coupon-2' ); $this->assertEquals( '599.00', $order->get_total(), $order->get_total() ); } + + /** + * Test a rounding issue on order totals when the order includes a percentage coupon and taxable and non-taxable items + * See: #25091. + */ + public function test_inclusive_tax_rounding_on_totals() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + + WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '20.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '1', + 'tax_rate_compound' => '0', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ) + ); + + WC_Tax::_insert_tax_rate( + array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '5.0000', + 'tax_rate_name' => 'VAT', + 'tax_rate_priority' => '2', + 'tax_rate_compound' => '1', + 'tax_rate_shipping' => '1', + 'tax_rate_order' => '1', + 'tax_rate_class' => '', + ) + ); + + + $product_1 = WC_Helper_Product::create_simple_product(); + $product_1->set_regular_price( 3.17 ); + $product_1->save(); + $product_1 = wc_get_product( $product_1->get_id() ); + + $product_2 = WC_Helper_Product::create_simple_product(); + $product_2->set_regular_price( 6.13 ); + $product_2->save(); + $product_2 = wc_get_product( $product_2->get_id() ); + + $product_3 = WC_Helper_Product::create_simple_product(); + $product_3->set_regular_price( 9.53 ); + $product_3->set_tax_status( 'none' ); + $product_3->save(); + $product_3 = wc_get_product( $product_3->get_id() ); + + $coupon = new WC_Coupon(); + $coupon->set_code( 'test-coupon-1' ); + $coupon->set_amount( 10 ); + $coupon->set_discount_type( 'percent' ); + $coupon->save(); + + + $order = wc_create_order( + array( + 'status' => 'pending', + 'customer_id' => 1, + 'customer_note' => '', + 'total' => '', + ) + ); + + $order->add_product( $product_1 ); + $order->add_product( $product_2 ); + $order->add_product( $product_3 ); + + $order->calculate_totals( true ); + + $order->apply_coupon( $coupon->get_code() ); + + $applied_coupons = $order->get_items( 'coupon' ); + $applied_coupon = current( $applied_coupons ); + + $this->assertEquals( '16.95', $order->get_total() ); + $this->assertEquals( '1.73', $order->get_total_tax() ); + $this->assertEquals( '1.69', $order->get_discount_total() ); + + $this->assertEquals( '1.69', $applied_coupon->get_discount() ); + } }