← Zurück zum Werkzeug
Werkzeugdokumentation

Bauplan Ortstaxe Wien

Was das Werkzeug tut, wie es rechnet, und welche Datei wofür zuständig ist.

manuel.tools/ortstaxe-wien/ kein Build, kein Framework native ES-Module 3 externe Abhängigkeiten ~3.900 Zeilen

01 Was es tut

Das Werkzeug liest den Einnahmen-Export (Transaktionsverlauf) von Airbnb als CSV — oder den früheren Reservierungs-Export — und rechnet daraus die Wiener Ortstaxe je Meldemonat aus. Es sagt dir, was du an die MA 6 überweist, mit welchem Verwendungszweck, und was in die jährliche Abgabenerklärung gehört.

Es läuft vollständig im Browser. Keine Datei verlässt das Gerät, solange du dich nicht anmeldest — und auch dann geht nur die Buchung in die Datenbank, nie eine gerechnete Zahl.

  1. CSV hineinziehenAirbnb-Export, ohne Vorbereitung. Der Parser kommt mit Komma und Semikolon zurecht und erkennt selbst, ob die Datei TT/MM oder MM/TT schreibt.
  2. Gastbeträge ergänzenWo die Spalte „Vom Gast bezahlt“ fehlt, schätzt das Werkzeug über die Servicegebühr und sagt dazu, dass es geschätzt hat. Einzeln nachtragen ist jederzeit möglich.
  3. Ablesen und meldenMeldemonat, Überweisung, Jahressumme, 90-Tage-Zähler. Als PDF drucken oder als CSV exportieren.
  4. Optional: speichernMit Google-Anmeldung landet der Bestand in Firestore und überlebt den Tab. Ohne Anmeldung verhält sich alles exakt wie vorher.

02 Wie gerechnet wird

Jede Buchung wird in einzelne Nächte zerlegt. Jede Nacht bekommt den Satz, der an diesem Kalendertag galt, und wird dem Monat zugeschlagen, in dem sie liegt. Deshalb kann eine einzige Buchung in zwei Meldeperioden fallen — der Meldemonat ist eine abgeleitete Sicht, keine Eigenschaft der Buchung.

← Stichtag 01.07.2026Stichtag 01.07.2027 →

Warum nicht einfach „mal 3,2 %“

Gerechnet wird auf den Betrag inklusive enthaltener Ortstaxe. Aus dem Satz e wird deshalb eine Schlüsselzahl e / (ustF + e), die direkt auf das Gastentgelt angewandt wird. Bis 30.06.2026 steckt zusätzlich der 11-%-Pauschalabzug nach § 12 Abs. 2 lit. c WTFG im effektiven Satz — im Code als 0.032 * 0.89.

Diese Zahlen sind gegen den Ortstaxerechner der Stadt Wien geprüft und stehen unter Änderungsverbot:

Satzohne UStbei 10 % USt
3,2 % (mit Pauschalabzug)0,0276910,025237
5 %0,0476190,043478
8 %0,0740740,067797

Der Prüffall

Eine Buchung 18.06.2026 – 19.07.2026 mit 1.644,80 € Gastentgelt, Basis ohne USt, Gebühr 0 muss 64,58 € ergeben: 19,10 € für 13 Nächte im Juni, 45,48 € für 18 Nächte im Juli. Der Fall läuft im Selbsttest mit und schlägt an, sobald jemand an der Rechnung dreht.

03 Zwei Befreiungen, zwei Zähler

Zwei Regelwerke greifen gleichzeitig, und sie zählen unterschiedlich. Das Werkzeug führt beide getrennt.

Drei-Monats-Befreiung

§ 11 Abs. 3 WTFG
Wer länger als drei Monate ununterbrochen bleibt, ist befreit — und zwar für den gesamten Aufenthalt, nicht erst ab dem vierten Monat. Solche Nächte tauchen in der Meldung mit null Steuer auf und werden im Monatsabschluss getrennt ausgewiesen.

90-Tage-Zähler

Wiener Bauordnung
Zählt Kurzzeitvermietung übers Kalenderjahr, getrennt nach kurz (bis 30 Nächte), grau (31 Nächte bis 3 Monate) und lang (befreit). Das ist eine Nutzungsgrenze, keine Steuer — deshalb ein eigener Zähler mit eigener Anzeige.

Fällig ist die Abgabe jeweils am 15. des Folgemonats (§ 13 Abs. 1 WTFG). Dazu kommt bis zum 15. Februar die elektronische Abgabenerklärung für das Vorjahr.

