Remove order subclass

This commit is contained in:
Mike Jolley 2017-07-27 10:49:47 +01:00
parent cfb04f0ead
commit 306db69eaf
9 changed files with 492 additions and 886 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();

View File

@ -1,511 +0,0 @@
<?php
/**
* Order/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.
*
* @author Automattic
* @package WooCommerce/Classes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Totals class.
*
* @todo woocommerce_tax_round_at_subtotal option - how should we handle this with precision?
* @todo Manual discounts.
* @since 3.2.0
*/
abstract class WC_Totals {
/**
* Reference to cart or order object.
*
* @since 3.2.0
* @var array
*/
protected $object;
/**
* 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();
/**
* Discount amounts in cents after calculation for the cart.
*
* @since 3.2.0
* @var array
*/
protected $discount_totals = array();
/**
* Precision so we can work in cents.
*
* @since 3.2.0
* @var int
*/
protected $precision = 1;
/**
* Whether to calculate taxes when calculating totals.
*
* @since 3.2.0
* @var bool
*/
protected $calculate_taxes = true;
/**
* 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,
'taxes' => array(),
'tax_total' => 0,
'shipping_total' => 0,
'shipping_tax_total' => 0,
'discounts_total' => 0,
'discounts_tax_total' => 0,
);
/**
* Sets up the items provided, and calculate totals.
*
* @since 3.2.0
* @param object $object Cart or order object to calculate totals for.
*/
public function __construct( &$object = null ) {
$this->precision = pow( 10, wc_get_price_decimals() );
$this->object = $object;
}
/**
* Run all calculations methods on the given items in sequence. @todo More documentation, and add other calculation methods for taxes and totals only?
*
* @since 3.2.0
* @param bool $calculate_taxes Whether to calculate taxes (optional).
*/
public function calculate( $calculate_taxes = true ) {
$this->calculate_taxes = (bool) $calculate_taxes;
$this->calculate_item_totals();
$this->calculate_fee_totals();
$this->calculate_shipping_totals();
$this->calculate_totals();
}
/**
* Handles a cart or order object passed in for calculation. Normalises data
* into the same format for use by this class.
*
* @since 3.2.0
*/
protected function set_items() {
$this->items = array();
}
/**
* Get and normalise fees.
*
* @since 3.2.0
*/
protected function set_fees() {
$this->fees = array();
}
/**
* Get shipping methods and normalise.
*
* @since 3.2.0
*/
protected function set_shipping() {
$this->shipping = array();
}
/**
* Set array of coupon objects from the cart or an order.
*
* @since 3.2.0
*/
protected function set_coupons() {
$this->coupons = array();
}
/**
* 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;
}
/**
* 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(),
);
}
/**
* 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.
*
* @since 3.2.0
* @param object $item Item to adjust the prices of.
* @return object
*/
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 ) {
// Work out a new base price without the shop's base tax.
$taxes = WC_Tax::calc_tax( $item->subtotal, $base_tax_rates, true, true );
// Now we have a new item price (excluding TAX).
$item->subtotal = $item->subtotal - array_sum( $taxes );
$item->price_includes_tax = false;
}
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 ) {
return $this->items[ $item_key ]->subtotal - $this->discount_totals[ $item_key ];
}
/**
* 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 ) {
$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() );
}
/**
* 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 ) {
return $in_cents ? $this->totals : array_map( array( $this, 'remove_precision' ), $this->totals );
}
/**
* Get all tax rows from items (including shipping and product line items).
*
* @since 3.2.0
* @return array
*/
protected function get_merged_taxes() {
$taxes = array();
foreach ( array_merge( $this->items, $this->fees, $this->shipping ) as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ] = array( 'tax_total' => 0, 'shipping_tax_total' => 0 );
}
}
foreach ( $this->items + $this->fees as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ]['tax_total'] = $taxes[ $rate_id ]['tax_total'] + $rate;
}
}
foreach ( $this->shipping as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ]['shipping_tax_total'] = $taxes[ $rate_id ]['shipping_tax_total'] + $rate;
}
}
return $taxes;
}
/*
|--------------------------------------------------------------------------
| Calculation methods.
|--------------------------------------------------------------------------
*/
/**
* Calculate item totals.
*
* @since 3.2.0
*/
protected function calculate_item_totals() {
$this->set_items();
$this->calculate_item_subtotals();
$this->calculate_discounts();
foreach ( $this->items as $item_key => $item ) {
$item->total = $this->get_discounted_price_in_cents( $item_key );
$item->total_tax = 0;
if ( $this->calculate_taxes && 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' ) ) ) );
if ( $this->calculate_taxes ) {
$this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) );
}
}
/**
* 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() {
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_taxes && wc_tax_enabled() && $item->product->is_taxable() ) {
$subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $this->get_item_tax_rates( $item ), $item->price_includes_tax );
$item->subtotal_tax = array_sum( $subtotal_taxes );
if ( $item->price_includes_tax ) {
$item->subtotal = $item->subtotal - $item->subtotal_tax;
}
}
}
$this->set_total( 'items_subtotal', array_sum( array_values( wp_list_pluck( $this->items, 'subtotal' ) ) ) );
if ( $this->calculate_taxes ) {
$this->set_total( 'items_subtotal_tax', array_sum( array_values( wp_list_pluck( $this->items, 'subtotal_tax' ) ) ) );
}
}
/**
* Calculate all discount and coupon amounts.
*
* @since 3.2.0
* @uses WC_Discounts class.
*/
protected function calculate_discounts() {
$this->set_coupons();
$discounts = new WC_Discounts( $this->items );
foreach ( $this->coupons as $coupon ) {
$discounts->apply_coupon( $coupon );
}
$this->discount_totals = $discounts->get_discounts( true );
$this->totals['discounts_total'] = array_sum( $this->discount_totals );
// See how much tax was 'discounted'.
if ( $this->calculate_taxes && wc_tax_enabled() ) {
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 );
}
}
}
}
/**
* 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() {
$this->set_fees();
$this->set_total( 'fees_total', array_sum( wp_list_pluck( $this->fees, 'total' ) ) );
if ( $this->calculate_taxes ) {
$this->set_total( 'fees_total_tax', array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) );
}
}
/**
* Calculate any shipping taxes.
*
* @since 3.2.0
*/
protected function calculate_shipping_totals() {
$this->set_shipping();
$this->set_total( 'shipping_total', array_sum( wp_list_pluck( $this->shipping, 'total' ) ) );
if ( $this->calculate_taxes ) {
$this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->shipping, 'total_tax' ) ) );
}
}
/**
* Main cart totals.
*
* @since 3.2.0
*/
protected function calculate_totals() {
if ( $this->calculate_taxes ) {
$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( '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 ) ) );
}
}
}

