Merge pull request #24828 from woocommerce/fix/24695

Adds shared code between Orders and Cart calculation logic.
This commit is contained in:
Claudio Sanches 2019-12-05 12:39:50 -03:00 committed by GitHub
commit d7e2a98aaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 283 additions and 70 deletions

View File

@ -18,6 +18,7 @@ require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-order.php';
* WC_Abstract_Order class.
*/
abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
use WC_Item_Totals;
/**
* Order Data array. This is the core order data exposed in APIs since 3.0.0.
@ -769,6 +770,23 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
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.
*
@ -1498,6 +1516,34 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$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.
*
@ -1508,18 +1554,13 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
public function calculate_totals( $and_taxes = true ) {
do_action( 'woocommerce_order_before_calculate_totals', $and_taxes, $this );
$cart_subtotal = 0;
$cart_total = 0;
$fees_total = 0;
$fees_total = 0;
$shipping_total = 0;
$cart_subtotal_tax = 0;
$cart_total_tax = 0;
// 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 = $this->get_cart_subtotal_for_order();
$cart_total = $this->get_cart_total_for_order();
// Sum shipping costs.
foreach ( $this->get_shipping_methods() as $shipping ) {
@ -1740,15 +1781,16 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
*/
public function get_subtotal_to_display( $compound = false, $tax_display = '' ) {
$tax_display = $tax_display ? $tax_display : get_option( 'woocommerce_tax_display_cart' );
$subtotal = 0;
$subtotal = $this->get_cart_subtotal_for_order();
if ( ! $compound ) {
foreach ( $this->get_items() as $item ) {
$subtotal += $item->get_subtotal();
if ( 'incl' === $tax_display ) {
$subtotal += $item->get_subtotal_tax();
if ( 'incl' === $tax_display ) {
$subtotal_taxes = 0;
foreach ( $this->get_items() as $item ) {
$subtotal_taxes += self::round_line_tax( $item->get_subtotal_tax(), false );
}
$subtotal += wc_round_tax_total( $subtotal_taxes );
}
$subtotal = wc_price( $subtotal, array( 'currency' => $this->get_currency() ) );
@ -1761,10 +1803,6 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
return '';
}
foreach ( $this->get_items() as $item ) {
$subtotal += $item->get_subtotal();
}
// Add Shipping Costs.
$subtotal += $this->get_shipping_total();

View File

@ -23,6 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
* @since 3.2.0
*/
final class WC_Cart_Totals {
use WC_Item_Totals;
/**
* Reference to cart object.
@ -199,15 +200,6 @@ final class WC_Cart_Totals {
);
}
/**
* Should we round at subtotal level only?
*
* @return bool
*/
protected function round_at_subtotal() {
return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
}
/**
* Handles a cart or order object passed in for calculation. Normalises data
* into the same format for use by this class.
@ -561,6 +553,16 @@ final class WC_Cart_Totals {
return $in_cents ? $this->totals : wc_remove_number_precision_deep( $this->totals );
}
/**
* Returns array of values for totals calculation.
*
* @param string $field Field name. Will probably be `total` or `subtotal`.
* @return array Items object
*/
protected function get_values_for_total( $field ) {
return array_values( wp_list_pluck( $this->items, $field ) );
}
/**
* Get taxes merged by type.
*
@ -598,6 +600,7 @@ final class WC_Cart_Totals {
/**
* Round merged taxes.
*
* @deprecated 3.9.0 `calculate_item_subtotals` should already appropriately round the tax values.
* @since 3.5.4
* @param array $taxes Taxes to round.
* @return array
@ -681,12 +684,8 @@ final class WC_Cart_Totals {
$this->cart->cart_contents[ $item_key ]['line_tax'] = wc_remove_number_precision( $item->total_tax );
}
$items_total = array_sum(
array_map(
array( $this, 'round_item_subtotal' ),
array_values( wp_list_pluck( $this->items, 'total' ) )
)
);
$items_total = $this->get_rounded_items_total( $this->get_values_for_total( 'total' ) );
$this->set_total( 'items_total', round( $items_total ) );
$this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) );
@ -712,11 +711,14 @@ final class WC_Cart_Totals {
protected function calculate_item_subtotals() {
$merged_subtotal_taxes = array(); // Taxes indexed by tax rate ID for storage later.
$adjust_non_base_location_prices = apply_filters( 'woocommerce_adjust_non_base_location_prices', true );
$is_customer_vat_exempt = $this->cart->get_customer()->get_is_vat_exempt();
foreach ( $this->items as $item_key => $item ) {
if ( $item->price_includes_tax ) {
if ( $this->cart->get_customer()->get_is_vat_exempt() ) {
if ( $is_customer_vat_exempt ) {
$item = $this->remove_item_base_taxes( $item );
} elseif ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
} elseif ( $adjust_non_base_location_prices ) {
$item = $this->adjust_non_base_location_price( $item );
}
}
@ -745,12 +747,8 @@ final class WC_Cart_Totals {
$this->cart->cart_contents[ $item_key ]['line_subtotal_tax'] = wc_remove_number_precision( $item->subtotal_tax );
}
$items_subtotal = array_sum(
array_map(
array( $this, 'round_item_subtotal' ),
array_values( wp_list_pluck( $this->items, 'subtotal' ) )
)
);
$items_subtotal = $this->get_rounded_items_total( $this->get_values_for_total( 'subtotal' ) );
$this->set_total( 'items_subtotal', round( $items_subtotal ) );
$this->set_total( 'items_subtotal_tax', wc_round_tax_total( array_sum( $merged_subtotal_taxes ), 0 ) );
@ -862,7 +860,7 @@ final class WC_Cart_Totals {
* @since 3.2.0
*/
protected function calculate_totals() {
$this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + array_sum( $this->get_merged_taxes( true ) ), 0 ) );
$this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + wc_round_tax_total( array_sum( $this->get_merged_taxes( true ) ), 0 ), 0 ) );
$this->cart->set_total_tax( array_sum( $this->get_merged_taxes( false ) ) );
// Allow plugins to hook and alter totals before final total is calculated.
@ -873,32 +871,4 @@ final class WC_Cart_Totals {
// Allow plugins to filter the grand total, and sum the cart totals in case of modifications.
$this->cart->set_total( max( 0, apply_filters( 'woocommerce_calculated_total', $this->get_total( 'total' ), $this->cart ) ) );
}
/**
* Apply rounding to an array of taxes before summing. Rounds to store DP setting, ignoring precision.
*
* @since 3.2.6
* @param float $value Tax value.
* @return float
*/
protected function round_line_tax( $value ) {
if ( ! $this->round_at_subtotal() ) {
$value = wc_round_tax_total( $value, 0 );
}
return $value;
}
/**
* Apply rounding to item subtotal before summing.
*
* @since 3.7.0
* @param float $value Item subtotal value.
* @return float
*/
protected function round_item_subtotal( $value ) {
if ( ! $this->round_at_subtotal() ) {
$value = round( $value );
}
return $value;
}
}

