Skip to content
This repository was archived by the owner on Jul 6, 2023. It is now read-only.

Commit 6b5a330

Browse files
committed
Rewrite plugin for Saleor 3.14
Over the past year several changes were made to Saleor, including: - dropping the vatlayer plugin - changing the behavior of plugin methods These changes require a rewrite of most of the plugin, which hopefully still retains the same functionality. For the plugin to work correctly, it's necessary to use the "TAX_APP" strategy in the tax setting of Saleor. Using TAX_APP delegates tax calculation to our plugin, however, unless the user has a valid VATIN, we actually use the update_checkout_prices_with_flat_rates method from the FLAT_RATES taxCalculationStrategy. This way we can: - reverse charge VAT for EU businesses - charge VAT for everyone else, according to their country Special thanks to Sergey for the support while going through this.
1 parent 8182bfe commit 6b5a330

File tree

3 files changed

+36
-101
lines changed

3 files changed

+36
-101
lines changed

saleor_vatrc/plugin.py

+30-90
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
from decimal import Decimal
2-
from typing import TYPE_CHECKING, List, Dict, Iterable, Optional, Union, Tuple
2+
from typing import TYPE_CHECKING, List, Optional, Union, Tuple
33
import logging
44

55
from stdnum.eu import vat
66
import stdnum.exceptions
77

8-
from prices import TaxedMoney, TaxedMoneyRange
9-
from saleor.checkout.interface import CheckoutTaxedPricesData
10-
from saleor.core.taxes import include_taxes_in_prices
8+
from prices import TaxedMoney, TaxedMoneyRange, Money
119
from saleor.order.interface import OrderTaxedPricesData
1210
from saleor.plugins.base_plugin import BasePlugin
13-
from saleor.plugins.manager import get_plugins_manager
11+
from saleor.tax.calculations.checkout import update_checkout_prices_with_flat_rates
1412

1513

1614
if TYPE_CHECKING:
1715
from saleor.account.models import Address
1816
from saleor.checkout.fetch import CheckoutInfo, CheckoutLineInfo
1917
from saleor.checkout.models import Checkout
20-
from saleor.discount import DiscountInfo
21-
from saleor.plugins.models import PluginConfiguration
2218

2319

