Zu jeder Buchung lässt sich ein Beleg hinterlegen, im Formular oder nachträglich über die Buchungsliste. Der Medientyp wird aus der Dateisignatur bestimmt, nicht aus Endung oder Browserangabe: eine als rechnung.pdf benannte Textdatei wird abgewiesen. SVG ist ausgeschlossen, weil es Skripte enthalten kann, die beim Anzeigen ausgeführt würden. Ausgeliefert wird mit nosniff und enger Content-Security-Policy. Zur Aufbewahrungspflicht nach §147 AO werden Belege nie überschrieben und nie gelöscht. Beim Ersetzen bleibt die alte Datei liegen; das Lösen der Verknüpfung entfernt nur den Verweis. Der Dateiname trägt den SHA-256-Anfang, die Buchung den vollständigen Hash. Ablage unter data/belege/<mandant>/<jahr>/ – je Mandant getrennt und im selben Bind-Mount wie die Datenbanken, sodass ein Backup des data-Ordners Buchungen und Belege gemeinsam erfasst. Bisher fehlte jede Möglichkeit, bestehende Mandantendateien um neue Spalten zu erweitern: CREATE TABLE IF NOT EXISTS lässt vorhandene Tabellen unverändert. Neu hinzugekommene Spalten werden jetzt beim Öffnen additiv ergänzt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
344 lines
15 KiB
Markdown
344 lines
15 KiB
Markdown
# open-eur-webapp
|
||
|
||
Einnahmen-Überschuss-Rechnung nach §4 Abs. 3 EStG für Einzelunternehmen und
|
||
Freiberufler – mit Kleinunternehmerregelung, Umsatzsteuer-Voranmeldung,
|
||
BWA, ELSTER-Anbindung und DATEV-/GoBD-Export.
|
||
|
||
Die Anwendung läuft lokal: ein Node-Prozess, eine SQLite-Datei, keine
|
||
Cloud-Anbindung.
|
||
|
||
```bash
|
||
npm install
|
||
npm run seed # Beispieldaten mit Regimewechsel 2025 -> 2026
|
||
npm run dev # Backend (3311) + Frontend (5173) im Entwicklungsmodus
|
||
```
|
||
|
||
Für den Dauerbetrieb:
|
||
|
||
```bash
|
||
npm run build
|
||
npm start # liefert API und Oberfläche auf http://127.0.0.1:3311
|
||
```
|
||
|
||
`npm test` prüft die Steuerlogik (Regimewechsel, EÜR, UStVA, AfA,
|
||
Steuernummernumsetzung).
|
||
|
||
`npm run seed` bricht ab, wenn bereits Buchungen vorhanden sind – sonst würden
|
||
die Beispieldaten ein zweites Mal eingespielt und alle Auswertungen verdoppeln.
|
||
`npm run seed -- reset` setzt die Bewegungsdaten zurück und legt sie neu an;
|
||
dabei gehen auch selbst erfasste Buchungen verloren.
|
||
|
||
## Betrieb mit Docker
|
||
|
||
```bash
|
||
cp .env.beispiel .env # optional, für ELSTER
|
||
export UID GID=$(id -g) # damit ./data dem eigenen Konto gehört
|
||
docker-compose up -d --build
|
||
```
|
||
|
||
Danach läuft die Anwendung auf <http://localhost:3311>.
|
||
|
||
**Keine benannten Volumes.** Der einzige Mount ist `./data:/app/data` – die
|
||
Buchhaltung liegt als gewöhnliche SQLite-Dateien im Projektverzeichnis und lässt
|
||
sich sichern, kopieren und einsehen, ohne Docker zu bemühen. Der Container läuft
|
||
mit der UID des aufrufenden Kontos, deshalb gehören die Dateien nicht root.
|
||
|
||
Weitere Festlegungen:
|
||
|
||
- **Port nur auf 127.0.0.1 veröffentlicht.** Die Anwendung kennt keine
|
||
Benutzeranmeldung, und hier liegen Steuernummer und Umsätze. Für den Zugriff
|
||
aus dem Netz gehört ein Reverse Proxy mit Authentifizierung davor.
|
||
- Das Dateisystem des Containers ist schreibgeschützt (`read_only`), beschreibbar
|
||
ist nur `/app/data` und ein `tmpfs` unter `/tmp`.
|
||
- `.env` wird **nicht** ins Image gebacken. `docker-compose` liest die Datei für
|
||
die Variablenersetzung und reicht die Werte als Umgebungsvariablen weiter.
|
||
- **ERiC ist nicht enthalten** und darf es auch nicht sein. Wer echt übermitteln
|
||
will, hebt in `docker-compose.yml` die Zeilen für `/opt/eric` und das
|
||
Zertifikat auf und setzt die Pfade in `.env`.
|
||
|
||
Beim Start meldet der Container, woher die Konfiguration stammt und ob ERiC
|
||
eingebunden ist:
|
||
|
||
```
|
||
open-eur-webapp läuft auf http://localhost:3311
|
||
Konfiguration aus der Prozessumgebung.
|
||
ELSTER: Simulationsmodus, es wird nichts übermittelt (…).
|
||
Mandanten: standard – aktiv ohne Angabe: standard
|
||
```
|
||
|
||
Ohne Docker bindet sich der Server weiterhin an `127.0.0.1`; im Container setzt
|
||
`HOST=0.0.0.0`, weil die Portfreigabe den Prozess sonst nicht erreicht.
|
||
|
||
## Mandanten
|
||
|
||
Jeder Mandant hat **eine eigene SQLite-Datei** unter `data/mandanten/`. Das ist
|
||
Absicht: bei einer gemeinsamen Datenbank mit `mandant_id`-Spalte genügt ein
|
||
einziges vergessenes `WHERE`, damit Testbuchungen in der echten EÜR oder in
|
||
einer ELSTER-Übermittlung auftauchen. Getrennte Dateien machen das strukturell
|
||
unmöglich.
|
||
|
||
Umgeschaltet wird oben in der Seitenleiste. Die Anwendung lädt dabei neu, damit
|
||
keine Formularstände des vorherigen Mandanten stehen bleiben.
|
||
|
||
**Testmandanten** sind als solche markiert, auf jeder Seite sichtbar
|
||
gekennzeichnet und **können nichts an das Finanzamt übermitteln** – es sei denn,
|
||
`ELSTER_TESTMERKER` ist gesetzt, dann sind Probeübermittlungen ohne steuerliche
|
||
Wirkung möglich.
|
||
|
||
```bash
|
||
npm run seed -- --mandant=test # legt einen Testmandanten an und befüllt ihn
|
||
```
|
||
|
||
Technisch hängt der Mandant am Request: Header `X-Mandant`, für Download-Links
|
||
(die keine Header setzen können) alternativ `?mandant=<bezeichner>`. Der Server
|
||
führt den Handler in einem AsyncLocalStorage-Kontext aus, sodass jeder
|
||
Datenbankzugriff automatisch auf der richtigen Datei landet.
|
||
|
||
Beim Entfernen eines Mandanten wird die Datei **nicht gelöscht**, sondern mit
|
||
Zeitstempel beiseitegelegt (`….sqlite.entfernt-<zeit>`).
|
||
|
||
## Kontenrahmen: SKR03 und SKR04
|
||
|
||
Beide stehen zur Wahl – **beim Anlegen eines Mandanten**, danach nicht mehr.
|
||
Kontonummern und Bezeichnungen stammen aus den amtlichen DATEV-Kontenrahmen
|
||
(Stand 2026); die Zuordnung zu EÜR-Posten, BWA-Gruppen und UStVA-Kennzahlen ist
|
||
fachliche Auslegung dieser Anwendung und gehört vor dem ersten Export mit der
|
||
Steuerkanzlei abgestimmt.
|
||
|
||
Der Wechsel ist gesperrt, weil dieselbe Kontonummer in beiden Rahmen etwas
|
||
anderes bedeutet:
|
||
|
||
| Konto | SKR03 | SKR04 |
|
||
|---|---|---|
|
||
| 1800 | Privatentnahmen allgemein | **Bank** |
|
||
| 2100 | Zinsen und ähnliche Aufwendungen | Privatentnahmen allgemein |
|
||
| 4830 | Abschreibungen auf Sachanlagen | Sonstige betriebliche Erträge |
|
||
| 4970 | Nebenkosten des Geldverkehrs | Zinserträge |
|
||
|
||
Aus demselben Grund ist das Standard-Gegenkonto rahmenabhängig (SKR03 `1200`,
|
||
SKR04 `1800`) und das Feld `kontenrahmen` in den Stammdaten schreibgeschützt –
|
||
sonst könnte der DATEV-Export einen Rahmen melden, den die Buchungszeilen nicht
|
||
verwenden.
|
||
|
||
## Belege
|
||
|
||
Zu jeder Buchung lässt sich ein Beleg hinterlegen – per Durchsuchen-Schaltfläche,
|
||
Ziehen-und-Ablegen oder <kbd>Strg</kbd>+<kbd>V</kbd>. Nachträglich geht es über
|
||
die Schaltfläche „+ Beleg" in der Buchungsliste.
|
||
|
||
**Zum Einfügen mit Strg+V:** Screenshots liegen als Bild in der Zwischenablage
|
||
und funktionieren überall. Beim Kopieren einer *Datei* im Dateimanager hängt es
|
||
vom System ab: Windows und macOS legen die Datei selbst ab, dann klappt es auch
|
||
für PDFs. Unter Linux liefern viele Dateimanager nur einen `file://`-Verweis, den
|
||
der Browser aus Sicherheitsgründen nicht lesen darf – dort bleibt
|
||
Ziehen-und-Ablegen der verlässliche Weg für PDFs.
|
||
|
||
Zulässig sind PDF, JPEG, PNG, WebP, TIFF und HEIC bis 20 MB. **Der Typ wird aus
|
||
der Dateisignatur bestimmt, nicht aus Endung oder Browserangabe** – eine als
|
||
`rechnung.pdf` benannte Textdatei wird abgewiesen. SVG ist ausgeschlossen, weil
|
||
es Skripte enthalten kann, die beim Anzeigen ausgeführt würden.
|
||
|
||
Die Dateien liegen unter `data/belege/<mandant>/<jahr>/` und damit im selben
|
||
Bind-Mount wie die Datenbanken; ein Backup des `data`-Ordners erfasst Buchungen
|
||
und Belege gemeinsam. Zur Aufbewahrungspflicht (§147 AO): Belege werden nie
|
||
überschrieben und nie gelöscht. Wird ein Beleg ersetzt oder die Verknüpfung
|
||
gelöst, bleibt die Datei erhalten – der Dateiname enthält den SHA-256-Anfang, die
|
||
Buchung den vollständigen Hash.
|
||
|
||
## Anlagevermögen oder Sofortabzug
|
||
|
||
Ob eine Ausgabe abzuschreiben ist, entscheidet das **Konto** (Kontotyp `anlage`),
|
||
nicht der Betrag – eine Werbekampagne für 3.000 € bleibt sofort abziehbar, ein
|
||
Drucker für 900 € nicht. Eine Buchung auf ein Anlagekonto wird deshalb immer
|
||
aktiviert; ein stiller Vollabzug ist nicht möglich.
|
||
|
||
Der Betrag entscheidet erst über die **Methode** (bei Kleinunternehmern zählt der
|
||
Bruttobetrag, weil die Umsatzsteuer dann zu den Anschaffungskosten gehört):
|
||
|
||
| Anschaffungskosten | Behandlung |
|
||
|---|---|
|
||
| ≤ 800 € | GWG, im Anschaffungsjahr voll abgesetzt (§6 Abs. 2 EStG) |
|
||
| ≤ 1.000 € | Sammelposten, über 5 Jahre aufgelöst (§6 Abs. 2a EStG) |
|
||
| > 1.000 € | linear über die Nutzungsdauer (§7 Abs. 1 EStG) – Nutzungsdauer ist Pflichtangabe |
|
||
|
||
Buchungen auf Anlagekonten ohne hinterlegtes Anlagegut (etwa aus einer früheren
|
||
Fassung) werden in der EÜR gesondert als prüfbedürftig ausgewiesen. Die Zahlen
|
||
werden dabei bewusst *nicht* stillschweigend korrigiert.
|
||
|
||
## Konfiguration
|
||
|
||
`.env.beispiel` nach `.env` kopieren und anpassen. Die Datei wird beim Start
|
||
automatisch gelesen; bereits gesetzte Umgebungsvariablen haben Vorrang.
|
||
**Relative Pfade darin beziehen sich immer auf das Projektverzeichnis**, nicht
|
||
auf das Verzeichnis, aus dem der Prozess gestartet wurde – sonst entstünden je
|
||
nach Startort verschiedene Datenbanken.
|
||
|
||
Ohne `.env` läuft alles außer der ELSTER-Übermittlung; diese arbeitet dann im
|
||
Simulationsmodus.
|
||
|
||
## Wenn der Start fehlschlägt
|
||
|
||
**`EMFILE: too many open files, watch ...`** (Linux, betrifft `tsx watch` und
|
||
Vite gleichermaßen)
|
||
|
||
Nicht das Dateideskriptor-Limit, sondern die Zahl der erlaubten
|
||
inotify-*Instanzen* ist erschöpft – typischerweise, weil VS Code und andere
|
||
Werkzeuge sie bereits aufgebraucht haben:
|
||
|
||
```bash
|
||
cat /proc/sys/fs/inotify/max_user_instances # meist 128
|
||
find /proc/*/fd -lname anon_inode:inotify | wc -l # tatsächlich belegt
|
||
```
|
||
|
||
Dauerhafte Abhilfe:
|
||
|
||
```bash
|
||
sudo sysctl -w fs.inotify.max_user_instances=1024
|
||
echo 'fs.inotify.max_user_instances=1024' | sudo tee /etc/sysctl.d/60-inotify.conf
|
||
```
|
||
|
||
Ohne Root-Rechte stattdessen:
|
||
|
||
```bash
|
||
npm run dev:polling # Server ohne Watcher, Vite mit Polling statt inotify
|
||
```
|
||
|
||
Der Server startet dann nicht automatisch neu – nach Änderungen am Backend
|
||
manuell neu starten.
|
||
|
||
---
|
||
|
||
## Der Kern: die Zeitachse der Besteuerungsform
|
||
|
||
Der Wechsel von der Kleinunternehmerregelung zur Regelbesteuerung ist nicht als
|
||
Schalter umgesetzt, sondern als **Zeitachse**:
|
||
|
||
| Gültig ab | Besteuerungsform |
|
||
|---|---|
|
||
| 15.01.2024 | Kleinunternehmer (§19 UStG) |
|
||
| 01.01.2026 | Regelbesteuerung |
|
||
|
||
Jede Buchung wird nach dem Regime behandelt, das **zu ihrem Leistungsdatum**
|
||
galt. Daraus folgt:
|
||
|
||
- Altbuchungen bleiben unverändert, wenn ein Wechsel eingetragen wird.
|
||
- Ein **unterjähriger** Wechsel ist möglich – nötig etwa beim Überschreiten der
|
||
100.000-EUR-Grenze, seit 2025 endet die Kleinunternehmerregelung dann sofort
|
||
mit dem überschreitenden Umsatz (§19 Abs. 1 Satz 3 UStG).
|
||
- Ein Jahr mit Wechsel weist Umsätze davor und danach in der EÜR getrennt aus.
|
||
|
||
Die Umsatzgrenzen werden laufend überwacht (25.000 EUR Vorjahr / 100.000 EUR
|
||
laufendes Jahr ab 2025; 22.000 / 50.000 bis 2024) und melden sich, bevor der
|
||
Wechsel fällig wird.
|
||
|
||
Die Regeln stehen an einer Stelle: [`server/src/domain/regime.ts`](server/src/domain/regime.ts).
|
||
|
||
## Was die Anwendung kann
|
||
|
||
**Erfassung**
|
||
- Buchungen mit Beleg-, Leistungs- und Zahlungsdatum; jedes Datum hat eine
|
||
eigene steuerliche Aufgabe (Regime, Soll-Versteuerung, Zuflussprinzip).
|
||
- Live-Vorschau: das Backend entscheidet über Steuersatz, Behandlung und
|
||
Vorsteuerabzug – nicht das Formular.
|
||
- Reverse Charge (§13b), innergemeinschaftliche Erwerbe und Lieferungen,
|
||
steuerfreie und nicht steuerbare Umsätze.
|
||
- Privatanteile, Bewirtung (70 %), offene Posten.
|
||
- Anlagevermögen mit linearer AfA zeitanteilig, GWG-Sofortabschreibung und
|
||
Sammelposten; Kleinunternehmer schreiben vom Bruttobetrag ab.
|
||
- GoBD: keine Löschung, sondern Storno per Gegenbuchung; Festschreibung;
|
||
Änderungsprotokoll.
|
||
|
||
**Auswertungen**
|
||
- **EÜR** in der Gliederung der Anlage EÜR, mit Anlagenverzeichnis.
|
||
- **UStVA** mit den amtlichen Kennzahlen, Ist- oder Soll-Versteuerung,
|
||
monatlich oder vierteljährlich, mit Aufriss auf die Einzelbuchungen.
|
||
- **BWA** im Aufbau der BWA Form 01: Monat, kumuliert, Vorjahresvergleich,
|
||
Quoten – wahlweise nach Zahlungs- oder Leistungszeitpunkt.
|
||
|
||
**Ausgabe**
|
||
- **ELSTER** über ERiC (siehe unten).
|
||
- **DATEV** Buchungsstapel im EXTF-Format (Version 700, Formatversion 13) plus
|
||
Kontenbeschriftungen – das Paket für die Steuerkanzlei.
|
||
- **GoBD-Datenträgerüberlassung** (GDPdU, Format Z3) mit `index.xml` und CSV –
|
||
das Paket für die Betriebsprüfung.
|
||
- EÜR und BWA zusätzlich als CSV.
|
||
|
||
## ELSTER-Übermittlung
|
||
|
||
**Ohne ERiC ist keine Übermittlung möglich – auch nicht mit einem eigenen
|
||
XML-Versand.** ERiC ist die einzige zugelassene Schnittstelle zur
|
||
Finanzverwaltung. Die Bibliothek ist proprietär, darf nicht weitergegeben
|
||
werden und ist deshalb hier nicht enthalten.
|
||
|
||
So wird sie eingebunden:
|
||
|
||
1. Im [ELSTER-Entwicklerportal](https://www.elster.de/elsterweb/entwickler)
|
||
registrieren und ERiC für die eigene Plattform herunterladen.
|
||
2. `.env.beispiel` nach `.env` kopieren und ausfüllen:
|
||
`ERIC_LIB_PATH`, `ERIC_PLUGIN_PATH`, `ELSTER_ZERTIFIKAT`, `ELSTER_PIN`,
|
||
`ELSTER_HERSTELLER_ID`.
|
||
3. Erste Versuche mit `ELSTER_TESTMERKER=700000004` – solche Übermittlungen
|
||
haben keine steuerliche Wirkung.
|
||
|
||
Ist ERiC nicht eingerichtet, läuft die Anwendung im **Simulationsmodus**: das
|
||
XML wird erzeugt, auf Wohlgeformtheit und Vollständigkeit geprüft und unter
|
||
`data/elster-ausgang/` abgelegt. Es wird nichts gesendet, und die Oberfläche
|
||
sagt das ausdrücklich.
|
||
|
||
Die Übermittlung ist verbindlich und nicht zurückzunehmen. Der Endpunkt
|
||
verlangt deshalb `"bestaetigt": true`, die Oberfläche eine Rückfrage.
|
||
|
||
Die Steuernummer wird automatisch vom Länderformat (`123/456/78901`) in das
|
||
13-stellige Bundesschema umgesetzt, das ELSTER erwartet – für alle 16
|
||
Bundesländer, siehe [`server/src/elster/steuernummer.ts`](server/src/elster/steuernummer.ts).
|
||
|
||
## Was vor dem Produktiveinsatz zu prüfen ist
|
||
|
||
Die Rechenwege sind mit Tests abgesichert, aber einige Angaben ändern sich
|
||
jährlich und müssen gegen die amtlichen Vorgaben abgeglichen werden:
|
||
|
||
- **Zeilennummern der Anlage EÜR** – sie ändern sich fast jedes Jahr. Der
|
||
fachliche Aufbau ist stabil, nur die Nummerierung nicht. Gepflegt werden sie
|
||
in `ZEILEN_NUMMERN` in
|
||
[`server/src/domain/euerZeilen.ts`](server/src/domain/euerZeilen.ts), getrennt
|
||
von der Berechnung. Stand: an der Fassung 2024/2025 orientiert.
|
||
- **ELSTER-Schemaversion** – die Datenart-Version (`UStVA_<Jahr>`) und der
|
||
Aufbau des Nutzdatenblocks müssen zu der Version passen, die im installierten
|
||
ERiC steckt. Der erste Lauf sollte mit gesetztem Testmerker erfolgen.
|
||
- **DATEV-Formatversion** – der Export folgt Version 700 / Formatversion 13.
|
||
Vor der ersten Übergabe eine Probedatei mit der Kanzlei einlesen.
|
||
- **Kontenrahmen** – `server/src/kontenrahmen/skr04.ts` ist ein praxisnaher
|
||
Auszug aus dem SKR04, kein vollständiger Kontenrahmen. Konten lassen sich in
|
||
der Oberfläche ergänzen und umwidmen.
|
||
|
||
Die Anwendung ersetzt keine steuerliche Beratung. Für die Richtigkeit der
|
||
Erklärung bleibt die abgebende Person verantwortlich.
|
||
|
||
## Aufbau
|
||
|
||
```
|
||
server/src/
|
||
db.ts SQLite-Schema, Änderungsprotokoll
|
||
domain/
|
||
regime.ts Zeitachse der Besteuerungsform, USt-Ermittlung, §19-Grenzen
|
||
euer.ts Einnahmen-Überschuss-Rechnung
|
||
euerZeilen.ts Gliederung + Zeilennummern je Veranlagungsjahr
|
||
ustva.ts Voranmeldung, Kennzahlen, Ist/Soll
|
||
bwa.ts BWA Form 01
|
||
afa.ts lineare AfA, GWG, Sammelposten
|
||
geld.ts Cent-Arithmetik, kaufmännische Rundung
|
||
elster/
|
||
steuernummer.ts Länderformat -> Bundesschema (alle 16 Länder)
|
||
ustvaXml.ts ELSTER-Datenteil für die UStVA
|
||
eric.ts ERiC-Anbindung über koffi + Simulationsmodus
|
||
export/
|
||
datev.ts EXTF-Buchungsstapel und Kontenbeschriftungen
|
||
gdpdu.ts GoBD-Datenträgerüberlassung
|
||
zip.ts minimaler ZIP-Writer
|
||
routes/ HTTP-Schnittstelle
|
||
web/src/ Oberfläche (React, Vite)
|
||
```
|
||
|
||
Alle Beträge werden als ganzzahlige Cent geführt, Steuersätze als Basispunkte
|
||
(1900 = 19 %). Damit gibt es keine Fließkommafehler in der Steuerberechnung.
|