E-Factura MwSt berechnen in TypeScript und React

Dein Rechnungseditor zeigt einen MwSt.-Betrag, das PDF stimmt damit überein, und das XML, das du bei ANAF hochlädst, muss trotzdem einer Regel folgen, die zeilenweise rechnender Code übersieht. Wer die e-Factura MwSt berechnen will, gruppiert die Positionen nach MwSt.-Satz, addiert ihre Nettobeträge und rundet die MwSt. einmal pro Gruppe. Jede Summe in der Datei muss dann aus diesen gerundeten Zahlen kommen, auf den Cent genau. Dieses Tutorial baut das in TypeScript: die MwSt-Sätze in Rumänien mit ihren Stichtagen, die MwSt pro Steuersatz in ganzzahligen Cent, die Validierungsregeln, die Summen vergleichen, und einen Editor für Rechnungspositionen in React. Die UI ist Invoice Line Items von Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral. Die Rechnung pro Satz ist reines TypeScript, das du überall einfügen kannst.
Welche MwSt.-Sätze gelten 2026 in Rumänien?
Zwei Sätze, beide seit dem 1. August 2025: 21 % als Normalsatz und 11 % als ermäßigter Satz. Gesetz 141/2025 hob den Normalsatz von 19 % an und ersetzte die alten ermäßigten Sätze von 9 % und 5 % durch einen einzigen Satz von 11 %. Ausgenommen ist ein Übergangssatz von 9 % für bestimmte Wohnungen, die über bis zum 1. August 2025 unterschriebene Verträge gekauft wurden; Gesetz 161/2026 hat ihn vom 31. Juli auf den 30. September 2026 verlängert (Contzilla, August 2026 (opens in a new tab)). Die Vorher-nachher-Tabelle des Steuergesetzbuchs von ANAF (opens in a new tab) zeigt beide Fassungen von Artikel 291. Stand Oktober 2026 nennt das konsolidierte Steuergesetzbuch von ANAF (opens in a new tab), zuletzt durch Gesetz 170/2026 geändert, weiterhin 21 % und 11 %.
Der Code ist die MwSt.-Kategorie, die eine e-Factura-Position trägt (BT-151): S für den Normalsatz, der auch den ermäßigten Satz abdeckt, E für steuerbefreit, AE für Reverse Charge und K für innergemeinschaftliche Lieferungen.
Für den Code zählen zwei Details. Die 11-%-Liste hat eigene Ausnahmen: Alkoholische Getränke, alkoholfreie Getränke unter dem KN-Code 2202, Lebensmittel mit zugesetztem Zucker und mindestens 10 g Zucker pro 100 g sowie Nahrungsergänzungsmittel bleiben bei 21 %. Speichere den Satz also an jedem Produkt. Und der Satz hängt von einem Datum ab: Artikel 291 (4) wendet den Satz an, der beim Steuertatbestand gilt, meist bei der Lieferung. Eine Korrektur, die du heute für eine Lieferung vom Juli 2025 ausstellst, trägt 19 %. Halte die Sätze deshalb mit Datum vor und lass das MwSt.-Select den alten Satz anbieten, wenn eine Korrektur ihn braucht:
RO_VAT_RATES aus der Lib invoicing enthält die aktuellen fünf Zeilen der Tabelle oben, und der Positionseditor nimmt über seinen Prop vatRates jede Liste an.
Warum wird die MwSt. bei e-Factura pro Satz statt pro Position berechnet?
Weil der Standard es so festlegt. Regel BR-CO-17 der EN 16931, die e-Factura anwendet, sagt sinngemäß: Der Steuerbetrag einer MwSt.-Kategorie (BT-117) ist ihr steuerpflichtiger Betrag (BT-116) mal dem Satz (BT-119), gerundet auf zwei Nachkommastellen. Das Steuergesetzbuch verlangt auf Papier dasselbe: Artikel 319 (20) fordert die Bemessungsgrundlage für jeden Satz und den MwSt.-Betrag in Lei für jeden Satz. Wer gerundete MwSt. pro Position addiert, bekommt eine andere MwSt. pro Satz, sobald mehrere Positionen in dieselbe Richtung runden.
Drei Positionen mit 21 % und eine mit 11 % zeigen es:
Jede Position rundet ihren halben Cent auf, dreimal hintereinander; der Satz rundet einmal. 7,25 ist die MwSt. für 21 % auf dieser Rechnung, und die Zahlen pro Position dürfen als Information auf dem PDF bleiben.
Wie berechne ich die MwSt. pro Satz in TypeScript ohne Float-Fehler?
Rechne Geldbeträge in ganzzahligen Cent und teile ganze Zahlen. Math.round(22.5 * 0.21 * 100) / 100 liefert 4,72, weil 22,5 x 0,21 als 4,7249999999999996 gespeichert wird. Wir haben jede Bemessungsgrundlage von 0,01 bis 1.000,00 Lei durchgerechnet: Bei 1.000 davon endet die MwSt. von 21 % auf genau einem halben Cent, und die Float-Formel rundet 188 davon in die falsche Richtung, während toFixed(2) 718 falsch macht. Ist der Zähler eine ganze Zahl, ist ein halber Cent genau ,5:
Eine Stornoposition trägt negative Beträge. Weil der Absolutwert gerundet wird, bleibt -7,245 bei -7,25, also beim selben Betrag, den der Validator berechnet, denn BR-CO-17 vergleicht Absolutwerte.
Welche e-Factura-Validierungsfehler prüfen die Summen?
BR-CO-10, BR-CO-11, BR-CO-13, BR-CO-14 und BR-CO-15 vergleichen die Dokumentsummen auf den Cent, und nur BR-S-08, BR-CO-17 und BR-S-09 lassen Spielraum: weniger als eine Einheit der Rechnungswährung. Wir haben die Regeln im CIUS-RO-Validierungspaket gelesen, das das rumänische Finanzministerium für e-Factura veröffentlicht, ro16931-ubl 1.0.9 (opens in a new tab). Es bettet die Regeln der EN 16931 aus den Validierungsartefakten der Europäischen Kommission (opens in a new tab) ein (Version 1.3.8; die Toleranzen unten haben wir im Oktober 2026 in diesem Release noch einmal geprüft):
BR-CO-17 akzeptiert eine MwSt., die weniger als 1 Leu (eine Einheit der Rechnungswährung) von Bemessungsgrundlage mal Satz abweicht. Gerundete MwSt. pro Position aufzusummieren besteht diese Regel deshalb auf den meisten Rechnungen: Jede Position weicht höchstens um einen halben Cent ab, es braucht also etwa 200 Positionen, die in dieselbe Richtung runden, um 1 Leu zu erreichen. Die e-Factura Validierungsfehler entstehen, wenn du beide Wege mischst: BT-117 aus der Aufschlüsselung pro Satz, während BT-110 die Summe der MwSt. pro Position ist, oder BT-112 als Summe der Bruttobeträge der Positionen. Diese Summen haben keine Toleranz, jede Regel oben ist als fatal markiert, und ein Cent Unterschied heißt, dass statt einer validierten Rechnung eine Fehlermeldung zurückkommt. Bau auch nicht auf den Spielraum. Dein PDF, deine Datenbank und die Buchhaltung des Käufers sollten alle 7,25 zeigen.
Wie mache ich aus den Summen e-Factura-Felder?
Leite jedes Feld aus denselben ganzen Zahlen ab, in einer festen Reihenfolge: Positionsnettobeträge, der Dokumentrabatt pro Satz, Bemessungsgrundlage und MwSt. jedes Satzes, dann die Dokumentsummen. Ein Rabatt auf die ganze Rechnung ist der Schritt, an dem Rechnungen mit gemischten Sätzen scheitern. BR-S-08 zählt nur die Abschläge mit derselben Kategorie und demselben Satz wie die Zeile, also wird ein Rabatt von 100 Lei auf einer Rechnung mit Positionen zu 21 % und 11 % zu zwei Abschlägen, einem pro Satz.
computeInvoiceTotals aus der Lib invoicing verteilt einen Dokumentrabatt bereits nach Wert auf die Positionen, und die letzte Position nimmt den Rest, damit die Cent-Beträge aufgehen. Die Funktion unten lässt den eigenen Rabatt jeder Position in BT-131, liest den Anteil am Dokumentrabatt pro Satz wieder heraus und berechnet die MwSt. jedes Satzes in Cent neu:
Nimm die Beispielrechnung aus der Editor-Demo weiter unten: 25 Lizenzen zu 250 Lei mit 10 % Rabatt, 6 Stunden Schulung zu 320 Lei und eine e-Factura-Integration zu 1.500 Lei, alles zu 21 %, dazu 12 gedruckte Handbücher zu 64,90 Lei zu 11 %. Mit einem Dokumentrabatt von 100 Lei liefert die Funktion Abschläge von 92,07 Lei bei 21 % und 7,93 Lei bei 11 %, Bemessungsgrundlagen von 8.952,93 und 770,87, MwSt. von 1.880,12 und 84,80 und insgesamt 11.688,72 Lei.
$ npx wingo-ui@latest add invoicingPrüf dann die Zahlen, die das XML tragen wird, egal woher sie kommen: aus einem Altsystem, einem gespeicherten Entwurf, einer Änderung von Hand. Die Prüfung läuft auf dem Server direkt vor dem Erzeugen der Datei und antwortet mit den Regel-IDs, die der Validator von ANAF verwendet:
Wie baue ich die bearbeitbaren Rechnungspositionen in React?
Gib dem Editor die Positionen, lies die Summen, die er berechnet, und rechne sie auf dem Server neu. Invoice Line Items bearbeitet die Positionen wie eine kleine Tabellenkalkulation: eine Produktsuche, die Einheit, Preis und MwSt. ausfüllt, ein MwSt.-Select mit RO_VAT_RATES als Standard, ein Rabatt pro Position mit Umschalter zwischen % und Lei, und Drag-and-drop zum Umsortieren. Die Preiszellen sind ein Currency Input, also wird 1234.5 beim Tippen zu 1,234.5 und beim Verlassen des Felds zu 1,234.50. Unter den Positionen zeigen die Summen die Bemessungsgrundlage, eine MwSt.-Zeile pro Satz und den Gesamtbetrag, alles aus einem Aufruf von computeInvoiceTotals bei jedem Tastendruck, und onValueChange(lines, totals) gibt dir dieselben Zahlen. Ist der Editor schmaler als 768px, wird jede Position zur Karte: Tipp sie an, um die Position auf Smartphones in einem Bottom Sheet zu bearbeiten (auf größeren Screens in einem Dialog), und die Summen klappen zu einer Zeile „Total“ zusammen, die die Aufschlüsselung öffnet.
$ npx wingo-ui@latest add invoice-line-itemsIn einem Formular schreibt der Prop name des Editors die Positionen als JSON in ein verstecktes Input, sodass eine Server Action sie mit dem Rest des Formulars bekommt:
Den ganzen Screen drumherum, mit Kundenauswahl und CUI-Abfrage, Serie und Nummer, Fälligkeiten, einer A4-Vorschau und dem e-Factura-Schalter, liefert der Block Invoice Page. Sein onIssue bekommt das Dokument mit lines und documentDiscount, den beiden Eingaben, die toEFacturaTotals braucht.
Was ändert sich bei Rechnungen in EUR und Positionen mit 0 %?
Die Rechnung darf in Euro sein; ihre MwSt. muss es aber auch in Lei geben. Artikel 319 (23) des Steuergesetzbuchs erlaubt für die Beträge jede Währung, solange die MwSt. in Lei zum Wechselkurs nach Artikel 290 angegeben ist. Im XML legt Regel BR-RO-030 auf jeder Rechnung, die nicht in RON ausgestellt ist, RON als Abrechnungswährung der MwSt. (BT-6) fest, und BR-53 verlangt dann die MwSt.-Summe in dieser Währung (BT-111). Die Invoice Page zeigt den RON-Gegenwert einer EUR-Summe zum BNR-Kurs; berechne BT-111 auf dem Server aus derselben MwSt. pro Satz.
Die Zeilen mit 0 % brauchen mehr als eine Null:
- Steuerbefreit (E): ein Code oder Text für den Befreiungsgrund auf der Aufschlüsselungszeile (BR-E-10) und eine MwSt. von 0 (BR-E-09).
- Reverse Charge (AE): der Grund „Reverse charge“ (BR-AE-10), eine MwSt. von 0 (BR-AE-09), die USt-IdNr. des Verkäufers und die USt-IdNr. oder Registriernummer des Käufers (BR-AE-02).
- Innergemeinschaftlich (K): der Grund „Intra-community supply“ (BR-IC-10) und die USt-IdNr. von Verkäufer und Käufer (BR-IC-02).
Für die IDs, die diese Regeln verlangen, gibt es eigene Guides: CUI-Validierung für Rumänien für die Steuer- und USt-IdNr. einer Firma, der CNP-Validator für Rumänien für B2C-Käufer (e-Factura akzeptiert 13 Nullen, wenn der Käufer keine CNP angibt) und IBAN-Validierung in JavaScript für das Konto, das du unter die Zahlungsbedingungen druckst.
Soll ich den Code kopieren oder die Komponenten installieren?
Kopier die Rechenlogik. vat-per-rate.ts hat keine Imports, und der Preflight braucht nur dessen vatCents und einen Typ, also passen beide in jedes TypeScript-Projekt; efactura-totals.ts baut auf computeInvoiceTotals aus der Lib invoicing und zwei Typen aus dem Positionseditor auf. Wenn du Rechnungspositionen in React willst, die die MwSt. pro Satz schon anzeigen, installierst du den Editor oben mit npx wingo-ui@latest add invoice-line-items; er bringt die Lib invoicing und Currency Input mit. Alle drei und die Invoice Page gehören zu Wingo UI Pro. Das Tutorial zum React Invoice Template baut die ganze Rechnung um sie herum, vom Positionseditor bis zur Druckansicht.
Für den Rest einer Admin-App rund um dieses Formular baut das Next.js-Admin-Dashboard-Tutorial Shell, Tabellen und Einstellungen, und weitere React-Builds Schritt für Schritt findest du in der Kategorie Build it.
Häufige Fragen
Welche MwSt.-Sätze gelten 2026 in Rumänien?
21 % als Normalsatz und 11 % als ermäßigter Satz, beide seit dem 1. August 2025 nach Gesetz 141/2025 und Artikel 291 des rumänischen Steuergesetzbuchs (Codul fiscal). Vor diesem Datum lag der Normalsatz bei 19 %, die ermäßigten Sätze bei 9 % und 5 %; ein Übergangssatz von 9 % für bestimmte Wohnungen, durch Gesetz 161/2026 verlängert, endete mit den Lieferungen bis zum 30. September 2026. Steuerbefreite Positionen, Reverse Charge und innergemeinschaftliche Lieferungen stehen auf einer e-Factura mit 0 % und den Kategoriecodes E, AE und K.
Wird die MwSt. bei e-Factura pro Position oder pro Satz berechnet?
Pro Satz. Regel BR-CO-17 definiert die MwSt. jedes Satzes als dessen steuerpflichtigen Betrag mal Satz, gerundet auf zwei Nachkommastellen, und Artikel 319 (20) des Steuergesetzbuchs verlangt den MwSt.-Betrag in Lei für jeden Satz. Die MwSt. auf einer Position dient nur zur Information und fließt nie in die Summen ein.
Warum lehnt ANAF meine e-Factura mit BR-CO-14 oder BR-CO-15 ab?
Beide Regeln vergleichen Summen auf den Cent. BR-CO-14 verlangt, dass die MwSt.-Summe der Rechnung der Summe der MwSt. pro Satz entspricht, und BR-CO-15, dass der Bruttobetrag gleich dem Nettobetrag plus dieser MwSt. ist. Sie schlagen fehl, wenn eine Summe aus gerundeten Positionsbeträgen kommt und eine andere aus der Aufschlüsselung pro Satz.
Akzeptiert e-Factura einen Cent Abweichung bei der MwSt.?
Bei der MwSt. eines Satzes ja: Im Validierungspaket ro16931-ubl 1.0.9 akzeptiert BR-CO-17 einen Wert, der weniger als eine Währungseinheit von Bemessungsgrundlage mal Satz abweicht. Die Regeln, die Beträge aufaddieren (BR-CO-10, BR-CO-11, BR-CO-13, BR-CO-14 und BR-CO-15), kennen überhaupt keine Toleranz.
Welcher MwSt.-Satz gilt, wenn eine Lieferung vom Juli 2025 im August 2025 abgerechnet wird?
In der Regel der Satz, der beim Steuertatbestand galt, und das ist bei einer Lieferung das Lieferdatum, nach Artikel 291 (4) des Steuergesetzbuchs. Für eine Lieferung im Juli 2025 sind das also 19 %, mit Ausnahmen für Anzahlungen und Rechnungen, die vor der Lieferung ausgestellt wurden.
- e-Factura
- MwSt.
- Rumänien
- TypeScript
- React
- Rechnungen