2420
logger = logging.getLogger(__name__)
@@ -42,6 +38,7 @@ class VatReverseCharge(BasePlugin):
4238
Read more at
4339
https://europa.eu/youreurope/business/taxation/vat/cross-border-vat/index_en.htm#withintheeu
4440
"""
41+
4542
PLUGIN_ID = "blender.saleor_vatrc"
4643
PLUGIN_NAME = "VAT reverse charge"
4744
PLUGIN_DESCRIPTION = (
@@ -51,46 +48,23 @@ class VatReverseCharge(BasePlugin):
5148
META_VATIN_KEY = "vatrc.vatin"
5249
META_VATIN_VALIDATED_KEY = "vatrc.vatin_validated"
5350

54-
# This plugin depends on another plugin for calculating VAT
55-
VAT_PLUGIN_ID = 'mirumee.taxes.vatlayer'
56-
5751
CONFIGURATION_PER_CHANNEL = True
5852
DEFAULT_ACTIVE = False
5953

60-
@property
61-
def vat_plugin(self) -> "PluginConfiguration":
62-
manager = get_plugins_manager()
63-
plugins = manager.plugins_per_channel[self.channel.slug]
64-
return next((p for p in plugins if p.PLUGIN_ID == self.VAT_PLUGIN_ID), None)
65-
66-
@property
67-
def vat_plugin_config(self) -> Dict[str, str]:
68-
# Convert to dict to easier get config elements of the plugin we depend on
69-
return {
70-
item["name"]: item["value"] for item in self.vat_plugin.configuration
71-
}
72-
7354
def _skip_plugin(
7455
self,
7556
checkout_info: "CheckoutInfo",
7657
previous_value: Union[
7758
TaxedMoney,
7859
TaxedMoneyRange,
7960
Decimal,
80-
CheckoutTaxedPricesData,
8161
OrderTaxedPricesData,
8262
],
8363
) -> bool:
8464
if not self.active:
8565
return True
8666

87-
# Skip when plugin that calculates VAT isn't active
88-
if not self.vat_plugin.active:
89-
return True
90-
91-
# If taxes aren't included into prices, there's no reverse charge to apply.
92-
if not include_taxes_in_prices():
93-
return True
67+
return False
9468

9569
def _skip_price_modification(
9670
self,
@@ -99,28 +73,21 @@ def _skip_price_modification(
9973
TaxedMoney,
10074
TaxedMoneyRange,
10175
Decimal,
102-
CheckoutTaxedPricesData,
10376
OrderTaxedPricesData,
10477
],
10578
) -> bool:
10679
# If there's no tax on the given prices
10780
if isinstance(previous_value, TaxedMoney):
10881
return previous_value.net == previous_value.gross
109-
if isinstance(previous_value, CheckoutTaxedPricesData):
110-
return (
111-
previous_value.price_with_sale.net
112-
== previous_value.price_with_sale.gross
113-
)
11482
if isinstance(previous_value, OrderTaxedPricesData):
11583
return (
116-
previous_value.price_with_discounts.net
117-
== previous_value.price_with_discounts.gross
84+
previous_value.price_with_discounts.net == previous_value.price_with_discounts.gross
11885
)
11986

12087
return False
12188

122-
def _get_seller_country_code(self) -> str:
123-
return self.vat_plugin_config.get("origin_country", "").upper()
89+
def _get_seller_country_code(self, checkout) -> str:
90+
return checkout.channel.default_country.code
12491

12592
def _get_buyer_country_code(self, address: Optional["Address"]) -> str:
12693
return address.country.code if address else ""
@@ -154,8 +121,10 @@ def _validate_vatin_metadata(
154121
checkout: "Checkout",
155122
address: Optional["Address"],
156123
) -> bool:
157-
vatin_metadata_value = checkout.get_value_from_metadata(self.META_VATIN_KEY)
158-
valid_vatin_previous = checkout.get_value_from_metadata(
124+
vatin_metadata_value = checkout.metadata_storage.get_value_from_metadata(
125+
self.META_VATIN_KEY
126+
)
127+
valid_vatin_previous = checkout.metadata_storage.get_value_from_metadata(
159128
self.META_VATIN_VALIDATED_KEY
160129
)
161130
buyer_country = self._get_buyer_country_code(address)
@@ -167,18 +136,18 @@ def _validate_vatin_metadata(
167136
# Does not look like a valid VATIN
168137
if not vatin_country or not vatin or vatin_country != buyer_country:
169138
logger.debug('Invalid VATIN format: missing or mismatching country code')
170-
checkout.delete_value_from_metadata(self.META_VATIN_KEY)
171-
checkout.delete_value_from_metadata(self.META_VATIN_VALIDATED_KEY)
172-
checkout.save(update_fields=["metadata"])
139+
checkout.metadata_storage.delete_value_from_metadata(self.META_VATIN_KEY)
140+
checkout.metadata_storage.delete_value_from_metadata(self.META_VATIN_VALIDATED_KEY)
141+
checkout.metadata_storage.save(update_fields=["metadata"])
173142
# Only validate the VATIN further if it differs from an already validated one:
174143
elif vatin != valid_vatin_previous and self._validate_vatin_value(vatin):
175144
logger.debug('Updating VATIN: %s', vatin)
176145
metadata_items = {
177146
self.META_VATIN_KEY: vatin,
178147
self.META_VATIN_VALIDATED_KEY: vatin,
179148
}
180-
checkout.store_value_in_metadata(items=metadata_items)
181-
checkout.save(update_fields=["metadata"])
149+
checkout.metadata_storage.store_value_in_metadata(items=metadata_items)
150+
checkout.metadata_storage.save(update_fields=["metadata"])
182151

183152
def _deduct_tax(self, previous_value: "TaxedMoney") -> "TaxedMoney":
184153
return TaxedMoney(gross=previous_value.net, net=previous_value.net)
@@ -188,61 +157,32 @@ def calculate_checkout_total(
188157
checkout_info: "CheckoutInfo",
189158
lines: List["CheckoutLineInfo"],
190159
address: Optional["Address"],
191-
discounts: Iterable["DiscountInfo"],
192160
previous_value: TaxedMoney,
193161
) -> TaxedMoney:
162+
194163
if self._skip_plugin(checkout_info, previous_value):
195164
return previous_value
196165

197166
checkout = checkout_info.checkout
198167

199168
self._validate_vatin_metadata(checkout, address)
200169

201-
if self._skip_price_modification(checkout_info, previous_value):
202-
return previous_value
203-
204-
valid_vatin = checkout.get_value_from_metadata(self.META_VATIN_VALIDATED_KEY)
170+
valid_vatin = checkout.metadata_storage.get_value_from_metadata(
171+
self.META_VATIN_VALIDATED_KEY
172+
)
205173
buyer_country_code = self._get_buyer_country_code(address)
206-
seller_country_code = self._get_seller_country_code()
174+
seller_country_code = self._get_seller_country_code(checkout)
207175
# If a valid VATIN is provided and the sale isn't within the same country,
208176
# reverse-charged applies.
209177
if valid_vatin and seller_country_code != buyer_country_code:
210178
# VAT is reverse-charged, so it must be excluded from total.
211179
return self._deduct_tax(previous_value)
212180

213-
return previous_value
214-
215-
def calculate_checkout_line_total(
216-
self,
217-
checkout_info: "CheckoutInfo",
218-
lines: List["CheckoutLineInfo"],
219-
checkout_line_info: "CheckoutLineInfo",
220-
address: Optional["Address"],
221-
discounts: Iterable["DiscountInfo"],
222-
previous_value: CheckoutTaxedPricesData,
223-
) -> CheckoutTaxedPricesData:
224-
if self._skip_plugin(checkout_info, previous_value):
225-
return previous_value
226-
227-
checkout = checkout_info.checkout
228-
229-
self._validate_vatin_metadata(checkout, address)
230-
231-
if self._skip_price_modification(checkout_info, previous_value):
232-
return previous_value
233-
234-
valid_vatin = checkout.get_value_from_metadata(self.META_VATIN_VALIDATED_KEY)
235-
buyer_country_code = self._get_buyer_country_code(address)
236-
seller_country_code = self._get_seller_country_code()
237-
# If a valid VATIN is provided and the sale isn't within the same country,
238-
# reverse-charged applies.
239-
if valid_vatin and seller_country_code != buyer_country_code:
240-
# VAT is reverse-charged, so it must be excluded from line total.
241-
return CheckoutTaxedPricesData(
242-
price_with_discounts=self._deduct_tax(
243-
previous_value.price_with_discounts
244-
),
245-
price_with_sale=self._deduct_tax(previous_value.price_with_sale),
246-
undiscounted_price=self._deduct_tax(previous_value.undiscounted_price),
247-
)
248-
return previous_value
181+
update_checkout_prices_with_flat_rates(
182+
checkout,
183+
checkout_info,
184+
lines,
185+
True,
186+
address,
187+
)
188+
return checkout.total

saleor_vatrc/templatetags/saleor_vatrc.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,14 @@ def _get_vat_plugin():
1212
vat_plugin_id = saleor_vatrc.plugin.VatReverseCharge.VAT_PLUGIN_ID
1313
manager = get_plugins_manager()
1414
plugins = manager.all_plugins
15-
vat_plugin = next(
16-
(
17-
p for p in plugins
18-
if p.PLUGIN_ID == vat_plugin_id),
19-
None
20-
)
15+
vat_plugin = next((p for p in plugins if p.PLUGIN_ID == vat_plugin_id), None)
2116
return vat_plugin
2217

2318

2419
@register.simple_tag(takes_context=True)
2520
def get_value_from_metadata(context, obj, key: str) -> bool:
2621
"""Retrieve a value from object's metadata."""
27-
return obj.get_value_from_metadata(key)
22+
return obj.metadata_storage.get_value_from_metadata(key)
2823

2924

3025
@register.simple_tag(takes_context=True)

setup.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name="saleor_vatrc",
5-
version="0.3",
5+
version="0.4",
66
description="Implements VAT reverse charge in Saleor",
77
author="Anna Sirota",
88
author_email="[email protected]",
@@ -13,7 +13,7 @@
1313
packages=["saleor_vatrc"],
1414
entry_points={
1515
"saleor.plugins": [
16-
"saleor_vatrc = saleor_vatrc.plugin:VatReverseCharge"
17-
]
18-
}
16+
"saleor_vatrc = saleor_vatrc.plugin:VatReverseCharge",
17+
],
18+
},
1919
)

0 commit comments

Comments
 (0)