In den Niederlanden und in vielen anderen Ländern müssen Parks Angaben zur “tourist tax” einreichen. Auf Deutsch landet man schnell bei der Ortstaxe:
Die Ortstaxe ist eine Form einer Tourismus- oder Fremdenverkehrssteuer. Quelle: Wikipedia: Ortstaxe
Für Parks geht es praktisch um die Beherbergungs- oder Übernachtungsvariante: eine Abgabe, die an einen Aufenthalt in einer Unterkunft, einem Ferienhaus, einem Stellplatz oder einem ähnlichen Ort gekoppelt ist.
Das Problem daran ist, dass mit dieser Definition viele Regeln verbunden sind. Viele davon liegen auf Gemeindeebene. Manche Gemeinden berechnen zum Beispiel keine Tourismusabgabe, wenn der Gast in derselben Gemeinde wohnt.
Es gibt aber noch mehr Regeln. Sie hängen mit der Art des Parks zusammen, den du betreibst: Glamping, Campingplätze, B&Bs und andere Unterkunftsformen. Und das gilt nur für die Niederlande. In Spanien liegt diese Steuer zum Beispiel auf Ebene der Region oder der autonomen Gemeinschaft. Wenn du solche Berechnungen länderübergreifend machen willst, entsteht sehr viel Handarbeit.
Bestehende Lösungen
Es gibt bereits Lösungen, aber sie sitzen meistens hinter verschlossenen Türen. Validierung und Debugging sind dadurch schwierig, und das Preismodell ist nichts, was ich einfach so übernehmen würde.
Bei Odeva glauben wir, dass Open Source die respektvollste Art ist, Software zu bauen. Lass Menschen deinen Code so verwenden, wie sie möchten. Willst du die Logik einbinden und ein Land hinzufügen? Versuchst du herauszufinden, warum die Steuer so funktioniert? Nur zu!
Ausgangspunkt
Für solche Berechnungen ist wiederholbare, erklärbare, quellenbasierte und testbare Logik der richtige Weg. Praktisch ist, dass niederländische Gemeinden alle ungefähr demselben Format folgen. Diese Daten einzulesen ist sehr einfach.
Steuerberechnungen fallen immer in zwei Gruppen.
- Ein fester Betrag (1 Erwachsener = €0.20,-, 1 Kind = €0.05,-)
- Ein Prozentsatz (
20.00 * 0.08 = €1.60,-)
Wenn wir Amsterdam als Beispiel nehmen, lässt sich die folgende Logik in JSON darstellen:
{
"id": "nl-amsterdam-2026-general",
"municipality_code": "0363",
"municipality_name": "Amsterdam",
"valid_from": "2026-01-01",
"calculation": {
"kind": "generic.percentage_of_base",
"params": {
"rate_pct": 7.0,
"base": "accommodation_fee_exclusive_of_tax"
},
"currency": "EUR"
},
"source": {
"source_url": "https://lokaleregelgeving.overheid.nl/CVDR750921",
"cvdr_id": "CVDR750921"
},
"confidence": "scraped"
}
Eine Regel ist Daten
Der Kern des Kits ist ein Datenmodell für Rulesets. Ein Ruleset hat eine Zuständigkeit (jurisdiction) und eine Liste von Regeln. Jede Regel hat Datumsangaben, einen Gültigkeitsbereich, einen Berechnungstyp (calculation.kind), optionale Prädikate (predicates), optionale Befreiungen (exemptions) und Quellenmetadaten.
Die vereinfachte Form sieht so aus:
{
"id": "nl-example-2026",
"domain": "tourist_tax",
"jurisdiction": {
"country_code": "NL",
"country_name": "Netherlands"
},
"rules": [
{
"id": "example-rule",
"municipality_code": "0000",
"municipality_name": "Example",
"valid_from": "2026-01-01",
"valid_to": null,
"applies_to": {
"accommodation_types": ["hotel"]
},
"calculation": {
"kind": "generic.per_person_per_night",
"params": {
"amount": 3.95
},
"currency": "EUR"
},
"predicates": [],
"exemptions": [
{
"kind": "guest.resident_of_same_municipality"
}
],
"source": {
"source_url": "https://lokaleregelgeving.overheid.nl/...",
"cvdr_id": "CVDR..."
},
"confidence": "scraped"
}
]
}
calculation.kind ist absichtlich mit einem Namespace versehen. generic.per_person_per_night ist nicht dasselbe wie generic.percentage_of_base, und beides ist nicht dasselbe wie ein niederländisches Forfait oder ein gestaffeltes Camping-Arrangement.
Die Registry enthält aktuell Berechnungstypen wie:
generic.per_night
generic.per_person_per_night
generic.per_person_per_night_discount_after_nights
generic.fixed_amount
generic.percentage_of_base
nl.tiered_by_stay_duration
nl.fixed_per_pitch_per_year
nl.forfait_per_person_per_night
Sie enthält auch Prädikate und Befreiungen, zum Beispiel:
guest.resident_of_same_municipality
guest.age_below
stay.wtza_care_institution
stay.coa_asylum_housing
stay.accommodation_brought_by
stay.pricing_arrangement
stay.supervised_minor_group
stay.seasonal_window
cross_tax.already_subject_to
Ein Beispiel
Breda ist ein gutes Beispiel, weil es zeigt, wie schnell das Modell wächst.
Eine Regel im Kit repräsentiert den allgemeinen Betrag pro Person und Nacht für Hotels, Apartments, Bungalows und Short-Stay-Unterkünfte:
{
"id": "nl-breda-2026-general-ppn",
"municipality_code": "0758",
"municipality_name": "Breda",
"valid_from": "2026-01-01",
"valid_to": "2026-12-31",
"applies_to": {
"accommodation_types": ["hotel", "apartment", "bungalow", "short_stay"]
},
"calculation": {
"kind": "generic.per_person_per_night",
"params": {
"amount": 3.95
},
"currency": "EUR"
}
}
Das sieht noch überschaubar aus.
Aber Camping hat mehr Dimensionen. Hat der Betreiber die Unterkunft bereitgestellt, oder bringt der Gast sie selbst mit? Nutzt der Betreiber einen Preis pro Nacht oder einen Arrangement-Preis? Wenden wir einen festen Betrag pro Person und Nacht an, oder eine Staffelung nach Aufenthaltsdauer?
Eine Camping-Arrangement-Regel sieht so aus:
{
"id": "nl-breda-2026-camping-operator-tiered",
"municipality_code": "0758",
"municipality_name": "Breda",
"valid_from": "2026-01-01",
"valid_to": "2026-12-31",
"applies_to": {
"accommodation_types": ["camping"]
},
"predicates": [
{
"kind": "stay.accommodation_brought_by",
"params": {
"value": "operator"
}
},
{
"kind": "stay.pricing_arrangement",
"params": {
"value": "arrangement"
}
}
],
"calculation": {
"kind": "nl.tiered_by_stay_duration",
"params": {
"tiers": [
{ "max_nights": 30, "amount": 45.00 },
{ "min_nights": 30, "max_nights": 120, "amount": 80.00 },
{ "min_nights": 120, "max_nights": 240, "amount": 120.00 },
{ "min_nights": 240, "max_nights": 360, "amount": 150.00 }
]
},
"currency": "EUR"
}
}
Die Berechnung ist schwer, weil zuerst die richtige Regel ausgewählt werden muss, bevor die Multiplikation passiert. Die Konformitätsfälle sind der Punkt, an dem das praktisch wird. Das Kit hat explizite Testfälle für Grenzen wie 29 Nächte, 30 Nächte, 119 Nächte und 120 Nächte. Genau in solchen Fällen weicht eine menschliche Implementierung häufig vom Regeltext ab.
Reservierungsinformationen
Neben diesen Rulesets müssen wir auch Folgendes wissen:
wo sind die Unterkünfte? (eine Reservierung kann mehrere Unterkünfte haben)
wann findet der Aufenthalt statt?
welche Art von Unterkunft ist es?
wer übernachtet dort?
wo wohnt der Gast?
gilt eine lokale Befreiung?
ist das eine Regel auf Buchungsebene oder auf Veranlagungsebene?
welche Quellenpublikation sagt das?
Zum Glück wissen wir all das bereits, wenn wir ein Reservierungssystem benutzen.
Länderübergreifende Regeln
Das ist auch der Grund, warum das Modell nicht annehmen kann, dass jede Regel an einer Gemeinde hängt.
Die Niederlande sind gemeindezentriert: Die Gemeinde entscheidet, ob Tourismusabgabe erhoben wird und wie der Tarif funktioniert. Spanien ist anders. Es gibt kein einzelnes nationales System für Tourismusabgaben, und die Steuern, um die es uns geht, sind meistens auf Ebene der autonomen Gemeinschaft oder der Region definiert.
Katalonien ist ein gutes Beispiel. Die IEET ist eine katalanische Steuer, verwaltet von der Catalan Tax Agency. Barcelona ist dann als Ort relevant, weil Barcelona einen eigenen Zuschlag und eine eigene Tarifbehandlung hat. Die Balearen sind ähnlich, weil die Tourist-Stay Tax eine Steuer der autonomen Gemeinschaft ist, keine kommunale Steuer.
Darum hat das Datenmodell jurisdiction und location_scope statt nur municipality_code. Eine niederländische Regel kann direkt an einer Gemeinde hängen. Eine Katalonien-Regel kann an ES-CT hängen und nur dann auf Barcelona eingegrenzt werden, wenn der Quelltext das wirklich tut.
Bonus: Quellen aktualisieren
Eine weitere Sache, mit der wir rechnen müssen: Tourismusabgaben werden aktualisiert. Das bedeutet, dass der vorherige Eintrag ein Enddatum bekommt und ein neuer Eintrag mit neuen Informationen auftaucht.
Das Kit hat einen geplanten Forgejo-Workflow namens Refresh Generated Data. Jeden Tag macht er:
downloads the CBS municipality dataset
imports and backfills municipality codes
harvests recent CVDR publications
selects relevant publications
extracts candidate data
analyzes it
generates draft fixtures
commits and tags a Ruby patch release if data changed
Die Quellenliste ist öffentlich:
CVDR SRU search API:
https://zoekdienst.overheid.nl/sru/Search?x-connection=cvdr
Official regulation publications:
https://lokaleregelgeving.overheid.nl/CVDR...
CBS municipality dataset 86247NED:
https://datasets.cbs.nl/CSV/CBS/nl/86247NED
Du kannst dir den Quellcode eines Commits ansehen, der automatisch bereitgestellt wurde:
c132d3d994c804f963466aa239c10a303d90f629
Refresh generated tax data
AuthorDate: 2026-05-05 04:18:29 UTC
Dieser Commit aktualisierte Haarlem-Daten. Er fügte ein neues 2026-05-06-Ruleset hinzu, änderte die vorherige Haarlem-Fixture so, dass sie am 2026-05-05 endete, und hob die Ruby-Package-Version von 0.2.1 auf 0.2.2 an.
Dieser Teil braucht eine sorgfältige Einschränkung: Generierte rechtliche Daten sind nicht automatisch endgültige rechtliche Wahrheit. Die generierte Haarlem-Fixture ist als scraped und draft markiert. Das ist absichtlich so. Die Pipeline ist ein quellenbasierter Weg, Änderungen zu erkennen und als Entwürfe zu erzeugen. Wir kuratieren Einträge weiterhin manuell. Ich mag diese Grenze, weil diese Einträge nicht perfekt sind, und Code ist es auch nicht.
Quellen
- GitHub-Spiegel: https://github.com/Odeva-Labs/tax-conformance-kit
- Ruby-Paket: https://rubygems.org/gems/tax_conformance_kit
Regierungs- und Quellenreferenzen:
- https://de.wikipedia.org/wiki/Ortstaxe
- https://business.gov.nl/regulations/tourist-tax/
- https://www.rijksoverheid.nl/onderwerpen/gemeenten/vraag-en-antwoord/wat-is-toeristenbelasting-en-wanneer-moet-ik-dit-betalen
- https://zoekdienst.overheid.nl/sru/Search?x-connection=cvdr
- https://lokaleregelgeving.overheid.nl/
- https://datasets.cbs.nl/CSV/CBS/nl/86247NED
- https://atc.gencat.cat/es/tributs/ieet/quota-tributaria
Möchten Sie ein System, das auf Flexibilität ausgelegt ist?
Treten Sie unserer Warteliste bei, um mehr darüber zu erfahren, wie Odeva die Zukunft der Verwaltung von Ferienvermietungen gestaltet.
Auf die Warteliste→