VAT and reverse charge
ScaiControl ships with EU-compliant VAT determination. At invoice finalisation, the system looks at the seller's country, the buyer's country, the buyer's VAT number, and whether the buyer is a business, and picks the right tax treatment. This page covers the rules and the data model.
The four rules#
services/billing/vat.py:determine_vat():
- Non-EU buyer → 0% VAT, no reverse charge, legal note
"Export outside the EU - VAT not applicable". - Seller not in EU (edge case) → 0% VAT, no note, label
"No VAT". - Domestic (seller country == buyer country) → seller's standard rate (NL 21%, DE 19%, FR 20%, etc.), no reverse charge.
- Intra-EU B2B with valid VAT number (different EU countries,
is_business=True,vat_numberset) → 0%, reverse charge, legal note"Reverse charge - Art. 196 EU VAT Directive". - Intra-EU B2C (no VAT number or not a business) → seller's country rate (per OSS rules, simplification).
The result is a VATDetermination dataclass:
1 2 3 4 5 6 | |
Standard rates#
The lookup table EU_STANDARD_VAT_RATES in vat.py holds the current rates for all 27 EU member states. Excerpt:
| Country | Rate |
|---|---|
| NL | 21.00% |
| DE | 19.00% |
| FR | 20.00% |
| BE | 21.00% |
| ES | 21.00% |
| IT | 22.00% |
| AT | 20.00% |
| PT | 23.00% |
| HU | 27.00% |
Reduced rates (e.g. NL 9% for books) are not auto-applied — the standard rate is the default. To bill at a reduced rate, set tax_rate explicitly on the line item before finalisation.
Per-line categories#
When VAT rules apply differently across an invoice's lines (rare but legal), each invoice_line_items row carries its own:
-
tax_category(UNCL 5305):S— standard rateAE— VAT reverse chargeG— VAT free for exportE— exempt from VATZ— zero-ratedO— services outside scopeK— VAT exempt for EEA intra-community supply
-
tax_rate(Numeric(5,2)) — explicit percentage, or null to inherit the invoice-level rate. -
tax_cents— computed at finalisation asamount_cents * rate / 100.
Most line items leave tax_category and tax_rate null; the invoice-level VAT determination fills them in based on determine_vat(). Setting them explicitly is the override path for special cases (e.g. one line of a domestic invoice covers a service that's locally exempt).
The frozen audit trail#
Once finalised, the per-rate breakdown is stored on the invoice as vat_details.entries[]:
1 2 3 4 5 6 7 8 9 10 11 12 | |
The label and legal_note are stored in English (canonical). The render layer localises them per the buyer's preferred_language — see Templates for how that flows through.
Localised legal notes#
When reverse_charge=True, the legal note shipped on the invoice is mandatory in EU law. ScaiControl ships translations for the five primary EU operating languages:
| Language | Reverse charge legal note |
|---|---|
| EN | Reverse charge - Art. 196 EU VAT Directive |
| NL | BTW verlegd – Art. 196 EU BTW-richtlijn |
| DE | Steuerschuldnerschaft des Leistungsempfängers – Art. 196 EU-MwSt-Richtlinie |
| FR | Autoliquidation de la TVA – Art. 196 de la directive TVA UE |
| ES | Inversión del sujeto pasivo – Art. 196 de la Directiva del IVA de la UE |
| IT | Inversione contabile dell'IVA – Art. 196 Direttiva IVA UE |
If the buyer's preferred_language isn't in this list, the English canonical text is used.
The export-outside-EU note is similarly localised.
VAT validation#
ScaiControl does NOT call VIES (the EU VAT number validation service) at finalisation. Validating each invoice would add a hard network dependency and a quota concern. Instead:
- Buyers are expected to register their VAT number once on their billing profile.
- The operator should periodically validate via VIES (or accept the buyer's certification) and update
tax_exemptontenant_billing_profilesif the number's invalid.
A future enhancement is an admin "validate VAT" button per tenant that hits VIES on demand and stores the result.
Reduced and exempt cases#
To bill at a non-standard rate or exempt a line:
1 2 3 4 5 6 7 8 9 | |
For the entire invoice to apply reverse charge regardless of determine_vat() (e.g. mixed-jurisdiction services), set every line's tax_category to AE and tax_rate to 0.00, and override invoice.reverse_charge = True.
Bottom line#
For 99% of EU billing, the four rules above are correct out of the box. The edge cases (reduced rates, partial exemptions, OSS thresholds for B2C beyond €10k) need operator judgement — ScaiControl exposes the knobs but doesn't enforce them.