04 Der Bildschirm

Von oben nach unten, so wie die Seite gelesen wird:

BereichWas dort steht
AblageflächeCSV hineinziehen oder auswählen. Darunter der aufklappbare Ablauf.
EinstellungenUSt-Basis, Gastgebergebühr, Gast-Servicegebühr, UID, Zählweise, Abgabenkonto. Sie gehen in jede Rechnung ein.
Wolken-LeisteAnmeldung, Objektwahl, Speicherstand. Bleibt verborgen, wenn die Datenbank nicht erreichbar ist.
MonatsabschlussPrüfen, abschließen, Belegpaket ziehen, Änderungsverlauf einsehen. Nur mit Anmeldung sichtbar.
90-Tage-ZählerKurzzeitvermietung pro Kalenderjahr, mit Restkontingent.
Meldung pro MonatDie Tabelle für VIETour: Nächte, Grundlage, Ortstaxe je Meldemonat.
PreisplanungAufklappbar unter den Ergebnissen: was je Aufenthalt nach Ortstaxe, Airbnb-Gebühr und eigenen Kosten bleibt, und welcher Nachtpreis unter anderen Bedingungen — etwa 8 % ab 01.07.2027 — gleich viel übrig lässt. Vorbelegt aus den Buchungen im Modell „nur Gastgeber zahlt“, gespeichert wird nichts.
ÜberweisungenWas an die MA 6 geht, mit Verwendungszweck aus Abgabenkonto plus MMJJJJ. Filterbar nach Fälligkeit (ab/bis, „bis heute fällig“); die Summe gilt dann nur für die gezeigten Zeilen.
AbgabenerklärungJahressummen für die Februar-Meldung.
BuchungenJede Zeile einzeln, mit editierbarem Gastbetrag und der Aufteilung auf Monate.

Die PDFs entstehen über Print-CSS, nicht über eine Bibliothek. body.print-months blendet für die Kurzfassung alles außer der Meldetabelle aus.

05 Welche Datei was macht

Die Trennung ist keine Kosmetik: Weil kern.js kein DOM anfasst, kann der Selbsttest den echten Rechenweg prüfen, statt ihn nachzubauen.

DateiZeilenAufgabe
index.html—Markup, CSS und Print-Layout. Lädt oberflaeche.js als Modul.
js/kern.js742Der Rechenkern. Reine Funktionen, kein document. CSV-Parser, Datumsordnung, Geldparser, compute, 90-Tage-Zähler, Jahressummen, die drei CSV-Exporte.
js/oberflaeche.js975Alles mit DOM. Render, Handler, Sitzungszustand, Import- und Speicherkette, Sperren. Rechnet nichts selbst.
js/daten.js306Die einzige Stelle, die mit Firebase spricht. Anmeldung, Laden, atomares Schreiben, Schnappschüsse, Monatssperren.
js/abschluss.js116Reine Monatsprüfung und Sperrvergleich nach Wirkung auf die Meldung. Nutzt denselben Rechenkern wie die Anzeige.
js/belegpaket.js59ZIP und PDF von Hand erzeugt — ohne CDN, ohne Build. Byteweise geschriebene ZIP-Header, ein Text-PDF in WinAnsi.
js/firebase-config.js29Projektdaten und die gepinnte SDK-Version.
selftest.js958Prüfungen des Rechenkerns. Wird nur bei ?selftest nachgeladen.

Die wichtigsten Funktionen im Kern

FunktionTut
parseCSVEigener Parser, kommt mit , und ; sowie Quotes zurecht. Geschrieben wird nach RFC 4180.
findColNormalisiert Spaltennamen, matcht deutsche und englische Kopfzeilen, dann Teilstring-Fallback.
datumsOrdnungEntscheidet einmal für die ganze Datei, ob TT/MM oder MM/TT gilt.
leseGeldPrüft den ganzen String gegen ein Zahlenformat und liefert {wert, status} — ein unlesbarer Betrag wird gemeldet, nicht still als 0 gelesen.
computeDas Herzstück: zerlegt jede Buchung in Nächte, ordnet Satz und Meldemonat zu, erkennt Befreiungen.
occupancyDer 90-Tage-Zähler nach kurz / grau / lang.
alsCsvZeilenFührt gespeicherte Dokumente in genau die Tabelle zurück, die compute ohnehin liest — damit gibt es keinen zweiten Weg in die Berechnung.
verschmelzeBuchungenFührt Import und Bestand zusammen, statt zu ersetzen. Ein von Hand gesetzter Gastbetrag überlebt einen Import, der an dieser Stelle nichts mitbringt.

