Repository: saleor/saleor

Issue:
Bug: Draft order uses incorrect tax rate for lines on changing order's country
### What are you trying to achieve?

Create a draft order with taxes for one country, and changing the country to another one, and have the taxes correctly recalculated with a new rate.

### Steps to reproduce the problem

The easiest way to recreate the issue is be using Saleor Dashboard:
1. Create channel A with the default country e.g. Germany.
2. Enable flat rates in channel A; create a tax class; configure tax rates for Germany by setting some value for the tax class, and leaving the "Country default rate" empty.
3. Add tax configuration for some other country, e.g. Poland, and provide tax rates for both the tax class as well as the "Country default rate".
4. Create a draft order without an address in channel A.
5. Add an order line with a product without a tax class (also product type cannot have a tax class). Taxes should be 0, since there is no default country rate or tax class.
6. Set order addresses to some valid addresses from Poland. Default rate for Poland is set and Saleor should recalculate taxes with the Polish rate. Currently, it doesn't happen and taxes stay 0.

### What did you expect to happen?

Taxes should be correctly recalculated when changing addresses in draft order.

### Logs

_No response_

### Environment

Saleor version: 3.12.22



Discussion:

Resolving patch:
diff --git a/saleor/tax/calculations/order.py b/saleor/tax/calculations/order.py
--- a/saleor/tax/calculations/order.py
+++ b/saleor/tax/calculations/order.py
@@ -51,10 +51,13 @@ def update_order_prices_with_flat_rates(
             default_tax_rate,
             country_code,
         )
-    elif order.shipping_tax_rate is not None:
-        # Use order.shipping_tax_rate if it was ever set before (it's non-null now).
-        # This is a valid case when recalculating shipping price and the tax class is
-        # null, because it was removed from the system.
+    elif (
+        order.shipping_tax_class_name is not None
+        and order.shipping_tax_rate is not None
+    ):
+        # Use order.shipping_tax_rate if it was ever set before (it's non-null now and
+        # the name is non-null). This is a valid case when recalculating shipping price
+        # and the tax class is null, because it was removed from the system.
         shipping_tax_rate = denormalize_tax_rate_from_db(order.shipping_tax_rate)
     else:
         shipping_tax_rate = default_tax_rate
@@ -135,10 +138,11 @@ def update_taxes_for_order_lines(
                 default_tax_rate,
                 country_code,
             )
-        elif line.tax_rate is not None:
-            # line.tax_class can be None when the tax class was removed from DB. In
-            # this case try to use line.tax_rate which stores the denormalized tax rate
-            # value that was originally used.
+        elif line.tax_class_name is not None and line.tax_rate is not None:
+            # If tax_class is None but tax_class_name is set, the tax class was set
+            # for this line before, but is now removed from the system. In this case
+            # try to use line.tax_rate which stores the denormalized tax rate value
+            # that was originally provided by the tax class.
             tax_rate = denormalize_tax_rate_from_db(line.tax_rate)
         else:
             tax_rate = default_tax_rate


Test patch:
diff --git a/saleor/tax/tests/test_order_calculations.py b/saleor/tax/tests/test_order_calculations.py
--- a/saleor/tax/tests/test_order_calculations.py
+++ b/saleor/tax/tests/test_order_calculations.py
@@ -7,7 +7,10 @@
 from ...core.prices import quantize_price
 from ...core.taxes import zero_money, zero_taxed_money
 from ...discount import DiscountValueType, OrderDiscountType
+from ...order import OrderStatus
+from ...order.calculations import fetch_order_prices_if_expired
 from ...order.utils import get_order_country
+from ...plugins.manager import get_plugins_manager
 from .. import TaxCalculationStrategy
 from ..calculations.order import (
     update_order_prices_with_flat_rates,
@@ -627,3 +630,59 @@ def test_use_original_tax_rate_when_tax_class_is_removed_from_order_line(
     assert order.total == TaxedMoney(
         net=Money("65.04", "USD"), gross=Money("80.00", "USD")
     )
+
+
+def test_use_default_country_rate_when_no_tax_class_was_set_before(
+    order_with_lines,
+):
+    # given
+    manager = get_plugins_manager()
+    order = order_with_lines
+    country = get_order_country(order)
+    TaxClassCountryRate.objects.create(country=country, rate=20)
+
+    prices_entered_with_tax = True
+    _enable_flat_rates(order, prices_entered_with_tax)
+    lines = order.lines.all()
+
+    # drop tax classes from lines and shipping, so that default country rate is used
+    for line in lines:
+        line.tax_class = None
+        line.tax_rate = Decimal("0")
+        line.tax_class_name = None
+        line.save(
+            update_fields=[
+                "tax_rate",
+                "tax_class",
+                "tax_class_name",
+            ]
+        )
+
+    order.shipping_method.tax_class.delete()
+    order.shipping_tax_class = None
+    order.shipping_tax_class_name = None
+    order.shipping_tax_rate = Decimal("0")
+    order.status = OrderStatus.DRAFT
+    order.save(
+        update_fields=[
+            "status",
+            "shipping_tax_class_name",
+            "shipping_tax_class",
+            "shipping_tax_rate",
+        ]
+    )
+    order.refresh_from_db()
+
+    # when
+    fetch_order_prices_if_expired(order, manager, force_update=True)
+    order.refresh_from_db()
+
+    # then
+    line = order.lines.first()
+    assert line.tax_rate == Decimal("0.20")
+    assert not line.tax_class
+    assert not line.tax_class_name
+
+    assert order.shipping_tax_rate == Decimal("0.20")
+    assert not order.shipping_tax_class
+    assert not order.shipping_tax_class_name