Merge pull request #24828 from woocommerce/fix/24695
Adds shared code between Orders and Cart calculation logic.
This commit is contained in:
commit
d7e2a98aaf
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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() );
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue