WC_Cart::calculate_totals PHP Method

calculate_totals() public method

Calculate totals for the items in the cart.
public calculate_totals ( )
    public function calculate_totals()
    {
        $this->reset();
        $this->coupons = $this->get_coupons();
        do_action('woocommerce_before_calculate_totals', $this);
        if ($this->is_empty()) {
            $this->set_session();
            return;
        }
        $tax_rates = array();
        $shop_tax_rates = array();
        $cart = $this->get_cart();
        /**
         * Calculate subtotals for items. This is done first so that discount logic can use the values.
         */
        foreach ($cart as $cart_item_key => $values) {
            $product = $values['data'];
            $line_price = $product->get_price() * $values['quantity'];
            $line_subtotal = 0;
            $line_subtotal_tax = 0;
            /**
             * No tax to calculate.
             */
            if (!$product->is_taxable()) {
                // Subtotal is the undiscounted price
                $this->subtotal += $line_price;
                $this->subtotal_ex_tax += $line_price;
                /**
                 * Prices include tax.
                 *
                 * 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.
                 */
            } elseif ($this->prices_include_tax) {
                // Get base tax rates
                if (empty($shop_tax_rates[$product->get_tax_class(true)])) {
                    $shop_tax_rates[$product->get_tax_class(true)] = WC_Tax::get_base_tax_rates($product->get_tax_class(true));
                }
                // Get item tax rates
                if (empty($tax_rates[$product->get_tax_class()])) {
                    $tax_rates[$product->get_tax_class()] = WC_Tax::get_rates($product->get_tax_class());
                }
                $base_tax_rates = $shop_tax_rates[$product->get_tax_class(true)];
                $item_tax_rates = $tax_rates[$product->get_tax_class()];
                /**
                 * ADJUST TAX - Calculations when base tax is not equal to the item tax.
                 *
                 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
                 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
                 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
                 */
                if ($item_tax_rates !== $base_tax_rates && apply_filters('woocommerce_adjust_non_base_location_prices', true)) {
                    // Work out a new base price without the shop's base tax
                    $taxes = WC_Tax::calc_tax($line_price, $base_tax_rates, true, true);
                    // Now we have a new item price (excluding TAX)
                    $line_subtotal = $line_price - array_sum($taxes);
                    // Now add modified taxes
                    $tax_result = WC_Tax::calc_tax($line_subtotal, $item_tax_rates);
                    $line_subtotal_tax = array_sum($tax_result);
                    /**
                     * Regular tax calculation (customer inside base and the tax class is unmodified.
                     */
                } else {
                    // Calc tax normally
                    $taxes = WC_Tax::calc_tax($line_price, $item_tax_rates, true);
                    $line_subtotal_tax = array_sum($taxes);
                    $line_subtotal = $line_price - array_sum($taxes);
                }
                /**
                 * Prices exclude tax.
                 *
                 * This calculation is simpler - work with the base, untaxed price.
                 */
            } else {
                // Get item tax rates
                if (empty($tax_rates[$product->get_tax_class()])) {
                    $tax_rates[$product->get_tax_class()] = WC_Tax::get_rates($product->get_tax_class());
                }
                $item_tax_rates = $tax_rates[$product->get_tax_class()];
                // Base tax for line before discount - we will store this in the order data
                $taxes = WC_Tax::calc_tax($line_price, $item_tax_rates);
                $line_subtotal_tax = array_sum($taxes);
                $line_subtotal = $line_price;
            }
            // Add to main subtotal
            $this->subtotal += $line_subtotal + $line_subtotal_tax;
            $this->subtotal_ex_tax += $line_subtotal;
        }
        // Order cart items by price so coupon logic is 'fair' for customers and not based on order added to cart.
        uasort($cart, apply_filters('woocommerce_sort_by_subtotal_callback', array($this, 'sort_by_subtotal')));
        /**
         * Calculate totals for items.
         */
        foreach ($cart as $cart_item_key => $values) {
            $product = $values['data'];
            // Prices
            $base_price = $product->get_price();
            $line_price = $product->get_price() * $values['quantity'];
            // Tax data
            $taxes = array();
            $discounted_taxes = array();
            /**
             * No tax to calculate.
             */
            if (!$product->is_taxable()) {
                // Discounted Price (price with any pre-tax discounts applied)
                $discounted_price = $this->get_discounted_price($values, $base_price, true);
                $line_subtotal_tax = 0;
                $line_subtotal = $line_price;
                $line_tax = 0;
                $line_total = round($discounted_price * $values['quantity'], wc_get_rounding_precision());
                /**
                 * Prices include tax.
                 */
            } elseif ($this->prices_include_tax) {
                $base_tax_rates = $shop_tax_rates[$product->get_tax_class(true)];
                $item_tax_rates = $tax_rates[$product->get_tax_class()];
                /**
                 * ADJUST TAX - Calculations when base tax is not equal to the item tax.
                 *
                 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
                 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
                 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
                 */
                if ($item_tax_rates !== $base_tax_rates && apply_filters('woocommerce_adjust_non_base_location_prices', true)) {
                    // Work out a new base price without the shop's base tax
                    $taxes = WC_Tax::calc_tax($line_price, $base_tax_rates, true, true);
                    // Now we have a new item price (excluding TAX)
                    $line_subtotal = round($line_price - array_sum($taxes), wc_get_rounding_precision());
                    $taxes = WC_Tax::calc_tax($line_subtotal, $item_tax_rates);
                    $line_subtotal_tax = array_sum($taxes);
                    // Adjusted price (this is the price including the new tax rate)
                    $adjusted_price = ($line_subtotal + $line_subtotal_tax) / $values['quantity'];
                    // Apply discounts and get the discounted price FOR A SINGLE ITEM
                    $discounted_price = $this->get_discounted_price($values, $adjusted_price, true);
                    // Convert back to line price
                    $discounted_line_price = $discounted_price * $values['quantity'];
                    // Now use rounded line price to get taxes.
                    $discounted_taxes = WC_Tax::calc_tax($discounted_line_price, $item_tax_rates, true);
                    $line_tax = array_sum($discounted_taxes);
                    $line_total = $discounted_line_price - $line_tax;
                    /**
                     * Regular tax calculation (customer inside base and the tax class is unmodified.
                     */
                } else {
                    // Work out a new base price without the item tax
                    $taxes = WC_Tax::calc_tax($line_price, $item_tax_rates, true);
                    // Now we have a new item price (excluding TAX)
                    $line_subtotal = $line_price - array_sum($taxes);
                    $line_subtotal_tax = array_sum($taxes);
                    // Calc prices and tax (discounted)
                    $discounted_price = $this->get_discounted_price($values, $base_price, true);
                    // Convert back to line price
                    $discounted_line_price = $discounted_price * $values['quantity'];
                    // Now use rounded line price to get taxes.
                    $discounted_taxes = WC_Tax::calc_tax($discounted_line_price, $item_tax_rates, true);
                    $line_tax = array_sum($discounted_taxes);
                    $line_total = $discounted_line_price - $line_tax;
                }
                // Tax rows - merge the totals we just got
                foreach (array_keys($this->taxes + $discounted_taxes) as $key) {
                    $this->taxes[$key] = (isset($discounted_taxes[$key]) ? $discounted_taxes[$key] : 0) + (isset($this->taxes[$key]) ? $this->taxes[$key] : 0);
                }
                /**
                 * Prices exclude tax.
                 */
            } else {
                $item_tax_rates = $tax_rates[$product->get_tax_class()];
                // Work out a new base price without the shop's base tax
                $taxes = WC_Tax::calc_tax($line_price, $item_tax_rates);
                // Now we have the item price (excluding TAX)
                $line_subtotal = $line_price;
                $line_subtotal_tax = array_sum($taxes);
                // Now calc product rates
                $discounted_price = $this->get_discounted_price($values, $base_price, true);
                $discounted_taxes = WC_Tax::calc_tax($discounted_price * $values['quantity'], $item_tax_rates);
                $discounted_tax_amount = array_sum($discounted_taxes);
                $line_tax = $discounted_tax_amount;
                $line_total = $discounted_price * $values['quantity'];
                // Tax rows - merge the totals we just got
                foreach (array_keys($this->taxes + $discounted_taxes) as $key) {
                    $this->taxes[$key] = (isset($discounted_taxes[$key]) ? $discounted_taxes[$key] : 0) + (isset($this->taxes[$key]) ? $this->taxes[$key] : 0);
                }
            }
            // Cart contents total is based on discounted prices and is used for the final total calculation
            $this->cart_contents_total += $line_total;
            /**
             * Store costs + taxes for lines. For tax inclusive prices, we do some extra rounding logic so the stored
             * values "add up" when viewing the order in admin. This does have the disadvatage of not being able to
             * recalculate the tax total/subtotal accurately in the future, but it does ensure the data looks correct.
             *
             * Tax exclusive prices are not affected.
             */
            if (!$product->is_taxable() || $this->prices_include_tax) {
                $this->cart_contents[$cart_item_key]['line_total'] = round($line_total + $line_tax - wc_round_tax_total($line_tax), $this->dp);
                $this->cart_contents[$cart_item_key]['line_subtotal'] = round($line_subtotal + $line_subtotal_tax - wc_round_tax_total($line_subtotal_tax), $this->dp);
                $this->cart_contents[$cart_item_key]['line_tax'] = wc_round_tax_total($line_tax);
                $this->cart_contents[$cart_item_key]['line_subtotal_tax'] = wc_round_tax_total($line_subtotal_tax);
                $this->cart_contents[$cart_item_key]['line_tax_data'] = array('total' => array_map('wc_round_tax_total', $discounted_taxes), 'subtotal' => array_map('wc_round_tax_total', $taxes));
            } else {
                $this->cart_contents[$cart_item_key]['line_total'] = $line_total;
                $this->cart_contents[$cart_item_key]['line_subtotal'] = $line_subtotal;
                $this->cart_contents[$cart_item_key]['line_tax'] = $line_tax;
                $this->cart_contents[$cart_item_key]['line_subtotal_tax'] = $line_subtotal_tax;
                $this->cart_contents[$cart_item_key]['line_tax_data'] = array('total' => $discounted_taxes, 'subtotal' => $taxes);
            }
        }
        // Only calculate the grand total + shipping if on the cart/checkout
        if (is_checkout() || is_cart() || defined('WOOCOMMERCE_CHECKOUT') || defined('WOOCOMMERCE_CART')) {
            // Calculate the Shipping
            $this->calculate_shipping();
            // Trigger the fees API where developers can add fees to the cart
            $this->calculate_fees();
            // Total up/round taxes and shipping taxes
            if ($this->round_at_subtotal) {
                $this->tax_total = WC_Tax::get_tax_total($this->taxes);
                $this->shipping_tax_total = WC_Tax::get_tax_total($this->shipping_taxes);
                $this->taxes = array_map(array('WC_Tax', 'round'), $this->taxes);
                $this->shipping_taxes = array_map(array('WC_Tax', 'round'), $this->shipping_taxes);
            } else {
                $this->tax_total = array_sum($this->taxes);
                $this->shipping_tax_total = array_sum($this->shipping_taxes);
            }
            // VAT exemption done at this point - so all totals are correct before exemption
            if (WC()->customer->get_is_vat_exempt()) {
                $this->remove_taxes();
            }
            // Allow plugins to hook and alter totals before final total is calculated
            do_action('woocommerce_calculate_totals', $this);
            // Grand Total - Discounted product prices, discounted tax, shipping cost + tax
            $this->total = max(0, apply_filters('woocommerce_calculated_total', round($this->cart_contents_total + $this->tax_total + $this->shipping_tax_total + $this->shipping_total + $this->fee_total, $this->dp), $this));
        } else {
            // Set tax total to sum of all tax rows
            $this->tax_total = WC_Tax::get_tax_total($this->taxes);
            // VAT exemption done at this point - so all totals are correct before exemption
            if (WC()->customer->get_is_vat_exempt()) {
                $this->remove_taxes();
            }
        }
        do_action('woocommerce_after_calculate_totals', $this);
        $this->set_session();
    }

