# E-Factura MwSt berechnen in TypeScript und React

> E-Factura MwSt berechnen in TypeScript: Rumäniens Sätze 21 % und 11 %, MwSt. einmal pro Satz runden, Summen, die ANAF auf den Cent prüft, plus React-Editor.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 9. Okt. 2026
- Kategorie: [Praxis: Screens und Apps](https://wingo-ui.com/de/blog/category/build)
- Lesezeit: 11 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/e-factura-vat-calculation-javascript
- Englische Fassung: https://wingo-ui.com/blog/e-factura-vat-calculation-javascript

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

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](https://wingo-ui.com/components/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](https://www.contzilla.ro/oficial-legea-161-2026-extinde-aplicarea-cotei-de-9-la-unele-locuinte-intre-7-august-si-30-septembrie-2026/)). [Die Vorher-nachher-Tabelle des Steuergesetzbuchs von ANAF](https://static.anaf.ro/static/10/Brasov/Brasov/tva_2025.pdf) zeigt beide Fassungen von Artikel 291. Stand Oktober 2026 nennt [das konsolidierte Steuergesetzbuch von ANAF](https://static.anaf.ro/static/10/Anaf/legislatie/Cod_fiscal_norme_2023.htm), zuletzt durch Gesetz 170/2026 geändert, weiterhin 21 % und 11 %.

| Satz | Code | Wofür |
|---|---|---|
| 21 % | S | alles, was weder steuerbefreit noch ermäßigt ist (19 % bis 31. Juli 2025) |
| 11 % | S | die Liste in Art. 291 (2), darunter Humanarzneimittel, die meisten Lebensmittel, Wasser und Abwasser, Bücher und Presse, Hotelübernachtungen, Restaurant und Catering |
| 0 % | E | steuerbefreite Umsätze, mit Befreiungsgrund |
| 0 % | AE | Reverse Charge, mit der USt-IdNr. des Käufers |
| 0 % | K | innergemeinschaftliche 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](https://wingo-ui.com/components/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:

| Position | Netto | MwSt. | Gerundet |
|---|---|---|---|
| Kurierversand, 21 % | 22,50 | 4,725 | 4,73 |
| Verpackung, 21 % | 7,50 | 1,575 | 1,58 |
| Etikettendruck, 21 % | 4,50 | 0,945 | 0,95 |
| Summe der 21-%-Positionen | 34,50 | | 7,26 |
| 21 % einmal auf 34,50 | 34,50 | 7,245 | 7,25 |
| Bücher, 3 x 14,50, 11 % | 43,50 | 4,785 | 4,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](https://mfinante.gov.ro/static/10/eFactura/ro16931-ubl-1.0.9.zip). Es bettet die Regeln der EN 16931 aus den [Validierungsartefakten der Europäischen Kommission](https://github.com/ConnectingEurope/eInvoicing-EN16931/tree/validation-1.3.8/ubl/schematron) ein (Version 1.3.8; die Toleranzen unten haben wir im Oktober 2026 in diesem Release noch einmal geprüft):

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

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.

> Live-Demo (Invoicing): 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). Probier sie unter [Invoicing](https://wingo-ui.com/components/invoicing) aus und installier sie mit `npx wingo-ui@latest add 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](https://wingo-ui.com/components/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](https://wingo-ui.com/components/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.

> Live-Demo (Invoice line items): 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. Probier sie unter [Invoice line items](https://wingo-ui.com/components/invoice-line-items) aus und installier sie mit `npx wingo-ui@latest add 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](https://wingo-ui.com/components/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](https://wingo-ui.com/de/blog/romanian-cui-validation-javascript) für die Steuer- und USt-IdNr. einer Firma, der [CNP-Validator für Rumänien](https://wingo-ui.com/de/blog/romanian-cnp-validator-javascript) für B2C-Käufer (e-Factura akzeptiert 13 Nullen, wenn der Käufer keine CNP angibt) und [IBAN-Validierung in JavaScript](https://wingo-ui.com/de/blog/iban-validation-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](https://wingo-ui.com/de/pricing). Das [Tutorial zum React Invoice Template](https://wingo-ui.com/de/blog/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](https://wingo-ui.com/de/blog/nextjs-admin-dashboard-tutorial) Shell, Tabellen und Einstellungen, und weitere [React-Builds Schritt für Schritt](https://wingo-ui.com/de/blog/category/build) findest du in der Kategorie Build it.

## Komponenten in diesem Artikel

- [Invoice line items](https://wingo-ui.com/components/invoice-line-items) (Pro): A React invoice line items editor that works like a small spreadsheet, with VAT per rate computed the e-Factura way. Installation: `npx wingo-ui@latest add invoice-line-items`
- [Invoicing](https://wingo-ui.com/components/invoicing) (Pro): Invoice math for React billing apps: VAT per rate the way e-Factura checks it, Romanian VAT rates, and CUI, IBAN and CNP validation. Installation: `npx wingo-ui@latest add invoicing`
- [Invoice Page](https://wingo-ui.com/components/invoice-page) (Pro): A React invoice page to edit and view Romanian B2B invoices: client, series, VAT lines, totals, an A4 preview, sending and e-Factura. Installation: `npx wingo-ui@latest add invoice-page`
- [Currency Input](https://wingo-ui.com/components/currency-input) (Pro): A React currency input: a money field formatted per locale as you type, with an optional currency selector and quick amount presets. Installation: `npx wingo-ui@latest add currency-input`

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

---

Quelle: https://wingo-ui.com/de/blog/e-factura-vat-calculation-javascript
