Kurz gesagt
Installiere quittio/quittio, trage deine Firma einmal in config/quittio.php ein und baue Rechnungen mit Quittio::invoice(). render() rechnet Summen und Steuer selbst, prüft die Rechnung, wählt das passende Format (für Unternehmen ZUGFeRD, für Behörden XRechnung) und liefert PDF und XML.
Voraussetzungen
- PHP 8.3 oder neuer mit den Erweiterungen
ctype,dom,gd,libxml,mbstring,xmlwriterundzlib;intlwird empfohlen. - Laravel 13. Ohne Laravel geht es auch, siehe E-Rechnungen mit PHP, ganz ohne Laravel.
- Optional für die offiziellen Prüfungen: Java für den KoSIT-Validator und Docker oder eine lokale Installation für veraPDF. Für den Einstieg brauchst du beides nicht.
Schritt 1: Installieren
Das Paket quittio/quittio bringt alles mit, was eine Laravel-App braucht: den Kern, die Laravel-Integration und das Länderpaket Deutschland mit den Pflichtangaben nach UStG.
composer require quittio/quittio # Core, Laravel-Integration und Länderpaket Deutschland
php artisan quittio:install # veröffentlicht config/quittio.phpSchritt 2: Deine Firma eintragen
Jede Rechnung nennt den Verkäufer, also deine Firma. Du trägst sie einmal in config/quittio.php ein, am besten über Umgebungsvariablen. Quittio::invoice() setzt sie dann in jede Rechnung ein.
// config/quittio.php (Auszug)
'seller' => [
'name' => env('QUITTIO_SELLER_NAME'), // Muster GmbH
'vat_id' => env('QUITTIO_SELLER_VAT_ID'), // DE123456789
'tax_number' => env('QUITTIO_SELLER_TAX_NUMBER'), // 12/345/67890
'address' => ['line1' => 'Hauptstraße 1', 'postcode' => '12345', 'city' => 'Musterstadt', 'country' => 'DE'],
'contact' => ['name' => 'Max Muster', 'phone' => '+49 123 456789', 'email' => '[email protected]'],
'bank_accounts' => [
['iban' => 'DE02120300000000202051', 'bic' => 'BYLADEM1001', 'name' => 'Muster GmbH'],
],
],| Schlüssel | Nötig | Warum |
|---|---|---|
name, address | immer | Deutsche Rechnungen nennen den Verkäufer mit vollständiger Anschrift |
vat_id oder tax_number | immer | Eine der beiden muss auf der Rechnung stehen; die USt-IdNr. ist vorzuziehen |
contact, bank_accounts | empfohlen | Steht auf der Rechnung; für XRechnung an Behörden Pflicht |
small_business | nur Kleinunternehmer | true kennzeichnet jede Rechnung als Kleinunternehmer-Rechnung ohne Umsatzsteuer |
Ohne USt-IdNr.?
Wer nur eine Steuernummer hat, braucht zusätzlich eine Handelsregisternummer (legal_registration) oder einen anderen Identifikator (identifiers). Das verlangt die EN 16931 mit der Regel BR-CO-26.
Wie die Rechnung mit Beispieldaten aussieht, zeigt dir die Vorschau:
php artisan quittio:preview --scenario=simpleSchritt 3: Die erste Rechnung bauen
Eine Rechnung beginnt mit Quittio::invoice(). Du ergänzt, was zu dieser Rechnung gehört, und beendest die Kette mit render():
use Quittio\Core\Builder\Party;
use Quittio\Core\Enums\UnitCode;
use Quittio\Country\DE\Tax\VatPreset;
use Quittio\Laravel\Facades\Quittio;
$result = Quittio::invoice() // deine Firma aus config/quittio.php
->number('RE-2026-0042')
->deliveryDate('2026-09-30') // Leistungsdatum, in Deutschland Pflicht
->buyer(Party::make('Kunde AG')
->vatId('DE987654321')
->address('Bahnhofstraße 5', '54321', 'Beispielstadt', 'DE'))
->line('Web development', netPrice: '95.00', quantity: 24, unit: UnitCode::Hour,
description: 'Customer portal, sprint 3')
->line('Hosting', netPrice: '49.00', unit: UnitCode::Month)
->line('Handbook of VAT law', netPrice: '39.00', vat: VatPreset::Reduced7) // 7 % statt 19 %
->dueInDays(14)->cashDiscount(days: 7, percent: '2.00')
->render();
return $result->download(); // oder $result->save('invoices', '2026/')number()ist deine Rechnungsnummer. Das Rechnungsdatum ist heute, außer du setztissueDate(). Für lückenlose Nummern sorgt deine App oder Quittio Managed.deliveryDate()ist das Leistungsdatum. Deutsche Rechnungen müssen es nennen, auch wenn es dem Rechnungsdatum entspricht.buyer()ist dein Kunde: Name, Anschrift und bei Unternehmen die USt-IdNr.line()fügt eine Position hinzu.netPriceist der Nettopreis pro Einheit. Ohnequantityist die Menge 1, ohnevatgilt der Standardsatz deines Landes (19 % in Deutschland).dueInDays()setzt das Zahlungsziel,cashDiscount()ein Skonto.
Beträge als Text
Beträge und Mengen übergibst du als Strings ('95.00'), nie als Float. Gleitkommazahlen können nicht jeden Betrag exakt speichern; Quittio lehnt sie ab. Summen und Umsatzsteuer berechnet Quittio immer selbst, nach den Rundungsregeln der EN 16931.
Schritt 4: Ausgeben, speichern, versenden
Das Ergebnis von render() enthält alles, was erzeugt wurde. Es lässt sich herunterladen, speichern oder direkt an eine Mail hängen:
$result->pdf(); // PDF/A-3 mit eingebettetem XML
$result->xml(); // XML
$result->filename(); // Rechnung_RE-2026-0042.pdf
$result->validation(); // Befunde der Prüfung, auch Warnungen
$result->save('invoices', '2026/'); // auf eine Storage-Disk
return $result->download(); // ->stream() / return $result
Mail::to($customer)->send(new YourInvoiceMail($result)); // das Ergebnis ist AttachableGroße Mengen renderst du in der Queue: Quittio::make($invoice)->queue()->save('invoices', '2026/').
Schritt 5: Das richtige Format
Meist musst du nichts tun: Ohne Formatangabe wählt Quittio pro Rechnung. Für einen deutschen Verkäufer gilt:
| Kunde | Format | Warum |
|---|---|---|
| Unternehmen in Deutschland | ZUGFeRD (Profil EN 16931) | E-Rechnung, die zugleich ein lesbares PDF ist |
| Behörde (Leitweg-ID als Käuferreferenz) | XRechnung (CII) | von öffentlichen Auftraggebern verlangt |
| Verbraucher | keine E-Rechnungspflicht | |
Kleinbetrag bis 250 €, mit ->smallAmount() markiert | Kleinbetragsrechnungen sind ausgenommen | |
| Unternehmen im EU-Ausland | ZUGFeRD (EN 16931) | lesbares PDF mit Standard-XML |
| Kunde außerhalb der EU | keine EU-Vorgaben |
Verlangt ein Kunde ein bestimmtes Format, nennst du es. Für Behörden brauchst du die Leitweg-ID:
$result = Quittio::invoice()
->buyer($authority)
->buyerReference('04011000-12345-03') // Leitweg-ID
->line('Consulting', netPrice: '120.00')
->render('xrechnung-cii');Mehr dazu im Beitrag ZUGFeRD, XRechnung oder Peppol.
Schritt 6: Eigene Modelle anbinden
Statt Käuferdaten bei jeder Rechnung zu wiederholen, liefern deine Modelle sie selbst. Dafür gibt es drei Verträge: ProvidesParty (Kunde), ProvidesInvoiceLine (Positionen) und ProvidesInvoiceData (eine ganze Rechnung, z. B. aus einer Bestellung).
use Quittio\Core\Builder\PartyBuilder;
use Quittio\Core\Contracts\ProvidesParty;
class User extends Authenticatable implements ProvidesParty
{
public function fillParty(PartyBuilder $party): void
{
$party->name($this->company ?? $this->name)
->vatId($this->vat_id)
->address($this->street, $this->postcode, $this->city, $this->country)
->electronicAddress($this->email, 'EM');
}
}
$invoice = Quittio::invoice()
->buyer($user) // ProvidesParty
->lines($order->items) // ProvidesInvoiceLine
->build();Die Rechnung behält eine Kopie der Werte. Ändert sich das Modell später, bleiben bereits erzeugte Rechnungen unverändert.
Schritt 7: Fehlermeldungen verstehen
Jede Rechnung wird vor dem Rendern geprüft. Ist sie ungültig, wirft render() eine ValidationException mit Regelcode, Feld und einem Hinweis zur Lösung, zum Beispiel bei einer XRechnung ohne Leitweg-ID:
The invoice is not valid (2 errors):
[QUITTIO-PROFILE-REQUIRED] BT-10: The field buyerReference (BT-10) is mandatory in profile XRechnung 3.0.
Provide the field or choose a profile that does not require it.
[BR-DE-15] BT-10: The buyer reference (BT-10) must be provided.
Set the Leitweg-ID with ->buyerReference('04011000-12345-03').Der Präfix verrät die Herkunft der Regel: BR- aus der EN 16931, BR-DE- aus XRechnung, PEPPOL- aus Peppol, DE-USTG- aus dem Umsatzsteuergesetz und QUITTIO- aus Quittio selbst. Mit check() statt render() siehst du alle Befunde auf einmal, ohne dass etwas geworfen wird.
Schritt 8: Testen
Der häufigste Test prüft, dass dein Code eine Rechnung erzeugt. Quittio::fake() ersetzt das Rendern durch eine schnelle Attrappe, die jeden Aufruf aufzeichnet:
use Quittio\Laravel\Facades\Quittio;
use Quittio\Laravel\Rendering\RenderedInvoice;
it('renders the invoice of an order', function () {
$fake = Quittio::fake();
$this->post('/orders/42/invoice')->assertOk();
Quittio::assertRenderedCount(1);
Quittio::assertRendered(fn (RenderedInvoice $r) => $r->format() === 'zugferd'
&& $r->data()->totals->amountDue->isEqualTo('1234.56'));
});Ob die Rechnung auch eine gültige E-Rechnung ist, prüfst du mit assertValidEInvoice() und den offiziellen Validatoren. Wie das in der CI läuft, steht im Beitrag E-Rechnungen validieren.
Schritt 9: Stornieren
Eine ausgestellte Rechnung wird nie geändert oder gelöscht. Um sie aufzuheben, stellst du eine Stornorechnung aus: eine neue Rechnung mit eigener Nummer, die auf das Original verweist.
$cancellation = $result->cancel('ST-2026-0001', reason: 'Charged twice');Teilstornos, Vorzeichen und Korrekturen erklärt der Beitrag Storno, Teilstorno, Korrektur.
Wie es weitergeht
- Stellt deine App Rechnungen selbst aus, brauchst du lückenlose Nummern, ein Archiv und Unveränderbarkeit. Das übernimmt Quittio Managed; Hintergründe im Beitrag Lückenlose Rechnungsnummern.
- Logo, Farben und Texte passt du in der Konfiguration an; eigene Vorlagen sind möglich.
- Eingehende E-Rechnungen liest du mit
Quittio::read().