32

I have a category of products which (legally) need to have their tax rate changed when you are ordering more than a certain quantity. I have extended the various tax models to get this working when you add a new product to the cart, but I am having problems when the user updates the quantities in the cart or adds additional products which tip the quantities already in the cart over the threshold amount.

Problem 1:

First of all, I'm not 100% which event(s) to observe. I've tried the following;

checkout_cart_save_after (based on this -> https://stackoverflow.com/questions/14362702/magento-programatically-update-cart-via-event)

checkout_cart_update_items_after (based on this -> https://stackoverflow.com/questions/5104482/programmatically-add-product-to-cart-with-price-change)

sales_quote_save_before (based on this -> https://stackoverflow.com/questions/7638858/magento-recalculate-cart-total-in-observer)

Problem 2:

I am able to access the quote items from the cart, there are loads of ways to do that it seems. I am also able to iterate through the individual items in the cart, update properties of those items and then save the items (at least temporarily). However, I am not then able to save the quote and recalculate the taxes in the checkout.

Part of the reason is that whilst I can access the cart quote, I am not sure which method to use to be able to write to it.

What I Have Tried:

What I've tried in terms of accessing the contents of the cart has depended on the event I've observed but I've tried all of the following;

1. 
$item = $observer->getQuoteItem;

2.
$cart = Mage::getSingleton('checkout/cart');
$cartItems = $cart->getCart()->getItems(); 

3.
$cart = $observer->getData('cart');
$quote = $cart->getData('quote');
$cartItems = $quote->getAllVisibleItems();

4.
$cartHelper = Mage::helper('checkout/cart');
$cartItems = $cartHelper->getCart()->getItems(); 

5.
$quote = Mage::getModel('checkout/cart');
$cartItems = $quote->getItems(); 

The one that seems to be at least allowing me to access the quote, run through them and update the items is

6.
$quote = Mage::getSingleton('checkout/session')->getQuote();
$cartItems = $quote->getAllVisibleItems();

This allows me to update each quote item when I iterate through (I believe using magic setters as I can't find any corresponding methods). I was hoping to be able to update the Tax Class ID for the quote item and then recalculate the taxes. If I use the following (where $taxClassId is different to the one already used by each quote item);

$item->setTaxClassId( $taxClassId );
$item->getProduct()->setIsSuperMode(true);
$item->save;

And then log the results;

Mage::log($item->debug(), null,'taxobserver.log', true);

It shows that I have indeed updated this quote item and changed the tax ID. However, if I then follow through and try to save the modified quote;

$quote->setTotalsCollectedFlag(false)->collectTotals();
$quote->save();     

And then debug again;

Mage::log($item->debug(), null,'taxobserver.log', true);

My changes have not been saved, the quote item change has been reset and the cart totals are not recalculated. Starting to wonder if finding a tall building to jump off might be the solution for this one.

1

4 Answers 4

35
+100

My suggestion would be to put an observer on the sales_quote_collect_totals_before event which is fired in the Mage_Sales_Model_Quote::collectTotals method before it starts the total collection process. Then from inside this observer method, iterate the quote items and change the tax class on the (already loaded) product object you can retrieve from the quote item.

After you set the information on the product object, whatever you do, DO NOT try and save it to the database. Having the tax class set as needed on the product object in memory will be good enough to have the collect totals logic found in Mage_Tax_Model_Sales_Total_Quote_Tax pickup which tax class it should base it's calculations on. Saving the product (as you seem to be trying to do in your code sample above) will cause major performance issues, will create race conditions in the calculation process, and is simply not good practice.

The reason that the events you are trying to work with are not enabling you to accomplish what you are trying to accomplish is because they all come after the total calculation, a process which is only be run once prior to saving the quote.

Worth pointing out about the collect totals process is that once run, without doing extra work, you cannot call it again to have it re-calculate based on changes you've made to the quote items. See this tie-bit I've taken from the blog series a colleage of mine recently put together on the collect totals process:

Now that you understand what occurs during the totals collection process, you may find it convenient or necessary to call it directly yourself. Before you start feeling too confident with using collectTotals for your own purposes, though, keep the following rule in mind:

Products cannot be added to the quote after collectTotals is run!

. . . unless the quote addresses' item caches are cleared.

Nearly every total model's "collect" method relies on fetching the quote items from the address and looping through them. The first time getAllItems is run on a quote address, the item collection is actually cached with a unique key, and it's this cached collection that is returned on subsequent calls.

If you do happen to have an inkling for really diving into the depths of how the collect totals process works, you can check out the first of the four part series on total collection here for more in-depth reading: Unravelling Magento's collectTotals: Introduction

To summarize, you need to be catching an event which runs before the collect totals process (and before getAllItems is called on the quote addresses) so that changes you make to the items will be used by the total collectors. I've not verified that the suggested sales_quote_collect_totals_before event runs before any calls to the getAllItems on the quote address, but I'm almost certain that it will work for what you need. But if not, hopefully I've provided enough context for you to figure out which event you need to catch to make it work.

3
  • This answer is well beyond what I'd hoped for. Brilliant, thanks for taking the time. Fantastic answer.
    – McNab
    Commented Jul 8, 2013 at 20:21
  • Argh! After spending all day on this and not getting it to work I I finally realised that it was down to a badly rewritten model elsewhere in the module. This works perfectly and I've learned loads from it, thanks again David.
    – McNab
    Commented Jul 11, 2013 at 22:20
  • @McNab Glad to hear you got it working. Definitely one of the more complex areas of Magento being dealt with. :)
    – davidalger
    Commented Jul 12, 2013 at 1:52
6

There is another event: sales_quote_item_set_product in Mage_Sales_Model_Quote_Item::setProduct

Mage::dispatchEvent('sales_quote_item_set_product', array(
            'product' => $product,
            'quote_item'=>$this
        ));

You can modify the product tax class using this event. Use the quoteItem getQty method to find the qty added to the cart.

4
  • Thanks for this, it is useful to know - I realise based on @davidalger's answer that I shouldn't be modifying the product's tax class now, but the quote model. Good to know though.
    – McNab
    Commented Jul 8, 2013 at 20:18
  • Well, you can actually modify the quote item tax class, you also have access in the event to the quoteItem object. Commented Jul 9, 2013 at 6:18
  • Ah ok - I misunderstood. Thanks for clarifying :)
    – McNab
    Commented Jul 9, 2013 at 7:34
  • @PandaWebStudio, how to set custom tax amount for quote item? here is my question,magento.stackexchange.com/questions/274520/… Commented May 15, 2019 at 7:01
2

Maybe trying to change the taxclass might not be the best approach. Why not create a similar product for those products that use the higher tax class and set a minimal qty in cart for that product. Then use the checkout_cart_update_items_after to swap products around based on the total qty in cart?

This is definitely not a 'best practice' but it might help you think in another direction.

Alternatively try setting up something with http://www.mageworx.com/multi-fees-magento-extension.html where you add a 'fee' for the extra tax. This might actually be a nicer way of doing it but it wont show up in the tax totals, more as an extra order total line.

4
  • Thanks for the reply Sander. It's a good idea and I hadn't thought of it. I'll tell you why I don't fancy that - the underlying products are all running though Unirgy's uMarketplace suite and it's been a monster of a task to get it to where it is as it's running as a price comparison site. I think that may be a big job. If I can't get this quote item updated I will have to look at it though.
    – McNab
    Commented Jul 8, 2013 at 10:56
  • Damn, I can't even upvote your answer as I've spent all my rep on this bounty! +1 :) Will upvote when I get some more.
    – McNab
    Commented Jul 8, 2013 at 11:00
  • haha no problem, I understand the issue and this wouldn't be a good solution for the case. I'd probably go with the multifees then, that's a 'cleaner' solution Commented Jul 8, 2013 at 11:03
  • FWIW, I would not suggest this route… if only because it would make accounting for sales and tracking inventory a potential nightmare. As the OP said, not best practice, but I'll add: It'll cause more troubles than it would solve.
    – davidalger
    Commented Jul 8, 2013 at 17:58
1

Use this <sales_quote_collect_totals_before>

and in your function do like

 public function quoteCollectTotalsBefore(Varien_Event_Observer $observer)
    $quote = $observer->getQuote();
    foreach ($quote->getAllItems() as $item)
    {
        $product = $item->getProduct();
        $product->setTaxClassId($product_tax_class_id);
    }
 }

I hope this will work definetely

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.