Merge pull request #16416 from woocommerce/manual-discounts-on-fees-shipping

Manual discounts on fees and shipping
This commit is contained in:
Claudiu Lodromanean 2017-08-11 11:30:27 -07:00 committed by GitHub
commit 15179b0e48
11 changed files with 159 additions and 69 deletions

View File

@ -193,6 +193,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$items = array_filter( $items );
foreach ( $items as $item_key => $item ) {
$item->set_order_id( $this->get_id() );
$item_id = $item->save();
// If ID changed (new item saved to DB)...
@ -1048,7 +1049,8 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$discounts = new WC_Discounts( $this );
foreach ( $coupons as $coupon ) {
$discounts->apply_discount( $coupon->get_code(), $coupon->get_id() );
$coupon_object = new WC_Coupon( $coupon->get_code() );
$discounts->apply_coupon( $coupon_object );
}
$item_discounts = $discounts->get_discounts_by_item();
@ -1212,24 +1214,6 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
return array_unique( $found_tax_classes );
}
/**
* Get count of all tax classes for items in the order.
*
* @since 3.2.0
* @return array
*/
public function get_item_tax_class_counts() {
$tax_classes = array_fill_keys( $this->get_items_tax_classes(), 0 );
foreach ( $this->get_items() as $item ) {
if ( ( $product = $item->get_product() ) && ( $product->is_taxable() || $product->is_shipping_taxable() ) ) {
$tax_classes[ $product->get_tax_class() ] += $item->get_quantity();
}
}
return $tax_classes;
}
/**
* Get tax location for this order.
*
@ -1356,46 +1340,58 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$cart_subtotal = 0;
$cart_total = 0;
$fee_total = 0;
$shipping_total = 0;
$discount_total = 0;
$discount_total_tax = 0;
$cart_subtotal_tax = 0;
$cart_total_tax = 0;
// Discounts are recalculated first based on the latest item costs.
// Sum line item costs.
foreach ( $this->get_items() as $item ) {
$cart_subtotal += $item->get_subtotal();
$cart_total += $item->get_total();
}
// Sum fee costs.
foreach ( $this->get_fees() as $item ) {
$fee_total += $item->get_total();
}
// Sum shipping costs.
foreach ( $this->get_shipping_methods() as $shipping ) {
$shipping_total += $shipping->get_total();
}
$this->set_shipping_total( $shipping_total );
// Calculate manual discounts.
$this->calculate_discounts();
foreach ( $this->get_items( 'discount' ) as $item ) {
$discount_total += $item->get_total() * -1;
}
// Calculate taxes for items, shipping, discounts.
if ( $and_taxes ) {
$this->calculate_taxes();
}
// Prepare the totals.
// Sum taxes.
foreach ( $this->get_items() as $item ) {
$cart_subtotal += $item->get_subtotal();
$cart_total += $item->get_total();
$cart_subtotal_tax += $item->get_subtotal_tax();
$cart_total_tax += $item->get_total_tax();
}
$this->calculate_shipping();
foreach ( $this->get_fees() as $item ) {
$fee_total += $item->get_total();
}
foreach ( $this->get_items( 'discount' ) as $item ) {
$discount_total += $item->get_total() * -1;
$discount_total_tax += $item->get_total_tax() * -1;
}
$grand_total = round( $cart_total - $discount_total + $fee_total + $this->get_shipping_total() + $this->get_cart_tax() + $this->get_shipping_tax(), wc_get_price_decimals() );
$this->set_discount_total( $cart_subtotal - $cart_total + $discount_total );
$this->set_discount_tax( $cart_subtotal_tax - $cart_total_tax + $discount_total_tax );
$this->set_total( $grand_total );
$this->set_total( round( $cart_total - $discount_total + $fee_total + $this->get_shipping_total() + $this->get_cart_tax() + $this->get_shipping_tax(), wc_get_price_decimals() ) );
$this->save();
return $grand_total;
return $this->get_total();
}
/**

View File

@ -63,14 +63,6 @@ if ( wc_tax_enabled() ) {
do_action( 'woocommerce_admin_order_items_after_line_items', $order->get_id() );
?>
</tbody>
<tbody id="order_discount_line_items">
<?php
foreach ( $discounts as $item_id => $item ) {
include( 'html-order-discount.php' );
}
do_action( 'woocommerce_admin_order_items_after_discounts', $order->get_id() );
?>
</tbody>
<tbody id="order_shipping_line_items">
<?php
$shipping_methods = WC()->shipping() ? WC()->shipping->load_shipping_methods() : array();
@ -88,6 +80,14 @@ if ( wc_tax_enabled() ) {
do_action( 'woocommerce_admin_order_items_after_fees', $order->get_id() );
?>
</tbody>
<tbody id="order_discount_line_items">
<?php
foreach ( $discounts as $item_id => $item ) {
include( 'html-order-discount.php' );
}
do_action( 'woocommerce_admin_order_items_after_discounts', $order->get_id() );
?>
</tbody>
<tbody id="order_refunds">
<?php
if ( $refunds = $order->get_refunds() ) {

View File

@ -186,12 +186,10 @@ function woocommerce_settings_get_option( $option_name, $default = '' ) {
* @param array $items Order items to save
*/
function wc_save_order_items( $order_id, $items ) {
// Allow other plugins to check change in order items before they are saved
// Allow other plugins to check change in order items before they are saved.
do_action( 'woocommerce_before_save_order_items', $order_id, $items );
$order = wc_get_order( $order_id );
// Line items and fees
// Line items and fees.
if ( isset( $items['order_item_id'] ) ) {
$data_keys = array(
'line_tax' => array(),
@ -203,7 +201,7 @@ function wc_save_order_items( $order_id, $items ) {
'line_subtotal' => null,
);
foreach ( $items['order_item_id'] as $item_id ) {
if ( ! $item = $order->get_item( absint( $item_id ) ) ) {
if ( ! $item = WC_Order_Factory::get_order_item( absint( $item_id ) ) ) {
continue;
}
@ -260,7 +258,7 @@ function wc_save_order_items( $order_id, $items ) {
);
foreach ( $items['shipping_method_id'] as $item_id ) {
if ( ! $item = $order->get_item( absint( $item_id ) ) ) {
if ( ! $item = WC_Order_Factory::get_order_item( absint( $item_id ) ) ) {
continue;
}
@ -299,10 +297,8 @@ function wc_save_order_items( $order_id, $items ) {
}
}
// Updates tax totals
$order = wc_get_order( $order_id );
$order->update_taxes();
// Calc totals - this also triggers save
$order->calculate_totals( false );
// Inform other plugins that the items have been saved

View File

@ -408,7 +408,7 @@ final class WC_Cart_Totals {
protected function calculate_item_totals() {
$this->set_items();
$this->calculate_item_subtotals();
$this->calculate_discounts();
$this->calculate_item_discounts();
foreach ( $this->items as $item_key => $item ) {
$item->total = $this->get_discounted_price_in_cents( $item_key );
@ -494,18 +494,18 @@ final class WC_Cart_Totals {
}
/**
* Calculate all discount and coupon amounts.
* Calculate coupon based discounts which change item prices.
*
* @since 3.2.0
* @uses WC_Discounts class.
*/
protected function calculate_discounts() {
protected function calculate_item_discounts() {
$this->set_coupons();
$discounts = new WC_Discounts( $this->object );
foreach ( $this->coupons as $coupon ) {
$discounts->apply_discount( $coupon );
$discounts->apply_coupon( $coupon );
}
$coupon_discount_amounts = $discounts->get_discounts_by_coupon( true );

View File

@ -24,6 +24,20 @@ class WC_Discounts {
*/
protected $items = array();
/**
* Stores fee total from cart/order. Manual discounts can discount this.
*
* @var int
*/
protected $fee_total = 0;
/**
* Stores shipping total from cart/order. Manual discounts can discount this.
*
* @var int
*/
protected $shipping_total = 0;
/**
* An array of discounts which have been applied to items.
*
@ -84,7 +98,8 @@ class WC_Discounts {
* @param array $order Cart object.
*/
public function set_items_from_order( $order ) {
$this->items = $this->discounts = $this->manual_discounts = array();
$this->items = $this->discounts = $this->manual_discounts = array();
$this->fee_total = $this->shipping_total = 0;
if ( ! is_a( $order, 'WC_Order' ) ) {
return;
@ -101,6 +116,12 @@ class WC_Discounts {
}
uasort( $this->items, array( $this, 'sort_by_price' ) );
foreach ( $order->get_fees() as $item ) {
$this->fee_total += wc_add_number_precision( $item->get_total() );
}
$this->shipping_total = wc_add_number_precision( $order->get_shipping_total() );
}
/**
@ -217,10 +238,15 @@ class WC_Discounts {
protected function get_total_after_discounts() {
$total_to_discount = 0;
// Sum line item costs.
foreach ( $this->items as $item ) {
$total_to_discount += $this->get_discounted_price_in_cents( $item );
}
// Manual discounts can also discount shipping and fees.
$total_to_discount += $this->shipping_total + $this->fee_total;
// Remove existing discount amounts.
foreach ( $this->manual_discounts as $key => $value ) {
$total_to_discount = $total_to_discount - $value->get_discount_total();
}
@ -275,7 +301,7 @@ class WC_Discounts {
}
if ( ! $discount->get_amount() ) {
return new WP_Error( 'invalid_coupon', __( 'Invalid discount', 'woocommerce' ) );
return new WP_Error( 'invalid_discount', __( 'Invalid discount', 'woocommerce' ) );
}
$total_to_discount = $this->get_total_after_discounts();
@ -300,7 +326,11 @@ class WC_Discounts {
* @param WC_Coupon $coupon Coupon object being applied to the items.
* @return bool|WP_Error True if applied or WP_Error instance in failure.
*/
protected function apply_coupon( $coupon ) {
public function apply_coupon( $coupon ) {
if ( ! is_a( $coupon, 'WC_Coupon' ) ) {
return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) );
}
$is_coupon_valid = $this->is_coupon_valid( $coupon );
if ( is_wp_error( $is_coupon_valid ) ) {

View File

@ -28,6 +28,37 @@ class WC_Order_Item_Discount extends WC_Order_Item {
),
);
/**
* Get item costs grouped by tax class.
*
* @since 3.2.0
* @param WC_Order $order Order object.
* @return array
*/
protected function get_tax_class_costs( $order ) {
$tax_classes = array_fill_keys( $order->get_items_tax_classes(), 0 );
$tax_classes['non-taxable'] = 0;
foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) {
if ( 'taxable' === $item->get_tax_status() ) {
$tax_classes[ $item->get_tax_class() ] += $item->get_total();
} else {
$tax_classes['non-taxable'] += $item->get_total();
}
}
foreach ( $order->get_items( array( 'shipping' ) ) as $item ) {
if ( 'taxable' === $item->get_tax_status() ) {
$class = 'inherit' === $item->get_tax_class() ? current( $order->get_items_tax_classes() ): $item->get_tax_class();
$tax_classes[ $class ] += $item->get_total();
} else {
$tax_classes['non-taxable'] += $item->get_total();
}
}
return $tax_classes;
}
/**
* Calculate item taxes.
*
@ -40,16 +71,21 @@ class WC_Order_Item_Discount extends WC_Order_Item {
return false;
}
if ( wc_tax_enabled() && ( $order = $this->get_order() ) ) {
// Apportion taxes to order items.
$order = $this->get_order();
$tax_class_counts = $order->get_item_tax_class_counts();
$item_count = $order->get_item_count();
$discount_taxes = array();
// Apportion taxes to order items, shipping, and fees.
$order = $this->get_order();
$tax_class_costs = $this->get_tax_class_costs( $order );
$total_costs = array_sum( $tax_class_costs );
$discount_taxes = array();
foreach ( $tax_class_counts as $tax_class => $tax_class_count ) {
$proportion = $tax_class_count / $item_count;
$cart_discount_proportion = $this->get_total() * $proportion;
$discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, WC_Tax::get_rates( $tax_class ) ) );
if ( $total_costs ) {
foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) {
if ( 'non-taxable' === $tax_class ) {
continue;
}
$proportion = $tax_class_cost / $total_costs;
$cart_discount_proportion = $this->get_total() * $proportion;
$discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, WC_Tax::get_rates( $tax_class ) ) );
}
}
$this->set_taxes( array( 'total' => $discount_taxes ) );

View File

@ -221,6 +221,16 @@ class WC_Order_Item_Shipping extends WC_Order_Item {
return $this->get_prop( 'taxes', $context );
}
/**
* Get tax class.
*
* @param string $context
* @return string
*/
public function get_tax_class( $context = 'view' ) {
return get_option( 'woocommerce_shipping_tax_class' );
}
/*
|--------------------------------------------------------------------------
| Array Access Methods

View File

@ -111,6 +111,24 @@ class WC_Order_Item extends WC_Data implements ArrayAccess {
return 1;
}
/**
* Get tax status.
* @return string
*/
public function get_tax_status() {
return 'taxable';
}
/**
* Get tax class.
*
* @param string $context
* @return string
*/
public function get_tax_class() {
return '';
}
/**
* Get parent order object.
* @return WC_Order

View File

@ -144,5 +144,6 @@ abstract class Abstract_WC_Order_Item_Type_Data_Store extends WC_Data_Store_WP i
public function clear_cache( &$item ) {
wp_cache_delete( 'item-' . $item->get_id(), 'order-items' );
wp_cache_delete( 'order-items-' . $item->get_order_id(), 'orders' );
wp_cache_delete( $item->get_id(), $this->meta_type . '_meta' );
}
}

View File

@ -1650,7 +1650,10 @@ function wc_list_pluck( $list, $callback_or_field, $index_key = null ) {
*/
$newlist = array();
foreach ( $list as $value ) {
if ( isset( $value->$index_key ) ) {
// Get index. @since 3.2.0 this supports a callback.
if ( is_callable( array( $value, $index_key ) ) ) {
$newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}();
} elseif ( isset( $value->$index_key ) ) {
$newlist[ $value->$index_key ] = $value->{$callback_or_field}();
} else {
$newlist[] = $value->{$callback_or_field}();

View File

@ -1639,7 +1639,7 @@ class WC_Tests_CRUD_Orders extends WC_Unit_Test_Case {
$order = WC_Helper_Order::create_order();
$order->add_discount( '50%' );
$this->assertEquals( 30, $order->get_total() );
$this->assertEquals( 25, $order->get_total() );
$discount = current( $order->get_items( 'discount' ) );
$order->remove_item( $discount->get_id() );