Was das Werkzeug tut, wie es rechnet, und welche Datei wofür zuständig ist.
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.
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.
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:
| Satz | ohne USt | bei 10 % USt |
|---|---|---|
| 3,2 % (mit Pauschalabzug) | 0,027691 | 0,025237 |
| 5 % | 0,047619 | 0,043478 |
| 8 % | 0,074074 | 0,067797 |
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.
Zwei Regelwerke greifen gleichzeitig, und sie zählen unterschiedlich. Das Werkzeug führt beide getrennt.
§ 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.
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.
Von oben nach unten, so wie die Seite gelesen wird:
| Bereich | Was dort steht |
|---|---|
| Ablagefläche | CSV hineinziehen oder auswählen. Darunter der aufklappbare Ablauf. |
| Einstellungen | USt-Basis, Gastgebergebühr, Gast-Servicegebühr, UID, Zählweise, Abgabenkonto. Sie gehen in jede Rechnung ein. |
| Wolken-Leiste | Anmeldung, Objektwahl, Speicherstand. Bleibt verborgen, wenn die Datenbank nicht erreichbar ist. |
| Monatsabschluss | Prüfen, abschließen, Belegpaket ziehen, Änderungsverlauf einsehen. Nur mit Anmeldung sichtbar. |
| 90-Tage-Zähler | Kurzzeitvermietung pro Kalenderjahr, mit Restkontingent. |
| Meldung pro Monat | Die Tabelle für VIETour: Nächte, Grundlage, Ortstaxe je Meldemonat. |
| Preisplanung | Aufklappbar 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. |
| Überweisungen | Was 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ärung | Jahressummen für die Februar-Meldung. |
| Buchungen | Jede 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.
Die Trennung ist keine Kosmetik: Weil kern.js kein DOM anfasst, kann der Selbsttest den echten Rechenweg prüfen, statt ihn nachzubauen.
| Datei | Zeilen | Aufgabe |
|---|---|---|
| index.html | — | Markup, CSS und Print-Layout. Lädt oberflaeche.js als Modul. |
| js/kern.js | 742 | Der Rechenkern. Reine Funktionen, kein document. CSV-Parser, Datumsordnung, Geldparser, compute, 90-Tage-Zähler, Jahressummen, die drei CSV-Exporte. |
| js/oberflaeche.js | 975 | Alles mit DOM. Render, Handler, Sitzungszustand, Import- und Speicherkette, Sperren. Rechnet nichts selbst. |
| js/daten.js | 306 | Die einzige Stelle, die mit Firebase spricht. Anmeldung, Laden, atomares Schreiben, Schnappschüsse, Monatssperren. |
| js/abschluss.js | 116 | Reine Monatsprüfung und Sperrvergleich nach Wirkung auf die Meldung. Nutzt denselben Rechenkern wie die Anzeige. |
| js/belegpaket.js | 59 | ZIP und PDF von Hand erzeugt — ohne CDN, ohne Build. Byteweise geschriebene ZIP-Header, ein Text-PDF in WinAnsi. |
| js/firebase-config.js | 29 | Projektdaten und die gepinnte SDK-Version. |
| selftest.js | 958 | Prüfungen des Rechenkerns. Wird nur bei ?selftest nachgeladen. |
| Funktion | Tut |
|---|---|
| parseCSV | Eigener Parser, kommt mit , und ; sowie Quotes zurecht. Geschrieben wird nach RFC 4180. |
| findCol | Normalisiert Spaltennamen, matcht deutsche und englische Kopfzeilen, dann Teilstring-Fallback. |
| datumsOrdnung | Entscheidet einmal für die ganze Datei, ob TT/MM oder MM/TT gilt. |
| leseGeld | Prüft den ganzen String gegen ein Zahlenformat und liefert {wert, status} — ein unlesbarer Betrag wird gemeldet, nicht still als 0 gelesen. |
| compute | Das Herzstück: zerlegt jede Buchung in Nächte, ordnet Satz und Meldemonat zu, erkennt Befreiungen. |
| occupancy | Der 90-Tage-Zähler nach kurz / grau / lang. |
| alsCsvZeilen | Führt gespeicherte Dokumente in genau die Tabelle zurück, die compute ohnehin liest — damit gibt es keinen zweiten Weg in die Berechnung. |
| verschmelzeBuchungen | Führt Import und Bestand zusammen, statt zu ersetzen. Ein von Hand gesetzter Gastbetrag überlebt einen Import, der an dieser Stelle nichts mitbringt. |
Es gibt zwei mögliche Quellen, und genau eine Funktion entscheidet: aktuelleZeilen().
Dann hat er Vorrang. Nach dem ersten Speichern ist die Datenbank die Wahrheit, nicht mehr die Datei.
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.
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.
| Datei | Inhalt |
|---|---|
| monatsabschluss.pdf | Der lesbare Beleg: Ortstaxe, steuerpflichtige und befreite Nächte, Einstellungen, Prüfhinweise, alle Buchungen. |
| buchungen.csv | Dieselben Buchungen maschinenlesbar, mit BOM für Excel. |
| abschluss.json | Der vollständige eingefrorene Stand. |
| einstellungen.json | Die Einstellungen, mit denen gerechnet wurde. |
| LESEMICH.txt | Was 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.
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.
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.
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.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.
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.
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.
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.
| Ebene | Fälle | Prüft |
|---|---|---|
| selftest.js | 377 | Den Rechenkern: Schlüsselzahlen, Stichtage, Parser, Befreiungen, den Rundlauf CSV → rechnen → Dokumente → zurück → rechnen. |
| daten-transaktionen.mjs | 27 | Den echten Datenzugriff mit SDK-Attrappe: Commitfehler, konkurrierende Revisionen, Sperren, Protokollaufteilung. |
| integration.mjs | 83 | Die echte Oberfläche in Chromium gegen eine Datenbank-Attrappe. Tippen, importieren, Objekt wechseln, abschließen. |
| firestore-regeln.mjs | 17 | Die 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.
? — die Unicode-Originale bleiben in CSV und JSON des Pakets erhalten.Die rechtlichen Grundlagen im Detail — Gesetzesstellen, FAQ der MA 6, Judikatur zur 30-Nächte-Grenze — stehen im Werkzeug selbst unter „Wie gerechnet wird“.