Search code examples
phpwordpresswoocommercefilteringcart

Woocommerce - Can I intercept the Line Price (Cart) calculation?


So for example, I have a product which costs £2/item if you buy singles or £1/item if you buy it in packs of 6.

Customers enter their quantity required, let's say 7, and then add it to the basket.

The total cost should be £8. £6 (1 pack) + £2 (1 single). But if I add it to the basket currently the price will come out as £7 as you'd expect (it just does 7 x the price set which is £1).

I need to intercept/filter the line price so that it calculates the number of packs, and the number of singles, and then returns the right amount. I can design the function and the math, I just need to know where to hook into.

i.e. is there a filter like woocommerce_get_cart or something similar? And if so, how do I alter the line price?

(p.s. I know I could achieve the with variable products for instance but that's not an option for stock control reasons).


Solution

  •     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();
    
            /**
             * Calculate subtotals for items. This is done first so that discount logic can use the values.
             */
            foreach ( $this->get_cart() as $cart_item_key => $values ) {
    
                $_product = $values['data'];
    
                // Count items + weight
                $this->cart_contents_weight += $_product->get_weight() * $values['quantity'];
                $this->cart_contents_count  += $values['quantity'];
    
                // Prices
                $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->tax_class ] ) ) {
                        $shop_tax_rates[ $_product->tax_class ] = WC_Tax::get_base_tax_rates( $_product->tax_class );
                    }
    
                    // 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->tax_class ];
                    $item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
    
                    /**
                     * ADJUST TAX - Calculations when base tax is not equal to the item tax
                     */
                    if ( $item_tax_rates !== $base_tax_rates ) {
    
                        // 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;
            }
    
            /**
             * Calculate totals for items
             */
            foreach ( $this->get_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            = WC_Tax::round( $discounted_price * $values['quantity'] );
    
                /**
                 * Prices include tax
                 */
                } elseif ( $this->prices_include_tax ) {
    
                    $base_tax_rates = $shop_tax_rates[ $_product->tax_class ];
                    $item_tax_rates = $tax_rates[ $_product->get_tax_class() ];
    
                    /**
                     * ADJUST TAX - Calculations when base tax is not equal to the item tax
                     */
                    if ( $item_tax_rates !== $base_tax_rates ) {
    
                        // 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_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
                        $discounted_price  = $this->get_discounted_price( $values, $adjusted_price, true );
                        $discounted_taxes  = WC_Tax::calc_tax( $discounted_price * $values['quantity'], $item_tax_rates, true );
                        $line_tax          = array_sum( $discounted_taxes );
                        $line_total        = ( $discounted_price * $values['quantity'] ) - $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 );
                        $discounted_taxes = WC_Tax::calc_tax( $discounted_price * $values['quantity'], $item_tax_rates, true );
                        $line_tax         = array_sum( $discounted_taxes );
                        $line_total       = ( $discounted_price * $values['quantity'] ) - $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
                $this->cart_contents[ $cart_item_key ]['line_total']        = $line_total;
                $this->cart_contents[ $cart_item_key ]['line_tax']          = $line_tax;
                $this->cart_contents[ $cart_item_key ]['line_subtotal']     = $line_subtotal;
                $this->cart_contents[ $cart_item_key ]['line_subtotal_tax'] = $line_subtotal_tax;
    
                // Store rates ID and costs - Since 2.2
                $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->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->is_vat_exempt() ) {
                    $this->remove_taxes();
                }
            }
    
            do_action( 'woocommerce_after_calculate_totals', $this );
    
            $this->set_session();
        }
    

    this is the original total calculations on WooCommerce.

    near the bottom you have:

        // Allow plugins to hook and alter totals before final total is calculated
        do_action( 'woocommerce_calculate_totals', $this );
    

    of which I believe you can use to do something like this:

        add_action('woocommerce_calculate_totals','my_woocommerce_calculate_totals');
        function my_woocommerce_calculate_totals($cart) {
             // get some hints from the code above and do something like foreach ( $cart->get_cart() as $cart_item_key => $values ) { }
             // do your math, then set $cart->cart_contents_total
        }