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

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. Okt. 2026
- Kategorie: [Praxis: Screens und Apps](https://wingo-ui.com/de/blog/category/build)
- Lesezeit: 10 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-invoice-template
- Englische Fassung: https://wingo-ui.com/blog/react-invoice-template

## Kurz gesagt

Ein React Invoice Template besteht aus zwei Ansichten auf ein einziges Rechnungsobjekt: einem Editor, in dem sich die Positionen wie eine Tabellenkalkulation verhalten, und einem Druckblatt mit festen Papierfarben, aus dem der Browser ein PDF macht. Berechne alle Summen in einer Funktion, die Positionsrabatte abzieht, einen Rabatt auf die ganze Rechnung vor der Steuer auf die Positionen verteilt und die Steuer einmal pro Satz rundet, und ruf genau diese Funktion vor dem Speichern auch auf dem Server auf.

Ein typisches React Invoice Template ist ein Formular mit einer Tabelle und einem Steuerfeld unter der Zwischensumme. Das reicht bis zur ersten echten Rechnung: ein Rabatt auf eine Position, ein zweiter auf das ganze Dokument, zwei Steuersätze und ein Kunde, der ein PDF will, das nicht wie ein Screenshot deiner App aussieht. Dieses Tutorial baut in Next.js die Version, die diese Fälle übersteht: einen Rechnungseditor, dessen Positionen sich wie eine Tabellenkalkulation verhalten, Summen mit der MwSt. pro Satz und eine Druckansicht, die der Browser als sauberes A4-PDF speichert. Der Editor 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 Summen und das Druckblatt sind Code, den du lesen und kopieren kannst.

## Was braucht ein React Invoice Template?

Ein Rechnungsobjekt und zwei Ansichten darauf: einen Editor, der es ändert, und ein Druckblatt, das es nur liest. Egal ob du das Ergebnis React Invoice Generator oder React Invoice Builder nennst, das Objekt enthält die Parteien, die Nummer, die Daten, die Währung, die Positionen und optional einen Rabatt auf das Dokument. Die Summen liegen im Browser nie neben den Positionen im State: Sie werden bei jedem Render aus den Positionen abgeleitet, im Browser für die Anzeige und vor dem Speichern noch einmal auf dem Server.

```ts
// lib/invoice-types.ts
import type { InvoiceDocumentDiscount, InvoiceLine } from "@/components/ui/invoice-line-items";

export type Party = { name: string; address: string[]; vatId?: string };

export type Invoice = {
  number: string;
  issueDate: Date;
  dueDate: Date;
  currency: string;
  seller: Party;
  buyer: Party;
  lines: InvoiceLine[];
  discount: InvoiceDocumentDiscount | null;
  notes?: string;
};
```

`InvoiceLine` ist `{ id, quantity, unitPrice, vatRate?, discount?, discountType? }` plus die gedruckten Felder `product`, `description`, `unit` und `vatCategory`. Der Satz steht als Dezimalzahl (0.2 für 20 %), und `vatCategory` unterscheidet die Zeilen mit 0 %. Eine Position zum Nullsatz und eine steuerbefreite Position werden so als verschiedene Zeilen gedruckt.

## Wie baue ich den Editor für die Rechnungspositionen?

Installier Invoice Line Items und gib der Komponente deine Produkte, deine Steuersätze und deine Währung. Die Positionen sind der Teil eines Rechnungseditors in React, den man bei jeder Rechnung anfasst. Deshalb steckt dort die meiste Arbeit an der Tastaturbedienung. Die Installation bringt die invoicing-Lib, die Produkt-Combobox und das Währungsfeld mit:

```bash
npx wingo-ui@latest add invoice-line-items
```

Die Standard-Steuersätze sind die rumänischen (21 %, 11 % und drei Kategorien mit 0 %), weil die Komponente für e-Factura-Rechnungen gebaut wurde. Deine eigene Liste gibst du über `vatRates` mit. Das Beispiel stellt einem britischen Kunden eine Rechnung in Pfund, mit den drei Sätzen, die [GOV.UK aufführt](https://www.gov.uk/vat-rates), Stand Oktober 2026: 20 % Normalsatz, 5 % ermäßigter Satz und 0 % Nullsatz.

```tsx
// app/(admin)/invoices/[id]/lines-editor.tsx
"use client";

import {
  InvoiceLineItems,
  type InvoiceDocumentDiscount,
  type InvoiceLine,
  type InvoiceProduct,
} from "@/components/ui/invoice-line-items";
import type { VatRateOption } from "@/lib/invoicing";

export const UK_VAT_RATES: VatRateOption[] = [
  { value: "20", rate: 0.2, category: "S", label: "20%", description: "Normalsatz" },
  { value: "5", rate: 0.05, category: "S", label: "5%", description: "Ermäßigter Satz" },
  { value: "0-Z", rate: 0, category: "Z", label: "0%", description: "Nullsatz" },
];

const PRODUCTS: InvoiceProduct[] = [
  { id: "layout", name: "Redaktionelles Design und Layout", unit: "project", unitPrice: 2400, vatRate: 0.2 },
  { id: "copyedit", name: "Lektorat", unit: "hr", unitPrice: 45, vatRate: 0.2, keywords: ["Korrektorat"] },
  { id: "books", name: "Gedruckte Bücher", unit: "pcs", unitPrice: 6.5, vatRate: 0, sku: "BK-200" },
];

type Props = {
  lines: InvoiceLine[];
  discount: InvoiceDocumentDiscount | null;
  onLinesChange: (lines: InvoiceLine[]) => void;
  onDiscountChange: (discount: InvoiceDocumentDiscount | null) => void;
};

export function LinesEditor({ lines, discount, onLinesChange, onDiscountChange }: Props) {
  return (
    <InvoiceLineItems
      value={lines}
      onValueChange={(next) => onLinesChange(next)}
      products={PRODUCTS}
      vatRates={UK_VAT_RATES}
      defaultVatRate={0.2}
      units={["project", "hr", "day", "pcs"]}
      documentDiscount={discount}
      onDocumentDiscountChange={onDiscountChange}
      currency="GBP"
      locale="en-GB"
      name="invoice[lines]"
    />
  );
}
```

Wählst du ein Produkt, übernimmt die Position seine Einheit, seinen Preis und seinen Steuersatz und behält die Menge. Tippst du einen Namen, der nicht im Katalog steht, kommt er als freier Text dazu, und der Preis bleibt markiert, bis du einen einträgst (`allowCustomProducts`, standardmäßig an). Bei einem großen Katalog ersetzt du `products` durch `onSearchProducts`, eine asynchrone Suche, die die Combobox mit Debounce aufruft.

Baust du die Tabelle für deine Rechnungspositionen mit `useFieldArray` aus react-hook-form, bekommst du Zeilen zum Hinzufügen und Entfernen und das Produkt aus Menge und Preis. Wie eine Tabellenkalkulation fühlt sie sich erst durch die Tastatur an. Tab geht in Lesereihenfolge durch die Zellen, Escape in einer Zahlenzelle stellt ihren vorherigen Wert wieder her, Enter springt in dieselbe Zelle der nächsten Position, und Cmd oder Strg + Enter fügt von überall eine Position hinzu. Alt + Pfeil nach oben oder unten verschiebt die aktuelle Position, und eine entfernte Position hinterlässt fünf Sekunden lang eine Zeile zum Rückgängigmachen.

> Live-Demo (Invoice line items): Geh mit Tab durch eine Position, ändere die Menge und den Steuersatz, entferne dann eine Position und klick auf Undo: Die Steuerzeilen unter der Tabelle aktualisieren sich bei jedem Tastendruck. 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`.

Auf dem Smartphone zeigt dieselbe Komponente jede Position als Karte. Tippst du eine an, bearbeitest du eine Kopie in einem Bottom Sheet, die du mit Save übernimmst. Der Wechsel richtet sich nach der Breite der Komponente selbst (`layout="auto"` zeigt die Tabelle ab 768 px Containerbreite). Auch ein schmales Seitenpanel auf dem Desktop bekommt also die Karten. Warum wir den Container messen, erklärt der [Guide zu Mobile-first-Komponenten in React](https://wingo-ui.com/de/blog/mobile-first-react-components).

## Wie sollten Rabatte auf einer Rechnung funktionieren?

Zuerst werden die Positionsrabatte abgezogen, dann wird ein Rabatt auf das Dokument vor der Steuer auf die Positionen verteilt. Ein Positionsrabatt, in Prozent oder als Betrag, geht vor allem anderen von dieser Position ab. Ein Rabatt auf das Dokument geht nach den Positionsrabatten von der Zwischensumme ab, und er muss vor der Steuer anteilig nach dem Wert der Positionen auf sie verteilt werden. Ziehst du ihn stattdessen von der Gesamtsumme ab, passt die für jeden Satz gedruckte Steuer nicht mehr zu ihrem steuerpflichtigen Betrag.

`computeInvoiceTotals` aus der [invoicing-Lib](https://wingo-ui.com/components/invoicing) erledigt beides und rundet die Steuer einmal pro Satz. Hier die Beispielrechnung, mit 10 % Rabatt auf die Design-Position und 5 % auf das ganze Dokument:

```ts
import { computeInvoiceTotals } from "@/lib/invoicing";

const totals = computeInvoiceTotals(
  [
    { id: "l1", quantity: 1, unitPrice: 2400, vatRate: 0.2, discount: 10 },
    { id: "l2", quantity: 14, unitPrice: 45, vatRate: 0.2 },
    { id: "l3", quantity: 200, unitPrice: 6.5, vatRate: 0 },
  ],
  {
    discount: { type: "percent", value: 5 },
    categoryFor: (rate) => (rate > 0 ? "S" : "Z"),
  },
);

totals.gross; // 4330
totals.lineDiscount; // 240
totals.documentDiscount; // 204.5
totals.net; // 3885.5
totals.vatBreakdown;
// [{ rate: 0.2, category: "S", net: 2650.5, vat: 530.1, total: 3180.6, lineCount: 2 },
//  { rate: 0, category: "Z", net: 1235, vat: 0, total: 1235, lineCount: 1 }]
totals.total; // 4415.6
```

Der Rabatt von 204,50 auf das Dokument teilt sich in 108,00, 31,50 und 65,00 auf die drei Positionen auf. Die letzte Position nimmt, was übrig bleibt, damit die Cent-Beträge aufgehen. Die Zeile mit 20 % wird auf 2.650,50 besteuert, also genau auf den Wert der beiden 20-%-Positionen nach beiden Rabatten.

## Warum die Steuer pro Satz statt pro Position berechnen?

Weil sich Rundungsfehler aufsummieren, wenn du jede Position einzeln rundest und dann addierst. Drei Positionen, deren Steuer jeweils auf einen halben Penny endet, runden dreimal auf; dieselbe Basis, einmal besteuert, rundet einmal auf. EN 16931, der europäische Standard für E-Rechnungen, definiert die Steuer jedes Satzes als steuerpflichtigen Betrag dieses Satzes mal Satz, gerundet auf zwei Nachkommastellen ([Regel BR-CO-17](https://docs.peppol.eu/poacc/billing/3.0/rules/ubl-tc434/BR-CO-17/)), und die Summen des Dokuments müssen sich aus diesen gerundeten Zeilen ergeben. Unser Beitrag zur [e-Factura-Steuerberechnung](https://wingo-ui.com/de/blog/e-factura-vat-calculation-javascript) geht die Regel mit einem durchgerechneten Beispiel und den Validierungsfehlern durch, die sie auslöst.

Für das Template heißt das nur eins: Die Steuer pro Position in `totals.lines` dient zur Information, und jede Summe, die du druckst, speicherst oder verschickst, kommt aus `vatBreakdown`.

Eine Grenze solltest du kennen, wenn du US-Sales-Tax abrechnest: Die Steuerspalte formatiert Sätze mit höchstens einer Nachkommastelle. Der kombinierte Satz von New York City mit 8⅞ % ([Publication 718](https://www.tax.ny.gov/pdf/publications/sales/pub718.pdf), gültig ab dem 1. März 2025) würde also als 8,9 % erscheinen. Rechne mit dem exakten Bruch und druck den Satz auf dem Blatt selbst.

## Wie baue ich eine saubere Druckansicht für die Rechnung?

Bau ein eigenes Blatt für das Papier und halt den Editor aus dem Ausdruck heraus. Eingabefelder, Ziehgriffe und Hover-Effekte haben auf Papier nichts zu suchen, und ein im Dark Mode gedruckter Editor verschwendet Toner. Das Blatt ist schlichtes HTML mit festen Papierfarben in beiden Themes. Das ist die eine Stelle, an der ein Wingo-UI-Screen fest codierte Farben nutzt: Das Blatt steht für eine gedruckte Seite, also ignoriert es das Theme.

```tsx
// app/(admin)/invoices/[id]/invoice-sheet.tsx
import { formatPrice } from "@/lib/commerce";
import { computeInvoiceTotals, formatVatRate } from "@/lib/invoicing";
import type { InvoiceLine } from "@/components/ui/invoice-line-items";
import type { Invoice } from "@/lib/invoice-types";

export function InvoiceSheet({ invoice, locale = "en-GB" }: { invoice: Invoice; locale?: string }) {
  const totals = computeInvoiceTotals(invoice.lines, {
    discount: invoice.discount,
    categoryFor: (rate, line) => (line as InvoiceLine).vatCategory ?? (rate > 0 ? "S" : "E"),
  });
  const money = (value: number) =>
    formatPrice(value, { currency: invoice.currency, locale, fractionDigits: 2 });
  const date = (value: Date) => value.toLocaleDateString(locale, { dateStyle: "medium" });

  return (
    <article className="mx-auto w-[210mm] bg-white p-[16mm] text-[12px] leading-relaxed text-neutral-900 [color-scheme:light] print:w-auto print:p-0">
      <header className="flex justify-between gap-8">
        <div>
          <p className="text-[18px] font-semibold">Rechnung {invoice.number}</p>
          <p className="text-neutral-500">
            Ausgestellt am {date(invoice.issueDate)}, fällig am {date(invoice.dueDate)}
          </p>
        </div>
        <div className="text-end">
          <p className="font-semibold">{invoice.seller.name}</p>
          {invoice.seller.address.map((row) => <p key={row}>{row}</p>)}
          {invoice.seller.vatId && <p>USt-IdNr. {invoice.seller.vatId}</p>}
        </div>
      </header>

      <section className="mt-8">
        <p className="text-[10px] font-semibold tracking-wider text-neutral-500 uppercase">Rechnung an</p>
        <p className="font-semibold">{invoice.buyer.name}</p>
        {invoice.buyer.address.map((row) => <p key={row}>{row}</p>)}
      </section>

      <table className="mt-8 w-full border-collapse">
        <thead className="border-b border-neutral-300 text-start text-neutral-500">
          <tr>
            <th className="py-2 text-start font-medium">Position</th>
            <th className="py-2 text-end font-medium">Menge</th>
            <th className="py-2 text-end font-medium">Preis</th>
            <th className="py-2 text-end font-medium">MwSt.</th>
            <th className="py-2 text-end font-medium">Netto</th>
          </tr>
        </thead>
        <tbody>
          {invoice.lines.map((line, i) => (
            <tr key={line.id} className="border-b border-neutral-200 align-top">
              <td className="py-2">
                {line.product}
                {line.description && <p className="text-neutral-500">{line.description}</p>}
              </td>
              <td className="py-2 text-end tabular-nums">{line.quantity} {line.unit}</td>
              <td className="py-2 text-end tabular-nums">{money(line.unitPrice)}</td>
              <td className="py-2 text-end tabular-nums">{formatVatRate(totals.lines[i].vatRate, locale)}</td>
              <td className="py-2 text-end tabular-nums">{money(totals.lines[i].net)}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <dl className="ms-auto mt-6 w-64 tabular-nums [&>div]:flex [&>div]:justify-between [&>div]:py-0.5">
        <div><dt>Zwischensumme</dt><dd>{money(totals.gross)}</dd></div>
        {totals.discount > 0 && <div><dt>Rabatte</dt><dd>-{money(totals.discount)}</dd></div>}
        {totals.vatBreakdown.map((row) => (
          <div key={`${row.rate}-${row.category}`} className="text-neutral-500">
            <dt>MwSt. {formatVatRate(row.rate, locale)} auf {money(row.net)}</dt>
            <dd>{money(row.vat)}</dd>
          </div>
        ))}
        <div className="mt-1 border-t border-neutral-900 pt-1 font-semibold">
          <dt>Gesamt</dt><dd>{money(totals.total)}</dd>
        </div>
      </dl>

      {invoice.notes && <p className="mt-10 text-neutral-500">{invoice.notes}</p>}
    </article>
  );
}
```

Die Netto-Spalte zeigt jede Position nach beiden Rabatten, also ergeben die Positionen zusammen die steuerpflichtigen Beträge in den Steuerzeilen. Das Blatt hat Rahmenlinien und keine Hintergrundfarben, weil Browser Hintergrundfarben beim Drucken standardmäßig weglassen.

Danach kommen die Seitenregeln in `app/globals.css`. `size` in `@page` ist laut [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/@page/size) Baseline 2024; für Kunden in Nordamerika nimmst du `letter`.

```css
@media print {
  @page {
    size: A4;
    margin: 16mm;
  }
  /* eine lange Rechnung wiederholt ihre Spaltenköpfe auf jeder Seite und trennt nie eine Position */
  thead {
    display: table-header-group;
  }
  tr {
    break-inside: avoid;
  }
}
```

Chromium wiederholt ein `thead` oben auf jeder gedruckten Seite seit einer Änderung vom Juni 2016, die das Verhalten an Firefox und Edge angeglichen hat ([der Commit](https://chromium.googlesource.com/chromium/src/+/81c0fc6d4e08e4e2bb4eb8a42747f21b13303616)). Die `display`-Regel macht die Absicht also nur ausdrücklich. Druck eine Testrechnung mit 40 Positionen in jedem Browser, den deine Kunden nutzen, bevor ein Kunde es tut.

### Wie drucke ich nur die Rechnung und nicht die App?

Markier alles andere mit `print:hidden` und render das Blatt mit `hidden print:block`, solange der Editor auf dem Bildschirm steht. Seitenleiste und Header deines Admin-Layouts brauchen ebenfalls `print:hidden`, weil sie außerhalb der Seitenkomponente liegen. Damit liefern `window.print()` und „Als PDF speichern“ im Browser ein PDF, auf dem nur das Dokument steht.

[react-to-print](https://github.com/MatthewHerbst/react-to-print) macht dasselbe über ein iframe: `useReactToPrint({ contentRef })` kopiert den referenzierten Knoten und die Styles der Seite in ein frisches Dokument und druckt dieses, also kannst du dir die `print:hidden`-Klassen sparen. Zu `@react-pdf/renderer` greifst du nur, wenn der Server PDFs ganz ohne menschliches Zutun bauen muss, etwa als Anhang geplanter E-Mails. Es rendert eigene Layout-Bausteine, du pflegst das Rechnungslayout dann also doppelt.

## Wie sieht der fertige Rechnungseditor aus?

Editor und Blatt teilen sich ein State-Objekt. Die Summen werden nie gespeichert, nur abgeleitet:

```tsx
// app/(admin)/invoices/[id]/invoice-editor.tsx
"use client";

import { useState } from "react";
import { Printer } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { Invoice } from "@/lib/invoice-types";
import { LinesEditor } from "./lines-editor";
import { InvoiceSheet } from "./invoice-sheet";
import { saveInvoice } from "./actions";

export function InvoiceEditor({ initial }: { initial: Invoice }) {
  const [invoice, setInvoice] = useState(initial);
  const [preview, setPreview] = useState(false);

  return (
    <form action={saveInvoice} className="flex flex-col gap-6">
      <input type="hidden" name="invoice[discount]" value={JSON.stringify(invoice.discount)} />
      <div className="flex flex-wrap items-center justify-end gap-2 print:hidden">
        <Button type="button" variant="ghost" onClick={() => setPreview((on) => !on)}>
          {preview ? "Bearbeiten" : "Vorschau"}
        </Button>
        <Button type="button" variant="outline" leftIcon={<Printer />} onClick={() => window.print()}>
          PDF herunterladen
        </Button>
        <Button type="submit">Speichern</Button>
      </div>

      <div className={preview ? "hidden" : "print:hidden"}>
        <LinesEditor
          lines={invoice.lines}
          discount={invoice.discount}
          onLinesChange={(lines) => setInvoice((prev) => ({ ...prev, lines }))}
          onDiscountChange={(discount) => setInvoice((prev) => ({ ...prev, discount }))}
        />
      </div>

      <div className={preview ? "overflow-x-auto" : "hidden print:block"}>
        <InvoiceSheet invoice={invoice} />
      </div>
    </form>
  );
}
```

In der Vorschau scrollt das 210mm breite Blatt auf dem Smartphone seitlich. Wenn deine Kunden Rechnungen auf dem Smartphone prüfen, miss den Container mit einem `ResizeObserver` und skalier das Blatt stattdessen mit einer CSS-Transformation. Genau das macht der Block Invoice Page weiter unten.

## Wie speichere ich die Rechnung, ohne den Summen des Browsers zu vertrauen?

Schick die Positionen, nie die Summen, und rechne auf dem Server mit derselben Funktion neu. Über die Prop `name` legt der Editor ein verstecktes Feld mit den Positionen als JSON an, also schickt ein natives Formular sie an eine Server Action:

```ts
// app/(admin)/invoices/[id]/actions.ts
"use server";

import { computeInvoiceTotals } from "@/lib/invoicing";
import type { InvoiceLine } from "@/components/ui/invoice-line-items";

export async function saveInvoice(form: FormData) {
  // in einer echten App beides mit zod validieren: das sind Nutzereingaben
  const lines = JSON.parse(String(form.get("invoice[lines]") ?? "[]")) as InvoiceLine[];
  const discount = JSON.parse(String(form.get("invoice[discount]") ?? "null"));
  // dieselben Optionen wie im Editor: eine Position ohne Satz fällt auf defaultVatRate zurück (Standard der Lib ist 0.21)
  const { total } = computeInvoiceTotals(lines, { discount, defaultVatRate: 0.2 });
  // Positionen, Rabatt und Summe mit deiner Datenschicht speichern; das Blatt berechnet den Rest aus den Positionen
  console.log("Rechnungssumme", total);
}
```

Die invoicing-Lib importiert nichts aus React. Sie läuft deshalb unverändert in einer Server Action, einem Route Handler oder einem Queue-Worker.

## Kann ich stattdessen mit einer kompletten Rechnungsseite starten?

Ja, wenn du aus Rumänien abrechnest. Der Block [Invoice Page](https://wingo-ui.com/components/invoice-page) packt denselben Positionseditor in einen kompletten Screen: eine Kundenauswahl, die nach Name, CUI oder Stadt sucht, Serien und Nummerierung, die vergebene Nummern überspringen, Vorgaben für das Fälligkeitsdatum mit 15, 30 und 45 Tagen, einen Entwurf, der sich 800 ms nach der letzten Änderung selbst speichert (Strg oder Cmd + S speichert sofort), eine A4-Vorschau, die auf dem Smartphone auf den Bildschirm skaliert, einen Dialog zum Versenden und den Schalter für e-Factura. Die Beschriftungen sind auf Englisch, die Felder folgen aber rumänischen B2B-Rechnungen. Für andere Länder ist die Komposition oben deshalb der bessere Startpunkt.

> Live-Demo (Invoice Page): Ändere eine Menge in den Positionen und schalte den Header dann auf Preview: Das A4-Blatt behält in beiden Themes seine Papierfarben und passt sich an ein Smartphone an. Probier sie unter [Invoice Page](https://wingo-ui.com/components/invoice-page) aus und installier sie mit `npx wingo-ui@latest add invoice-page`.

Für den Rest des Backoffice drumherum baut das [Tutorial zum Next.js-Admin-Dashboard](https://wingo-ui.com/de/blog/nextjs-admin-dashboard-tutorial) die App-Hülle, die Datentabellen und die Einstellungen, und weitere Screens, Schritt für Schritt gebaut, findest du in der Kategorie [Praxis: Screens und Apps](https://wingo-ui.com/de/blog/category/build).

## Nächster Schritt

Installier den Editor mit `npx wingo-ui@latest add invoice-line-items`, kopier das Blatt und die Druckregeln und druck eine lange Testrechnung in beiden Themes. Invoice Line Items, die invoicing-Lib und Invoice Page gehören zu [Wingo UI Pro](https://wingo-ui.com/de/pricing).

## Komponenten in diesem Artikel

- [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`
- [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`

## Häufige Fragen

### Wie mache ich in React eine druckbare Rechnung?

Render die Rechnung als schlichtes HTML-Blatt mit festen weißen Papierfarben, blende den Editor und die App-Hülle mit print:hidden von Tailwind aus, leg das Papierformat mit @page fest und ruf window.print() auf. Die Option „Als PDF speichern“ des Browsers erzeugt dann ein PDF, auf dem nur das Blatt steht.

### Soll ich für Rechnungen react-to-print oder @react-pdf/renderer nehmen?

Nimm den Druck des Browsers, direkt oder über react-to-print, wenn die Rechnung schon als HTML auf dem Bildschirm steht und ein Mensch sie druckt. Nimm @react-pdf/renderer, wenn der Server PDFs selbstständig erzeugen muss, etwa als E-Mail-Anhang, und nimm in Kauf, dass dessen Layout-Bausteine getrennt von deinen Web-Komponenten sind.

### Wie ziehe ich einen Rabatt von der ganzen Rechnung ab, wenn die Positionen verschiedene Steuersätze haben?

Verteil den Rabatt vor jeder Steuer anteilig nach dem Wert, den die Positionen nach ihren eigenen Rabatten haben, und gib den Rundungsrest der letzten Position. Dann sinkt der steuerpflichtige Betrag jedes Satzes um seinen Anteil, und die Steuer pro Satz bleibt exakt.

### Rechnet man die Steuer auf Rechnungen pro Position oder pro Satz?

Pro Satz, wenn die Rechnung Mehrwertsteuer ausweist: Addier die steuerpflichtigen Beträge aller Positionen mit diesem Satz und runde die Steuer einmal. So definiert EN 16931, der europäische Standard für E-Rechnungen, die Steuer jedes Satzes; die Steuer pro Position kann dann zur Information auf dem Dokument stehen.

### Gibt es ein kostenloses React Invoice Template?

Das Druckblatt, das Druck-CSS und die Rundungsregeln aus diesem Tutorial kannst du in jede React-App kopieren. Die Teile von Wingo UI, die es importiert, also Invoice Line Items, die invoicing-Lib mit computeInvoiceTotals und der Block Invoice Page, gehören zum Pro-Plan.

---

Quelle: https://wingo-ui.com/de/blog/react-invoice-template
