Hallo zusammen,
vorab kurz zu meinem Hintergrund, damit ihr meine Meldung besser einordnen könnt: Ich bin kein hauptberuflicher Entwickler, sondern baue gerade mein eigenes kleines Geschäft auf. Da ich mit den bestehenden Lösungen unzufrieden war, habe ich angefangen, mir nebenbei eine eigene kleine Buchhaltungssoftware zu basteln.
Bei meinen Recherchen bin ich auf RechnungsFee gestoßen – ich finde euer Projekt wirklich großartig und möchte gerne einen kleinen Beitrag zu der Entwicklung leisten! Beim Testen und Vergleichen bin ich eher zufällig über ein paar Punkte gestolpert, die ich danach gezielt nachgestellt habe. Ich melde diese hier, da mehrere davon für meinen eigenen Anwendungsfall relevant wären.
================================================================
A. REPRODUZIERT
- UStVA: die Zahllast berücksichtigt KZ 62 nicht
-------------------------------------------------
Biespieldaten, die ich benutzt habe.
Schritte:
- Einnahme 1.190,00 EUR brutto, 19 % -> KZ 81 = 1.000,00 EUR
- Ausgabe über die Kategorie "Einfuhrumsatzsteuer (Zoll/DHL)", Betrag 50,00 EUR
-> KZ 62 = 50,00 EUR
- UStVA für den Zeitraum berechnen
Ergebnis:
Die Zeile KZ 62 wird korrekt mit 50,00 EUR ausgewiesen. Die angezeigte
Umsatzsteuer-Vorauszahlung beträgt trotzdem 190,00 EUR. Richtig wären 140,00 EUR.
Das PDF derselben Voranmeldung weist korrekt 140,00 EUR aus. Anzeige und PDF
widersprechen sich also bei identischer Datenlage. Der über "Speichern" abgelegte
Wert ist der falsche Wert aus der Anzeige, nicht der aus dem PDF.
Gegenprobe: mit 190,00 EUR statt 50,00 EUR Einfuhrumsatzsteuer bleibt die
angezeigte Zahllast unverändert bei 190,00 EUR - die Kennzahl wird also gar nicht
einbezogen, es ist kein Rundungsthema.
Vermutete Ursache:
- api/ustva.py:436 -> vst = kz["kz_66"] + kz["kz_61"] + kz["kz_62"] + kz["kz_67"]
- UStVAPage.tsx, berechneZahllast() -> ['66','61','67'].reduce(...)
KZ 62 fehlt im Frontend.
- §13b Abs. 2 wird als Abs. 1 vorbelegt (KZ 46/47 statt 84/85)
---------------------------------------------------------------
Beispieldaten
Schritte:
Neue Ausgabe-Buchung anlegen und eine Kategorie mit konto_skr03 = 3125
("Drittland-Dienstleistungen (§13b Abs. 2)") wählen.
Ergebnis:
Das Feld "Steuerlicher Sonderfall" springt auf
"§13b Abs. 1 - EU-Dienstleistungen -> KZ 46/47/67".
Erwartet wäre "§13b Abs. 2 -> KZ 84/85/67". Gebucht landet der Vorgang in KZ 46/47.
Vermutete Ursache:
BuchungForm.tsx ordnet 3125/5925 dem Zweig 13b_abs1 zu. In journal.py:357 ist genau
das bereits korrigiert, mit Kommentar zu Issue #375 ("3125/5925
(Drittland-Dienstleistungen) ist §13b Abs. 2, nicht Abs. 1"). Die Korrektur greift
aber nicht, weil sie unter "if not data.ust_sonderfall" steht und das Formular das
Feld immer mitsendet - der Wert aus dem Frontend gewinnt.
Die Zahllast ändert sich dadurch nicht, aber KZ 46 ist die Kennzahl, die gegen die
ZM-Daten der EU abgeglichen wird.
- Split-Buchung: der Sonderfall geht ganz verloren
---------------------------------------------------
Beispieldaten
Schritte:
Split-Buchung mit Position 1 = obige §13b-Abs.-2-Kategorie, Position 2 = Bürobedarf.
Ergebnis:
Position 1 bekommt gar keinen Sonderfall - weder 46/47 noch 84/85 - und taucht in
der UStVA nicht auf.
Vermutete Ursache:
In create_split_buchung (journal.py ab Z. 745) fehlt ein Zweig für 3125/5925
vollständig. Zusätzlich wird dort noch über
_sf_konten = (kat.konto_skr03, kat.konto_skr04) als gemeinsames Tupel geprüft - also
genau die Form, die laut Kommentar zu Issue #326 mehrdeutig ist, weil Kontonummern
zwischen SKR03 und SKR04 nicht eindeutig sind.
- E-Rechnungs-Import: "Rechnung erstellen" bleibt wirkungslos bei reiner XML-Datei
-----------------------------------------------------------------------------------
Schritte:
Eine standalone XRechnung/CII-XML-Datei per Drag & Drop in den
Eingangsrechnungs-Import ziehen.
Ergebnis:
Die Analyse läuft durch, Lieferant, Beträge und Positionen werden korrekt angezeigt.
Ein Klick auf "Rechnung erstellen ->" bewirkt nichts: keine Reaktion, keine
Fehlermeldung. Über den Datei-Auswahl-Dialog statt Drag & Drop funktioniert derselbe
Vorgang.
Vermutete Ursache:
Der Button ruft "if (datei) onWeiter(ergebnis, datei)". Im tauri://drag-drop-Handler
wird setDatei(...) aber nur innerhalb von "if (res.temp_url)" gesetzt. Eine reine
XML-Datei hat kein PDF und damit keine temp_url, datei bleibt null, der Klick läuft
ins Leere.
- Kalender schließt sich nach der Auswahl nicht
------------------------------------------------
Nach Klick auf das Kalendersymbol und Auswahl eines Datums wird das Datum
übernommen, das native Auswahlfeld bleibt aber offen stehen. Betrifft alle
Datumsfelder.
In DateInput.tsx hängt showPicker() an einem versteckten <input type="date"> mit
"w-0 h-0 opacity-0 pointer-events-none". Der native Picker ist damit an einem
0x0-Element ohne Pointer-Events verankert.
- GoBD: der Integritätsnachweis funktioniert in beide Richtungen nicht
-----------------------------------------------------------------------
Das ist aus meiner Sicht der schwerwiegendste Punkt. Ich habe ihn zweimal an eigenen
Echtdaten nachgestellt, ohne die Datenbank jemals von außen anzufassen.
Schritte:
- Zehn Journalbuchungen ganz normal über die Oberfläche anlegen (teils einzeln,
teils als Split-Buchung, teils über die Korrektur-Funktion mit Storno).
- GoBD-Export für das Wirtschaftsjahr erzeugen.
- RechnungsFee schließen und wieder starten - ohne einen einzigen Datensatz
anzufassen.
- GoBD-Export erneut erzeugen.
Ergebnis, erster Export:
"NICHT BESTANDEN: 6 Datensaetze haben ungueltige Signaturen.
Die Daten koennten manipuliert worden sein."
6 von 10 Einträgen UNGÜLTIG. Manipuliert wurde nichts - alle Buchungen stammen aus
der Oberfläche.
Auffällig ist, welche vier gültig waren: der älteste Eintrag und die drei
STORNO-Buchungen. Storno-Einträge übernehmen ihre Felder aus einem bereits aus der
Datenbank geladenen Original, während neu erfasste Buchungen aus den Formularwerten
im Speicher signiert werden, bevor sie geschrieben werden. Das deutet für mich
darauf hin, dass die Signatur den Weg in die Datenbank und zurück nicht unverändert
übersteht - ich konnte das aber nicht abschließend verifizieren.
Ergebnis, zweiter Export nach reinem Neustart ohne Datenänderung:
"Gültige Signaturen 10 / Ungültige Signaturen 0"
_migrate_signaturen() hat die sechs Signaturen beim Start neu berechnet und
überschrieben - ohne Protokoll, ohne Hinweis, ohne dass irgendwo festgehalten wird,
welche Datensätze betroffen waren.
Der Integritätsnachweis versagt dadurch in beide Richtungen:
- Falsch positiv: ein sauberer Export bescheinigt dem Steuerpflichtigen mögliche
Manipulation. Genau das würde man einer Betriebsprüfung vorlegen.
- Falsch negativ: eine tatsächliche Änderung an der Journal-Tabelle würde beim
nächsten Start auf exakt demselben Weg repariert und danach als GÜLTIG
ausgewiesen.
Das Programm kann zwischen "unsere eigene Signatur war instabil" und "jemand hat
einen Betrag geändert" nicht unterscheiden, beides wird gleich behandelt.
Weitere Beobachtungen dazu:
- _migrate_signaturen() entfernt beim Start alle vier Schutz-Trigger und
_setup_gobd_triggers() legt sie danach neu an. Beide Aufrufe stehen in startup()
ohne try/except: schlägt die Signatur-Migration fehl, werden die Trigger nicht
wieder angelegt und die Datenbank bleibt ungeschützt.
- Der Docstring sagt "In beiden Fällen werden keine Buchungsdaten geändert - nur der
Hash wird neu berechnet". Der Block direkt darunter (Issue #132) setzt jedoch
kategorie_id auf immutable-Einträgen - ein signiertes Feld, das die EÜR-Zeile
bestimmt.
- Es gibt keine Signaturverkettung: jede Signatur deckt nur den eigenen Datensatz
ab. Das Löschen eines kompletten Eintrags ist dadurch nicht feststellbar, da
nichts darauf verweist.
- Eine Änderungsprotokoll-Tabelle konnte ich im Modell nicht finden. Für
Nutzerkorrekturen ist der Storno-Weg sauber gelöst, für die Umschreibungen beim
Start gibt es keinerlei Nachweis.
Der Prüfbericht widerspricht sich außerdem selbst:
- Seite 1: "Die enthaltenen Journaldaten sind unveraenderbar (immutable) gespeichert
und durch SHA-256-Signaturen gesichert."
- Seite 3: "NICHT BESTANDEN ... Die Daten koennten manipuliert worden sein."
- Seite 4: "Alle in diesem ZIP-Archiv enthaltenen Daten wurden unveraendert aus der
RechnungsFee-Datenbank exportiert."
- Split-Buchung: falscher Steuerbefreiungsgrund und Vorsteuer ohne Grundlage
-----------------------------------------------------------------------------
Schritte:
Split-Buchung mit einer Position auf "Drittland-Dienstleistungen (§13b Abs. 2)"
(Kategorie mit konto_skr03 = 3125), Betrag 100,00 EUR, 19 %.
Ergebnis aus meinem Export (Ist -> Erwartet):
Steuerbefreiung "§4 Nr. 1b UStG" -> (keine)
Sonderfall (leer) -> 13b_abs2
Netto 84,03 -> 100,00
USt 0,00 -> 19,00 (KZ 84/85)
Vorsteuer 15,97 -> 19,00 (KZ 67)
§ 4 Nr. 1b UStG ist die Befreiung für ausgehende innergemeinschaftliche Lieferungen
und steht hier auf einer Eingangsleistung aus dem Drittland.
Ursache scheint mir journal.py:745:
if kat and kat.konto_skr03 in ("8125", "3125") and not steuerbefreiung_grund:
steuerbefreiung_grund = "§4 Nr. 1b UStG"
Das ist die gemischte Prüfung, vor der der Kommentar zu Issue #326 warnt - in
_felder_aus_data ist sie korrigiert, in create_split_buchung nicht. Die Kategorie
"Innergemeinschaftliche Lieferungen" hat konto_skr04 = 3125,
"Drittland-Dienstleistungen (§13b Abs. 2)" hat konto_skr03 = 3125 - beide werden von
dieser Bedingung erfasst.
Praktisch werden die 100,00 EUR dadurch als Bruttobetrag behandelt statt als
Bemessungsgrundlage des Reverse Charge: es wird keine Umsatzsteuer angemeldet und
gleichzeitig 15,97 EUR Vorsteuer gezogen, die nie jemand in Rechnung gestellt hat.
In meinem Prüfbericht fließt das in "Abziehbare Vorsteuer (Ausgaben) 287,90 EUR" und
damit in die ausgewiesene rechnerische Zahllast ein.
Bei Rückfragen zu einzelnen Punkten gerne melden.
Viele Grüße und danke für die tolle Arbeit!