2017-07-26 11:44:06 +00:00
< ? php
/**
* Cart totals calculation class .
*
* Methods are protected and class is final to keep this as an internal API .
* May be opened in the future once structure is stable .
*
2017-12-04 21:40:12 +00:00
* Rounding guide :
* - if something is being stored e . g . item total , store unrounded . This is so taxes can be recalculated later accurately .
* - if calculating a total , round ( if settings allow ) .
*
2020-08-05 16:36:24 +00:00
* @ package WooCommerce\Classes
2018-03-16 17:47:18 +00:00
* @ version 3.2 . 0
2017-07-26 11:44:06 +00:00
*/
2020-10-01 08:57:12 +00:00
use Automattic\WooCommerce\Utilities\NumberUtil ;
2017-07-26 11:44:06 +00:00
if ( ! defined ( 'ABSPATH' ) ) {
exit ;
}
/**
* WC_Cart_Totals class .
*
* @ since 3.2 . 0
*/
2017-07-27 09:49:47 +00:00
final class WC_Cart_Totals {
2019-10-14 15:14:13 +00:00
use WC_Item_Totals ;
2017-07-27 09:49:47 +00:00
/**
2017-07-27 12:48:58 +00:00
* Reference to cart object .
2017-07-27 09:49:47 +00:00
*
* @ since 3.2 . 0
2017-09-27 16:12:45 +00:00
* @ var WC_Cart
2017-07-27 09:49:47 +00:00
*/
2017-08-18 12:48:53 +00:00
protected $cart ;
2017-07-27 09:49:47 +00:00
2017-07-27 12:48:58 +00:00
/**
* Reference to customer object .
*
* @ since 3.2 . 0
* @ var array
*/
protected $customer ;
2017-07-27 09:49:47 +00:00
/**
* Line items to calculate .
*
* @ since 3.2 . 0
* @ var array
*/
protected $items = array ();
/**
* Fees to calculate .
*
* @ since 3.2 . 0
* @ var array
*/
protected $fees = array ();
/**
* Shipping costs .
*
* @ since 3.2 . 0
* @ var array
*/
protected $shipping = array ();
/**
* Applied coupon objects .
*
* @ since 3.2 . 0
* @ var array
*/
protected $coupons = array ();
/**
2017-08-18 12:48:53 +00:00
* Item / coupon discount totals .
2017-07-27 09:49:47 +00:00
*
* @ since 3.2 . 0
* @ var array
*/
2017-08-18 12:48:53 +00:00
protected $coupon_discount_totals = array ();
/**
* Item / coupon discount tax totals .
*
* @ since 3.2 . 0
* @ var array
*/
protected $coupon_discount_tax_totals = array ();
2017-07-27 09:49:47 +00:00
2017-07-27 12:48:58 +00:00
/**
* Should taxes be calculated ?
*
* @ var boolean
*/
protected $calculate_tax = true ;
2017-07-27 09:49:47 +00:00
/**
* Stores totals .
*
* @ since 3.2 . 0
* @ var array
*/
protected $totals = array (
2018-03-16 17:47:18 +00:00
'fees_total' => 0 ,
'fees_total_tax' => 0 ,
'items_subtotal' => 0 ,
'items_subtotal_tax' => 0 ,
'items_total' => 0 ,
'items_total_tax' => 0 ,
'total' => 0 ,
'shipping_total' => 0 ,
'shipping_tax_total' => 0 ,
'discounts_total' => 0 ,
2017-07-27 09:49:47 +00:00
);
2017-07-26 11:44:06 +00:00
/**
* Sets up the items provided , and calculate totals .
*
* @ since 3.2 . 0
2017-09-27 16:12:45 +00:00
* @ throws Exception If missing WC_Cart object .
2017-09-27 16:16:33 +00:00
* @ param WC_Cart $cart Cart object to calculate totals for .
2017-07-26 11:44:06 +00:00
*/
2017-07-28 12:02:39 +00:00
public function __construct ( & $cart = null ) {
2017-09-27 16:12:45 +00:00
if ( ! is_a ( $cart , 'WC_Cart' ) ) {
throw new Exception ( 'A valid WC_Cart object is required' );
2017-07-26 11:44:06 +00:00
}
2017-09-27 16:12:45 +00:00
$this -> cart = $cart ;
$this -> calculate_tax = wc_tax_enabled () && ! $cart -> get_customer () -> get_is_vat_exempt ();
$this -> calculate ();
2017-07-26 11:44:06 +00:00
}
2017-07-27 09:49:47 +00:00
/**
2020-10-23 13:27:39 +00:00
* Run all calculation methods on the given items in sequence .
2017-07-27 09:49:47 +00:00
*
* @ since 3.2 . 0
*/
protected function calculate () {
$this -> calculate_item_totals ();
$this -> calculate_shipping_totals ();
2017-08-18 12:48:53 +00:00
$this -> calculate_fee_totals ();
2017-07-27 09:49:47 +00:00
$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 ,
2017-08-18 12:48:53 +00:00
'tax_class' => '' ,
'taxable' => false ,
2017-07-27 09:49:47 +00:00
'quantity' => 0 ,
'product' => false ,
'price_includes_tax' => false ,
'subtotal' => 0 ,
'subtotal_tax' => 0 ,
2019-01-11 12:59:23 +00:00
'subtotal_taxes' => array (),
2017-07-27 09:49:47 +00:00
'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 (
2017-08-18 12:48:53 +00:00
'object' => null ,
'tax_class' => '' ,
'taxable' => false ,
2017-07-27 09:49:47 +00:00
'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 (
2017-08-18 12:48:53 +00:00
'object' => null ,
'tax_class' => '' ,
'taxable' => false ,
2017-07-27 09:49:47 +00:00
'total' => 0 ,
'total_tax' => 0 ,
'taxes' => array (),
);
}
2017-07-26 11:44:06 +00:00
/**
* Handles a cart or order object passed in for calculation . Normalises data
* into the same format for use by this class .
*
* Each item is made up of the following props , in addition to those returned by get_default_item_props () for totals .
2018-03-16 17:47:18 +00:00
* - key : An identifier for the item ( cart item key or line item ID ) .
2017-07-26 11:44:06 +00:00
* - cart_item : For carts , the cart item from the cart which may include custom data .
* - quantity : The qty for this line .
* - price : The line price in cents .
* - product : The product object this cart item is for .
*
* @ since 3.2 . 0
*/
2017-08-18 12:48:53 +00:00
protected function get_items_from_cart () {
2017-07-26 13:32:43 +00:00
$this -> items = array ();
2017-08-18 12:48:53 +00:00
foreach ( $this -> cart -> get_cart () as $cart_item_key => $cart_item ) {
2017-07-26 11:44:06 +00:00
$item = $this -> get_default_item_props ();
2017-11-02 16:18:51 +00:00
$item -> key = $cart_item_key ;
2017-07-26 13:32:43 +00:00
$item -> object = $cart_item ;
2017-08-18 12:48:53 +00:00
$item -> tax_class = $cart_item [ 'data' ] -> get_tax_class ();
$item -> taxable = 'taxable' === $cart_item [ 'data' ] -> get_tax_status ();
2017-07-26 14:47:30 +00:00
$item -> price_includes_tax = wc_prices_include_tax ();
2017-07-26 11:44:06 +00:00
$item -> quantity = $cart_item [ 'quantity' ];
2021-08-12 23:27:05 +00:00
$item -> price = wc_add_number_precision_deep ( ( float ) $cart_item [ 'data' ] -> get_price () * ( int ) $cart_item [ 'quantity' ] );
2017-07-26 11:44:06 +00:00
$item -> product = $cart_item [ 'data' ];
2017-07-27 12:48:58 +00:00
$item -> tax_rates = $this -> get_item_tax_rates ( $item );
2017-07-26 11:44:06 +00:00
$this -> items [ $cart_item_key ] = $item ;
}
}
2017-08-22 15:12:37 +00:00
/**
* Get item costs grouped by tax class .
*
* @ since 3.2 . 0
* @ return array
*/
protected function get_tax_class_costs () {
$item_tax_classes = wp_list_pluck ( $this -> items , 'tax_class' );
$shipping_tax_classes = wp_list_pluck ( $this -> shipping , 'tax_class' );
$fee_tax_classes = wp_list_pluck ( $this -> fees , 'tax_class' );
$costs = array_fill_keys ( $item_tax_classes + $shipping_tax_classes + $fee_tax_classes , 0 );
$costs [ 'non-taxable' ] = 0 ;
foreach ( $this -> items + $this -> fees + $this -> shipping as $item ) {
if ( 0 > $item -> total ) {
continue ;
}
if ( ! $item -> taxable ) {
$costs [ 'non-taxable' ] += $item -> total ;
} elseif ( 'inherit' === $item -> tax_class ) {
$costs [ reset ( $item_tax_classes ) ] += $item -> total ;
} else {
$costs [ $item -> tax_class ] += $item -> total ;
}
}
return array_filter ( $costs );
}
2017-07-26 11:44:06 +00:00
/**
* Get fee objects from the cart . Normalises data
* into the same format for use by this class .
*
* @ since 3.2 . 0
*/
2017-08-18 12:48:53 +00:00
protected function get_fees_from_cart () {
2017-07-26 11:44:06 +00:00
$this -> fees = array ();
2017-08-18 12:48:53 +00:00
$this -> cart -> calculate_fees ();
2017-07-26 11:44:06 +00:00
2017-08-23 11:15:06 +00:00
$fee_running_total = 0 ;
2017-08-18 12:48:53 +00:00
foreach ( $this -> cart -> get_fees () as $fee_key => $fee_object ) {
$fee = $this -> get_default_fee_props ();
$fee -> object = $fee_object ;
$fee -> tax_class = $fee -> object -> tax_class ;
$fee -> taxable = $fee -> object -> taxable ;
$fee -> total = wc_add_number_precision_deep ( $fee -> object -> amount );
2017-07-26 11:44:06 +00:00
2017-08-23 11:15:06 +00:00
// Negative fees should not make the order total go negative.
if ( 0 > $fee -> total ) {
2020-10-01 08:57:12 +00:00
$max_discount = NumberUtil :: round ( $this -> get_total ( 'items_total' , true ) + $fee_running_total + $this -> get_total ( 'shipping_total' , true ) ) * - 1 ;
2017-08-23 11:15:06 +00:00
if ( $fee -> total < $max_discount ) {
$fee -> total = $max_discount ;
}
}
$fee_running_total += $fee -> total ;
2017-08-22 15:12:37 +00:00
if ( $this -> calculate_tax ) {
if ( 0 > $fee -> total ) {
// Negative fees should have the taxes split between all items so it works as a true discount.
2017-08-22 16:02:48 +00:00
$tax_class_costs = $this -> get_tax_class_costs ();
2017-08-22 15:12:37 +00:00
$total_cost = array_sum ( $tax_class_costs );
if ( $total_cost ) {
foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) {
if ( 'non-taxable' === $tax_class ) {
continue ;
}
$proportion = $tax_class_cost / $total_cost ;
$cart_discount_proportion = $fee -> total * $proportion ;
$fee -> taxes = wc_array_merge_recursive_numeric ( $fee -> taxes , WC_Tax :: calc_tax ( $fee -> total * $proportion , WC_Tax :: get_rates ( $tax_class ) ) );
}
}
} elseif ( $fee -> object -> taxable ) {
$fee -> taxes = WC_Tax :: calc_tax ( $fee -> total , WC_Tax :: get_rates ( $fee -> tax_class , $this -> cart -> get_customer () ), false );
2017-07-27 12:48:58 +00:00
}
2017-07-26 11:44:06 +00:00
}
2018-03-16 17:47:18 +00:00
$fee -> taxes = apply_filters ( 'woocommerce_cart_totals_get_fees_from_cart_taxes' , $fee -> taxes , $fee , $this );
2017-12-04 21:40:12 +00:00
$fee -> total_tax = array_sum ( array_map ( array ( $this , 'round_line_tax' ), $fee -> taxes ) );
2017-08-22 15:12:37 +00:00
2017-08-22 15:20:23 +00:00
// Set totals within object.
2017-08-23 11:15:06 +00:00
$fee -> object -> total = wc_remove_number_precision_deep ( $fee -> total );
$fee -> object -> tax_data = wc_remove_number_precision_deep ( $fee -> taxes );
$fee -> object -> tax = wc_remove_number_precision_deep ( $fee -> total_tax );
2017-08-22 15:20:23 +00:00
2017-07-26 11:44:06 +00:00
$this -> fees [ $fee_key ] = $fee ;
}
}
/**
* Get shipping methods from the cart and normalise .
*
* @ since 3.2 . 0
*/
2017-08-18 12:48:53 +00:00
protected function get_shipping_from_cart () {
2017-07-26 11:44:06 +00:00
$this -> shipping = array ();
2017-10-27 16:30:33 +00:00
if ( ! $this -> cart -> show_shipping () ) {
return ;
}
2017-08-18 12:48:53 +00:00
foreach ( $this -> cart -> calculate_shipping () as $key => $shipping_object ) {
2017-07-26 11:44:06 +00:00
$shipping_line = $this -> get_default_shipping_props ();
2017-07-26 13:32:43 +00:00
$shipping_line -> object = $shipping_object ;
2017-08-18 12:48:53 +00:00
$shipping_line -> tax_class = get_option ( 'woocommerce_shipping_tax_class' );
$shipping_line -> taxable = true ;
2017-07-27 09:51:08 +00:00
$shipping_line -> total = wc_add_number_precision_deep ( $shipping_object -> cost );
2017-12-04 21:40:12 +00:00
$shipping_line -> taxes = wc_add_number_precision_deep ( $shipping_object -> taxes , false );
2020-06-23 19:19:35 +00:00
$shipping_line -> taxes = array_map ( array ( $this , 'round_item_subtotal' ), $shipping_line -> taxes );
$shipping_line -> total_tax = array_sum ( $shipping_line -> taxes );
2017-07-27 12:48:58 +00:00
$this -> shipping [ $key ] = $shipping_line ;
2017-07-26 11:44:06 +00:00
}
}
/**
* Return array of coupon objects from the cart . Normalises data
* into the same format for use by this class .
*
* @ since 3.2 . 0
*/
2017-08-18 12:48:53 +00:00
protected function get_coupons_from_cart () {
$this -> coupons = $this -> cart -> get_coupons ();
2017-08-10 16:24:27 +00:00
foreach ( $this -> coupons as $coupon ) {
switch ( $coupon -> get_discount_type () ) {
2018-03-16 17:47:18 +00:00
case 'fixed_product' :
2017-08-10 16:24:27 +00:00
$coupon -> sort = 1 ;
break ;
2018-03-16 17:47:18 +00:00
case 'percent' :
2017-08-10 16:24:27 +00:00
$coupon -> sort = 2 ;
break ;
2018-03-16 17:47:18 +00:00
case 'fixed_cart' :
2017-08-10 16:24:27 +00:00
$coupon -> sort = 3 ;
break ;
default :
$coupon -> sort = 0 ;
break ;
}
2018-03-13 10:26:22 +00:00
// Allow plugins to override the default order.
$coupon -> sort = apply_filters ( 'woocommerce_coupon_sort' , $coupon -> sort , $coupon );
2017-08-10 16:24:27 +00:00
}
2017-08-18 12:48:53 +00:00
uasort ( $this -> coupons , array ( $this , 'sort_coupons_callback' ) );
2017-08-10 16:24:27 +00:00
}
/**
2017-09-20 17:41:48 +00:00
* Sort coupons so discounts apply consistently across installs .
*
* In order of priority ;
2018-03-16 17:47:18 +00:00
* - sort param
2017-09-20 17:41:48 +00:00
* - usage restriction
* - coupon value
* - ID
2017-08-10 16:24:27 +00:00
*
2017-08-15 11:21:12 +00:00
* @ param WC_Coupon $a Coupon object .
* @ param WC_Coupon $b Coupon object .
2017-08-10 16:24:27 +00:00
* @ return int
*/
2017-08-18 12:48:53 +00:00
protected function sort_coupons_callback ( $a , $b ) {
2017-08-10 16:24:27 +00:00
if ( $a -> sort === $b -> sort ) {
2017-09-20 17:41:48 +00:00
if ( $a -> get_limit_usage_to_x_items () === $b -> get_limit_usage_to_x_items () ) {
if ( $a -> get_amount () === $b -> get_amount () ) {
return $b -> get_id () - $a -> get_id ();
}
return ( $a -> get_amount () < $b -> get_amount () ) ? - 1 : 1 ;
}
return ( $a -> get_limit_usage_to_x_items () < $b -> get_limit_usage_to_x_items () ) ? - 1 : 1 ;
2017-08-10 16:24:27 +00:00
}
return ( $a -> sort < $b -> sort ) ? - 1 : 1 ;
2017-07-26 11:44:06 +00:00
}
2017-10-18 16:31:35 +00:00
/**
* Ran to remove all base taxes from an item . Used when prices include tax , and the customer is tax exempt .
*
* @ since 3.2 . 2
* @ param object $item Item to adjust the prices of .
* @ return object
*/
protected function remove_item_base_taxes ( $item ) {
2017-11-23 11:08:10 +00:00
if ( $item -> price_includes_tax && $item -> taxable ) {
2019-01-04 16:46:52 +00:00
if ( apply_filters ( 'woocommerce_adjust_non_base_location_prices' , true ) ) {
$base_tax_rates = WC_Tax :: get_base_tax_rates ( $item -> product -> get_tax_class ( 'unfiltered' ) );
} else {
/**
* If we want all customers to pay the same price on this store , we should not remove base taxes from a VAT exempt user ' s price ,
* but just the relevent tax rate . See issue #20911.
*/
$base_tax_rates = $item -> tax_rates ;
}
2017-10-18 16:31:35 +00:00
// Work out a new base price without the shop's base tax.
2018-03-16 17:47:18 +00:00
$taxes = WC_Tax :: calc_tax ( $item -> price , $base_tax_rates , true );
2017-10-18 16:31:35 +00:00
// Now we have a new item price (excluding TAX).
2020-10-01 08:57:12 +00:00
$item -> price = NumberUtil :: round ( $item -> price - array_sum ( $taxes ) );
2017-10-18 16:31:35 +00:00
$item -> price_includes_tax = false ;
}
return $item ;
}
2017-07-26 11:44:06 +00:00
/**
2017-07-27 09:49:47 +00:00
* Only ran if woocommerce_adjust_non_base_location_prices is true .
*
* If the customer is outside of the base location , this removes the base
* taxes . This is off by default unless the filter is used .
*
2017-08-08 08:24:26 +00:00
* Uses edit context so unfiltered tax class is returned .
*
2017-07-27 09:49:47 +00:00
* @ since 3.2 . 0
* @ param object $item Item to adjust the prices of .
* @ return object
*/
protected function adjust_non_base_location_price ( $item ) {
2017-11-23 11:08:10 +00:00
if ( $item -> price_includes_tax && $item -> taxable ) {
2017-10-18 16:31:35 +00:00
$base_tax_rates = WC_Tax :: get_base_tax_rates ( $item -> product -> get_tax_class ( 'unfiltered' ) );
2017-07-27 09:49:47 +00:00
2017-10-18 16:31:35 +00:00
if ( $item -> tax_rates !== $base_tax_rates ) {
// Work out a new base price without the shop's base tax.
2018-03-16 17:47:18 +00:00
$taxes = WC_Tax :: calc_tax ( $item -> price , $base_tax_rates , true );
$new_taxes = WC_Tax :: calc_tax ( $item -> price - array_sum ( $taxes ), $item -> tax_rates , false );
2017-07-27 09:49:47 +00:00
2017-11-02 20:00:20 +00:00
// Now we have a new item price.
2019-04-17 15:49:46 +00:00
$item -> price = $item -> price - array_sum ( $taxes ) + array_sum ( $new_taxes );
2017-10-18 16:31:35 +00:00
}
2017-07-27 09:49:47 +00:00
}
return $item ;
}
/**
* Get discounted price of an item with precision ( in cents ) .
*
* @ since 3.2 . 0
* @ param object $item_key Item to get the price of .
* @ return int
*/
protected function get_discounted_price_in_cents ( $item_key ) {
2017-08-08 08:24:26 +00:00
$item = $this -> items [ $item_key ];
2017-11-02 16:18:51 +00:00
$price = isset ( $this -> coupon_discount_totals [ $item_key ] ) ? $item -> price - $this -> coupon_discount_totals [ $item_key ] : $item -> price ;
2017-08-02 18:07:33 +00:00
return $price ;
2017-07-27 09:49:47 +00:00
}
/**
* Get tax rates for an item . Caches rates in class to avoid multiple look ups .
*
* @ param object $item Item to get tax rates for .
* @ return array of taxes
*/
protected function get_item_tax_rates ( $item ) {
2018-05-01 10:56:39 +00:00
if ( ! wc_tax_enabled () ) {
return array ();
}
2019-01-21 15:38:48 +00:00
$tax_class = $item -> product -> get_tax_class ();
$item_tax_rates = 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 -> cart -> get_customer () );
// Allow plugins to filter item tax rates.
return apply_filters ( 'woocommerce_cart_totals_get_item_tax_rates' , $item_tax_rates , $item , $this -> cart );
2017-08-18 12:48:53 +00:00
}
/**
* Get item costs grouped by tax class .
*
* @ since 3.2 . 0
* @ return array
*/
protected function get_item_costs_by_tax_class () {
$tax_classes = array (
'non-taxable' => 0 ,
);
foreach ( $this -> items + $this -> fees + $this -> shipping as $item ) {
if ( ! isset ( $tax_classes [ $item -> tax_class ] ) ) {
$tax_classes [ $item -> tax_class ] = 0 ;
}
if ( $item -> taxable ) {
$tax_classes [ $item -> tax_class ] += $item -> total ;
} else {
$tax_classes [ 'non-taxable' ] += $item -> total ;
}
}
return $tax_classes ;
2017-07-27 09:49:47 +00:00
}
/**
* Get a single total with or without precision ( in cents ) .
*
* @ since 3.2 . 0
* @ param string $key Total to get .
* @ param bool $in_cents Should the totals be returned in cents , or without precision .
* @ return int | float
*/
public function get_total ( $key = 'total' , $in_cents = false ) {
$totals = $this -> get_totals ( $in_cents );
return isset ( $totals [ $key ] ) ? $totals [ $key ] : 0 ;
}
/**
* Set a single total .
*
* @ since 3.2 . 0
* @ param string $key Total name you want to set .
* @ param int $total Total to set .
*/
2020-10-02 07:45:09 +00:00
protected function set_total ( $key , $total ) {
2017-07-27 09:49:47 +00:00
$this -> totals [ $key ] = $total ;
}
/**
* Get all totals with or without precision ( in cents ) .
*
* @ since 3.2 . 0
* @ param bool $in_cents Should the totals be returned in cents , or without precision .
* @ return array .
*/
public function get_totals ( $in_cents = false ) {
2017-07-27 09:51:08 +00:00
return $in_cents ? $this -> totals : wc_remove_number_precision_deep ( $this -> totals );
2017-07-27 09:49:47 +00:00
}
2019-10-14 18:53:16 +00:00
/**
* 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 ) );
}
2017-07-27 09:49:47 +00:00
/**
2017-08-18 12:48:53 +00:00
* Get taxes merged by type .
2017-07-27 09:49:47 +00:00
*
2017-08-18 12:48:53 +00:00
* @ since 3.2 . 0
2018-03-16 17:47:18 +00:00
* @ param bool $in_cents If returned value should be in cents .
* @ param array | string $types Types to merge and return . Defaults to all .
2017-07-27 09:49:47 +00:00
* @ return array
*/
2017-08-18 12:48:53 +00:00
protected function get_merged_taxes ( $in_cents = false , $types = array ( 'items' , 'fees' , 'shipping' ) ) {
$items = array ();
2017-07-27 09:49:47 +00:00
$taxes = array ();
2017-08-18 12:48:53 +00:00
if ( is_string ( $types ) ) {
$types = array ( $types );
2017-07-27 09:49:47 +00:00
}
2017-08-18 12:48:53 +00:00
foreach ( $types as $type ) {
if ( isset ( $this -> $type ) ) {
$items = array_merge ( $items , $this -> $type );
2017-07-27 09:49:47 +00:00
}
}
2017-08-18 12:48:53 +00:00
foreach ( $items as $item ) {
2017-07-27 09:49:47 +00:00
foreach ( $item -> taxes as $rate_id => $rate ) {
2017-08-18 12:48:53 +00:00
if ( ! isset ( $taxes [ $rate_id ] ) ) {
$taxes [ $rate_id ] = 0 ;
}
2017-12-04 21:40:12 +00:00
$taxes [ $rate_id ] += $this -> round_line_tax ( $rate );
2017-07-27 09:49:47 +00:00
}
}
2017-08-18 12:48:53 +00:00
return $in_cents ? $taxes : wc_remove_number_precision_deep ( $taxes );
}
2018-08-29 11:34:22 +00:00
/**
* Round merged taxes .
*
2019-10-14 15:14:13 +00:00
* @ deprecated 3.9 . 0 `calculate_item_subtotals` should already appropriately round the tax values .
2019-01-10 13:20:02 +00:00
* @ since 3.5 . 4
2018-08-29 11:34:22 +00:00
* @ param array $taxes Taxes to round .
* @ return array
*/
2019-01-10 13:23:49 +00:00
protected function round_merged_taxes ( $taxes ) {
2020-03-02 08:48:24 +00:00
foreach ( $taxes as $rate_id => $tax ) {
$taxes [ $rate_id ] = $this -> round_line_tax ( $tax );
2018-08-29 11:34:22 +00:00
}
return $taxes ;
}
2017-08-18 12:48:53 +00:00
/**
* Combine item taxes into a single array , preserving keys .
*
* @ since 3.2 . 0
2018-03-16 17:47:18 +00:00
* @ param array $item_taxes Taxes to combine .
2017-08-18 12:48:53 +00:00
* @ return array
*/
2017-08-18 14:05:01 +00:00
protected function combine_item_taxes ( $item_taxes ) {
2017-08-18 12:48:53 +00:00
$merged_taxes = array ();
2017-08-18 14:05:01 +00:00
foreach ( $item_taxes as $taxes ) {
foreach ( $taxes as $tax_id => $tax_amount ) {
if ( ! isset ( $merged_taxes [ $tax_id ] ) ) {
$merged_taxes [ $tax_id ] = 0 ;
}
$merged_taxes [ $tax_id ] += $tax_amount ;
}
2017-08-18 12:48:53 +00:00
}
return $merged_taxes ;
2017-07-27 09:49:47 +00:00
}
/*
|--------------------------------------------------------------------------
| Calculation methods .
|--------------------------------------------------------------------------
*/
/**
* Calculate item totals .
2017-07-26 11:44:06 +00:00
*
* @ since 3.2 . 0
*/
protected function calculate_item_totals () {
2017-08-18 12:48:53 +00:00
$this -> get_items_from_cart ();
2017-07-27 09:49:47 +00:00
$this -> calculate_item_subtotals ();
2017-08-18 12:48:53 +00:00
$this -> calculate_discounts ();
2017-07-27 09:49:47 +00:00
foreach ( $this -> items as $item_key => $item ) {
$item -> total = $this -> get_discounted_price_in_cents ( $item_key );
$item -> total_tax = 0 ;
2017-07-27 14:31:10 +00:00
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 .
2017-08-18 12:48:53 +00:00
* $this -> cart is the cart object .
2017-07-27 14:31:10 +00:00
*/
$item -> total = wc_add_number_precision (
2017-08-18 12:48:53 +00:00
apply_filters ( 'woocommerce_get_discounted_price' , wc_remove_number_precision ( $item -> total ), $item -> object , $this -> cart )
2017-07-27 14:31:10 +00:00
);
}
2017-07-27 12:48:58 +00:00
if ( $this -> calculate_tax && $item -> product -> is_taxable () ) {
2018-06-20 12:16:14 +00:00
$total_taxes = apply_filters ( 'woocommerce_calculate_item_totals_taxes' , WC_Tax :: calc_tax ( $item -> total , $item -> tax_rates , $item -> price_includes_tax ), $item , $this );
2017-12-04 21:40:12 +00:00
$item -> taxes = $total_taxes ;
$item -> total_tax = array_sum ( array_map ( array ( $this , 'round_line_tax' ), $item -> taxes ) );
2017-07-27 09:49:47 +00:00
if ( $item -> price_includes_tax ) {
2017-12-04 21:40:12 +00:00
// Use unrounded taxes so we can re-calculate from the orders screen accurately later.
$item -> total = $item -> total - array_sum ( $item -> taxes );
2017-07-27 09:49:47 +00:00
}
}
2017-07-31 18:48:34 +00:00
2017-08-18 12:48:53 +00:00
$this -> cart -> cart_contents [ $item_key ][ 'line_tax_data' ][ 'total' ] = wc_remove_number_precision_deep ( $item -> taxes );
$this -> cart -> cart_contents [ $item_key ][ 'line_total' ] = wc_remove_number_precision ( $item -> total );
$this -> cart -> cart_contents [ $item_key ][ 'line_tax' ] = wc_remove_number_precision ( $item -> total_tax );
2017-07-27 09:49:47 +00:00
}
2017-07-26 11:44:06 +00:00
2019-11-25 20:00:45 +00:00
$items_total = $this -> get_rounded_items_total ( $this -> get_values_for_total ( 'total' ) );
2019-10-14 15:14:13 +00:00
2020-03-03 11:04:35 +00:00
$this -> set_total ( 'items_total' , $items_total );
2017-07-27 09:49:47 +00:00
$this -> set_total ( 'items_total_tax' , array_sum ( array_values ( wp_list_pluck ( $this -> items , 'total_tax' ) ) ) );
2017-09-15 18:15:50 +00:00
$this -> cart -> set_cart_contents_total ( $this -> get_total ( 'items_total' ) );
$this -> cart -> set_cart_contents_tax ( array_sum ( $this -> get_merged_taxes ( false , 'items' ) ) );
$this -> cart -> set_cart_contents_taxes ( $this -> get_merged_taxes ( false , 'items' ) );
2017-07-26 11:44:06 +00:00
}
2017-07-27 09:49:47 +00:00
/**
* Subtotals are costs before discounts .
*
2021-01-22 16:13:41 +00:00
* To prevent rounding issues we need to work with the inclusive price where possible
* otherwise we ' ll see errors such as when working with a 9.99 inc price , 20 % VAT which would
2017-07-27 09:49:47 +00:00
* be 8.325 leading to totals being 1 p off .
*
2021-01-22 16:13:41 +00:00
* Pre tax coupons come off the price the customer thinks they are paying - tax is calculated
2017-07-27 09:49:47 +00:00
* afterwards .
*
* e . g . $ 100 bike with $ 10 coupon = customer pays $ 90 and tax worked backwards from that .
*
* @ since 3.2 . 0
*/
protected function calculate_item_subtotals () {
2019-01-11 12:59:23 +00:00
$merged_subtotal_taxes = array (); // Taxes indexed by tax rate ID for storage later.
2019-10-14 15:14:13 +00:00
$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 ();
2017-07-31 18:48:34 +00:00
foreach ( $this -> items as $item_key => $item ) {
2017-10-18 16:31:35 +00:00
if ( $item -> price_includes_tax ) {
2019-10-14 15:14:13 +00:00
if ( $is_customer_vat_exempt ) {
2017-10-18 16:31:35 +00:00
$item = $this -> remove_item_base_taxes ( $item );
2019-10-14 15:14:13 +00:00
} elseif ( $adjust_non_base_location_prices ) {
2017-10-18 16:31:35 +00:00
$item = $this -> adjust_non_base_location_price ( $item );
}
2017-07-27 09:49:47 +00:00
}
2017-08-08 08:24:26 +00:00
2017-11-02 16:18:51 +00:00
$item -> subtotal = $item -> price ;
2017-08-18 12:53:22 +00:00
2017-07-27 12:48:58 +00:00
if ( $this -> calculate_tax && $item -> product -> is_taxable () ) {
2019-01-11 12:59:23 +00:00
$item -> subtotal_taxes = WC_Tax :: calc_tax ( $item -> subtotal , $item -> tax_rates , $item -> price_includes_tax );
$item -> subtotal_tax = array_sum ( array_map ( array ( $this , 'round_line_tax' ), $item -> subtotal_taxes ) );
2017-07-27 09:49:47 +00:00
if ( $item -> price_includes_tax ) {
2017-12-04 21:40:12 +00:00
// Use unrounded taxes so we can re-calculate from the orders screen accurately later.
2019-01-11 12:59:23 +00:00
$item -> subtotal = $item -> subtotal - array_sum ( $item -> subtotal_taxes );
}
foreach ( $item -> subtotal_taxes as $rate_id => $rate ) {
if ( ! isset ( $merged_subtotal_taxes [ $rate_id ] ) ) {
$merged_subtotal_taxes [ $rate_id ] = 0 ;
}
$merged_subtotal_taxes [ $rate_id ] += $this -> round_line_tax ( $rate );
2017-07-27 09:49:47 +00:00
}
}
2017-07-31 18:48:34 +00:00
2019-01-11 12:59:23 +00:00
$this -> cart -> cart_contents [ $item_key ][ 'line_tax_data' ] = array ( 'subtotal' => wc_remove_number_precision_deep ( $item -> subtotal_taxes ) );
2017-08-18 12:48:53 +00:00
$this -> cart -> cart_contents [ $item_key ][ 'line_subtotal' ] = wc_remove_number_precision ( $item -> subtotal );
$this -> cart -> cart_contents [ $item_key ][ 'line_subtotal_tax' ] = wc_remove_number_precision ( $item -> subtotal_tax );
2017-07-27 09:49:47 +00:00
}
2019-01-11 12:59:23 +00:00
2019-11-25 20:00:45 +00:00
$items_subtotal = $this -> get_rounded_items_total ( $this -> get_values_for_total ( 'subtotal' ) );
2019-10-14 15:14:13 +00:00
2021-03-10 15:14:47 +00:00
// Prices are not rounded here because they should already be rounded based on settings in `get_rounded_items_total` and in `round_line_tax` method calls.
2021-02-22 09:51:29 +00:00
$this -> set_total ( 'items_subtotal' , $items_subtotal );
$this -> set_total ( 'items_subtotal_tax' , array_sum ( $merged_subtotal_taxes ), 0 );
2017-07-27 10:01:48 +00:00
2017-08-18 12:48:53 +00:00
$this -> cart -> set_subtotal ( $this -> get_total ( 'items_subtotal' ) );
$this -> cart -> set_subtotal_tax ( $this -> get_total ( 'items_subtotal_tax' ) );
2017-07-27 09:49:47 +00:00
}
/**
2017-08-18 12:48:53 +00:00
* Calculate COUPON based discounts which change item prices .
2017-07-27 09:49:47 +00:00
*
* @ since 3.2 . 0
* @ uses WC_Discounts class .
*/
2017-08-18 12:48:53 +00:00
protected function calculate_discounts () {
$this -> get_coupons_from_cart ();
2017-07-27 09:49:47 +00:00
2017-08-18 12:48:53 +00:00
$discounts = new WC_Discounts ( $this -> cart );
2017-07-27 09:49:47 +00:00
2017-11-02 16:18:51 +00:00
// Set items directly so the discounts class can see any tax adjustments made thus far using subtotals.
$discounts -> set_items ( $this -> items );
2017-07-27 09:49:47 +00:00
foreach ( $this -> coupons as $coupon ) {
2017-08-11 15:16:36 +00:00
$discounts -> apply_coupon ( $coupon );
2017-07-27 09:49:47 +00:00
}
2017-08-08 08:24:26 +00:00
$coupon_discount_amounts = $discounts -> get_discounts_by_coupon ( true );
$coupon_discount_tax_amounts = array ();
2017-07-27 09:49:47 +00:00
2017-07-28 14:35:41 +00:00
// See how much tax was 'discounted' per item and per coupon.
2017-07-27 12:48:58 +00:00
if ( $this -> calculate_tax ) {
2017-07-28 14:35:41 +00:00
foreach ( $discounts -> get_discounts ( true ) as $coupon_code => $coupon_discounts ) {
$coupon_discount_tax_amounts [ $coupon_code ] = 0 ;
2017-07-28 12:02:39 +00:00
2017-08-18 12:48:53 +00:00
foreach ( $coupon_discounts as $item_key => $coupon_discount ) {
2017-07-28 14:35:41 +00:00
$item = $this -> items [ $item_key ];
2017-07-28 12:02:39 +00:00
2017-07-28 14:35:41 +00:00
if ( $item -> product -> is_taxable () ) {
2017-11-02 16:18:51 +00:00
// Item subtotals were sent, so set 3rd param.
2021-02-22 09:48:05 +00:00
$item_tax = array_sum ( WC_Tax :: calc_tax ( $coupon_discount , $item -> tax_rates , $item -> price_includes_tax ) );
2017-11-02 16:18:51 +00:00
// Sum total tax.
2017-07-28 14:35:41 +00:00
$coupon_discount_tax_amounts [ $coupon_code ] += $item_tax ;
2017-07-28 12:02:39 +00:00
2017-11-02 16:18:51 +00:00
// Remove tax from discount total.
if ( $item -> price_includes_tax ) {
$coupon_discount_amounts [ $coupon_code ] -= $item_tax ;
}
}
2017-08-15 15:00:38 +00:00
}
2017-08-08 08:24:26 +00:00
}
2017-07-28 14:35:41 +00:00
}
2017-08-08 08:24:26 +00:00
2017-11-02 16:18:51 +00:00
$this -> coupon_discount_totals = ( array ) $discounts -> get_discounts_by_item ( true );
$this -> coupon_discount_tax_totals = $coupon_discount_tax_amounts ;
2017-08-08 08:24:26 +00:00
2017-10-13 13:36:35 +00:00
if ( wc_prices_include_tax () ) {
$this -> set_total ( 'discounts_total' , array_sum ( $this -> coupon_discount_totals ) - array_sum ( $this -> coupon_discount_tax_totals ) );
$this -> set_total ( 'discounts_tax_total' , array_sum ( $this -> coupon_discount_tax_totals ) );
} else {
$this -> set_total ( 'discounts_total' , array_sum ( $this -> coupon_discount_totals ) );
$this -> set_total ( 'discounts_tax_total' , array_sum ( $this -> coupon_discount_tax_totals ) );
}
2017-08-23 11:15:06 +00:00
2017-08-18 12:48:53 +00:00
$this -> cart -> set_coupon_discount_totals ( wc_remove_number_precision_deep ( $coupon_discount_amounts ) );
$this -> cart -> set_coupon_discount_tax_totals ( wc_remove_number_precision_deep ( $coupon_discount_tax_amounts ) );
2017-10-26 14:27:10 +00:00
// Add totals to cart object. Note: Discount total for cart is excl tax.
$this -> cart -> set_discount_total ( $this -> get_total ( 'discounts_total' ) );
$this -> cart -> set_discount_tax ( $this -> get_total ( 'discounts_tax_total' ) );
2017-07-27 09:49:47 +00:00
}
2017-07-26 11:44:06 +00:00
/**
* Triggers the cart fees API , grabs the list of fees , and calculates taxes .
*
* 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
*/
protected function calculate_fee_totals () {
2017-08-18 12:48:53 +00:00
$this -> get_fees_from_cart ();
2017-08-22 15:20:23 +00:00
2017-07-27 09:49:47 +00:00
$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' ) ) );
2017-07-26 11:44:06 +00:00
2017-08-22 15:20:23 +00:00
$this -> cart -> fees_api () -> set_fees ( wp_list_pluck ( $this -> fees , 'object' ) );
2017-08-18 12:48:53 +00:00
$this -> cart -> set_fee_total ( wc_remove_number_precision_deep ( array_sum ( wp_list_pluck ( $this -> fees , 'total' ) ) ) );
$this -> cart -> set_fee_tax ( wc_remove_number_precision_deep ( array_sum ( wp_list_pluck ( $this -> fees , 'total_tax' ) ) ) );
$this -> cart -> set_fee_taxes ( wc_remove_number_precision_deep ( $this -> combine_item_taxes ( wp_list_pluck ( $this -> fees , 'taxes' ) ) ) );
2017-07-26 11:44:06 +00:00
}
/**
* Calculate any shipping taxes .
*
* @ since 3.2 . 0
*/
protected function calculate_shipping_totals () {
2017-08-18 12:48:53 +00:00
$this -> get_shipping_from_cart ();
2017-07-27 09:49:47 +00:00
$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' ) ) );
2017-07-27 10:01:48 +00:00
2017-08-18 12:48:53 +00:00
$this -> cart -> set_shipping_total ( $this -> get_total ( 'shipping_total' ) );
$this -> cart -> set_shipping_tax ( $this -> get_total ( 'shipping_tax_total' ) );
$this -> cart -> set_shipping_taxes ( wc_remove_number_precision_deep ( $this -> combine_item_taxes ( wp_list_pluck ( $this -> shipping , 'taxes' ) ) ) );
2017-07-26 11:44:06 +00:00
}
/**
* Main cart totals .
*
* @ since 3.2 . 0
*/
protected function calculate_totals () {
2020-10-01 08:57:12 +00:00
$this -> set_total ( 'total' , NumberUtil :: 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 ) );
2021-03-09 12:59:19 +00:00
$items_tax = array_sum ( $this -> get_merged_taxes ( false , array ( 'items' ) ) );
2021-03-10 15:14:47 +00:00
// Shipping and fee taxes are rounded seperately because they were entered excluding taxes (as opposed to item prices, which may or may not be including taxes depending upon settings).
2021-03-09 12:59:19 +00:00
$shipping_and_fee_taxes = NumberUtil :: round ( array_sum ( $this -> get_merged_taxes ( false , array ( 'fees' , 'shipping' ) ) ), wc_get_price_decimals () );
$this -> cart -> set_total_tax ( $items_tax + $shipping_and_fee_taxes );
2017-07-27 12:48:58 +00:00
// Allow plugins to hook and alter totals before final total is calculated.
if ( has_action ( 'woocommerce_calculate_totals' ) ) {
2017-08-18 12:48:53 +00:00
do_action ( 'woocommerce_calculate_totals' , $this -> cart );
2017-07-27 12:48:58 +00:00
}
// Allow plugins to filter the grand total, and sum the cart totals in case of modifications.
2017-08-18 12:48:53 +00:00
$this -> cart -> set_total ( max ( 0 , apply_filters ( 'woocommerce_calculated_total' , $this -> get_total ( 'total' ), $this -> cart ) ) );
2017-07-26 11:44:06 +00:00
}
}