Merge pull request #16275 from woocommerce/feature/discounts-class-totals-refactored

Cart totals class and calculation improvements
This commit is contained in:
Claudiu Lodromanean 2017-07-27 09:01:55 -07:00 committed by GitHub
commit 74d33bd510
12 changed files with 608 additions and 459 deletions

View File

@ -1001,22 +1001,20 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
}
/**
* Calculate taxes for all line items and shipping, and store the totals and tax rows.
* Get tax location for this order.
*
* If by default the taxes are based on the shipping address and the current order doesn't
* have any, it would use the billing address rather than using the Shopping base location.
*
* Will use the base country unless customer addresses are set.
* @param $args array Added in 3.0.0 to pass things like location.
* @since 3.2.0
* @param $args array Override the location.
* @return array
*/
public function calculate_taxes( $args = array() ) {
protected function get_tax_location( $args = array() ) {
$tax_based_on = get_option( 'woocommerce_tax_based_on' );
if ( 'shipping' === $tax_based_on && ! $this->get_shipping_country() ) {
$tax_based_on = 'billing';
}
$args = wp_parse_args( $args, array(
$args = wp_parse_args( $args, array(
'country' => 'billing' === $tax_based_on ? $this->get_billing_country() : $this->get_shipping_country(),
'state' => 'billing' === $tax_based_on ? $this->get_billing_state() : $this->get_shipping_state(),
'postcode' => 'billing' === $tax_based_on ? $this->get_billing_postcode() : $this->get_shipping_postcode(),
@ -1032,75 +1030,38 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$args['city'] = '';
}
// Calc taxes for line items
return $args;
}
/**
* Calculate taxes for all line items and shipping, and store the totals and tax rows.
*
* If by default the taxes are based on the shipping address and the current order doesn't
* have any, it would use the billing address rather than using the Shopping base location.
*
* Will use the base country unless customer addresses are set.
*
* @param array $args Added in 3.0.0 to pass things like location.
*/
public function calculate_taxes( $args = array() ) {
$calculate_tax_for = $this->get_tax_location( $args );
$shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );
if ( 'inherit' === $shipping_tax_class ) {
$shipping_tax_class = current( array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), $this->get_items_tax_classes() ) );
}
// Trigger tax recalculation for all items.
foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) {
$tax_class = $item->get_tax_class();
$tax_status = $item->get_tax_status();
if ( '0' !== $tax_class && 'taxable' === $tax_status && wc_tax_enabled() ) {
$tax_rates = WC_Tax::find_rates( array(
'country' => $args['country'],
'state' => $args['state'],
'postcode' => $args['postcode'],
'city' => $args['city'],
'tax_class' => $tax_class,
) );
$total = $item->get_total();
$taxes = WC_Tax::calc_tax( $total, $tax_rates, false );
if ( $item->is_type( 'line_item' ) ) {
$subtotal = $item->get_subtotal();
$subtotal_taxes = WC_Tax::calc_tax( $subtotal, $tax_rates, false );
$item->set_taxes( array( 'total' => $taxes, 'subtotal' => $subtotal_taxes ) );
} else {
$item->set_taxes( array( 'total' => $taxes ) );
}
} else {
$item->set_taxes( false );
}
$item->calculate_taxes( $calculate_tax_for );
$item->save();
}
// Calc taxes for shipping
foreach ( $this->get_shipping_methods() as $item_id => $item ) {
if ( wc_tax_enabled() ) {
$shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );
// Inherit tax class from items
if ( 'inherit' === $shipping_tax_class ) {
$tax_rates = array();
$tax_classes = array_merge( array( '' ), WC_Tax::get_tax_class_slugs() );
$found_tax_classes = $this->get_items_tax_classes();
foreach ( $tax_classes as $tax_class ) {
if ( in_array( $tax_class, $found_tax_classes ) ) {
$tax_rates = WC_Tax::find_shipping_rates( array(
'country' => $args['country'],
'state' => $args['state'],
'postcode' => $args['postcode'],
'city' => $args['city'],
'tax_class' => $tax_class,
) );
break;
}
}
} else {
$tax_rates = WC_Tax::find_shipping_rates( array(
'country' => $args['country'],
'state' => $args['state'],
'postcode' => $args['postcode'],
'city' => $args['city'],
'tax_class' => $shipping_tax_class,
) );
}
$item->set_taxes( array( 'total' => WC_Tax::calc_tax( $item->get_total(), $tax_rates, false ) ) );
} else {
$item->set_taxes( false );
}
$item->calculate_taxes( array_merge( $calculate_tax_for, array( 'tax_class' => $shipping_tax_class ) ) );
$item->save();
}
$this->update_taxes();
}
@ -1150,7 +1111,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$this->add_item( $item );
}
// Save tax totals
// Save tax totals.
$this->set_shipping_tax( WC_Tax::round( array_sum( $shipping_taxes ) ) );
$this->set_cart_tax( WC_Tax::round( array_sum( $cart_taxes ) ) );
$this->save();
@ -1174,7 +1135,6 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$this->calculate_taxes();
}
// line items
foreach ( $this->get_items() as $item ) {
$cart_subtotal += $item->get_subtotal();
$cart_total += $item->get_total();

View File

@ -1,8 +1,8 @@
<?php
/**
* Order/cart totals calculation class.
* Cart totals calculation class.
*
* Methods are private and class is final to keep this as an internal API.
* Methods are protected and class is final to keep this as an internal API.
* May be opened in the future once structure is stable.
*
* @author Automattic
@ -14,24 +14,27 @@ if ( ! defined( 'ABSPATH' ) ) {
}
/**
* WC_Totals class.
* WC_Cart_Totals class.
*
* @todo consider extending this for cart vs orders if lots of conditonal logic is needed.
* @todo Instead of setting cart totals from here, do it from a subclass.
* @todo woocommerce_tax_round_at_subtotal option - how should we handle this with precision?
* @todo woocommerce_calculate_totals action for carts.
* @todo woocommerce_calculated_total filter for carts.
* @since 3.2.0
* @since 3.2.0
*/
class WC_Totals {
final class WC_Cart_Totals {
/**
* Reference to cart or order object.
* Reference to cart object.
*
* @since 3.2.0
* @var array
*/
private $object;
protected $object;
/**
* Reference to customer object.
*
* @since 3.2.0
* @var array
*/
protected $customer;
/**
* Line items to calculate.
@ -39,7 +42,7 @@ class WC_Totals {
* @since 3.2.0
* @var array
*/
private $items = array();
protected $items = array();
/**
* Fees to calculate.
@ -47,7 +50,7 @@ class WC_Totals {
* @since 3.2.0
* @var array
*/
private $fees = array();
protected $fees = array();
/**
* Shipping costs.
@ -55,7 +58,15 @@ class WC_Totals {
* @since 3.2.0
* @var array
*/
private $shipping = array();
protected $shipping = array();
/**
* Applied coupon objects.
*
* @since 3.2.0
* @var array
*/
protected $coupons = array();
/**
* Discount amounts in cents after calculation for the cart.
@ -63,15 +74,14 @@ class WC_Totals {
* @since 3.2.0
* @var array
*/
private $discount_totals = array();
protected $discount_totals = array();
/**
* Precision so we can work in cents.
* Should taxes be calculated?
*
* @since 3.2.0
* @var int
* @var boolean
*/
private $precision = 1;
protected $calculate_tax = true;
/**
* Stores totals.
@ -79,7 +89,7 @@ class WC_Totals {
* @since 3.2.0
* @var array
*/
private $totals = array(
protected $totals = array(
'fees_total' => 0,
'fees_total_tax' => 0,
'items_subtotal' => 0,
@ -99,13 +109,84 @@ class WC_Totals {
* Sets up the items provided, and calculate totals.
*
* @since 3.2.0
* @param object $cart Cart or order object to calculate totals for.
* @param object $cart Cart object to calculate totals for.
* @param object $customer Customer who owns this cart.
*/
public function __construct( &$cart = null ) {
$this->precision = pow( 10, wc_get_price_decimals() );
$this->object = $cart;
$this->set_items();
$this->calculate();
public function __construct( &$cart = null, &$customer = null ) {
$this->object = $cart;
$this->calculate_tax = wc_tax_enabled() && ! $customer->get_is_vat_exempt();
if ( is_a( $cart, 'WC_Cart' ) ) {
$this->calculate();
}
}
/**
* Run all calculations methods on the given items in sequence.
*
* @since 3.2.0
*/
protected function calculate() {
$this->calculate_item_totals();
$this->calculate_fee_totals();
$this->calculate_shipping_totals();
$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,
'quantity' => 0,
'product' => false,
'price_includes_tax' => false,
'subtotal' => 0,
'subtotal_tax' => 0,
'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(
'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(
'total' => 0,
'total_tax' => 0,
'taxes' => array(),
);
}
/**
* Should we round at subtotal level only?
*
* @return bool
*/
protected function round_at_subtotal() {
return 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' );
}
/**
@ -121,104 +202,80 @@ class WC_Totals {
*
* @since 3.2.0
*/
private function set_items() {
if ( is_a( $this->object, 'WC_Cart' ) ) {
foreach ( $this->object->get_cart() as $cart_item_key => $cart_item ) {
$item = $this->get_default_item_props();
$item->key = $cart_item_key;
$item->cart_item = $cart_item;
$item->quantity = $cart_item['quantity'];
$item->price = $this->add_precision( $cart_item['data']->get_price() ) * $cart_item['quantity'];
$item->product = $cart_item['data'];
$this->items[ $cart_item_key ] = $item;
}
protected function set_items() {
$this->items = array();
foreach ( $this->object->get_cart() as $cart_item_key => $cart_item ) {
$item = $this->get_default_item_props();
$item->object = $cart_item;
$item->price_includes_tax = wc_prices_include_tax();
$item->quantity = $cart_item['quantity'];
$item->subtotal = wc_add_number_precision_deep( $cart_item['data']->get_price() ) * $cart_item['quantity'];
$item->product = $cart_item['data'];
$item->tax_rates = $this->get_item_tax_rates( $item );
$this->items[ $cart_item_key ] = $item;
}
}
/**
* Add precision (deep) to a price.
* Get fee objects from the cart. Normalises data
* into the same format for use by this class.
*
* @since 3.2.0
* @param int|array $value Value to remove precision from.
* @return float
* @since 3.2.0
*/
private function add_precision( $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$value[ $key ] = $this->add_precision( $subvalue );
protected function set_fees() {
$this->fees = array();
$this->object->calculate_fees();
foreach ( $this->object->get_fees() as $fee_key => $fee_object ) {
$fee = $this->get_default_fee_props();
$fee->object = $fee_object;
$fee->total = wc_add_number_precision_deep( $fee->object->amount );
if ( $this->calculate_tax && $fee->object->taxable ) {
$fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class, $this->customer ), false );
$fee->total_tax = array_sum( $fee->taxes );
if ( ! $this->round_at_subtotal() ) {
$fee->total_tax = wc_round_tax_total( $fee->total_tax, 0 );
}
}
} else {
$value = $value * $this->precision;
$this->fees[ $fee_key ] = $fee;
}
return $value;
}
/**
* Remove precision (deep) from a price.
* Get shipping methods from the cart and normalise.
*
* @since 3.2.0
* @param int|array $value Value to remove precision from.
* @return float
* @since 3.2.0
*/
private function remove_precision( $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$value[ $key ] = $this->remove_precision( $subvalue );
protected function set_shipping() {
$this->shipping = array();
foreach ( $this->object->calculate_shipping() as $key => $shipping_object ) {
$shipping_line = $this->get_default_shipping_props();
$shipping_line->object = $shipping_object;
$shipping_line->total = wc_add_number_precision_deep( $shipping_object->cost );
$shipping_line->taxes = wc_add_number_precision_deep( $shipping_object->taxes );
$shipping_line->total_tax = wc_add_number_precision_deep( array_sum( $shipping_object->taxes ) );
if ( ! $this->round_at_subtotal() ) {
$shipping_line->total_tax = wc_round_tax_total( $shipping_line->total_tax, 0 );
}
} else {
$value = wc_format_decimal( $value / $this->precision, wc_get_price_decimals() );
$this->shipping[ $key ] = $shipping_line;
}
return $value;
}
/**
* Get default blank set of props used per item.
* Return array of coupon objects from the cart. Normalises data
* into the same format for use by this class.
*
* @since 3.2.0
* @return array
*/
private function get_default_item_props() {
return (object) array(
'key' => '',
'quantity' => 0,
'price' => 0,
'product' => false,
'price_includes_tax' => wc_prices_include_tax(),
'subtotal' => 0,
'subtotal_tax' => 0,
'subtotal_taxes' => array(),
'total' => 0,
'total_tax' => 0,
'taxes' => array(),
'discounted_price' => 0,
);
}
/**
* Get default blank set of props used per fee.
*
* @since 3.2.0
* @return array
*/
private function get_default_fee_props() {
return (object) array(
'total_tax' => 0,
'taxes' => array(),
);
}
/**
* Get default blank set of props used per shipping row.
*
* @since 3.2.0
* @return array
*/
private function get_default_shipping_props() {
return (object) array(
'total' => 0,
'total_tax' => 0,
'taxes' => array(),
);
protected function set_coupons() {
$this->coupons = $this->object->get_coupons();
}
/**
@ -231,16 +288,15 @@ class WC_Totals {
* @param object $item Item to adjust the prices of.
* @return object
*/
private function adjust_non_base_location_price( $item ) {
protected function adjust_non_base_location_price( $item ) {
$base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->tax_class );
$item_tax_rates = $this->get_item_tax_rates( $item );
if ( $item_tax_rates !== $base_tax_rates ) {
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, true );
$taxes = WC_Tax::calc_tax( $item->subtotal, $base_tax_rates, true, true );
// Now we have a new item price (excluding TAX).
$item->price = $item->price - array_sum( $taxes );
$item->subtotal = $item->subtotal - array_sum( $taxes );
$item->price_includes_tax = false;
}
return $item;
@ -250,11 +306,11 @@ class WC_Totals {
* Get discounted price of an item with precision (in cents).
*
* @since 3.2.0
* @param object $item Item to get the price of.
* @param object $item_key Item to get the price of.
* @return int
*/
private function get_discounted_price_in_cents( $item ) {
return $item->price - $this->discount_totals[ $item->key ];
protected function get_discounted_price_in_cents( $item_key ) {
return $this->items[ $item_key ]->subtotal - $this->discount_totals[ $item_key ];
}
/**
@ -263,21 +319,9 @@ class WC_Totals {
* @param object $item Item to get tax rates for.
* @return array of taxes
*/
private function get_item_tax_rates( $item ) {
protected function get_item_tax_rates( $item ) {
$tax_class = $item->product->get_tax_class();
return 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() );
}
/**
* Return array of coupon objects from the cart or an order.
*
* @since 3.2.0
* @return array
*/
private function get_coupons() {
if ( is_a( $this->object, 'WC_Cart' ) ) {
return $this->object->get_coupons();
}
return 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->customer );
}
/**
@ -300,7 +344,7 @@ class WC_Totals {
* @param string $key Total name you want to set.
* @param int $total Total to set.
*/
private function set_total( $key = 'total', $total ) {
protected function set_total( $key = 'total', $total ) {
$this->totals[ $key ] = $total;
}
@ -312,7 +356,7 @@ class WC_Totals {
* @return array.
*/
public function get_totals( $in_cents = false ) {
return $in_cents ? $this->totals : array_map( array( $this, 'remove_precision' ), $this->totals );
return $in_cents ? $this->totals : wc_remove_number_precision_deep( $this->totals );
}
/**
@ -321,7 +365,7 @@ class WC_Totals {
* @since 3.2.0
* @return array
*/
private function get_merged_taxes() {
protected function get_merged_taxes() {
$taxes = array();
foreach ( array_merge( $this->items, $this->fees, $this->shipping ) as $item ) {
@ -351,17 +395,53 @@ class WC_Totals {
*/
/**
* Run all calculations methods on the given items in sequence.
* Calculate item totals.
*
* @since 3.2.0
*/
private function calculate() {
protected function calculate_item_totals() {
$this->set_items();
$this->calculate_item_subtotals();
$this->calculate_discounts();
$this->calculate_item_totals();
$this->calculate_fee_totals();
$this->calculate_shipping_totals();
$this->calculate_totals();
foreach ( $this->items as $item_key => $item ) {
$item->total = $this->get_discounted_price_in_cents( $item_key );
$item->total_tax = 0;
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.
* $this->object is the cart object.
*/
$item->total = wc_add_number_precision(
apply_filters( 'woocommerce_get_discounted_price', wc_remove_number_precision( $item->total ), $item->object, $this->object )
);
}
if ( $this->calculate_tax && $item->product->is_taxable() ) {
$item->taxes = WC_Tax::calc_tax( $item->total, $item->tax_rates, $item->price_includes_tax );
$item->total_tax = array_sum( $item->taxes );
if ( ! $this->round_at_subtotal() ) {
$item->total_tax = wc_round_tax_total( $item->total_tax, 0 );
}
if ( $item->price_includes_tax ) {
$item->total = $item->total - $item->total_tax;
} else {
$item->total = $item->total;
}
}
}
$this->set_total( 'items_total', array_sum( array_values( wp_list_pluck( $this->items, 'total' ) ) ) );
$this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) );
$this->object->subtotal = $this->get_total( 'items_total' ) + $this->get_total( 'items_total_tax' );
$this->object->subtotal_ex_tax = $this->get_total( 'items_total' );
}
/**
@ -378,18 +458,18 @@ class WC_Totals {
*
* @since 3.2.0
*/
private function calculate_item_subtotals() {
protected function calculate_item_subtotals() {
foreach ( $this->items as $item ) {
if ( $item->price_includes_tax && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
$item = $this->adjust_non_base_location_price( $item );
}
if ( $this->calculate_tax && $item->product->is_taxable() ) {
$subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $item->tax_rates, $item->price_includes_tax );
$item->subtotal_tax = array_sum( $subtotal_taxes );
$item->subtotal = $item->price;
$item->subtotal_tax = 0;
if ( wc_tax_enabled() && $item->product->is_taxable() ) {
$item->subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $this->get_item_tax_rates( $item ), $item->price_includes_tax );
$item->subtotal_tax = array_sum( $item->subtotal_taxes );
if ( ! $this->round_at_subtotal() ) {
$item->subtotal_tax = wc_round_tax_total( $item->subtotal_tax, 0 );
}
if ( $item->price_includes_tax ) {
$item->subtotal = $item->subtotal - $item->subtotal_tax;
@ -398,21 +478,23 @@ class WC_Totals {
}
$this->set_total( 'items_subtotal', array_sum( array_values( wp_list_pluck( $this->items, 'subtotal' ) ) ) );
$this->set_total( 'items_subtotal_tax', array_sum( array_values( wp_list_pluck( $this->items, 'subtotal_tax' ) ) ) );
$this->object->subtotal = $this->get_total( 'items_total' ) + $this->get_total( 'items_total_tax' );
$this->object->subtotal_ex_tax = $this->get_total( 'items_total' );
}
/**
* Calculate all discount and coupon amounts.
*
* @todo Manual discounts.
* @todo record coupon totals and counts for cart.
*
* @since 3.2.0
* @uses WC_Discounts class.
*/
private function calculate_discounts() {
protected function calculate_discounts() {
$this->set_coupons();
$discounts = new WC_Discounts( $this->items );
foreach ( $this->get_coupons() as $coupon ) {
foreach ( $this->coupons as $coupon ) {
$discounts->apply_coupon( $coupon );
}
@ -420,44 +502,19 @@ class WC_Totals {
$this->totals['discounts_total'] = array_sum( $this->discount_totals );
// See how much tax was 'discounted'.
if ( wc_tax_enabled() ) {
if ( $this->calculate_tax ) {
foreach ( $this->discount_totals as $cart_item_key => $discount ) {
$item = $this->items[ $cart_item_key ];
if ( $item->product->is_taxable() ) {
$tax_rates = $this->get_item_tax_rates( $item );
$taxes = WC_Tax::calc_tax( $discount, $tax_rates, false );
$this->totals['discounts_tax_total'] += array_sum( $taxes );
$taxes = WC_Tax::calc_tax( $discount, $item->tax_rates, false );
$this->totals['discounts_tax_total'] += $this->round_at_subtotal() ? array_sum( $taxes ) : wc_round_tax_total( array_sum( $taxes ), 0 );
}
}
}
}
/**
* Totals are costs after discounts. @todo move cart specific setters to subclass?
*
* @since 3.2.0
*/
private function calculate_item_totals() {
foreach ( $this->items as $item ) {
$item->total = $this->get_discounted_price_in_cents( $item );
$item->total_tax = 0;
if ( wc_tax_enabled() && $item->product->is_taxable() ) {
$item->taxes = WC_Tax::calc_tax( $item->total, $this->get_item_tax_rates( $item ), $item->price_includes_tax );
$item->total_tax = array_sum( $item->taxes );
if ( $item->price_includes_tax ) {
$item->total = $item->total - $item->total_tax;
} else {
$item->total = $item->total;
}
}
}
$this->set_total( 'items_total', array_sum( array_values( wp_list_pluck( $this->items, 'total' ) ) ) );
$this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) );
$this->object->subtotal = $this->get_total( 'items_total' ) + $this->get_total( 'items_total_tax' );
$this->object->subtotal_ex_tax = $this->get_total( 'items_total' );
$applied_coupons = $discounts->get_applied_coupons();
$this->object->coupon_discount_amounts = wp_list_pluck( $applied_coupons, 'discount' );
$this->object->coupon_discount_tax_amounts = wp_list_pluck( $applied_coupons, 'discount_tax' );
}
/**
@ -466,35 +523,17 @@ class WC_Totals {
* 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
* @todo logic is unqiue to carts.
*/
private function calculate_fee_totals() {
$this->fees = array();
$this->object->calculate_fees();
foreach ( $this->object->get_fees() as $fee_key => $fee_object ) {
$fee = $this->get_default_fee_props();
$fee->object = $fee_object;
$fee->total = $this->add_precision( $fee->object->amount );
if ( wc_tax_enabled() && $fee->object->taxable ) {
$fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->object->tax_class ), false );
$fee->total_tax = array_sum( $fee->taxes );
}
$this->fees[ $fee_key ] = $fee;
}
// Store totals to self.
protected function calculate_fee_totals() {
$this->set_fees();
$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' ) ) );
// Transfer totals to the cart.
foreach ( $this->fees as $fee_key => $fee ) {
$this->object->fees[ $fee_key ]->tax = $this->remove_precision( $fee->total_tax );
$this->object->fees[ $fee_key ]->tax_data = $this->remove_precision( $fee->taxes );
$this->object->fees[ $fee_key ]->tax = wc_remove_number_precision_deep( $fee->total_tax );
$this->object->fees[ $fee_key ]->tax_data = wc_remove_number_precision_deep( $fee->taxes );
}
$this->object->fee_total = $this->remove_precision( array_sum( wp_list_pluck( $this->fees, 'total' ) ) );
$this->object->fee_total = wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total' ) ) );
}
/**
@ -502,19 +541,13 @@ class WC_Totals {
*
* @since 3.2.0
*/
private function calculate_shipping_totals() {
$this->shipping = array();
foreach ( $this->object->calculate_shipping() as $key => $shipping_object ) {
$shipping_line = $this->get_default_shipping_props();
$shipping_line->total = $this->add_precision( $shipping_object->cost );
$shipping_line->taxes = array_map( array( $this, 'add_precision' ), $shipping_object->taxes );
$shipping_line->total_tax = array_sum( $shipping_object->taxes );
$this->shipping[ $key ] = $shipping_line;
}
protected function calculate_shipping_totals() {
$this->set_shipping();
$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' ) ) );
$this->object->shipping_total = $this->get_total( 'shipping_total' );
$this->object->shipping_tax_total = $this->get_total( 'shipping_tax_total' );
}
/**
@ -522,15 +555,24 @@ class WC_Totals {
*
* @since 3.2.0
*/
private function calculate_totals() {
protected function calculate_totals() {
$this->set_total( 'taxes', $this->get_merged_taxes() );
$this->set_total( 'tax_total', array_sum( wp_list_pluck( $this->get_total( 'taxes', true ), 'tax_total' ) ) );
$this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->get_total( 'taxes', true ), 'shipping_tax_total' ) ) );
$this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + $this->get_total( 'tax_total', true ) + $this->get_total( 'shipping_tax_total', true ) ) );
$this->object->total = $this->get_total( 'total' );
$this->object->tax_total = $this->get_total( 'tax_total' );
$this->object->shipping_total = $this->get_total( 'shipping_total' );
$this->object->shipping_tax_total = $this->get_total( 'shipping_tax_total' );
// Add totals to cart object.
$this->object->taxes = wp_list_pluck( $this->get_total( 'taxes' ), 'shipping_tax_total' );
$this->object->shipping_taxes = wp_list_pluck( $this->get_total( 'taxes' ), 'tax_total' );
$this->object->tax_total = $this->get_total( 'tax_total' );
$this->object->total = $this->get_total( 'total' );
// Allow plugins to hook and alter totals before final total is calculated.
if ( has_action( 'woocommerce_calculate_totals' ) ) {
do_action( 'woocommerce_calculate_totals', $this->object );
}
// Allow plugins to filter the grand total, and sum the cart totals in case of modifications.
$totals_to_sum = wc_add_number_precision_deep( array( $this->object->cart_contents_total, $this->object->tax_total, $this->object->shipping_tax_total, $this->object->shipping_total, $this->object->fee_total ) );
$this->object->total = max( 0, apply_filters( 'woocommerce_calculated_total', wc_remove_number_precision( round( array_sum( $totals_to_sum ) ) ), $this->object ) );
}
}

View File

@ -390,29 +390,7 @@ class WC_Coupon extends WC_Legacy_Coupon {
$discount = $single ? $discount : $discount * $cart_item_qty;
}
$discount = (float) min( $discount, $discounting_amount );
// Handle the limit_usage_to_x_items option
if ( ! $this->is_type( array( 'fixed_cart' ) ) ) {
if ( $discounting_amount ) {
if ( null === $this->get_limit_usage_to_x_items() ) {
$limit_usage_qty = $cart_item_qty;
} else {
$limit_usage_qty = min( $this->get_limit_usage_to_x_items(), $cart_item_qty );
$this->set_limit_usage_to_x_items( max( 0, ( $this->get_limit_usage_to_x_items() - $limit_usage_qty ) ) );
}
if ( $single ) {
$discount = ( $discount * $limit_usage_qty ) / $cart_item_qty;
} else {
$discount = ( $discount / $cart_item_qty ) * $limit_usage_qty;
}
}
}
$discount = round( $discount, wc_get_rounding_precision() );
return apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $discounting_amount, $cart_item, $single, $this );
return apply_filters( 'woocommerce_coupon_get_discount_amount', round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ), $discounting_amount, $cart_item, $single, $this );
}
/*

View File

@ -14,8 +14,6 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Discounts class.
*
* @todo this class will need to be called instead get_discounted_price, in the cart?
*/
class WC_Discounts {
@ -40,20 +38,12 @@ class WC_Discounts {
*/
protected $applied_coupons = array();
/**
* Precision so we can work in cents.
*
* @var int
*/
protected $precision = 1;
/**
* Constructor.
*
* @param array $items Items to discount.
*/
public function __construct( $items = array() ) {
$this->precision = pow( 10, wc_get_price_decimals() );
$this->set_items( $items );
}
@ -75,7 +65,7 @@ class WC_Discounts {
* @return array
*/
public function get_discount( $key ) {
return isset( $this->discounts[ $key ] ) ? $this->remove_precision( $this->discounts[ $key ] ) : 0;
return isset( $this->discounts[ $key ] ) ? wc_remove_number_precision_deep( $this->discounts[ $key ] ) : 0;
}
/**
@ -86,7 +76,7 @@ class WC_Discounts {
* @return array
*/
public function get_discounts( $in_cents = false ) {
return $in_cents ? $this->discounts : array_map( array( $this, 'remove_precision' ), $this->discounts );
return $in_cents ? $this->discounts : wc_remove_number_precision_deep ( $this->discounts );
}
/**
@ -97,7 +87,7 @@ class WC_Discounts {
* @return float
*/
public function get_discounted_price( $item ) {
return $this->remove_precision( $this->get_discounted_price_in_cents( $item ) );
return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) );
}
/**
@ -119,14 +109,14 @@ class WC_Discounts {
* @return array
*/
public function get_applied_coupons() {
return array_map( array( $this, 'remove_precision' ), $this->applied_coupons );
return wc_remove_number_precision_deep( $this->applied_coupons );
}
/**
* Set cart/order items which will be discounted.
*
* @since 3.2.0
* @param array $items List items, normailised, by WC_Totals.
* @param array $items List items.
*/
public function set_items( $items ) {
$this->items = array();
@ -134,7 +124,11 @@ class WC_Discounts {
$this->applied_coupons = array();
if ( ! empty( $items ) && is_array( $items ) ) {
$this->items = $items;
foreach ( $items as $key => $item ) {
$this->items[ $key ] = $item;
$this->items[ $key ]->key = $key;
$this->items[ $key ]->price = $item->subtotal;
}
$this->discounts = array_fill_keys( array_keys( $items ), 0 );
}
@ -153,67 +147,57 @@ class WC_Discounts {
return false;
}
if ( ! isset( $this->applied_coupons[ $coupon->get_code() ] ) ) {
$this->applied_coupons[ $coupon->get_code() ] = 0;
}
$is_coupon_valid = $this->is_coupon_valid( $coupon );
if ( is_wp_error( $is_coupon_valid ) ) {
return $is_coupon_valid;
}
// @todo how can we support the old woocommerce_coupon_get_discount_amount filter?
$items_to_apply = $this->get_items_to_apply_coupon( $coupon );
if ( ! isset( $this->applied_coupons[ $coupon->get_code() ] ) ) {
$this->applied_coupons[ $coupon->get_code() ] = array(
'discount' => 0,
'discount_tax' => 0,
);
}
$items_to_apply = $this->get_items_to_apply_coupon( $coupon );
$coupon_type = $coupon->get_discount_type();
// Core discounts are handled here as of 3.2.
switch ( $coupon->get_discount_type() ) {
case 'percent' :
$this->applied_coupons[ $coupon->get_code() ] += $this->apply_percentage_discount( $items_to_apply, $coupon->get_amount() );
$this->apply_percentage_discount( $items_to_apply, $coupon->get_amount(), $coupon );
break;
case 'fixed_product' :
$this->applied_coupons[ $coupon->get_code() ] += $this->apply_fixed_product_discount( $items_to_apply, $coupon->get_amount() * $this->precision );
$this->apply_fixed_product_discount( $items_to_apply, wc_add_number_precision( $coupon->get_amount() ), $coupon );
break;
case 'fixed_cart' :
$this->applied_coupons[ $coupon->get_code() ] += $this->apply_fixed_cart_discount( $items_to_apply, $coupon->get_amount() * $this->precision );
$this->apply_fixed_cart_discount( $items_to_apply, wc_add_number_precision( $coupon->get_amount() ), $coupon );
break;
default :
if ( has_action( 'woocommerce_discounts_apply_coupon_' . $coupon_type ) ) {
// Allow custom coupon types to control this in their class per item, unless the new action is used.
do_action( 'woocommerce_discounts_apply_coupon_' . $coupon_type, $coupon, $items_to_apply, $this );
} else {
// Fallback to old coupon-logic.
foreach ( $items_to_apply as $item ) {
$discounted_price = $this->get_discounted_price_in_cents( $item );
$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price : $discounted_price;
$discount = min( $discounted_price, $coupon->get_discount_amount( $price_to_discount, $item->object ) );
$discount_tax = $this->get_item_discount_tax( $item, $discount );
// Store totals.
$this->discounts[ $item->key ] += $discount;
if ( $coupon ) {
$this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount;
$this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax;
}
}
}
break;
}
}
/**
* Add precision (deep) to a price.
*
* @since 3.2.0
* @param int|array $value Value to remove precision from.
* @return float
*/
protected function add_precision( $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$value[ $key ] = $this->add_precision( $subvalue );
}
} else {
$value = $value * $this->precision;
}
return $value;
}
/**
* Remove precision (deep) from a price.
*
* @since 3.2.0
* @param int|array $value Value to remove precision from.
* @return float
*/
protected function remove_precision( $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$value[ $key ] = $this->remove_precision( $subvalue );
}
} else {
$value = wc_format_decimal( $value / $this->precision, wc_get_price_decimals() );
}
return $value;
}
/**
* Sort by price.
*
@ -263,7 +247,7 @@ class WC_Discounts {
if ( 0 === $this->get_discounted_price_in_cents( $item ) ) {
continue;
}
if ( ! $coupon->is_valid_for_product( $item->product, $item->cart_item ) && ! $coupon->is_valid_for_cart() ) {
if ( ! $coupon->is_valid_for_product( $item->product, $item->object ) && ! $coupon->is_valid_for_cart() ) {
continue;
}
if ( $limit_usage_qty && $applied_count > $limit_usage_qty ) {
@ -284,36 +268,38 @@ class WC_Discounts {
}
/**
* Apply a discount amount to an item and ensure it does not go negative.
* Apply percent discount to items and return an array of discounts granted.
*
* @since 3.2.0
* @param object $item Get data for this item.
* @param int $discount Amount of discount.
* @return int Amount discounted.
* @param array $items_to_apply Array of items to apply the coupon to.
* @param int $amount Amount of discount.
* @param WC_Coupon $coupon Coupon object if appliable. Passed through filters.
* @return int Total discounted.
*/
protected function add_item_discount( &$item, $discount ) {
$discounted_price = $this->get_discounted_price_in_cents( $item );
$discount = $discount > $discounted_price ? $discounted_price : $discount;
$this->discounts[ $item->key ] = $this->discounts[ $item->key ] + $discount;
return $discount;
}
/**
* Apply percent discount to items.
*
* @since 3.2.0
* @param array $items_to_apply Array of items to apply the coupon to.
* @param int $amount Amount of discount.
* @return int total discounted in cents
*/
protected function apply_percentage_discount( $items_to_apply, $amount ) {
$total_discounted = 0;
protected function apply_percentage_discount( $items_to_apply, $amount, $coupon = null ) {
$total_discount = 0;
foreach ( $items_to_apply as $item ) {
$total_discounted += $this->add_item_discount( $item, $amount * ( $this->get_discounted_price_in_cents( $item ) / 100 ) );
}
// Find out how much price is available to discount for the item.
$discounted_price = $this->get_discounted_price_in_cents( $item );
return $total_discounted;
// Get the price we actually want to discount, based on settings.
$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price;
// Run coupon calculations.
$discount = $amount * ( $price_to_discount / 100 );
$discount = min( $discounted_price, apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $price_to_discount, $item->object, false, $coupon ) );
$discount_tax = $this->get_item_discount_tax( $item, $discount );
// Store totals.
$total_discount += $discount;
$this->discounts[ $item->key ] += $discount;
if ( $coupon ) {
$this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount;
$this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax;
}
}
return $total_discount;
}
/**
@ -321,17 +307,34 @@ class WC_Discounts {
*
* @since 3.2.0
* @param array $items_to_apply Array of items to apply the coupon to.
* @param int $discount Amount of discout.
* @return int total discounted in cents
* @param int $amount Amount of discount.
* @param WC_Coupon $coupon Coupon object if appliable. Passed through filters.
* @return int Total discounted.
*/
protected function apply_fixed_product_discount( $items_to_apply, $discount ) {
$total_discounted = 0;
protected function apply_fixed_product_discount( $items_to_apply, $amount, $coupon = null ) {
$total_discount = 0;
foreach ( $items_to_apply as $item ) {
$total_discounted += $this->add_item_discount( $item, $discount * $item->quantity );
}
// Find out how much price is available to discount for the item.
$discounted_price = $this->get_discounted_price_in_cents( $item );
return $total_discounted;
// Get the price we actually want to discount, based on settings.
$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price;
// Run coupon calculations.
$discount = $amount * $item->quantity;
$discount = min( $discounted_price, apply_filters( 'woocommerce_coupon_get_discount_amount', $discount, $price_to_discount, $item->object, false, $coupon ) );
$discount_tax = $this->get_item_discount_tax( $item, $discount );
// Store totals.
$total_discount += $discount;
$this->discounts[ $item->key ] += $discount;
if ( $coupon ) {
$this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount;
$this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax;
}
}
return $total_discount;
}
/**
@ -340,49 +343,92 @@ class WC_Discounts {
* @since 3.2.0
* @param array $items_to_apply Array of items to apply the coupon to.
* @param int $cart_discount Fixed discount amount to apply.
* @return int total discounted in cents
* @param WC_Coupon $coupon Coupon object if appliable. Passed through filters.
* @return int Total discounted.
*/
protected function apply_fixed_cart_discount( $items_to_apply, $cart_discount ) {
$items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) );
protected function apply_fixed_cart_discount( $items_to_apply, $cart_discount, $coupon = null ) {
$total_discount = 0;
$items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) );
if ( ! $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ) ) {
return 0;
return $total_discount;
}
$per_item_discount = floor( $cart_discount / $item_count ); // round it down to the nearest cent number.
$amount_discounted = 0;
$per_item_discount = floor( $cart_discount / $item_count ); // round it down to the nearest cent.
if ( $per_item_discount > 0 ) {
foreach ( $items_to_apply as $item ) {
$amount_discounted += $this->add_item_discount( $item, $per_item_discount * $item->quantity );
}
$total_discounted = $this->apply_fixed_product_discount( $items_to_apply, $per_item_discount, $coupon );
/**
* If there is still discount remaining, repeat the process.
*/
if ( $amount_discounted > 0 && $amount_discounted < $cart_discount ) {
$amount_discounted += $this->apply_fixed_cart_discount( $items_to_apply, $cart_discount - $amount_discounted );
if ( $total_discounted > 0 && $total_discounted < $cart_discount ) {
$total_discounted = $total_discounted + $this->apply_fixed_cart_discount( $items_to_apply, $cart_discount - $total_discounted );
}
} elseif ( $cart_discount > 0 ) {
/**
* Deal with remaining fractional discounts by splitting it over items
* until the amount is expired, discounting 1 cent at a time.
*/
foreach ( $items_to_apply as $item ) {
for ( $i = 0; $i < $item->quantity; $i ++ ) {
$amount_discounted += $this->add_item_discount( $item, 1 );
if ( $amount_discounted >= $cart_discount ) {
break 2;
}
} elseif ( $cart_discount > 0 ) {
$total_discounted = $this->apply_fixed_cart_discount_remainder( $items_to_apply, $cart_discount, $coupon );
}
return $total_discount;
}
/**
* Deal with remaining fractional discounts by splitting it over items
* until the amount is expired, discounting 1 cent at a time.
*
* @since 3.2.0
* @param array $items_to_apply Array of items to apply the coupon to.
* @param int $cart_discount Fixed discount amount to apply.
* @param WC_Coupon $coupon Coupon object if appliable. Passed through filters.
* @return int Total discounted.
*/
protected function apply_fixed_cart_discount_remainder( $items_to_apply, $remaining_discount, $coupon = null ) {
$total_discount = 0;
foreach ( $items_to_apply as $item ) {
for ( $i = 0; $i < $item->quantity; $i ++ ) {
// Find out how much price is available to discount for the item.
$discounted_price = $this->get_discounted_price_in_cents( $item );
// Get the price we actually want to discount, based on settings.
$price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $item->price: $discounted_price;
// Run coupon calculations.
$discount = min( $discounted_price, 1 );
$discount_tax = $this->get_item_discount_tax( $item, $discount );
// Store totals.
$total_discount += $discount;
$this->discounts[ $item->key ] += $discount;
if ( $coupon ) {
$this->applied_coupons[ $coupon->get_code() ]['discount'] += $discount;
$this->applied_coupons[ $coupon->get_code() ]['discount_tax'] += $discount_tax;
}
if ( $amount_discounted >= $cart_discount ) {
break;
if ( $total_discount >= $remaining_discount ) {
break 2;
}
}
if ( $total_discount >= $remaining_discount ) {
break;
}
}
return $total_discount;
}
return $amount_discounted;
/**
* Return discounted tax amount for an item.
*
* @param object $item
* @param int $discount_amount
* @return int
*/
protected function get_item_discount_tax( $item, $discount_amount ) {
if ( $item->product->is_taxable() ) {
$taxes = WC_Tax::calc_tax( $discount_amount, $item->tax_rates, false );
return array_sum( $taxes );
}
return 0;
}
/*
@ -605,7 +651,7 @@ class WC_Discounts {
$valid = false;
foreach ( $this->items as $item ) {
if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->cart_item ) ) {
if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) {
$valid = true;
break;
}

View File

@ -28,6 +28,27 @@ class WC_Order_Item_Shipping extends WC_Order_Item {
),
);
/**
* Calculate item taxes.
*
* @since 3.2.0
* @param array $calculate_tax_for Location data to get taxes for. Required.
* @return bool True if taxes were calculated.
*/
public function calculate_taxes( $calculate_tax_for = array() ) {
if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'], $calculate_tax_for['tax_class'] ) ) {
return false;
}
if ( wc_tax_enabled() ) {
$tax_rates = WC_Tax::find_shipping_rates( $calculate_tax_for );
$taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false );
$this->set_taxes( array( 'total' => $taxes ) );
} else {
$this->set_taxes( false );
}
return true;
}
/*
|--------------------------------------------------------------------------
| Setters

View File

@ -152,7 +152,8 @@ class WC_Order_Item extends WC_Data implements ArrayAccess {
*/
/**
* Type checking
* Type checking.
*
* @param string|array $type
* @return boolean
*/
@ -160,6 +161,34 @@ class WC_Order_Item extends WC_Data implements ArrayAccess {
return is_array( $type ) ? in_array( $this->get_type(), $type ) : $type === $this->get_type();
}
/**
* Calculate item taxes.
*
* @since 3.2.0
* @param array $calculate_tax_for Location data to get taxes for. Required.
* @return bool True if taxes were calculated.
*/
public function calculate_taxes( $calculate_tax_for = array() ) {
if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) {
return false;
}
if ( '0' !== $this->get_tax_class() && 'taxable' === $this->get_tax_status() && wc_tax_enabled() ) {
$calculate_tax_for['tax_class'] = $this->get_tax_class();
$tax_rates = WC_Tax::find_rates( $calculate_tax_for );
$taxes = WC_Tax::calc_tax( $this->get_total(), $tax_rates, false );
if ( method_exists( $this, 'get_subtotal' ) ) {
$subtotal_taxes = WC_Tax::calc_tax( $this->get_subtotal(), $tax_rates, false );
$this->set_taxes( array( 'total' => $taxes, 'subtotal' => $subtotal_taxes ) );
} else {
$this->set_taxes( array( 'total' => $taxes ) );
}
} else {
$this->set_taxes( false );
}
return true;
}
/*
|--------------------------------------------------------------------------
| Meta Data Handling

View File

@ -452,13 +452,18 @@ class WC_Tax {
* Used by get_rates(), get_shipping_rates().
*
* @param $tax_class string Optional, passed to the filter for advanced tax setups.
* @param object $customer Override the customer object to get their location.
* @return array
*/
public static function get_tax_location( $tax_class = '' ) {
public static function get_tax_location( $tax_class = '', $customer = null ) {
$location = array();
if ( ! empty( WC()->customer ) ) {
$location = WC()->customer->get_taxable_address();
if ( is_null( $customer ) && ! empty( WC()->customer ) ) {
$customer = WC()->customer;
}
if ( ! empty( $customer ) ) {
$location = $customer->get_taxable_address();
} elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) {
$location = array(
WC()->countries->get_base_country(),
@ -468,17 +473,19 @@ class WC_Tax {
);
}
return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class );
return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer );
}
/**
* Get's an array of matching rates for a tax class.
* @param string $tax_class
*
* @param string $tax_class Tax class to get rates for.
* @param object $customer Override the customer object to get their location.
* @return array
*/
public static function get_rates( $tax_class = '' ) {
public static function get_rates( $tax_class = '', $customer = null ) {
$tax_class = sanitize_title( $tax_class );
$location = self::get_tax_location( $tax_class );
$location = self::get_tax_location( $tax_class, $customer );
$matched_tax_rates = array();
if ( sizeof( $location ) === 4 ) {
@ -526,10 +533,11 @@ class WC_Tax {
/**
* Gets an array of matching shipping tax rates for a given class.
*
* @param string Tax Class
* @return mixed
* @param string $tax_class Tax class to get rates for.
* @param object $customer Override the customer object to get their location.
* @return mixed
*/
public static function get_shipping_tax_rates( $tax_class = null ) {
public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) {
// See if we have an explicitly set shipping tax class
$shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' );
@ -537,7 +545,7 @@ class WC_Tax {
$tax_class = $shipping_tax_class;
}
$location = self::get_tax_location( $tax_class );
$location = self::get_tax_location( $tax_class, $customer );
$matched_tax_rates = array();
if ( sizeof( $location ) === 4 ) {

View File

@ -334,7 +334,7 @@ final class WooCommerce {
include_once( WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php' );
include_once( WC_ABSPATH . 'includes/class-wc-background-emailer.php' );
include_once( WC_ABSPATH . 'includes/class-wc-discounts.php' );
include_once( WC_ABSPATH . 'includes/class-wc-totals.php' );
include_once( WC_ABSPATH . 'includes/class-wc-cart-totals.php' );
/**
* Data stores - used to store and retrieve CRUD object data from the database.

View File

@ -1443,6 +1443,66 @@ function wc_get_rounding_precision() {
return $precision;
}
/**
* Add precision to a number and return an int.
*
* @since 3.2.0
* @param float $value Number to add precision to.
* @return int
*/
function wc_add_number_precision( $value ) {
$precision = pow( 10, wc_get_price_decimals() );
return $value * $precision;
}
/**
* Remove precision from a number and return a float.
*
* @since 3.2.0
* @param float $value Number to add precision to.
* @return float
*/
function wc_remove_number_precision( $value ) {
$precision = pow( 10, wc_get_price_decimals() );
return wc_format_decimal( $value / $precision, wc_get_price_decimals() );
}
/**
* Add precision to an array of number and return an array of int.
*
* @since 3.2.0
* @param array $value Number to add precision to.
* @return int
*/
function wc_add_number_precision_deep( $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$value[ $key ] = wc_add_number_precision_deep( $subvalue );
}
} else {
$value = wc_add_number_precision( $value );
}
return $value;
}
/**
* Remove precision from an array of number and return an array of int.
*
* @since 3.2.0
* @param array $value Number to add precision to.
* @return int
*/
function wc_remove_number_precision_deep( $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$value[ $key ] = wc_remove_number_precision_deep( $subvalue );
}
} else {
$value = wc_remove_number_precision( $value );
}
return $value;
}
/**
* Get a shared logger instance.
*

View File

@ -216,11 +216,12 @@ function wc_trim_zeros( $price ) {
/**
* Round a tax amount.
*
* @param mixed $tax
* @param double $tax Amount to round.
* @param int $dp DP to round. Defaults to wc_get_price_decimals.
* @return double
*/
function wc_round_tax_total( $tax ) {
$dp = wc_get_price_decimals();
function wc_round_tax_total( $tax, $dp = null ) {
$dp = is_null( $dp ) ? wc_get_price_decimals() : absint( $dp );
// @codeCoverageIgnoreStart
if ( version_compare( phpversion(), '5.3', '<' ) ) {

View File

@ -24,11 +24,11 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case {
'taxes' => array(),
'discounted_price' => 0,
);
$item->key = $cart_item_key;
$item->cart_item = $cart_item;
$item->quantity = $cart_item['quantity'];
$item->price = $cart_item['data']->get_price() * $precision * $cart_item['quantity'];
$item->product = $cart_item['data'];
$item->object = $cart_item;
$item->quantity = $cart_item['quantity'];
$item->subtotal = $cart_item['data']->get_price() * $precision * $cart_item['quantity'];
$item->product = $cart_item['data'];
$item->tax_rates = WC_Tax::get_rates( $item->product->get_tax_class() );
$items[ $cart_item_key ] = $item;
}
return $items;
@ -89,34 +89,38 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case {
$coupon = WC_Helper_Coupon::create_coupon( 'test' );
$coupon->set_amount( 50 );
$coupon->set_discount_type( 'percent' );
$coupon->save();
$discounts->apply_coupon( $coupon );
$this->assertEquals( array( 'test' => 5 ), $discounts->get_applied_coupons() );
$this->assertEquals( array( 'test' => array( 'discount' => 5, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() );
$coupon2 = WC_Helper_Coupon::create_coupon( 'test2' );
$coupon2->set_code( 'test2' );
$coupon2->set_amount( 50 );
$coupon2->set_discount_type( 'percent' );
$coupon->save();
$discounts->apply_coupon( $coupon2 );
$this->assertEquals( array( 'test' => 5, 'test2' => 2.50 ), $discounts->get_applied_coupons() );
$this->assertEquals( array( 'test' => array( 'discount' => 5, 'discount_tax' => 0 ), 'test2' => array( 'discount' => 2.50, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() );
$discounts->apply_coupon( $coupon );
$this->assertEquals( array( 'test' => 6.25, 'test2' => 2.50 ), $discounts->get_applied_coupons() );
$this->assertEquals( array( 'test' => array( 'discount' => 6.25, 'discount_tax' => 0 ), 'test2' => array( 'discount' => 2.50, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() );
// Test different coupon types.
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product->get_id(), 2 );
$coupon->set_discount_type( 'fixed_product' );
$coupon->set_amount( 2 );
$coupon->save();
$discounts->set_items( $this->get_items_for_discounts_class() );
$discounts->apply_coupon( $coupon );
$this->assertEquals( array( 'test' => 4 ), $discounts->get_applied_coupons() );
$this->assertEquals( array( 'test' => array( 'discount' => 4, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() );
$coupon->set_discount_type( 'fixed_cart' );
$coupon->save();
$discounts->set_items( $this->get_items_for_discounts_class() );
$discounts->apply_coupon( $coupon );
$this->assertEquals( array( 'test' => 2 ), $discounts->get_applied_coupons() );
$this->assertEquals( array( 'test' => array( 'discount' => 2, 'discount_tax' => 0 ) ), $discounts->get_applied_coupons() );
// Cleanup.
WC()->cart->empty_cart();

View File

@ -67,7 +67,7 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
add_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_cart_fees_callback' ) );
$this->totals = new WC_Totals( WC()->cart );
$this->totals = new WC_Cart_Totals( WC()->cart, WC()->customer );
}
/**