06 Woher die Zahlen gerade kommen

Es gibt zwei mögliche Quellen, und genau eine Funktion entscheidet: aktuelleZeilen().

Liegt ein Bestand aus der Datenbank vor?

Dann hat er Vorrang. Nach dem ersten Speichern ist die Datenbank die Wahrheit, nicht mehr die Datei.

Sonst

Gilt die zuletzt geladene CSV. Das ist der Zustand ohne Anmeldung — und der vollständige Funktionsumfang des Werkzeugs.

Daraus folgt etwas Feines: Ein geleertes Eingabefeld hebt die Überschreibung auf und gibt den Wert der aktuellen Quelle frei — im CSV-Betrieb den Dateiwert, danach den gespeicherten. Deshalb ist gastbetragQuelle ein Vergleich und kein Herkunftsvermerk: „manuell“ heißt, jemand hat etwas anderes gesetzt als die Quelle sagt.

07 Monatsabschluss und Belegpaket

Ein Abschluss friert einen geprüften Stand ein. Er bestätigt keine Abgabe bei VIETour — er dokumentiert, was du geprüft hast und wann.

Zum Abschließen bestätigst du Vollständigkeit und Belegprüfung; liegen rechnerische Hinweise vor, musst du auch die ausdrücklich akzeptieren. Danach ist gesperrt, was der Monat gemeldet hat: steuerpflichtige und befreite Nächte, Ortstaxe, Entgelt und Grundlage auf den Cent, und die offenen Posten seiner Buchungen. Eine Änderung, die eine dieser Zahlen verschiebt — auch eine neue Buchung, eine Stornierung oder eine Wiederherstellung —, wird im gemeinsamen Speicherweg abgewiesen; eine, die sie gleich lässt (eine nachgereichte zweite Rate, ein ergänzter Bruttobetrag, ein anderer Name), geht durch. Ein Import prüft je Buchung, hält nur die zurück, die einen abgeschlossenen Monat verschieben, und speichert den Rest — auch harmlose Änderungen im selben Monat; die Meldung nennt Buchung, Monat und Zahl vorher → nachher. Wiederöffnen verlangt einen Grund von mindestens fünf Zeichen und behält den alten Abschluss im Protokoll.

Was im ZIP liegt

DateiInhalt
monatsabschluss.pdfDer lesbare Beleg: Ortstaxe, steuerpflichtige und befreite Nächte, Einstellungen, Prüfhinweise, alle Buchungen.
buchungen.csvDieselben Buchungen maschinenlesbar, mit BOM für Excel.
abschluss.jsonDer vollständige eingefrorene Stand.
einstellungen.jsonDie Einstellungen, mit denen gerechnet wurde.
LESEMICH.txtWas das Paket ist — und was es nicht ist.

Steuerpflichtige und befreite Nächte stehen getrennt. Ein Monat mit einem Langzeitgast kann 34 Nächte umfassen, von denen nur 3 steuerpflichtig sind. Das PDF weist beide Zahlen aus, damit der Beleg nicht mit der Meldetabelle streitet.

Externe Originalbelege von Airbnb sind nicht enthalten und weiter separat aufzubewahren.

08 Die Regeln beim Speichern

Der Synchronisierungsteil ist der heikelste Teil des Werkzeugs, weil dort mehrere Dinge gleichzeitig passieren: Ein Autospeichern läuft, du wechselst das Objekt, ein Import ist unterwegs. Die folgenden Regeln sind keine Stilfragen — jede steht für einen Fehler, der einmal da war.

Gespeichert wird die Buchung, nie der Meldemonat

Eine Buchung vom 18.06. bis 19.07. gehört in zwei Meldeperioden, der 90-Tage-Zähler rechnet übers Kalenderjahr, die Drei-Monats-Befreiung über den ganzen Aufenthalt. Der Monat ist abgeleitet. Käme er in die Datenbank, gäbe es zwei Wahrheiten, die auseinanderlaufen.

Jeder Auftrag hängt an drei Dingen — festgehalten beim Einreihen

  • ziel — an welchem Objekt. Sonst landet nach einem Objektwechsel der Bestand von Wohnung A unter Wohnung B.
  • bestandVersion — an welchem Bestand. Steigt bei Objektwechsel, Laden, Import, Wiederherstellung, neuer Datei. Ein Auftrag aus einem älteren Bestand wird verworfen.
  • eingabeStand — an welchem Bearbeitungsstand. „Gespeichert“ darf nur melden, wer diesen Stand geschrieben hat.

