Manual discounts and negative taxes

This commit is contained in:
Mike Jolley 2017-08-09 18:53:06 +01:00
parent 1328e17069
commit f71dc64d35
6 changed files with 285 additions and 86 deletions

View File

@ -874,64 +874,51 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
* @return void
*/
public function add_discount( $discount ) {
$discounts = new WC_Discounts( $this );
// See if we have a coupon code.
$coupon_code = wc_format_coupon_code( $discount );
$coupon = new WC_Coupon( $coupon_code );
if ( $coupon->get_code() === $coupon_code && $coupon->is_valid() ) {
$applied = $discounts->apply_discount( $coupon );
$discounts = new WC_Discounts( $this );
$applied = $discounts->apply_discount( $coupon );
if ( $applied && ! is_wp_error( $applied ) ) {
$item_discounts = $discounts->get_discounts_by_item();
$coupon_discounts = $discounts->get_discounts_by_coupon();
// Add discounts to line items.
if ( $item_discounts ) {
foreach ( $item_discounts as $item_id => $amount ) {
$item = $this->get_item( $item_id );
$item->set_total( $amount );
$item->save();
}
unset( $this->items['line_items'] ); // Remove read line items variable so new totals are loaded from DB.
}
// Add WC_Order_Item_Coupon objects for applied coupons.
if ( $coupon_discounts ) {
foreach ( $coupon_discounts as $coupon_code => $amount ) {
$item = new WC_Order_Item_Coupon();
$item->set_props( array(
'code' => $coupon_code,
'discount' => $amount,
'discount_tax' => '', // @todo This needs to be calculated somehow like the cart class does. Maybe we need an order calculation class?
) );
$item->save();
$this->add_item( $item );
}
}
}
} else {
$applied = $discounts->apply_discount( $discount );
// Add WC_Order_Item_Discount object.
$item = new WC_Order_Item_Discount();
$item->set_amount( $discount );
$item->save();
$this->add_item( $item );
}
if ( $applied && ! is_wp_error( $applied ) ) {
$item_discounts = $discounts->get_discounts_by_item();
$coupon_discounts = $discounts->get_discounts_by_coupon();
$manual_discounts = $discounts->get_manual_discounts();
// Add discounts to line items.
if ( $item_discounts ) {
foreach ( $item_discounts as $item_id => $amount ) {
$item = $this->get_item( $item_id );
$item->set_total( $amount );
$item->save();
}
unset( $this->items['line_items'] ); // Remove read line items variable so new totals are loaded from DB.
}
// Add WC_Order_Item_Coupon objects for applied coupons.
if ( $coupon_discounts ) {
foreach ( $coupon_discounts as $coupon_code => $amount ) {
$item = new WC_Order_Item_Coupon();
$item->set_props( array(
'code' => $coupon_code,
'discount' => $amount,
'discount_tax' => '', // @todo This needs to be calculated somehow like the cart class does. Maybe we need an order calculation class?
) );
$this->add_item( $item );
}
}
// Add WC_Order_Item_Discount objects for applied discounts.
if ( $manual_discounts ) {
foreach ( $manual_discounts as $manual_discount_key => $manual_discount ) {
$item = new WC_Order_Item_Discount();
$item->set_props( array(
'amount' => wc_remove_number_precision( $manual_discount->get_amount() ),
'discount_type' => $manual_discount->get_discount_type(),
'discount_total' => wc_remove_number_precision( $manual_discount->get_discount_total() ),
) );
$this->add_item( $item );
}
}
// @todo we need to work out if manual discounts get calculated here, during calculate_totals, or inside a dedicated class.
// Total recalc.
$this->calculate_totals( true );
}
$this->calculate_totals( true );
}
/**
@ -1069,7 +1056,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
}
}
return array_unique( $found_tax_classes );
return $found_tax_classes;
}
/**
@ -1120,11 +1107,11 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$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() ) );
$shipping_tax_class = current( array_intersect( array_merge( array( '' ), WC_Tax::get_tax_class_slugs() ), array_unique( $this->get_items_tax_classes() ) ) );
}
// Trigger tax recalculation for all items.
foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) {
foreach ( $this->get_items( array( 'line_item', 'fee', 'discount' ) ) as $item_id => $item ) {
$item->calculate_taxes( $calculate_tax_for );
$item->save();
}
@ -1146,7 +1133,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$existing_taxes = $this->get_taxes();
$saved_rate_ids = array();
foreach ( $this->get_items( array( 'line_item', 'fee' ) ) as $item_id => $item ) {
foreach ( $this->get_items( array( 'line_item', 'fee', 'discount' ) ) as $item_id => $item ) {
$taxes = $item->get_taxes();
foreach ( $taxes['total'] as $tax_rate_id => $tax ) {
$cart_taxes[ $tax_rate_id ] = isset( $cart_taxes[ $tax_rate_id ] ) ? $cart_taxes[ $tax_rate_id ] + (float) $tax : (float) $tax;
@ -1197,16 +1184,23 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
* @return float calculated grand total.
*/
public function calculate_totals( $and_taxes = true ) {
$cart_subtotal = 0;
$cart_total = 0;
$fee_total = 0;
$cart_subtotal_tax = 0;
$cart_total_tax = 0;
$cart_subtotal = 0;
$cart_total = 0;
$fee_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.
$this->calculate_discounts();
// Calculate taxes for items, shipping, discounts.
if ( $and_taxes ) {
$this->calculate_taxes();
}
// Prepare the totals.
foreach ( $this->get_items() as $item ) {
$cart_subtotal += $item->get_subtotal();
$cart_total += $item->get_total();
@ -1220,16 +1214,43 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$fee_total += $item->get_total();
}
$grand_total = round( $cart_total + $fee_total + $this->get_shipping_total() + $this->get_cart_tax() + $this->get_shipping_tax(), wc_get_price_decimals() );
foreach ( $this->get_items( 'discount' ) as $item ) {
$discount_total += $item->get_total();
$discount_total_tax += $item->get_total_tax();
}
$this->set_discount_total( $cart_subtotal - $cart_total );
$this->set_discount_tax( $cart_subtotal_tax - $cart_total_tax );
$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->save();
return $grand_total;
}
/**
* Calculate actual discount amounts for each discount row from line items.
*
* @todo consider moving to totals class.
*/
protected function calculate_discounts() {
$discounts = new WC_Discounts( $this );
$discount_items = $this->get_items( 'discount' );
// Re-calc manual discounts based on new line items.
foreach ( $discount_items as $discount ) {
$result = $discounts->apply_discount( ( 'fixed' === $discount->get_discount_type() ? $discount->get_amount() : $discount->get_amount() . '%' ), $discount->get_id() );
}
// Set discount totals.
foreach ( $discounts->get_manual_discounts() as $manual_discount_key => $manual_discount ) {
$discount_item = $discount_items[ $manual_discount_key ];
$discount_item->set_total( wc_remove_number_precision( $manual_discount->get_discount_total() ) * -1 );
$discount_item->save();
}
}
/**
* Get item subtotal - this is the cost before discount.
*

View File

@ -30,10 +30,35 @@ if ( ! defined( 'ABSPATH' ) ) {
<td class="item_cost" width="1%">&nbsp;</td>
<td class="quantity" width="1%">&nbsp;</td>
<td class="line_cost" width="1%">
<?php echo wc_price( $item->get_discount_total() ); ?>
<?php echo wc_price( $item->get_total() ); ?>
</td>
<?php
// taxes?
if ( ( $tax_data = $item->get_taxes() ) && wc_tax_enabled() ) {
foreach ( $order_taxes as $tax_item ) {
$tax_item_id = $tax_item->get_rate_id();
$tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : '';
?>
<td class="line_tax" width="1%">
<div class="view">
<?php
echo ( '' !== $tax_item_total ) ? wc_price( wc_round_tax_total( $tax_item_total ), array( 'currency' => $order->get_currency() ) ) : '&ndash;';
if ( $refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'fee' ) ) {
echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>';
}
?>
</div>
<div class="edit" style="display: none;">
<input type="text" name="line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo wc_format_localized_price( 0 ); ?>" value="<?php echo ( isset( $tax_item_total ) ) ? esc_attr( wc_format_localized_price( $tax_item_total ) ) : ''; ?>" class="line_tax wc_input_price" />
</div>
<div class="refund" style="display: none;">
<input type="text" name="refund_line_tax[<?php echo absint( $item_id ); ?>][<?php echo esc_attr( $tax_item_id ); ?>]" placeholder="<?php echo wc_format_localized_price( 0 ); ?>" class="refund_line_tax wc_input_price" data-tax_id="<?php echo esc_attr( $tax_item_id ); ?>" />
</div>
</td>
<?php
}
}
?>
<td class="wc-order-edit-line-item">
<?php if ( $order->is_editable() ) : ?>

View File

@ -256,9 +256,10 @@ class WC_Discounts {
* Apply a discount to all items.
*
* @param string|object $raw_discount Accepts a string (fixed or percent discounts), or WC_Coupon object.
* @param string $discount_id Optional ID for the discount. Generated if not defined.
* @return bool|WP_Error True if applied or WP_Error instance in failure.
*/
public function apply_discount( $raw_discount ) {
public function apply_discount( $raw_discount, $discount_id = null ) {
if ( is_a( $raw_discount, 'WC_Coupon' ) ) {
return $this->apply_coupon( $raw_discount );
}
@ -285,7 +286,9 @@ class WC_Discounts {
$discount->set_discount_total( min( $discount->get_amount(), $total_to_discount ) );
}
$this->manual_discounts[ $this->generate_discount_id( $discount ) ] = $discount;
$discount_id = $discount_id ? $discount_id : $this->generate_discount_id( $discount );
$this->manual_discounts[ $discount_id ] = $discount;
return true;
}

View File

@ -19,11 +19,46 @@ class WC_Order_Item_Discount extends WC_Order_Item {
* @var array
*/
protected $extra_data = array(
'amount' => 0, // Discount amount.
'discount_type' => 'fixed', // Fixed or percent type.
'discount_total' => 0,
'amount' => 0, // Discount amount.
'discount_type' => 'fixed', // Fixed or percent type.
'total' => '',
'total_tax' => '',
'taxes' => array(
'total' => array(),
),
);
/**
* 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 ( wc_tax_enabled() && ( $order = $this->get_order() ) ) {
// Apportion taxes to order items.
$order = $this->get_order();
$tax_class_counts = array_count_values( $order->get_items_tax_classes() );
$item_count = $order->get_item_count();
$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 ) ) );
}
$this->set_taxes( array( 'total' => $discount_taxes ) );
} else {
$this->set_taxes( false );
}
return true;
}
/*
|--------------------------------------------------------------------------
| Setters
@ -33,10 +68,16 @@ class WC_Order_Item_Discount extends WC_Order_Item {
/**
* Set amount.
*
* @param string $value Value to set.
* @param string $raw_discount Value to set.
*/
public function set_amount( $value ) {
$this->set_prop( 'amount', $value );
public function set_amount( $raw_discount ) {
if ( strstr( $raw_discount, '%' ) ) {
$this->set_prop( 'amount', trim( $raw_discount, '%' ) );
$this->set_discount_type( 'percent' );
} elseif ( is_numeric( $raw_discount ) && 0 < absint( $raw_discount ) ) {
$this->set_prop( 'amount', absint( $raw_discount ) );
$this->set_discount_type( 'fixed' );
}
}
/**
@ -49,12 +90,39 @@ class WC_Order_Item_Discount extends WC_Order_Item {
}
/**
* Set discount total.
* Set total.
*
* @param string $value Value to set.
*/
public function set_discount_total( $value ) {
$this->set_prop( 'discount_total', wc_format_decimal( $value ) );
public function set_total( $value ) {
$this->set_prop( 'total', wc_format_decimal( $value ) );
}
/**
* Set total tax.
*
* @param string $value Value to set.
*/
public function set_total_tax( $value ) {
$this->set_prop( 'total_tax', wc_format_decimal( $value ) );
}
/**
* Set taxes.
*
* This is an array of tax ID keys with total amount values.
* @param array $raw_tax_data
*/
public function set_taxes( $raw_tax_data ) {
$raw_tax_data = maybe_unserialize( $raw_tax_data );
$tax_data = array(
'total' => array(),
);
if ( ! empty( $raw_tax_data['total'] ) ) {
$tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] );
}
$this->set_prop( 'taxes', $tax_data );
$this->set_total_tax( array_sum( $tax_data['total'] ) );
}
/*
@ -93,12 +161,32 @@ class WC_Order_Item_Discount extends WC_Order_Item {
}
/**
* Get discount_total.
* Get total fee.
*
* @param string $context View or edit context.
* @param string $context
* @return string
*/
public function get_discount_total( $context = 'view' ) {
return $this->get_prop( 'discount_total', $context );
public function get_total( $context = 'view' ) {
return $this->get_prop( 'total', $context );
}
/**
* Get total tax.
*
* @param string $context
* @return string
*/
public function get_total_tax( $context = 'view' ) {
return $this->get_prop( 'total_tax', $context );
}
/**
* Get fee taxes.
*
* @param string $context
* @return array
*/
public function get_taxes( $context = 'view' ) {
return $this->get_prop( 'taxes', $context );
}
}

View File

@ -18,21 +18,22 @@ class WC_Order_Item_Discount_Data_Store extends Abstract_WC_Order_Item_Type_Data
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array( 'discount_type', 'discount_total', 'amount' );
protected $internal_meta_keys = array( 'discount_type', 'amount', '_line_total', '_line_tax', '_line_tax_data' );
/**
* Read/populate data properties specific to this order item.
*
* @since 3.0.0
* @param WC_Order_Item_Coupon $item
* @param WC_Order_Item_Discount $item
*/
public function read( &$item ) {
parent::read( $item );
$id = $item->get_id();
$item->set_props( array(
'discount_type' => get_metadata( 'order_item', $id, 'discount_type', true ),
'discount_total' => get_metadata( 'order_item', $id, 'discount_total', true ),
'amount' => get_metadata( 'order_item', $id, 'amount', true ),
'discount_type' => get_metadata( 'order_item', $id, 'discount_type', true ),
'total' => get_metadata( 'order_item', $id, '_line_total', true ),
'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ),
'amount' => get_metadata( 'order_item', $id, 'amount', true ),
) );
$item->set_object_read( true );
}
@ -42,13 +43,15 @@ class WC_Order_Item_Discount_Data_Store extends Abstract_WC_Order_Item_Type_Data
* Ran after both create and update, so $item->get_id() will be set.
*
* @since 3.0.0
* @param WC_Order_Item_Coupon $item
* @param WC_Order_Item_Discount $item
*/
public function save_item_data( &$item ) {
$id = $item->get_id();
$save_values = array(
'discount_type' => $item->get_discount_type( 'edit' ),
'discount_total' => $item->get_discount_total( 'edit' ),
'_line_total' => $item->get_total( 'edit' ),
'_line_tax' => $item->get_total_tax( 'edit' ),
'_line_tax_data' => $item->get_taxes( 'edit' ),
'amount' => $item->get_amount( 'edit' ),
);
foreach ( $save_values as $key => $value ) {

View File

@ -1169,3 +1169,62 @@ function wc_cart_round_discount( $value, $precision ) {
return round( $value, $precision );
}
}
/**
* Array merge and sum function.
*
* Source: https://gist.github.com/Nickology/f700e319cbafab5eaedc
*
* @since 3.2.0
* @return array
*/
function wc_array_merge_recursive_numeric() {
$arrays = func_get_args();
// If there's only one array, it's already merged.
if ( 1 === count( $arrays ) ) {
return $arrays[0];
}
// Remove any items in $arrays that are NOT arrays.
foreach ( $arrays as $key => $array ) {
if ( ! is_array( $array ) ) {
unset( $arrays[ $key ] );
}
}
// We start by setting the first array as our final array.
// We will merge all other arrays with this one.
$final = array_shift( $arrays );
foreach ( $arrays as $b ) {
foreach ( $final as $key => $value ) {
// If $key does not exist in $b, then it is unique and can be safely merged.
if ( ! isset( $b[ $key ] ) ) {
$final[ $key ] = $value;
} else {
// If $key is present in $b, then we need to merge and sum numeric values in both.
if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) {
// If both values for these keys are numeric, we sum them.
$final[ $key ] = $value + $b[ $key ];
} elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) {
// If both values are arrays, we recursively call ourself.
$final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] );
} else {
// If both keys exist but differ in type, then we cannot merge them.
// In this scenario, we will $b's value for $key is used.
$final[ $key ] = $b[ $key ];
}
}
}
// Finally, we need to merge any keys that exist only in $b.
foreach ( $b as $key => $value ) {
if ( ! isset( $final[ $key ] ) ) {
$final[ $key ] = $value;
}
}
}
return $final;
}