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.
Unless someone else comes across this question and knows a proper answer, it seems that Magento does not store the rates inside tax/calculation_rule
but instead in tax/calculation
table. When a rule's rates are updated in the backend, Magento will natively drop all the rows in the table associated with the rule and create new ones. This is evidenced by the fact that each row's ID value is auto-incremented, even if none of the values of that row changed.
My goal was to have new rates created dynamically and then added to an existing rule. I have opted to just create a new rule dynamically also. It's going to create more overhead in the DB this way (each tax rate has its own tax rule), but I don't see a clean way around that.
Best Answer
Since you've referenced a previous question of mine, I feel I should respond.
In the research I've done on modifying tax rates based on various factors (in my case, mostly an API call to a tax rate provider to discover a user's current/accurate tax rate) it seems safest to me to get out of Magento's way when possible.
By this, I mean that modifying core processes to change how tax is calculated is going to give you all sorts of fits and it may be less cumbersome and cleaner overall to instead create your own tax rates/rules dynamically based on the factors you require.
In my case it ended up being much cleaner to observe the same
sales_quote_collect_totals_before
event and simply determine if an existing rate/rule has been created and can be used based on current context (in my case, postal codes). If it is, then use it, if not, then let's create a new tax rate/rule programmatically which is then available to be used duringcollectTotals()
.When I say "much cleaner", the main things are what Magento will already do with your tax rates if they exist as expected. If I was trying to set the tax rate itself, then you have additional areas you need to cover so that these values display properly during checkout. In contrast, simply creating a new tax rate/rule dynamically removes this constraint and all the other areas of Magento (frontend or backend) that display/modify/use these rates will function normally as if nothing changed.
Again, just to reiterate, I did try doing this both ways, using
$item->setTaxAmount()
as well as programmatically create tax rules. The module where I was modifying the item/quote's tax was much more complex and prone to error (especially front-end display issues). In the end I decided my method was better simply because we are dealing with monetary values that the customer is required to pay. I would rather have a server process that was a little "heavier" but did things correctly than one that was prone to error but was more obvious in its implementation. I cannot accept losing a customer because they were confused by the taxes during checkout.