Ein Vorgang, der den Bestand austauscht, sperrt die Eingaben

Abwarten genügt nicht: Während einer Wiederherstellung blieb die alte Tabelle bearbeitbar, und ein Tastendruck erzeugte einen Auftrag, der sich brav dahinter einreihte — und sie damit rückgängig machte. Gesperrt wird vor dem ersten await, gezählt, und sowohl im DOM als auch im Eingabe-Handler geprüft.

Zurückspielen ist ein Abgleich, kein Überschreiben

Buchungen, die erst nach dem Schnappschuss dazukamen, werden ausdrücklich entfernt — sonst ließe sich ein fehlerhafter Import zusätzlicher Zeilen nicht rückgängig machen. Löschen und Zurückschreiben laufen als eine Transaktion.

Grenzen: höchstens 400 geänderte Buchungen je Vorgang, höchstens 700 kB JSON je Verwaltungs- oder Protokolldokument. Größere Vorgänge werden vollständig abgewiesen — kein Teilwrite. Unveränderte Zeilen zählen nicht mit.

09 Datenbank und Rollout

Der Bestand liegt in Firestore, je Benutzer und Objekt getrennt:

users/{uid}/einstellungen/aktuell
users/{uid}/objekte/{objektId}
  ├─ buchungen/{code}            die Buchung selbst
  ├─ verwaltung/aktuell          Revision + Monatssperren
  ├─ verlauf/{id}                Änderungsprotokoll, unveränderlich
  ├─ abschlusshistorie/{id}      eingefrorene Belege, unveränderlich
  └─ schnappschuesse/{zeitpunkt} Stand vor jedem Import

Datumsangaben stehen als ISO-Zeichenkette, nicht als Firestore-Timestamp: Die Rechnung arbeitet auf UTC-Kalendertagen, und ein Timestamp holt genau die Zeitzonenfehler zurück, die vermieden werden sollen.

Buchungen, Verlauf, Einstellungen und Objektrevision werden gemeinsam in einer Transaktion geschrieben. Die Zugriffsregeln verlangen dafür, dass die Objektrevision im selben Vorgang um genau eins steigt — dadurch kann ein veralteter Client nicht über einen fremden Stand schreiben.

Ein Merge rollt die Regeln nicht aus. GitHub Pages liefert nur index.html und js/; firestore.rules lebt im Firebase-Projekt. Wer die Regeln ändert, ohne sie auszurollen, erzeugt „neue Anwendung, alte Regeln“ — und das meldet sich als Missing or insufficient permissions, schon beim Laden.

Dafür gibt es .github/workflows/firestore-regeln.yml: Es prüft die Regeln erst gegen den Firestore-Emulator und rollt nur bei bestandener Prüfung aus. Auslösbar auch von Hand über Actions → Firestore-Regeln → Run workflow. Nach jedem Ausrollen offene Tabs neu laden.

10 Was geprüft wird

Vier Ebenen, weil keine allein reicht. Der Selbsttest prüft den Rechenkern — was zwischen Eingabe, Import und Speichern passiert, erreicht er nicht, und genau dort lagen die schwersten Fehler.

EbeneFällePrüft
selftest.js377Den Rechenkern: Schlüsselzahlen, Stichtage, Parser, Befreiungen, den Rundlauf CSV → rechnen → Dokumente → zurück → rechnen.
daten-transaktionen.mjs27Den echten Datenzugriff mit SDK-Attrappe: Commitfehler, konkurrierende Revisionen, Sperren, Protokollaufteilung.
integration.mjs83Die echte Oberfläche in Chromium gegen eine Datenbank-Attrappe. Tippen, importieren, Objekt wechseln, abschließen.
firestore-regeln.mjs17Die Zugriffsregeln gegen den lokalen Emulator: Eigentümerzugriff, Revisionsprüfung, fremde Zugriffe, unveränderliche Protokolle.

Der Grundsatz dahinter: Ein Test, der seine Eingabe selbst konstruiert statt sie durch den echten Pfad laufen zu lassen, ist grün und wertlos. Genau so war gastbetragQuelle einmal nur in den Tests vorhanden, während der Produktivpfad den Wert nie erzeugte.

11 Grenzen

Die rechtlichen Grundlagen im Detail — Gesetzesstellen, FAQ der MA 6, Judikatur zur 30-Nächte-Grenze — stehen im Werkzeug selbst unter „Wie gerechnet wird“.