TaxPart tax base type and issues with taxes
When adding a TaxPart to a Content Type it adds the possibility to set a custom tax rate to a content item. Though, the specific tax rate can only be an integer while it should be possible to set it as a decimal. As an example, here in Quebec / Canada the combined tax rate of GST/QST is 14.975%. Having the tax rate to be an integer doesn't allow me to set this tax rate.
VAT is a single tax but in come countries you need to have 2. One for the country and one for the province / state. Here in Quebec we have
GST = Global Sales Tax = 5% QST = Quebec Sales Tax = 9.975%
And to be able to calculate the proper sale tax you can do (Net Price * 14.975%) which is equivalent.
Else it is (Net Price + (Net Price * GST)) = Total GST (Total GST + (Total GST * QST)) = Total Combined Tax
Example:
1 + (1 * 5%) = 1.05$ 1.05 + (1.05 * 9.975%) = 1.15$
Also, in some countries this tax is not combined and calculated differently. So we need to have the option to set this Tax to be combined or not.
Also, the shopping cart only displays Gross and Net Prices. Nothing about the tax amount charged and also which tax. Right now if you want a tax to be applied you need to manually set it on the Content Item created with the TaxPart attached to the Content Type while it should pick up automatically the Tax based on user location as a default. The issue I see is that we currently can't sell these products outside of Canada because it won't pickup any sales tax from the custom Tax table. We would need to create different content items for each countries where we want to sell these which doesn't make sense.
To make this easier here is a calculator:
https://www.canada.ca/en/revenue-agency/services/tax/businesses/topics/gst-hst-businesses/charge-collect-which-rate/calculator.html
So, to make this more complicated. Some of these provinces uses an harmonized sales tax called HST. But others uses a combined tax.
Though, the specific tax rate can only be an integer while it should be possible to set it as a decimal.
Under the hood TaxPart.TaxRate is simply a NumericField with little configuration (only the minimum and maximum values are set). Any integer limitation is unintentional. For some reason Orchard Core expects you to set the number of decimal digits in the field settings and it's zero by default. This was news to me, but apparently it's easy to change and on the back end we already treat the tax rate as a decimal.
We could add this to the TaxPartMigrations, although I'm not sure what would be the most appropriate "Scale" value.
VAT is a single tax but in some countries you need to have 2. One for the country and one for the province / state. [...] Also, in some countries this tax is not combined and calculated differently. So we need to have the option to set this Tax to be combined or not.
Since OCC doesn't have an invoicing or accounting systems (yet), I don't think we need to worry about this distinction right now. I don't really understand what you mean by "calculated differently", can you give a specific example?
Also, the shopping cart only displays Gross and Net Prices. Nothing about the tax amount charged and also which tax.
Could you elaborate why would that be desirable for the buyer?
Right now if you want a tax to be applied you need to manually set it on the Content Item created with the TaxPart attached to the Content Type while it should pick up automatically the Tax based on user location as a default. The issue I see is that we currently can't sell these products outside of Canada because it won't pickup any sales tax from the custom Tax table
The TaxPart.TaxRate is the simplest possible implementation. As mentioned in the documentation, it's only suitable for stores that only ship locally.. On the other hand, the Custom Tax Rates feature already uses the user location for picking a rate. You should not fill the Tax Rate field in the product, if you want to use custom tax rates. I don't remember 100%, but I think if you set the Tax Rate, that takes precedence over the custom tax rates table. You should definitely not mix the two.
While the checkout is not an invoice ; when I do a purchase as a company I need to know how much tax I will pay on a purchase. It also gives me a hint if my account is properly set and if I will be charged the proper tax.
Also, when I send a charge to Stripe it does not create an invoice with tax details either. So, we I need to be able to receive a summary of that purchase with tax details. Right now I added this on the "Success" page that shows after the Stripe charge successfully processed. And I added a print button to that page. It is common to see these things on that page.
Here is an example of checkout from Newegg.ca website:
And again, for me, just using the custom tax table did not add any tax to the checkout. The Gross Price doesn't change. I'm forced to use the TaxPart on each content items.
So, if the goal of the TaxPart was to add the ability to charge no tax on specific products that can't have tax added. Why not just have a boolean? Or, is it possible that a tax is different per product in a country?
Like milk = 5% and bread = 10% ? I've never seen such a thing. I'm questioning the need of that TaxPart unless a site would need the ability to have the same product multiple times like
Content Item 1 = milk for Hungary = 5% with tax code set to VAT Content Item 2 = milk for Canada = 0% with tax code set to HST
Which is strange. I would rather create 2 different tenants for these 2 shops. Then, the TaxPart, I really don't understand it. It makes things even more confusing to me. Why not simply document how to use the Custom Tax Rates?
The Product Part itself could have a "Is Taxable" boolean field that could tell when a product can be charged with a tax or not. That makes more sense if that was the intent of that TaxPart. I just think that most people may do the same error that I did. Thinking it is necessary to add it to the Content Type so that taxes gets applied to it to the shopping cart.
And again, for me, just using the custom tax table did not add any tax to the checkout. The Gross Price doesn't change. I'm forced to use the TaxPart on each content items.
This sounds like a bug, sounds like the Custom Tax Rates feature doesn't work for you at all. Please create a separate issue for that.
So, if the goal of the TaxPart was to add the ability to charge no tax on specific products that can't have tax added. Why not just have a boolean? Or, is it possible that a tax is different per product in a country? Like milk = 5% and bread = 10% ? I've never seen such a thing.
Yes, that kind. Such VAT exceptions are actually fairly common in the EU. TaxPart contains a tax code field which can be used to identify product classes (e.g. different VAT ranges) as a condition in the custom tax ranges table next to the buyer address.
For example, in Hungary egg is 5%, flour is 18%, but full cake is 27% VAT (yes, really). From what I can gather, Canada also has GST exemptions but mostly just for farm supplies and livestock, not for consumer goods.
A store in the EU could put the Union Customs Codes or something similar into their Tax Code field and then specify multiple tax rates for a country in the Custom Tax Rate table. This way there is no need to create multiple content items for a single product.
I would rather create 2 different tenants for these 2 shops.
That's only a valid approach if you don't want to use inventory tracking. I'd avoid that just like I'd avoid multiple content items for the same product in a single tenant. Tracking inventory or applying individual product discounts becomes exponentially more complicated once you break the equivalence between content item and SKU.
Why not simply document how to use the Custom Tax Rates?
It's already documented how to use it, as I've linked in this thread above. For reference, here it is again. If you think the documentation is confusing or unclear, that's fair. Let's discuss that in a separate issue.
Ok, so all in all. After retesting everything. The custom tax table works. Sorry about that. The issue is that it was only displaying Net Price in the checkout which I fixed by adding the applied tax on a subtotal line. That's the confusion I had and it is why I think showing the applied tax/taxes to the checkout would make sense. Also what made this even more confusing is that the charges are displaying the net price on the success page. Which is not the amount that was charged to the client in Stripe. So I thought that the taxes of the custom table were not applied because of these different confusing elements.
So, basically, what you are saying is that the TaxPart should be avoided if you want to sell your products in different locations. And it can be replaced by the custom tax table. So, this is why I think the TaxPart is a redundancy unless it adds functionality that the custom tax table can't do. Question is, do we really need this TaxPart in the system? That's the nuance that I don't get I guess. And what I meant about documentation is: If the TaxPart can be replaced by custom tax table then we should document that.
There is no issue to fix other than decide if we show the Tax/Taxes on the checkout and success pages. And also, if we would want to be compliant with accounting systems we should add the ability to have "combined" taxes as I explained. This way we can show the federal and provincial taxes on 2 separate lines but they need to be calculated as "combined". I believe that in some place these taxes are not combined. You would calculate (net price + (net price * tax 1) ) + (net price + (net price * tax 2) ) separately if not combined.
Also, on a different topic. As you can see on my screenshot. The custom attribute "Rental Date" is a custom Date Range Attribute I added for this website. We could add one in OCC by default as it is really common to want to rent products for a period of time. Also, this custom Attribute shows the technical name of the attribute while we should be able to show the "Display Text" of that attribute and to be able to translate it with a dynamical translation because it would be a T[somevar].
Ok, so to explain the issue. When I add a TaxPart to a content type. I'm forced by validation to enter a value and to choose a tax code else I don't get passed form validation. So, I cannot set the TaxPart form values to empty value. Which made me remove the TaxPart on my Content Types.
The reasons why I removed it are:
- It is not necessary for the type of products we are selling.
- I don't want users to need to calculate a Gross Price out of the Net Price. I don't want users to pick a custom Tax Code either.
- I just want to enter a Net Price on my products and let the system calculate taxes automatically.
So, I reopened this issue because I think we need to handle the use case where the TaxPart is not associated to a Content Type. This would be an easy fix. I don't think having a TaxPart should be necessary for being able to calculate tax on a product. Else, I need to understand why it is necessary.
Thanks. 😄
Ok, so I tested the TaxPart on latest main branch and it seems that the form validation error doesn't repro. That's a good thing.
So here is my theme working with the latest main branch. You can see I added the Test Product to the shopping cart. Though, when I try to add the Localized Product (which doesn't have a TaxPart). It fails to add the product to the cart with a NRE.
Sorry for the late reply, @Skrypt. That's invalid and generally unsafe. You can't mix products with or without tax part. If you use the latest version from git or Cloudsmith (after #708), you can create/update a product with 0 in the tax rate and no gross price specified. Do that, if you actually want to intentionally declare the product tax free.
https://github.com/user-attachments/assets/0a2052ae-a8be-4087-8b40-e6b1a28e5fea
Also CC @canadacubachina.
should we really need AllOrNoneAsync()?
I have made the fix for myself. I simply removed the need for the TaxPart. It doesn't make sense to me that the it throws an exception like it does right now. If the TaxPart becomes mandatory when someone starts using it then it should be better integrated somehow. I'm sure there's a way to still apply taxes from the custom rate table or simply apply no tax to that product with a fallback to the Product Price field. It should be equivalent than setting 0% tax to a TaxPart.
Sorry for the late reply, @Skrypt. That's invalid and generally unsafe. You can't mix products with or without tax part. If you use the latest version from git or Cloudsmith (after #708), you can create/update a product with 0 in the tax rate and no gross price specified. Do that, if you actually want to intentionally declare the product tax free.
screen-recording.webm Also CC @canadacubachina.
Followed this logic for Test Free Product, set Gross Price to 0 ( I have to I think), got the following Error
Exception has occurred: CLR/System.DivideByZeroException
*
An exception of type 'System.DivideByZeroException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'Attempted to divide by zero.'
at System.Decimal.DecCalc.VarDecDiv(DecCalc& d1, DecCalc& d2)
at System.Decimal.op_Division(Decimal d1, Decimal d2)
at OrchardCore.Commerce.Events.PromotionShoppingCartEvents.<DisplayingAsync>d__6.MoveNext() in /Users/admin/Documents/Orchard_server/OC_Commerce_Modules/OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs:line 84
at OrchardCore.Commerce.Services.ShoppingCartHelpers.<CreateShoppingCartViewModelAsync>d__12.MoveNext() in /Users/admin/Documents/Orchard_server/OC_Commerce_Modules/OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs:line 115
at OrchardCore.Commerce.Services.ShoppingCartHelpers.<EstimateProductAsync>d__20.MoveNext() in /Users/admin/Documents/Orchard_server/OC_Commerce_Modules/OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs:line 217
at OrchardCore.Commerce.Drivers.DiscountPartDisplayDriver.StoredDiscountPartDisplayDriver.<DisplayAsync>d__3.MoveNext() in /Users/admin/Documents/Orchard_server/OC_Commerce_Modules/OrchardCore.Commerce/src/Modules/OrchardCore.Commerce/Drivers/DiscountPartDisplayDriver.cs:line 79
at OrchardCore.ContentManagement.Display.ContentDisplay.ContentPartDisplayDriver`1.<OrchardCore-ContentManagement-Display-ContentDisplay-IContentPartDisplayDriver-BuildDisplayAsync>d__4.MoveNext()
at OrchardCore.ContentManagement.Display.ContentItemDisplayCoordinator.<BuildDisplayAsync>d__7.MoveNext()
I fixed all of these for myself. I don't think the Taxpart is necessary to define a 0% taxed product. If the product doesn't have a Taxpart attached to it then it doesn't have any tax applied to it. That's the logic I went for. Then, of course, OCC has these exceptions that most lambda users don't really appreciate ; so I needed to fix it by using a Null check on the Taxpart simply.
Followed this logic for Test Free Product, set Gross Price to 0 ( I have to I think), got the following Error
@infofromca I couldn't reproduce your error, but based on the exception the fix in the PR #711 should work for you. Can you check?
should we really need AllOrNoneAsync()?
It may be necessary for compliance/liability depending on region. I have added a site setting to opt out of the check in #711. Go to Admin > Configuration > Commerce > Tax and tick the "Skip consistent applicability check" check box.
Also you can always customize your own ITaxProvider implementation by copying LocalTaxProvider.cs into a new class and then replacing the original in your Startup file.
services.RemoveByImplementation<TaxRateTaxProvider>();
services.AddScoped<ITaxProvider, MyTaxProvider>();
after reviewing the code, I guess this is good way to resolve the issue, although It is a little difficult for me to test on another branch beyond Main. I will test it after I pull it from Main.
@sarahelsaig would you like to merge it to Main?