Files
orion/app/utils/vat.py
Samir Boulahtit f20266167d
Some checks failed
CI / ruff (push) Failing after 7s
CI / pytest (push) Failing after 1s
CI / architecture (push) Failing after 9s
CI / dependency-scanning (push) Successful in 27s
CI / audit (push) Successful in 8s
CI / docs (push) Has been skipped
fix(lint): auto-fix ruff violations and tune lint rules
- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.)
- Added ignore rules for patterns intentional in this codebase:
  E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from),
  SIM108/SIM105/SIM117 (readability preferences)
- Added per-file ignores for tests and scripts
- Excluded broken scripts/rename_terminology.py (has curly quotes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:10:42 +01:00

245 lines
7.1 KiB
Python

# app/utils/vat.py
"""
VAT calculation utilities for the OMS.
Provides centralized VAT logic used by both order_service and invoice_service
to ensure consistency between order tax calculation and invoice VAT.
VAT Logic:
- Luxembourg domestic: 17% (standard), 8% (reduced), 3% (super-reduced), 14% (intermediate)
- EU cross-border B2C with OSS: Use destination country VAT rate
- EU cross-border B2C without OSS: Use Luxembourg VAT rate (origin principle)
- EU B2B with valid VAT number: Reverse charge (0% VAT)
- Non-EU: VAT exempt (0%)
"""
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
class VATRegime(str, Enum):
"""VAT regime for order/invoice calculation."""
DOMESTIC = "domestic" # Same country as seller
OSS = "oss" # EU cross-border with OSS registration
REVERSE_CHARGE = "reverse_charge" # B2B with valid VAT number
ORIGIN = "origin" # Cross-border without OSS (use origin VAT)
EXEMPT = "exempt" # VAT exempt (non-EU)
@dataclass
class VATResult:
"""Result of VAT determination."""
regime: VATRegime
rate: Decimal
destination_country: str | None
label: str | None
# EU VAT rates by country code (2024 standard rates)
EU_VAT_RATES: dict[str, Decimal] = {
"AT": Decimal("20.00"), # Austria
"BE": Decimal("21.00"), # Belgium
"BG": Decimal("20.00"), # Bulgaria
"HR": Decimal("25.00"), # Croatia
"CY": Decimal("19.00"), # Cyprus
"CZ": Decimal("21.00"), # Czech Republic
"DK": Decimal("25.00"), # Denmark
"EE": Decimal("22.00"), # Estonia
"FI": Decimal("24.00"), # Finland
"FR": Decimal("20.00"), # France
"DE": Decimal("19.00"), # Germany
"GR": Decimal("24.00"), # Greece
"HU": Decimal("27.00"), # Hungary
"IE": Decimal("23.00"), # Ireland
"IT": Decimal("22.00"), # Italy
"LV": Decimal("21.00"), # Latvia
"LT": Decimal("21.00"), # Lithuania
"LU": Decimal("17.00"), # Luxembourg (standard)
"MT": Decimal("18.00"), # Malta
"NL": Decimal("21.00"), # Netherlands
"PL": Decimal("23.00"), # Poland
"PT": Decimal("23.00"), # Portugal
"RO": Decimal("19.00"), # Romania
"SK": Decimal("20.00"), # Slovakia
"SI": Decimal("22.00"), # Slovenia
"ES": Decimal("21.00"), # Spain
"SE": Decimal("25.00"), # Sweden
}
# Luxembourg specific VAT rates
LU_VAT_RATES = {
"standard": Decimal("17.00"),
"intermediate": Decimal("14.00"),
"reduced": Decimal("8.00"),
"super_reduced": Decimal("3.00"),
}
# Country names for labels
COUNTRY_NAMES: dict[str, str] = {
"AT": "Austria",
"BE": "Belgium",
"BG": "Bulgaria",
"HR": "Croatia",
"CY": "Cyprus",
"CZ": "Czech Republic",
"DK": "Denmark",
"EE": "Estonia",
"FI": "Finland",
"FR": "France",
"DE": "Germany",
"GR": "Greece",
"HU": "Hungary",
"IE": "Ireland",
"IT": "Italy",
"LV": "Latvia",
"LT": "Lithuania",
"LU": "Luxembourg",
"MT": "Malta",
"NL": "Netherlands",
"PL": "Poland",
"PT": "Portugal",
"RO": "Romania",
"SK": "Slovakia",
"SI": "Slovenia",
"ES": "Spain",
"SE": "Sweden",
}
def get_vat_rate_for_country(country_iso: str) -> Decimal:
"""
Get standard VAT rate for EU country.
Args:
country_iso: ISO 2-letter country code
Returns:
VAT rate as Decimal (0.00 for non-EU countries)
"""
return EU_VAT_RATES.get(country_iso.upper(), Decimal("0.00"))
def get_vat_rate_label(country_iso: str, vat_rate: Decimal) -> str:
"""
Get human-readable VAT rate label.
Args:
country_iso: ISO 2-letter country code
vat_rate: VAT rate as Decimal
Returns:
Human-readable label (e.g., "Luxembourg VAT 17%")
"""
country_name = COUNTRY_NAMES.get(country_iso.upper(), country_iso)
return f"{country_name} VAT {vat_rate}%"
def is_eu_country(country_iso: str) -> bool:
"""Check if country is in the EU."""
return country_iso.upper() in EU_VAT_RATES
def determine_vat_regime(
seller_country: str,
buyer_country: str,
buyer_vat_number: str | None = None,
seller_oss_registered: bool = False,
) -> VATResult:
"""
Determine VAT regime and rate for an order/invoice.
VAT Decision Logic:
1. Same country = domestic VAT
2. B2B with valid VAT number = reverse charge (0%)
3. Cross-border + OSS registered = destination country VAT
4. Cross-border + no OSS = origin country VAT
5. Non-EU = VAT exempt (0%)
Args:
seller_country: Seller's country (ISO 2-letter code)
buyer_country: Buyer's country (ISO 2-letter code)
buyer_vat_number: Buyer's VAT number (for B2B detection)
seller_oss_registered: Whether seller is registered for OSS
Returns:
VATResult with regime, rate, destination country, and label
"""
seller_country = seller_country.upper() if seller_country else "LU"
buyer_country = buyer_country.upper() if buyer_country else "LU"
# Same country = domestic VAT
if seller_country == buyer_country:
vat_rate = get_vat_rate_for_country(seller_country)
label = get_vat_rate_label(seller_country, vat_rate) if vat_rate > 0 else None
return VATResult(
regime=VATRegime.DOMESTIC,
rate=vat_rate,
destination_country=None,
label=label,
)
# Different EU countries
if is_eu_country(buyer_country):
# B2B with valid VAT number = reverse charge
if buyer_vat_number:
return VATResult(
regime=VATRegime.REVERSE_CHARGE,
rate=Decimal("0.00"),
destination_country=buyer_country,
label="Reverse charge",
)
# B2C cross-border
if seller_oss_registered:
# OSS: use destination country VAT
vat_rate = get_vat_rate_for_country(buyer_country)
label = get_vat_rate_label(buyer_country, vat_rate)
return VATResult(
regime=VATRegime.OSS,
rate=vat_rate,
destination_country=buyer_country,
label=label,
)
# No OSS: use origin country VAT
vat_rate = get_vat_rate_for_country(seller_country)
label = get_vat_rate_label(seller_country, vat_rate)
return VATResult(
regime=VATRegime.ORIGIN,
rate=vat_rate,
destination_country=buyer_country,
label=label,
)
# Non-EU = VAT exempt
return VATResult(
regime=VATRegime.EXEMPT,
rate=Decimal("0.00"),
destination_country=buyer_country,
label="VAT exempt",
)
def calculate_vat_amount(subtotal_cents: int, vat_rate: Decimal) -> int:
"""
Calculate VAT amount from subtotal.
Args:
subtotal_cents: Subtotal in cents
vat_rate: VAT rate as percentage (e.g., 17.00 for 17%)
Returns:
VAT amount in cents
"""
if vat_rate <= 0:
return 0
# Calculate: tax = subtotal * (rate / 100)
subtotal_decimal = Decimal(str(subtotal_cents))
tax_decimal = subtotal_decimal * (vat_rate / Decimal("100"))
# Round to nearest cent
return int(round(tax_decimal))