Merge pull request #16416 from woocommerce/manual-discounts-on-fees-shipping
Manual discounts on fees and shipping
This commit is contained in:
commit
15179b0e48
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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() ) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 ) ) {
|
||||
|
|
|
@ -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 ) );
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}();
|
||||
|
|
|
@ -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() );
|
||||
|
|
Loading…
Reference in New Issue