Zum Inhalt springen
    WingoUI
    KomponentenVorlagenDokuPreiseKI-AgentenBlogChangelog
    1. Startseite
    2. Blog
    3. Praxis: Screens und Apps
    4. E-Factura MwSt berechnen in TypeScript und React
    Read in English
    1. Blog
    2. Praxis: Screens und Apps
    3. E-Factura MwSt berechnen in TypeScript und React
    Praxis: Screens und Apps
    Praxis: Screens und Apps

    E-Factura MwSt berechnen in TypeScript und React

    SR
    Serban Rusu · Gründer von Wingo UI
    9. Okt. 2026 · 11 Min. Lesezeit

    Auf dieser Seite

    0%
    1. Welche MwSt.-Sätze gelten 2026 in Rumänien?
    2. Warum wird die MwSt. bei e-Factura pro Satz statt pro Position berechnet?
    3. Wie berechne ich die MwSt. pro Satz in TypeScript ohne Float-Fehler?
    4. Welche e-Factura-Validierungsfehler prüfen die Summen?
    5. Wie mache ich aus den Summen e-Factura-Felder?
    6. Wie baue ich die bearbeitbaren Rechnungspositionen in React?
    7. Was ändert sich bei Rechnungen in EUR und Positionen mit 0 %?
    8. Soll ich den Code kopieren oder die Komponenten installieren?
    9. Häufige Fragen
      1. Welche MwSt.-Sätze gelten 2026 in Rumänien?
      2. Wird die MwSt. bei e-Factura pro Position oder pro Satz berechnet?
      3. Warum lehnt ANAF meine e-Factura mit BR-CO-14 oder BR-CO-15 ab?
      4. Akzeptiert e-Factura einen Cent Abweichung bei der MwSt.?
      5. Welcher MwSt.-Satz gilt, wenn eine Lieferung vom Juli 2025 im August 2025 abgerechnet wird?

    Kurz gesagt

    Um die e-Factura MwSt zu berechnen, gruppierst du die Rechnungspositionen nach MwSt.-Kategorie und Satz, addierst ihre Nettobeträge und rundest Bemessungsgrundlage mal Satz einmal pro Gruppe auf zwei Nachkommastellen; die MwSt. pro Position dient nur zur Information. Jede Summe im Dokument muss dann aus genau diesen gerundeten Zahlen kommen, denn die ANAF-Validierung prüft die Summen auf den Cent, auch wenn sie bei der MwSt. eines Satzes eine Abweichung von weniger als 1 Leu zulässt. Seit dem 1. August 2025 gelten in Rumänien 21 % als Normalsatz und 11 % als ermäßigter Satz.

    Veröffentlicht am 9. Okt. 2026

    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 %.

    SatzCodeWofür
    21 %Salles, was weder steuerbefreit noch ermäßigt ist (19 % bis 31. Juli 2025)
    11 %Sdie Liste in Art. 291 (2), darunter Humanarzneimittel, die meisten Lebensmittel, Wasser und Abwasser, Bücher und Presse, Hotelübernachtungen, Restaurant und Catering
    0 %Esteuerbefreite Umsätze, mit Befreiungsgrund
    0 %AEReverse Charge, mit der USt-IdNr. des Käufers
    0 %Kinnergemeinschaftliche Lieferungen, mit der USt-IdNr. des Käufers

    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:

    ts
    import { RO_VAT_RATES, type VatRateOption } from "@/lib/invoicing";
    // der Normalsatz nach dem Datum des Steuertatbestands (Steuergesetzbuch Art. 291 (4))
    const RO_STANDARD_RATE = [
    { from: "2017-01-01", rate: 0.19 },
    { from: "2025-08-01", rate: 0.21 },
    ];
    export const standardRateOn = (isoDate: string) =>
    RO_STANDARD_RATE.findLast((entry) => entry.from <= isoDate)?.rate;
    // das MwSt.-Select für Korrekturen von Lieferungen vor dem 1. August 2025
    export const RATES_FOR_CORRECTIONS: VatRateOption[] = [
    ...RO_VAT_RATES,
    { value: "19", rate: 0.19, category: "S", label: "19%", description: "Normalsatz bis 31. Juli 2025" },
    ];

    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:

    PositionNettoMwSt.Gerundet
    Kurierversand, 21 %22,504,7254,73
    Verpackung, 21 %7,501,5751,58
    Etikettendruck, 21 %4,500,9450,95
    Summe der 21-%-Positionen34,507,26
    21 % einmal auf 34,5034,507,2457,25
    Bücher, 3 x 14,50, 11 %43,504,7854,79

    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:

    ts
    // lib/vat-per-rate.ts
    export type VatLine = {
    quantity: number; // bis zu 3 Nachkommastellen: 1,5 Stunden
    unitPrice: number; // ohne MwSt., in Lei, bis zu 2 Nachkommastellen
    vatRate: number; // ein Prozentwert: 21, 11 oder 0
    };
    export type VatRow = { rate: number; netCents: number; vatCents: number };
    // eine ganze Zahl geteilt durch eine Zehnerpotenz: ein halber Cent ist genau ,5 und rundet von null weg
    export const divRound = (numerator: number, denominator: number) =>
    Math.sign(numerator) * Math.round(Math.abs(numerator) / denominator);
    export const lineNetCents = (line: VatLine) =>
    divRound(Math.round(line.quantity * 1000) * Math.round(line.unitPrice * 100), 1000);
    // 21 % von 3450 Cent: 3450 x 2100 / 10000 = 724,5, also 725
    export const vatCents = (netCents: number, rate: number) =>
    divRound(netCents * Math.round(rate * 100), 10_000);
    export function vatPerRate(lines: VatLine[]): VatRow[] {
    const nets = new Map<number, number>();
    for (const line of lines) nets.set(line.vatRate, (nets.get(line.vatRate) ?? 0) + lineNetCents(line));
    return [...nets]
    .map(([rate, netCents]) => ({ rate, netCents, vatCents: vatCents(netCents, rate) }))
    .sort((a, b) => b.rate - a.rate);
    }
    ts
    vatPerRate([
    { quantity: 1, unitPrice: 22.5, vatRate: 21 },
    { quantity: 1, unitPrice: 7.5, vatRate: 21 },
    { quantity: 1, unitPrice: 4.5, vatRate: 21 },
    { quantity: 3, unitPrice: 14.5, vatRate: 11 },
    ]);
    // [{ rate: 21, netCents: 3450, vatCents: 725 }, { rate: 11, netCents: 4350, vatCents: 479 }]

    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):

    RegelWas sie prüftSpielraum
    BR-CO-10Summe der Positionsnettobeträge (BT-106) = die Nettobeträge der Positionen (BT-131) addiertkeiner
    BR-CO-11Abschläge auf Dokumentebene (BT-107) = die Abschlagsbeträge (BT-92) addiertkeiner
    BR-CO-13Summe ohne MwSt. (BT-109) = BT-106 - BT-107 + Zuschlägekeiner
    BR-CO-14MwSt.-Summe (BT-110) = die MwSt. jedes Satzes (BT-117) addiertkeiner
    BR-CO-15Summe mit MwSt. (BT-112) = BT-109 + BT-110keiner
    BR-S-08Bemessungsgrundlage jedes Satzes (BT-116) = seine Positionen - seine Abschläge + seine Zuschlägeunter 1 Einheit
    BR-CO-17MwSt. jedes Satzes = Bemessungsgrundlage x Satz, gerundet auf 2 Nachkommastellenunter 1 Einheit
    BR-S-09dasselbe für Zeilen mit Normalsatzunter 1 Einheit
    BR-E-08die Bemessungsgrundlage der steuerbefreiten Zeilekeiner
    BR-AE-08die Bemessungsgrundlage der Reverse-Charge-Zeilekeiner
    UBL-DT-01jeder Betrag hat höchstens 2 Nachkommastellen; Stückpreise sind ausgenommenkeiner

    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:

    ts
    // lib/efactura-totals.ts
    import { computeInvoiceTotals, type VatCategory } from "@/lib/invoicing";
    import type { InvoiceDocumentDiscount, InvoiceLine } from "@/components/ui/invoice-line-items";
    import { vatCents } from "@/lib/vat-per-rate";
    type Group = { rate: number; category: VatCategory };
    // jeder Betrag in Cent; rate ist der Prozentwert, den das XML trägt (BT-119)
    export type EFacturaTotals = {
    lines: (Group & { id: string; net: number })[]; // BT-131
    allowances: (Group & { amount: number })[]; // BG-20, einer pro Satz
    breakdown: (Group & { taxable: number; vat: number })[]; // BG-23: BT-116, BT-117
    lineNet: number; // BT-106
    allowanceTotal: number; // BT-107
    taxExclusive: number; // BT-109
    vat: number; // BT-110
    taxInclusive: number; // BT-112
    };
    const cents = (amount: number) => Math.round(amount * 100);
    const percent = (rate: number) => Number((rate * 100).toFixed(2));
    const sum = (values: number[]) => values.reduce((a, b) => a + b, 0);
    const categoryOf = (rate: number, line: InvoiceLine): VatCategory =>
    line.vatCategory ?? (rate > 0 ? "S" : "E");
    // B2B: Stückpreise ohne MwSt.
    export function toEFacturaTotals(lines: InvoiceLine[], discount: InvoiceDocumentDiscount | null): EFacturaTotals {
    const totals = computeInvoiceTotals(lines, {
    discount,
    categoryFor: (rate, line) => categoryOf(rate, line as InvoiceLine),
    });
    const groups = new Map<string, Group & { taxable: number; allowance: number }>();
    const outLines = totals.lines.map((line, i) => {
    const group = { rate: percent(line.vatRate), category: categoryOf(line.vatRate, lines[i]) };
    // BT-131 behält den eigenen Rabatt der Position; ihr Anteil am Dokumentrabatt wird ein Abschlag
    const net = cents(line.gross) - cents(line.discount);
    const key = `${group.rate}|${group.category}`;
    const entry = groups.get(key) ?? { ...group, taxable: 0, allowance: 0 };
    entry.taxable += cents(line.net);
    entry.allowance += net - cents(line.net);
    groups.set(key, entry);
    return { ...group, id: line.id, net };
    });
    const breakdown = [...groups.values()].map(({ rate, category, taxable }) => ({
    rate,
    category,
    taxable,
    vat: category === "S" ? vatCents(taxable, rate) : 0,
    }));
    const allowances = [...groups.values()]
    .filter((group) => group.allowance > 0)
    .map(({ rate, category, allowance }) => ({ rate, category, amount: allowance }));
    const lineNet = sum(outLines.map((line) => line.net));
    const allowanceTotal = sum(allowances.map((allowance) => allowance.amount));
    const vat = sum(breakdown.map((row) => row.vat));
    const taxExclusive = lineNet - allowanceTotal;
    return { lines: outLines, allowances, breakdown, lineNet, allowanceTotal, taxExclusive, vat, taxInclusive: taxExclusive + vat };
    }

    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.

    Ein Dokumentrabatt von 100 $ ist schon gesetzt: Tipp bei einer Menge auf + oder −, und die Zeilen für 21 % und 11 % berechnen ihre MwSt. jeweils aus ihrer eigenen Bemessungsgrundlage neu (diese Demo rechnet in Dollar)
    $ npx wingo-ui@latest add invoicing
    ProDoku: Invoicing

    Prü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:

    ts
    // lib/efactura-preflight.ts
    import type { EFacturaTotals } from "@/lib/efactura-totals";
    import { vatCents } from "@/lib/vat-per-rate";
    const sum = (values: number[]) => values.reduce((a, b) => a + b, 0);
    export function preflight(t: EFacturaTotals): string[] {
    const problems: string[] = [];
    if (t.lineNet !== sum(t.lines.map((l) => l.net))) problems.push("BR-CO-10");
    if (t.allowanceTotal !== sum(t.allowances.map((a) => a.amount))) problems.push("BR-CO-11");
    if (t.taxExclusive !== t.lineNet - t.allowanceTotal) problems.push("BR-CO-13");
    if (t.vat !== sum(t.breakdown.map((r) => r.vat))) problems.push("BR-CO-14");
    if (t.taxInclusive !== t.taxExclusive + t.vat) problems.push("BR-CO-15");
    for (const row of t.breakdown) {
    const same = (x: { rate: number; category: string }) => x.rate === row.rate && x.category === row.category;
    const base = sum(t.lines.filter(same).map((l) => l.net)) - sum(t.allowances.filter(same).map((a) => a.amount));
    // innergemeinschaftliche Regeln heißen IC, die anderen nach ihrem Kategoriecode
    if (row.taxable !== base) problems.push(`BR-${row.category === "K" ? "IC" : row.category}-08 bei ${row.rate}%`);
    // ANAF lässt hier weniger als 1 Leu Abweichung zu; wir akzeptieren 0
    if (row.category === "S" && row.vat !== vatCents(row.taxable, row.rate)) problems.push(`BR-CO-17 bei ${row.rate}%`);
    }
    return problems;
    }

    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.

    Tipp eine Position an, ändere ihre MwSt. von 21 % auf 11 % und speichere sie, dann öffne die Aufschlüsselung unter Total: Bemessungsgrundlage und MwSt. jedes Satzes folgen jeder Änderung
    $ npx wingo-ui@latest add invoice-line-items
    ProDoku: Invoice line items

    In 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:

    tsx
    // app/invoices/new/invoice-form.tsx
    "use client";
    import { useActionState, useState } from "react";
    import { Alert } from "@/components/ui/alert";
    import { Button } from "@/components/ui/button";
    import {
    InvoiceLineItems,
    type InvoiceDocumentDiscount,
    type InvoiceProduct,
    } from "@/components/ui/invoice-line-items";
    import { issueInvoice } from "./actions";
    export function InvoiceForm({ products }: { products: InvoiceProduct[] }) {
    const [discount, setDiscount] = useState<InvoiceDocumentDiscount | null>(null);
    const [state, formAction, pending] = useActionState(issueInvoice, { problems: [] });
    return (
    <form action={formAction} className="flex flex-col gap-4">
    <InvoiceLineItems
    name="lines"
    products={products}
    documentDiscount={discount}
    onDocumentDiscountChange={setDiscount}
    currency="RON"
    locale="en-US"
    />
    <input type="hidden" name="discount" value={JSON.stringify(discount)} />
    {state.problems.length > 0 && (
    <Alert tone="danger" title="Diese Summen würden an e-Factura scheitern" description={state.problems.join(", ")} />
    )}
    <Button type="submit" loading={pending} className="self-end">
    Rechnung ausstellen
    </Button>
    </form>
    );
    }
    ts
    // app/invoices/new/actions.ts
    "use server";
    import type { InvoiceLine } from "@/components/ui/invoice-line-items";
    import { preflight } from "@/lib/efactura-preflight";
    import { toEFacturaTotals } from "@/lib/efactura-totals";
    export async function issueInvoice(_previous: { problems: string[] }, form: FormData) {
    // in einer echten App mit zod parsen; Summen aus dem Browser dienen nur der Anzeige
    const lines = JSON.parse(String(form.get("lines") ?? "[]")) as InvoiceLine[];
    const discount = JSON.parse(String(form.get("discount") || "null"));
    const totals = toEFacturaTotals(lines, discount);
    const problems = preflight(totals);
    if (problems.length > 0) return { problems };
    // das UBL aus `totals` bauen, mit der Rechnungsnummer speichern, dann ins SPV hochladen
    return { problems: [] };
    }

    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.

    Komponenten in diesem Artikel

    • Invoice line items

      A React invoice line items editor that works like a small spreadsheet, with VAT per rate computed the e-Factura way.

      Pro
    • Invoicing

      Invoice math for React billing apps: VAT per rate the way e-Factura checks it, Romanian VAT rates, and CUI, IBAN and CNP validation.

      Pro
    • Invoice Page

      A React invoice page to edit and view Romanian B2B invoices: client, series, VAT lines, totals, an A4 preview, sending and e-Factura.

      Pro
    • Currency Input

      A React currency input: a money field formatted per locale as you type, with an optional currency selector and quick amount presets.

      Pro

    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

    Teilen

    SR

    Über den Autor

    Serban Rusu

    Gründer von Wingo UI

    Serban Rusu hat Wingo UI gegründet. Er baut die Komponenten, die CLI und den MCP-Server und schreibt über React-UIs für Smartphones und KI-Agents.

    Mehr von Serban
    Weiterlesen

    Ähnliche Artikel

    Alle Artikel
    Praxis: Screens und Apps

    React Invoice Template: Positionen, MwSt. und Druckansicht

    Bau ein React Invoice Template in Next.js: Positionen wie in Excel, Rabatte mit exakter MwSt., Steuer pro Satz und eine Druckansicht als sauberes PDF.

    SRSerban Rusu·16. Okt. 2026·10 Min. Lesezeit
    Komponenten-Guides

    React Currency Input: Intl-Formatierung ohne Cursor-Sprünge

    So baust du einen React Currency Input, der beim Tippen mit Intl formatiert, den Cursor stehen lässt, das Symbol nach Locale setzt und eine reine Zahl sendet.

    SRSerban Rusu·16. Okt. 2026·10 Min. Lesezeit
    Komponenten-Guides

    CNP prüfen: rumänische Personennummer in TypeScript und Zod

    CNP prüfen in TypeScript: die gewichtete Prüfziffer, echte Geburtsdaten, der neue Kreiscode 70, eine zod-Regel, ein React-Feld und sicheres Speichern.

    SRSerban Rusu·9. Okt. 2026·13 Min. Lesezeit
    Newsletter

    Neue Artikel per E-Mail

    Neue Guides, Tutorials und Vergleiche aus dem Wingo UI Blog, sobald sie erscheinen. Die meisten Artikel sind auf Englisch.

    Kein Spam. Du kannst dich jederzeit abmelden.

    WingoUI

    Animierte, konfigurierbare, mobile-first React-Komponenten und Blöcke. Kopier den Quellcode, pass ihn an und lass deinen Coding-Agenten damit bauen.

    Produkt

    • Komponenten
    • Vorlagen
    • Preise
    • KI-Agenten
    • Theme-Generator

    Ressourcen

    • Doku
    • Blog
    • Changelog
    • Changelog-RSS
    • llms.txt

    Rechtliches

    • AGB
    • Datenschutz
    • Rückerstattung
    • Lizenz
    • Impressum

    Komponenten-Kategorien

    • Buttons & Actions
    • Inputs
    • Forms
    • Navigation
    • Overlays
    • Feedback
    • Data Display
    • Tables & Lists
    • Charts & Stats
    • Layout
    • Media
    • AI Kit
    • Text & Effects
    • Mobile
    • Commerce
    • Marketing Sections
    • Blocks
    • Hooks & Utilities

    © 2026 Wingo UI

    Wingo UI