woocommerce/includes/class-wc-cart-totals.php

873 lines
28 KiB
PHP
Raw Normal View History

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).
*
2017-07-26 11:44:06 +00:00
* @package WooCommerce/Classes
* @version 3.2.0
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 {
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(
'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
/**
2017-07-27 12:48:58 +00:00
* Run all calculations 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,
'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.
* - 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'];
$item->price = wc_add_number_precision_deep( $cart_item['data']->get_price() * $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
$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
// Negative fees should not make the order total go negative.
if ( 0 > $fee->total ) {
$max_discount = round( $this->get_total( 'items_total', true ) + $fee_running_total + $this->get_total( 'shipping_total', true ) ) * -1;
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
}
$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.
$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();
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 );
$shipping_line->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $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();
foreach ( $this->coupons as $coupon ) {
switch ( $coupon->get_discount_type() ) {
case 'fixed_product':
$coupon->sort = 1;
break;
case 'percent':
$coupon->sort = 2;
break;
case 'fixed_cart':
$coupon->sort = 3;
break;
default:
$coupon->sort = 0;
break;
}
// Allow plugins to override the default order.
$coupon->sort = apply_filters( 'woocommerce_coupon_sort', $coupon->sort, $coupon );
}
2017-08-18 12:48:53 +00:00
uasort( $this->coupons, array( $this, 'sort_coupons_callback' ) );
}
/**
* Sort coupons so discounts apply consistently across installs.
*
* In order of priority;
* - sort param
* - usage restriction
* - coupon value
* - ID
*
2017-08-15 11:21:12 +00:00
* @param WC_Coupon $a Coupon object.
* @param WC_Coupon $b Coupon object.
* @return int
*/
2017-08-18 12:48:53 +00:00
protected function sort_coupons_callback( $a, $b ) {
if ( $a->sort === $b->sort ) {
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;
}
return ( $a->sort < $b->sort ) ? -1 : 1;
2017-07-26 11:44:06 +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 ) {
if ( $item->price_includes_tax && $item->taxable ) {
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;
}
// Work out a new base price without the shop's base tax.
$taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true );
// Now we have a new item price (excluding TAX).
$item->price = round( $item->price - array_sum( $taxes ) );
$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 ) {
if ( $item->price_includes_tax && $item->taxable ) {
$base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) );
2017-07-27 09:49:47 +00:00
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 );
$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.
$item->price = $item->price - array_sum( $taxes ) + array_sum( $new_taxes );
}
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 ) {
if ( ! wc_tax_enabled() ) {
return array();
}
$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.
*/
protected function set_total( $key = 'total', $total ) {
$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
}
/**
* 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
* @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 );
}
/**
* Round merged taxes.
*
* @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
* @param array $taxes Taxes to round.
* @return array
*/
2019-01-10 13:23:49 +00:00
protected function round_merged_taxes( $taxes ) {
foreach ( $taxes as $rate_id => $tax ) {
$taxes[ $rate_id ] = $this->round_line_tax( $tax );
}
return $taxes;
}
2017-08-18 12:48:53 +00:00
/**
* Combine item taxes into a single array, preserving keys.
*
* @since 3.2.0
* @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-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
$items_total = $this->get_rounded_items_total( $this->get_values_for_total( 'total' ) );
$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' ) ) ) );
$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.
*
* 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.
* be 8.325 leading to totals being 1p off.
*
* Pre tax coupons come off the price the customer thinks they are paying - tax is calculated.
* 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() {
$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 ( $is_customer_vat_exempt ) {
$item = $this->remove_item_base_taxes( $item );
} elseif ( $adjust_non_base_location_prices ) {
$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() ) {
$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.
$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
}
}
$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
}
$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 ) );
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
// See how much tax was 'discounted' per item and per coupon.
2017-07-27 12:48:58 +00:00
if ( $this->calculate_tax ) {
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 ) {
$item = $this->items[ $item_key ];
2017-07-28 12:02:39 +00:00
if ( $item->product->is_taxable() ) {
2017-11-02 16:18:51 +00:00
// Item subtotals were sent, so set 3rd param.
2017-11-02 20:00:20 +00:00
$item_tax = wc_round_tax_total( array_sum( WC_Tax::calc_tax( $coupon_discount, $item->tax_rates, $item->price_includes_tax ) ), 0 );
2017-11-02 16:18:51 +00:00
// Sum total tax.
$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-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-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 ) );
// 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() {
$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 ) );
2017-08-18 12:48:53 +00:00
$this->cart->set_total_tax( array_sum( $this->get_merged_taxes( false ) ) );
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
}
}