Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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():

  1. Non-EU buyer → 0% VAT, no reverse charge, legal note "Export outside the EU - VAT not applicable".
  2. Seller not in EU (edge case) → 0% VAT, no note, label "No VAT".
  3. Domestic (seller country == buyer country) → seller's standard rate (NL 21%, DE 19%, FR 20%, etc.), no reverse charge.
  4. Intra-EU B2B with valid VAT number (different EU countries, is_business=True, vat_number set) → 0%, reverse charge, legal note "Reverse charge - Art. 196 EU VAT Directive".
  5. Intra-EU B2C (no VAT number or not a business) → seller's country rate (per OSS rules, simplification).

The result is a VATDetermination dataclass:

python
1
2
3
4
5
6
@dataclass(frozen=True)
class VATDetermination:
    tax_rate: Decimal           # e.g. Decimal("21.00")
    reverse_charge: bool
    tax_label: str              # "VAT 21.00%" | "VAT 0% (Reverse Charge)" | …
    legal_note: str | None

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 rate
    • AE — VAT reverse charge
    • G — VAT free for export
    • E — exempt from VAT
    • Z — zero-rated
    • O — services outside scope
    • K — 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 as amount_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[]:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "entries": [
    {
      "category": "S",
      "rate": "21.00",
      "label": "VAT 21.00%",
      "taxable_cents": 15000,
      "amount_cents": 3150,
      "legal_note": null
    }
  ]
}

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.

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_exempt on tenant_billing_profiles if 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:

python
1
2
3
4
5
6
7
8
9
# In line_items input to POST /admin/invoices:
{
  "description": "Educational training package",
  "quantity": 10, "unit_price_cents": 5000,
  "amount_cents": 50000,
  "line_type": "manual",
  "tax_category": "E",        # exempt
  "tax_rate": "0.00",
}

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.

Updated 2026-05-18 01:48:39 View source (.md) rev 2