View File

@ -337,6 +337,11 @@ final class WooCommerce {
include_once WC_ABSPATH . 'includes/interfaces/class-wc-webhooks-data-store-interface.php';
include_once WC_ABSPATH . 'includes/interfaces/class-wc-queue-interface.php';
/**
* Core traits.
*/
include_once WC_ABSPATH . 'includes/traits/trait-wc-item-totals.php';
/**
* Abstract classes.
*/

View File

@ -0,0 +1,89 @@
<?php
/**
* This ongoing trait will have shared calculation logic between WC_Abstract_Order and WC_Cart_Totals classes.
*
* @package WooCommerce/Traits
* @version 3.9.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Trait WC_Item_Totals.
*
* Right now this do not have much, but plan is to eventually move all shared calculation logic between Orders and Cart in this file.
*
* @since 3.9.0
*/
trait WC_Item_Totals {
/**
* Line items to calculate. Define in child class.
*
* @since 3.9.0
* @param string $field Field name to calculate upon.
*
* @return array having `total`|`subtotal` property.
*/
abstract protected function get_values_for_total( $field );
/**
* Return rounded total based on settings. Will be used by Cart and Orders.
*
* @since 3.9.0
*
* @param array $values Values to round. Should be with precision.
*
* @return float|int Appropriately rounded value.
*/
public static function get_rounded_items_total( $values ) {
return array_sum(
array_map(
array( self::class, 'round_item_subtotal' ),
$values
)
);
}
/**
* Apply rounding to item subtotal before summing.
*
* @since 3.9.0
* @param float $value Item subtotal value.
* @return float
*/
public static function round_item_subtotal( $value ) {
if ( ! self::round_at_subtotal() ) {
$value = round( $value );
}
return $value;
}
/**
* Should always round at subtotal?
*
* @since 3.9.0
* @return bool
*/
protected static function round_at_subtotal() {
return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
}
/**
* Apply rounding to an array of taxes before summing. Rounds to store DP setting, ignoring precision.
*
* @since 3.2.6
* @param float $value Tax value.
* @param bool $in_cents Whether precision of value is in cents.
* @return float
*/
protected static function round_line_tax( $value, $in_cents = true ) {
if ( ! self::round_at_subtotal() ) {
$value = wc_round_tax_total( $value, $in_cents ? 0 : null );
}
return $value;
}
}

View File

@ -20,6 +20,62 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
WC()->customer->set_is_vat_exempt( false );
}
/**
* Test whether totals are correct when discount is applied.
*/
public function test_cart_total_with_discount_and_taxes() {
update_option( 'woocommerce_prices_include_tax', 'yes' );
update_option( 'woocommerce_calc_taxes', 'yes' );
update_option( 'woocommerce_tax_round_at_subtotal', 'yes' );
WC()->cart->empty_cart();
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '20.0000',
'tax_rate_name' => 'TAX20',
'tax_rate_priority' => '1',
'tax_rate_compound' => '0',
'tax_rate_shipping' => '0',
'tax_rate_order' => '1',
'tax_rate_class' => '20percent',
);
$tax_rate_20 = WC_Tax::_insert_tax_rate( $tax_rate );
// Create product with price 19.
$product = WC_Helper_Product::create_simple_product();
$product->set_price( 8.99 );
$product->set_regular_price( 8.99 );
$product->set_tax_class( '20percent' );
$product->save();
$coupon = WC_Helper_Coupon::create_coupon( 'off5', array( 'coupon_amount' => 5 ) );
// Create a flat rate method.
$flat_rate_settings = array(
'enabled' => 'yes',
'title' => 'Flat rate',
'availability' => 'all',
'countries' => '',
'tax_status' => 'taxable',
'cost' => '9.59',
);
update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings );
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon->get_code() );
WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) );
WC()->cart->calculate_totals();
$this->assertEquals( '13.58', WC()->cart->get_total( 'edit' ) );
$this->assertEquals( 0.66, WC()->cart->get_total_tax() );
$this->assertEquals( 4.17, WC()->cart->get_discount_total() );
$this->assertEquals( 0.83, WC()->cart->get_discount_tax() );
}
/**
* Test for subtotals and multiple tax rounding.
* Ticket:

View File

@ -0,0 +1,55 @@
<?php
/**
* Class WC_Tests_Order file.
*
* @package WooCommerce|Tests|Order
*/
/**
* Class WC_Tests_Order.
*/
class WC_Tests_Order extends WC_Unit_Test_Case {
/**
* Test for total when round at subtotal is enabled.
*
* @link https://github.com/woocommerce/woocommerce/issues/24695
*/
public function test_order_calculate_total_rounding_24695() {
update_option( 'woocommerce_prices_include_tax', 'yes' );
update_option( 'woocommerce_calc_taxes', 'yes' );
update_option( 'woocommerce_tax_round_at_subtotal', 'yes' );
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '7.0000',
'tax_rate_name' => 'CGST',
'tax_rate_priority' => '1',
'tax_rate_compound' => '0',
'tax_rate_shipping' => '0',
'tax_rate_order' => '1',
'tax_rate_class' => 'tax_1',
);
WC_Tax::_insert_tax_rate( $tax_rate );
$product1 = WC_Helper_Product::create_simple_product();
$product1->set_regular_price( 2 );
$product1->save();
$product2 = WC_Helper_Product::create_simple_product();
$product2->set_regular_price( 2.5 );
$product2->save();
$order = new WC_Order();
$order->add_product( $product1, 1 );
$order->add_product( $product2, 4 );
$order->save();
$order->calculate_totals( true );
$this->assertEquals( 12, $order->get_total() );
$this->assertEquals( 0.79, $order->get_total_tax() );
}
}

View File

@ -62,7 +62,7 @@ class WC_Tests_Order_Coupons extends WC_Unit_Test_Case {
array(
'product' => $product,
'quantity' => 1,
'subtotal' => 909.09, // Ex tax.
'subtotal' => 909.09, // Ex tax 10%.
'total' => 726.36,
)
);