Usage Example

    /**
     * Prepare recurring amounts, taxes etc for subscription items
     * 
     * @access public
     * @param int $order_id
     * @return void
     */
    public function new_order($order_id)
    {
        global $woocommerce;

        // Iterate over real cart and work with subscription products (if any)
        foreach ($woocommerce->cart->cart_contents as $cart_item_key => $cart_item) {

            $id = !empty($cart_item['variation_id']) ? $cart_item['variation_id'] : $cart_item['product_id'];

            if (Subscriptio_Subscription_Product::is_subscription($id)) {

                $product = new WC_Product($id);

                // Store all required renewal order fields here
                $renewal_order = array(
                    'taxes'     => array(),
                    'shipping'  => array(),
                );

                // Create fake cart to mimic renewal order
                $renewal_cart = new WC_Cart();

                // Add product to cart
                $renewal_cart->add_to_cart(
                    $cart_item['product_id'],
                    $cart_item['quantity'],
                    (isset($cart_item['variation_id']) ? $cart_item['variation_id'] : ''),
                    (isset($cart_item['variation']) ? $cart_item['variation'] : '')
                );

                // Get fake cart item key
                $renewal_cart_item_keys = array_keys($renewal_cart->cart_contents);
                $renewal_cart_item_key = array_shift($renewal_cart_item_keys);

                // Set renewal price
                $renewal_cart->cart_contents[$renewal_cart_item_key]['data']->price = Subscriptio_Subscription_Product::get_recurring_price($id);

                // Add shipping
                if ($product->needs_shipping() && $renewal_cart->needs_shipping()) {

                    // Get instance of checkout object to retrieve shipping options
                    $wc_checkout = WC_Checkout::instance();

                    // Iterate over shipping packages
                    foreach ($woocommerce->shipping->get_packages() as $package_key => $package) {

                        // Check if this rate was selected
                        if (isset($package['rates'][$wc_checkout->shipping_methods[$package_key]])) {

                            // Check if it contains current subscription
                            if (isset($package['contents'][$cart_item_key])) {

                                // Save shipping details for further calculation
                                $shipping_details = array(
                                    'shipping_method'   => $wc_checkout->shipping_methods[$package_key],
                                    'destination'       => $package['destination'],
                                );

                                // Save shipping address
                                $renewal_order['shipping_address'] = array(
                                    // First three lines may need to be changed to make this compatible with shipping extensions that allow multiple shipping addresses
                                    '_shipping_first_name'  => $wc_checkout->posted['ship_to_different_address'] ? $wc_checkout->posted['shipping_first_name'] : $wc_checkout->posted['billing_first_name'],
                                    '_shipping_last_name'   => $wc_checkout->posted['ship_to_different_address'] ? $wc_checkout->posted['shipping_last_name'] : $wc_checkout->posted['billing_last_name'],
                                    '_shipping_company'     => $wc_checkout->posted['ship_to_different_address'] ? $wc_checkout->posted['shipping_company'] : $wc_checkout->posted['billing_company'],
                                    '_shipping_address_1'   => $shipping_details['destination']['address'],
                                    '_shipping_address_2'   => $shipping_details['destination']['address_2'],
                                    '_shipping_city'        => $shipping_details['destination']['city'],
                                    '_shipping_state'       => $shipping_details['destination']['state'],
                                    '_shipping_postcode'    => $shipping_details['destination']['postcode'],
                                    '_shipping_country'     => $shipping_details['destination']['country'],
                                );

                                break;
                            }
                        }
                    }

                    // Got the shipping method and address for the package that contains current subscription?
                    if (!isset($shipping_details)) {
                        continue;
                    }

                    // Get packages based on renewal order details
                    $packages = apply_filters('woocommerce_cart_shipping_packages', array(
                        0 => array(
                            'contents'          => $renewal_cart->get_cart(),
                            'contents_cost'     => isset($renewal_cart->cart_contents[$renewal_cart_item_key]['line_total']) ? $renewal_cart->cart_contents[$renewal_cart_item_key]['line_total'] : 0,
                            'applied_coupons'   => $renewal_cart->applied_coupons,
                            'destination'       => $shipping_details['destination'],
                        ),
                    ));

                    // Now we need to calculate shipping costs but this requires overwriting session variables
                    // In order not to affect real cart, we will overwrite them but then set them back to original values
                    $original_session = array(
                        'chosen_shipping_methods'   => $woocommerce->session->get('chosen_shipping_methods'),
                        'shipping_method_counts'    => $woocommerce->session->get('shipping_method_counts'),
                    );

                    // Set fake renewal values
                    $woocommerce->session->set('chosen_shipping_methods', array($shipping_details['shipping_method']));
                    $woocommerce->session->set('shipping_method_counts', array(1));

                    // Override chosen shipping method in case there's a mismatch in shipping_method_counts (more than one available)
                    add_filter('woocommerce_shipping_chosen_method', array($this, 'set_shipping_chosen_method'));
                    $this->temp_shipping_chosen_method = $shipping_details['shipping_method'];

                    // Calculate shipping for fake renewal order now
                    $woocommerce->shipping->calculate_shipping($packages);

                    // Remove filter
                    remove_filter('woocommerce_shipping_chosen_method', array($this, 'set_shipping_chosen_method'));
                    $this->temp_shipping_chosen_method = null;
                }

                // Recalculate totals
                $renewal_cart->calculate_totals();

                // Get taxes
                foreach ($renewal_cart->get_tax_totals() as $rate_key => $rate) {
                    $renewal_order['taxes'][] = array(
                        'name'                  => $rate_key,
                        'rate_id'               => $rate->tax_rate_id,
                        'label'                 => $rate->label,
                        'compound'              => absint($rate->is_compound ? 1 : 0),
                        'tax_amount'            => wc_format_decimal(isset($renewal_cart->taxes[$rate->tax_rate_id]) ? $renewal_cart->taxes[$rate->tax_rate_id] : 0),
                        'shipping_tax_amount'   => wc_format_decimal(isset($renewal_cart->shipping_taxes[$rate->tax_rate_id]) ? $renewal_cart->shipping_taxes[$rate->tax_rate_id] : 0),
                    );
                }

                // Get renewal_order_shipping
                $renewal_order['renewal_order_shipping'] = wc_format_decimal($renewal_cart->shipping_total);

                // Get renewal_order_shipping_tax
                $renewal_order['renewal_order_shipping_tax'] = wc_format_decimal($renewal_cart->shipping_tax_total);

                // Get renewal_cart_discount
                $renewal_order['renewal_cart_discount'] = wc_format_decimal($renewal_cart->get_cart_discount_total());

                // Get renewal_order_discount
                $renewal_order['renewal_order_discount'] = wc_format_decimal($renewal_cart->get_order_discount_total());

                // Get renewal_order_tax
                $renewal_order['renewal_order_tax'] = wc_format_decimal($renewal_cart->tax_total);

                // Get renewal_order_total
                $renewal_order['renewal_order_total'] = wc_format_decimal($renewal_cart->total, get_option('woocommerce_price_num_decimals'));

                // Get renewal_line_subtotal
                $renewal_order['renewal_line_subtotal'] = wc_format_decimal($renewal_cart->cart_contents[$renewal_cart_item_key]['line_subtotal']);

                // Get renewal_line_subtotal_tax
                $renewal_order['renewal_line_subtotal_tax'] = wc_format_decimal($renewal_cart->cart_contents[$renewal_cart_item_key]['line_subtotal_tax']);

                // Get renewal_line_total
                $renewal_order['renewal_line_total'] = wc_format_decimal($renewal_cart->cart_contents[$renewal_cart_item_key]['line_total']);

                // Get renewal_line_tax
                $renewal_order['renewal_line_tax'] = wc_format_decimal($renewal_cart->cart_contents[$renewal_cart_item_key]['line_tax']);

                // Get shipping details
                if ($product->needs_shipping()) {

                    if (isset($woocommerce->shipping->packages[0]['rates'][$shipping_details['shipping_method']])) {

                        $method = $woocommerce->shipping->packages[0]['rates'][$shipping_details['shipping_method']];

                        $renewal_order['shipping'] = array(
                            'name'      => $method->label,
                            'method_id' => $method->id,
                            'cost'      => wc_format_decimal( $method->cost ),
                        );
                    }

                    // Set session variables to original values and recalculate shipping for original order which is being processed now
                    $woocommerce->session->set('chosen_shipping_methods', $original_session['chosen_shipping_methods']);
                    $woocommerce->session->set('shipping_method_counts', $original_session['shipping_method_counts']);
                    $woocommerce->shipping->calculate_shipping($packages);
                }

                // Save to object property so it can be accessed from another method
                $this->renewal_orders['by_cart_item_key'][$cart_item_key] = $renewal_order;
            }
        }
    }
All Usage Examples Of WC_Cart::calculate_totals
WC_Cart