import re from .money_amount import Money, money from .money_apply_rate import apply_rate from .retail_oss_vat_data import OSS_RULES, OSS_RULES_HISTORY from .retail_oss_vat_types import OssVat _ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") _COUNTRY = re.compile(r"^[A-Z]{2}$") def _country(code: str, name: str) -> str: # The EU writes Greece as EL in VAT matters; ISO 3166 says GR. Accept both. if not isinstance(code, str) or not _COUNTRY.match(code): raise ValueError('%s must be an ISO 3166-1 alpha-2 code, received "%s"' % (name, code)) return "GR" if code == "EL" else code def _is_member(code: str) -> bool: return any(r.rule == "standard-rate" and r.country == code for r in OSS_RULES) def _lookup(rule: str, where: str, on_date: str) -> int: best = None for r in OSS_RULES: if r.rule != rule or r.country != where: continue if on_date < r.valid_from or (r.valid_to is not None and on_date > r.valid_to): continue if best is None or r.valid_from > best.valid_from: best = r if best is not None: return best.value # A history=current build carries only the rules still in force; answering # an older date with today's rate is the failure the dates exist to prevent. if OSS_RULES_HISTORY != "full": earliest = None for r in OSS_RULES: if r.rule == rule and r.country == where and (earliest is None or r.valid_from < earliest): earliest = r.valid_from if earliest is not None and on_date < earliest: raise ValueError( 'no OSS rule "%s" for %s on %s: this build was installed with history=%s, ' "so it only carries rules from %s. Reinstall with history=full for older supplies." % (rule, where, on_date, OSS_RULES_HISTORY, earliest) ) raise ValueError('no OSS rule "%s" for %s on %s' % (rule, where, on_date)) def oss_vat( net: Money, seller_country: str, customer_country: str, eu_sales_previous_year: Money, eu_sales_this_year: Money, opted_in: bool, on_date: str, ) -> OssVat: """VAT on a business-to-consumer sale to another EU member state: the seller's own rate while its cross-border sales stay within EUR 10,000 (and it has not opted in), the customer's state's rate through the One Stop Shop once they exceed it, and always the customer's for sellers outside the EU. """ if not isinstance(on_date, str) or not _ISO_DATE.match(on_date): raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,)) seller = _country(seller_country, "sellerCountry") customer = _country(customer_country, "customerCountry") if not _is_member(customer): raise ValueError('customer country "%s" is not an EU member state' % customer_country) for name, amount in (("euSalesPreviousYear", eu_sales_previous_year), ("euSalesThisYear", eu_sales_this_year)): if amount.currency != "EUR": raise ValueError("%s must be in EUR, received %s" % (name, amount.currency)) if amount.minor < 0: raise ValueError("%s must not be negative, received %d" % (name, amount.minor)) threshold = _lookup("distance-selling-threshold", "EU", on_date) threshold_exceeded = eu_sales_previous_year.minor > threshold or eu_sales_this_year.minor > threshold if seller == customer: scheme, tax_country = "domestic", customer elif _is_member(seller) and not threshold_exceeded and not opted_in: scheme, tax_country = "origin", seller else: scheme, tax_country = "oss", customer basis_points = _lookup("standard-rate", tax_country, on_date) vat = apply_rate(net, basis_points, "half-up") return OssVat( tax_country=tax_country, scheme=scheme, basis_points=basis_points, threshold_exceeded=threshold_exceeded, net=net, vat=vat, gross=money(net.minor + vat.minor, net.currency), )