View File

@ -19,25 +19,156 @@ if ( ! defined( 'ABSPATH' ) ) {
* @todo woocommerce_calculate_totals action for carts.
* @todo woocommerce_calculated_total filter for carts.
* @todo record coupon totals and counts for cart.
* @todo woocommerce_tax_round_at_subtotal option - how should we handle this with precision?
* @todo Manual discounts.
*
* @since 3.2.0
*/
final class WC_Cart_Totals extends WC_Totals {
final class WC_Cart_Totals {
/**
* Reference to cart or order object.
*
* @since 3.2.0
* @var array
*/
protected $object;
/**
* 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();
/**
* Discount amounts in cents after calculation for the cart.
*
* @since 3.2.0
* @var array
*/
protected $discount_totals = array();
/**
* 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,
'taxes' => array(),
'tax_total' => 0,
'shipping_total' => 0,
'shipping_tax_total' => 0,
'discounts_total' => 0,
'discounts_tax_total' => 0,
);
/**
* Sets up the items provided, and calculate totals.
*
* @since 3.2.0
* @param object $object Cart or order object to calculate totals for.
* @param object $cart Cart object to calculate totals for.
*/
public function __construct( &$object = null ) {
parent::__construct( $object );
public function __construct( &$cart = null ) {
$this->object = $object;
if ( is_a( $object, 'WC_Cart' ) ) {
if ( is_a( $cart, 'WC_Cart' ) ) {
$this->calculate();
}
}
/**
* Run all calculations methods on the given items in sequence. @todo More documentation, and add other calculation methods for taxes and totals only?
*
* @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(),
);
}
/**
* Handles a cart or order object passed in for calculation. Normalises data
* into the same format for use by this class.
@ -118,17 +249,219 @@ final class WC_Cart_Totals extends WC_Totals {
}
/**
* Totals are costs after discounts.
* 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.
*
* @since 3.2.0
* @param object $item Item to adjust the prices of.
* @return object
*/
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 ) {
// Work out a new base price without the shop's base tax.
$taxes = WC_Tax::calc_tax( $item->subtotal, $base_tax_rates, true, true );
// Now we have a new item price (excluding TAX).
$item->subtotal = $item->subtotal - array_sum( $taxes );
$item->price_includes_tax = false;
}
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 ) {
return $this->items[ $item_key ]->subtotal - $this->discount_totals[ $item_key ];
}
/**
* 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 ) {
$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() );
}
/**
* 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 ) {
return $in_cents ? $this->totals : array_map( array( $this, 'remove_precision' ), $this->totals );
}
/**
* Get all tax rows from items (including shipping and product line items).
*
* @since 3.2.0
* @return array
*/
protected function get_merged_taxes() {
$taxes = array();
foreach ( array_merge( $this->items, $this->fees, $this->shipping ) as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ] = array( 'tax_total' => 0, 'shipping_tax_total' => 0 );
}
}
foreach ( $this->items + $this->fees as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ]['tax_total'] = $taxes[ $rate_id ]['tax_total'] + $rate;
}
}
foreach ( $this->shipping as $item ) {
foreach ( $item->taxes as $rate_id => $rate ) {
$taxes[ $rate_id ]['shipping_tax_total'] = $taxes[ $rate_id ]['shipping_tax_total'] + $rate;
}
}
return $taxes;
}
/*
|--------------------------------------------------------------------------
| Calculation methods.
|--------------------------------------------------------------------------
*/
/**
* Calculate item totals.
*
* @since 3.2.0
*/
protected function calculate_item_totals() {
parent::calculate_item_totals();
$this->set_items();
$this->calculate_item_subtotals();
$this->calculate_discounts();
foreach ( $this->items as $item_key => $item ) {
$item->total = $this->get_discounted_price_in_cents( $item_key );
$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' );
}
/**
* 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() {
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 ( wc_tax_enabled() && $item->product->is_taxable() ) {
$subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $this->get_item_tax_rates( $item ), $item->price_includes_tax );
$item->subtotal_tax = array_sum( $subtotal_taxes );
if ( $item->price_includes_tax ) {
$item->subtotal = $item->subtotal - $item->subtotal_tax;
}
}
}
$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.
*
* @since 3.2.0
* @uses WC_Discounts class.
*/
protected function calculate_discounts() {
$this->set_coupons();
$discounts = new WC_Discounts( $this->items );
foreach ( $this->coupons as $coupon ) {
$discounts->apply_coupon( $coupon );
}
$this->discount_totals = $discounts->get_discounts( true );
$this->totals['discounts_total'] = array_sum( $this->discount_totals );
// See how much tax was 'discounted'.
if ( wc_tax_enabled() ) {
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 );
}
}
}
}
/**
* Triggers the cart fees API, grabs the list of fees, and calculates taxes.
*
@ -137,7 +470,9 @@ final class WC_Cart_Totals extends WC_Totals {
* @since 3.2.0
*/
protected function calculate_fee_totals() {
parent::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' ) ) );
foreach ( $this->fees as $fee_key => $fee ) {
$this->object->fees[ $fee_key ]->tax = $this->remove_precision( $fee->total_tax );
@ -152,8 +487,9 @@ final class WC_Cart_Totals extends WC_Totals {
* @since 3.2.0
*/
protected function calculate_shipping_totals() {
parent::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' );
}
@ -164,9 +500,8 @@ final class WC_Cart_Totals extends WC_Totals {
* @since 3.2.0
*/
protected function calculate_totals() {
parent::calculate_totals();
$this->object->tax_total = $this->get_total( 'tax_total' );
$this->object->total = $this->get_total( 'total' );
$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( '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 ) ) );
}
}

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

@ -1,126 +0,0 @@
<?php
/**
* Order 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.
*
* @author Automattic
* @package WooCommerce/Classes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Order_Totals class.
*
* @since 3.2.0
*/
final class WC_Order_Totals extends WC_Totals {
/**
* Sets up the items provided, and calculate totals.
*
* @since 3.2.0
* @param object $object Cart or order object to calculate totals for.
*/
public function __construct( &$object = null ) {
parent::__construct( $object );
if ( is_a( $object, 'WC_Order' ) ) {
// Get items from the order. @todo call calculate or make it manual?
$this->set_items();
$this->set_fees();
$this->set_shipping();
$this->set_coupons();
}
}
/**
* Handles an order object passed in for calculation. Normalises data into the same format for use by this class.
*
* @since 3.2.0
*/
protected function set_items() {
$this->items = array();
foreach ( $this->object->get_items() as $item_key => $item_object ) {
$item = $this->get_default_item_props();
$item_taxes = $item_object->get_taxes();
$item->object = $item_object;
$item->product = $item_object->get_product();
$item->quantity = $item_object->get_quantity();
$item->subtotal = $this->add_precision( $item_object->get_subtotal() );
$item->subtotal_tax = $this->add_precision( $item_object->get_subtotal_tax() );
$item->total = $this->add_precision( $item_object->get_total() );
$item->total_tax = $this->add_precision( $item_object->get_total_tax() );
$item->taxes = $this->add_precision( $item_taxes['total'] );
$this->items[ $item_key ] = $item;
}
}
/**
* Get fee objects from the cart. Normalises data into the same format for use by this class.
*
* @since 3.2.0
*/
protected function set_fees() {
$this->fees = array();
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->get_total() );
$fee->taxes = $this->add_precision( $fee_object->get_taxes() );
$fee->total_tax = $this->add_precision( $fee_object->get_total_tax() );
$this->fees[ $fee_key ] = $fee;
}
}
/**
* Get shipping methods from the cart and normalise.
*
* @since 3.2.0
*/
protected function set_shipping() {
$this->shipping = array();
foreach ( $this->object->get_shipping_methods() as $key => $shipping_object ) {
$shipping_line = $this->get_default_shipping_props();
$shipping_line_taxes = $shipping_object->get_taxes();
$shipping_line->object = $shipping_object;
$shipping_line->total = $this->add_precision( $shipping_object->get_total() );
$shipping_line->taxes = $this->add_precision( $shipping_line_taxes['total'] );
$shipping_line->total_tax = $this->add_precision( $shipping_object->get_total_tax() );
$this->shipping[ $key ] = $shipping_line;
}
}
/**
* Return array of coupon objects from the cart. Normalises data
* into the same format for use by this class.
*
* @since 3.2.0
*/
protected function set_coupons() {
//$this->coupons = $this->object->get_coupons(); @todo get_used_coupons = codes
}
/**
* Main cart totals. Set all order totals here after calculation.
*
* @since 3.2.0
*/
protected function calculate_totals() {
parent::calculate_totals();
$this->object->set_discount_total( $this->get_total( 'discounts_total' ) );
$this->object->set_discount_tax( $this->get_total( 'discounts_tax_total' ) );
$this->object->set_shipping_total( $this->get_total( 'shipping_total' ) );
$this->object->set_shipping_tax( $this->get_total( 'shipping_tax_total' ) );
$this->object->set_cart_tax( $this->get_total( 'tax_total' ) );
$this->object->set_total( $this->get_total( 'total' ) );
}
}

View File

@ -303,7 +303,6 @@ final class WooCommerce {
include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-log-handler.php' );
include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-deprecated-hooks.php' );
include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-session.php' );
include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-totals.php' );
/**
* Core classes.
@ -336,7 +335,6 @@ final class WooCommerce {
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-cart-totals.php' );
include_once( WC_ABSPATH . 'includes/class-wc-order-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

@ -1,161 +0,0 @@
<?php
/**
* Tests for the totals class.
*
* @package WooCommerce\Tests\Discounts
*/
/**
* WC_Tests_Order_Totals
*/
class WC_Tests_Order_Totals extends WC_Unit_Test_Case {
/**
* Totals class for getter tests.
*
* @var object
*/
protected $totals;
/**
* ID tracking for cleanup.
*
* @var array
*/
protected $ids = array();
/**
* Order being tested.
*
* @var array
*/
protected $order;
/**
* Setup the cart for totals calculation.
*/
public function setUp() {
$this->ids = array();
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '20.0000',
'tax_rate_name' => 'VAT',
'tax_rate_priority' => '1',
'tax_rate_compound' => '0',
'tax_rate_shipping' => '1',
'tax_rate_order' => '1',
'tax_rate_class' => '',
);
$tax_rate_id = WC_Tax::_insert_tax_rate( $tax_rate );
update_option( 'woocommerce_calc_taxes', 'yes' );
$product = WC_Helper_Product::create_simple_product();
$product2 = WC_Helper_Product::create_simple_product();
WC_Helper_Shipping::create_simple_flat_rate();
$shipping_taxes = WC_Tax::calc_shipping_tax( '10', WC_Tax::get_shipping_tax_rates() );
$rate = new WC_Shipping_Rate( 'flat_rate_shipping', 'Flat rate shipping', '10', $shipping_taxes, 'flat_rate' );
$shipping_item = new WC_Order_Item_Shipping();
$shipping_item->set_props( array(
'method_title' => $rate->label,
'method_id' => $rate->id,
'total' => wc_format_decimal( $rate->cost ),
'taxes' => $rate->taxes,
) );
foreach ( $rate->get_meta_data() as $key => $value ) {
$shipping_item->add_meta_data( $key, $value, true );
}
$coupon = new WC_Coupon;
$coupon->set_code( 'test-coupon-10' );
$coupon->set_amount( 10 );
$coupon->set_discount_type( 'percent' );
$coupon->save();
$this->ids['tax_rate_ids'][] = $tax_rate_id;
$this->ids['products'][] = $product;
$this->ids['products'][] = $product2;
$this->ids['coupons'][] = $coupon;
$this->order = new WC_Order();
$this->order->add_product( $product, 1 );
$this->order->add_product( $product2, 2 );
$this->order->add_item( $shipping_item );
// @todo add coupon
// @todo add fee
$this->order->save();
$this->totals = new WC_Order_Totals( $this->order );
}
/**
* Clean up after test.
*/
public function tearDown() {
$this->order->delete();
WC_Helper_Shipping::delete_simple_flat_rate();
update_option( 'woocommerce_calc_taxes', 'no' );
remove_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_cart_fees_callback' ) );
foreach ( $this->ids['products'] as $product ) {
$product->delete( true );
}
foreach ( $this->ids['coupons'] as $coupon ) {
$coupon->delete( true );
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' );
}
foreach ( $this->ids['tax_rate_ids'] as $tax_rate_id ) {
WC_Tax::_delete_tax_rate( $tax_rate_id );
}
}
/**
* Test that order totals get updated.
*/
public function test_order_totals() {
$this->totals->calculate();
// $10 item + ($10*2) item + $10 shipping + (.2*$40) tax = $48.00
// These tests will need to be updated when coupons + fees get added.
$this->assertEquals( 48.00, $this->order->get_total() );
$this->assertEquals( 10.00, $this->order->get_shipping_total() );
$this->assertEquals( 2.00, $this->order->get_shipping_tax() );
$this->assertEquals( 6.00, $this->order->get_cart_tax() );
$this->assertEquals( 8.00, $this->order->get_total_tax() );
$this->assertEquals( 0, $this->order->get_discount_total() );
// Calculating ex tax shouldn't modify existing tax fields.
$this->totals->calculate( false );
$this->assertEquals( 48.00, $this->order->get_total() );
$this->assertEquals( 10.00, $this->order->get_shipping_total() );
$this->assertEquals( 2.00, $this->order->get_shipping_tax() );
$this->assertEquals( 6.00, $this->order->get_cart_tax() );
$this->assertEquals( 8.00, $this->order->get_total_tax() );
$this->assertEquals( 0, $this->order->get_discount_total() );
}
/**
* Test that non-tax order fields get updated in ex tax calculate.
*/
public function test_order_totals_ex_taxes() {
$this->totals->calculate( false );
$this->assertEquals( 10.00, $this->order->get_shipping_total() );
$this->assertEquals( 40.00, $this->order->get_subtotal() );
$this->assertEquals( 0, $this->order->get_discount_total() );
// These don't get calculated when not calculating taxes.
$this->assertEquals( 0, $this->order->get_shipping_tax() );
$this->assertEquals( 0, $this->order->get_cart_tax() );
$this->assertEquals( 0, $this->order->get_total_tax() );
$this->assertEquals( 0, $this->order->get_total() );
}
}