Die YAML-Datei beschreibt, wie CSV-Spalten auf Rechnungsdaten und Positionen gemappt werden.
Grundstruktur#
csv:
separator: ","
encoding: "UTF-8"
hasHeader: true
decimalSeparator: "."
dateFormat: "dd.MM.yyyy"
zugferd:
version: "2.3"
profile: "Extended"
format: "CII"
mapping:
invoice: {}
buyer: {}
seller: {}
delivery: {}
orderReference: {}
lineItems: {}
totals: {}
paymentTerms: {}
paymentMeans: {}FieldMapping#
Fast alle Mapping-Felder verwenden dieselbe FieldMapping-Struktur:
feldName:
value: "Fester Wert"
column: "CSV_SPALTE"
default: "Fallback"
scheme: "VA"
rules:
- when:
column: "PRODUCT_CODE"
regex: "^003"
value: "DAY"| Schlüssel | Beschreibung |
|---|---|
value | Fester Wert. Hat Vorrang vor allen anderen Angaben. |
column | CSV-Spalte, aus der der Wert gelesen wird. |
default | Fallback, wenn value, column und rules keinen Wert liefern. |
scheme | Optionaler Schema-Qualifier, z. B. für elektronische Adresse oder Steuerregistrierung. |
rules | Geordnete Regex-Regeln, mit denen Werte aus anderen CSV-Spalten abgeleitet werden. |
Auflösungsreihenfolge:
value, wenn gesetztcolumn, wenn gesetzt und der CSV-Wert nicht leer istrulesvon oben nach untendefault, wenn gesetzt- leerer String bzw. fachlicher Code-Fallback im Mapper
Regex-Regeln#
Regex-Regeln erlauben flexible Ableitungen ohne Codeänderung. Die erste passende Regel gewinnt. Ungültige Ausdrücke werden geloggt und ignoriert.
unitCode:
default: "C62"
rules:
- when:
column: "PRODUCT_CODE"
regex: "^002"
value: "ANN"
- when:
column: "PRODUCT_CODE"
regex: "^003"
value: "DAY"
- when:
column: "PRODUCT_NAME"
regex: "(?i)vor-ort-schulung|schulung"
value: "DAY"Groß-/Kleinschreibung lässt sich mit Inline-Optionen wie (?i) steuern. Im columns-Modus werden Regelspalten positionsbezogen aufgelöst: bei column: "PRODUCT_CODE" und Suffix-Muster wird für Position 0 PRODUCT_CODE_0, für Position 1 PRODUCT_CODE_1 geprüft.
Beispiel für den Rechnungskopf#
invoice:
invoiceNumber:
column: "INVOICE_NO"
invoiceDate:
column: "INVOICE_DUEDATE"
typeCode:
value: "380" # BT-3: Rechnungstyp, ohne Angabe 380
currency:
value: "EUR"
orderReferenceId:
column: "SALESORDER_ID"
referenceOrderNo:
column: "SALESORDER_ID"
name:
value: "WARENRECHNUNG"
note:
column: "INVOICE_NOTE"Rechnungstyp, Storno und Gutschrift#
invoice.typeCode (ab Version 1.2.0) setzt den Rechnungstyp (BT-3) nach UNTDID 1001. Ohne Angabe schreibt csv2zugferd 380 (Rechnung). Für ein Storno oder eine kaufmännische Gutschrift ist 381 der richtige Code, für eine Teilkorrektur 384. 389 steht für die Selbstfakturierung im Gutschriftsverfahren (§ 14 Abs. 2 UStG), bei der der Leistungsempfänger abrechnet – ein Storno ist keine Gutschrift in diesem Sinn.
| Code | Bedeutung | Einsatz |
|---|---|---|
380 | Rechnung | Standard |
381 | Gutschrift (kaufmännisch), Stornorechnung | vollständiges Storno, Rückerstattung |
384 | Rechnungskorrektur | Teilkorrektur von Preis, Menge oder Steuersatz |
389 | Selbstfakturierung | Leistungsempfänger rechnet ab; Aussteller und Verkäufer sind vertauscht |
326, 386, 875–877 | Teil-, Vorauszahlungs- und Bau-Abschlagsrechnungen | werden unverändert durchgereicht |
Der Typ lässt sich aus einer CSV-Spalte ableiten, die Ursprungsrechnung wird mit precedingInvoices (BT-25, BT-26) referenziert:
invoice:
typeCode:
default: "380"
rules:
- when:
column: "Belegart"
regex: "(?i)^(gutschrift|storno)"
value: "381"
- when:
column: "Belegart"
regex: "(?i)^korrektur"
value: "384"
precedingInvoices: # eine oder mehrere Vorgängerrechnungen
- number:
column: "Ursprungsrechnung" # BT-25
date:
column: "Ursprungsdatum" # BT-26Fehlt eine Belegart-Spalte, kann die Regel auf das Vorzeichen einer Betragsspalte prüfen (regex: "^-"). Jeder Eintrag in precedingInvoices erzeugt ein ram:InvoiceReferencedDocument; Einträge ohne Nummer werden übersprungen, das Datum ist optional. Das Element wird ab Profil BasicWL geschrieben, im Profil Minimum nicht.
Beträge schreibt csv2zugferd so, wie die CSV sie liefert. Bei 381 müssen sie positiv sein, das Minus steckt im Code; negative Beträge bei 381 lehnen Validatoren und Empfänger ab. Eine Rechnung mit negativen Beträgen (380) ist laut Factur-X zulässig, wird aber von manchen Empfängersystemen als normale Rechnung verbucht. Empfohlen ist 381 mit positiven Beträgen und precedingInvoices. Für name eignet sich STORNORECHNUNG oder RECHNUNGSKORREKTUR statt GUTSCHRIFT; das Element erscheint nur im Profil Extended.
Warnungen im Log, ohne Abbruch: 381 oder 384 ohne precedingInvoices (bei XRechnung und 384: BR-DE-26), ein Code außerhalb von 326, 380, 381, 384, 389, 875, 876, 877 im Profil XRechnung (BR-DE-17) sowie precedingInvoices im Profil Minimum. Ein unbekannter typeCode bricht die Verarbeitung ab.
Beispiel für Käuferdaten#
buyer:
name:
column: "ACCOUNT_NAME"
id:
column: "ACCOUNT_ID"
postalCode:
column: "INVOICE_BILL_CODE"
city:
column: "INVOICE_BILL_CITY"
street:
column: "INVOICE_BILL_STREET"
country:
value: "DE"
contact:
column: "CONTACT_LASTNAME"
taxId:
column: "BUYER_VAT_ID"
taxScheme:
value: "VA"
electronicAddress:
column: "BUYER_VAT_ID"
scheme: "GermanyVatNumber"id ist optional und trägt die Käuferkennung (BT-46), in der Regel die Kundennummer des Käufers beim Verkäufer. Sie erscheint im XML als ram:ID der BuyerTradeParty. Ohne id wird kein Element geschrieben.
taxScheme akzeptiert FC (Steuernummer) oder VA (Umsatzsteuer-ID).
Unterstützte Werte für scheme bei electronicAddress:
| Scheme | Bedeutung |
|---|---|
GermanyVatNumber | Deutsche Umsatzsteuer-ID (EAS 9930) |
LuxemburgVatNumber | Luxemburgische Umsatzsteuer-ID (EAS 9938) |
ElectronicMailSmtp oder EM | E-Mail-Adresse (EAS EM) |
Andere Werte fallen auf GermanyVatNumber zurück.
Beispiel für Verkäuferdaten#
Verkäuferdaten können aus CSV-Spalten kommen oder als feste Stammdaten in der YAML-Datei stehen. Für wiederkehrende Verkäuferdaten sind feste Werte empfohlen.
seller:
name:
value: "Muster Lieferant GmbH"
id:
value: "1234567890"
postalCode:
value: "80333"
city:
value: "Muenchen"
street:
value: "Lieferantenstrasse 20"
country:
value: "DE"
taxRegistrations:
- value: "201/113/40209"
scheme: "FC"
- value: "DE123456789"
scheme: "VA"
electronicAddress:
value: "DE123456789"
scheme: "GermanyVatNumber"id ist optional und trägt die Verkäuferkennung (BT-29), zum Beispiel die Lieferantennummer, unter der der Käufer den Verkäufer führt. Sie erscheint im XML als ram:ID der SellerTradeParty. Ohne id wird kein Element geschrieben.
| Scheme | Bedeutung |
|---|---|
FC | Steuernummer |
VA | Umsatzsteuer-ID |
taxRegistrations unterstützt sowohl feste Werte per value als auch CSV-Werte per column.
Beispiel für Lieferdaten#
delivery:
deliveryNoteNumber:
column: "DELIVERY_NOTE_NO"
deliveryNoteDate:
column: "DELIVERY_NOTE_DATE"
actualDeliveryDate:
column: "DELIVERY_DATE"Alle drei Felder sind optional. deliveryNoteNumber und deliveryNoteDate werden gemeinsam als Lieferscheinreferenz geschrieben. actualDeliveryDate setzt das tatsächliche Lieferdatum (BT-72).
Sind weder Lieferschein noch Lieferdatum konfiguriert, schreibt csv2zugferd ab Version 1.2.1 das Rechnungsdatum als Leistungsdatum und protokolliert eine Warnung. Der Lieferblock ist im ZUGFeRD-Schema Pflicht; ohne Inhalt wäre die XML-Datei ungültig.
Beispiel für Bestellreferenz#
orderReference:
orderNumber:
column: "ORDER_NO"
orderDate:
column: "ORDER_DATE"orderNumber entspricht BT-13 (Bestellnummer des Käufers). orderDate ist optional.
Beispiel für lineItems im rows-Modus mit Rabatt#
lineItems:
mode: "rows"
fields:
name:
column: "Artikel_Name"
description:
column: "Artikel_Beschreibung"
sellerAssignedId:
column: "Artikelnummer"
netUnitPrice:
column: "Einzelpreis_Netto"
billedQuantity:
column: "Menge"
taxPercent:
column: "MwSt_Prozent"
unitCode:
value: "C62"
taxType:
value: "VAT"
taxCategoryCode:
value: "S"
allowanceCharge:
enabled: true
isDiscount:
value: true
chargePercentage:
column: "Rabatt_Prozent"
reason:
column: "Rabatt_Grund"isDiscount: true erzeugt einen Positionsrabatt (BG-27), false einen Positionszuschlag (BG-28). Beide landen als SpecifiedTradeAllowanceCharge in der Positionsabrechnung mit Basisbetrag, Prozentsatz und Grund. Der Zeilennettobetrag (BT-131) ist bereits um den Rabatt gemindert bzw. um den Zuschlag erhöht; die Summen auf Dokumentebene folgen daraus. Im columns-Modus wird Rabatt stattdessen über discountPercent in fields konfiguriert.
Beispiel für lineItems im columns-Modus#
lineItems:
mode: "columns"
columnPattern:
style: "suffix"
separator: "_"
startIndex: 0
zeroPadding: 0
fields:
name:
column: "PRODUCT_NAME"
sellerAssignedId:
column: "PRODUCT_CODE"
netUnitPrice:
column: "PRODUCT_PRICE"
billedQuantity:
column: "PRODUCT_QUANTITY"
taxPercent:
column: "INVOICE_TAX"
discountPercent:
column: "PRODUCT_DISCOUNT"
fixedFields:
unitCode:
default: "C62"
rules:
- when:
column: "PRODUCT_CODE"
regex: "^002"
value: "ANN"
- when:
column: "PRODUCT_CODE"
regex: "^003"
value: "DAY"
- when:
column: "PRODUCT_NAME"
regex: "(?i)vor-ort-schulung|schulung"
value: "DAY"
taxType:
value: "VAT"
taxCategoryCode:
value: "S"Wichtig dabei:
PRODUCT_NAME,PRODUCT_PRICEund ähnliche Felder werden als Fragment für nummerierte Spalten wiePRODUCT_NAME_0verwendet.INVOICE_TAXkann im selben Block als globale Rechnungs-Spalte verwendet werden und gilt dann für jede erzeugte Position.sellerAssignedIdist für interne Produktnummern.descriptionbleibt für echte Artikelbeschreibungen reserviert.fixedFieldsunterstützt vollständigesFieldMappinginklusiverules— nützlich z. B. für positionsabhängige Einheitencodes.
Rabatte#
discountPercent akzeptiert Werte wie 10, 10.00 oder 10%. Der Rabatt wird validierungsfreundlich als rabattierter Netto-Einzelpreis abgebildet:
- Brutto-Listenpreis →
GrossPriceProductTradePrice(= Originalpreis) - Rabattierter Netto-Einzelpreis →
NetPriceProductTradePrice(netPrice × (1 − percent/100), 4 Dezimalstellen) - Zeilennettobetrag = rabattierter Einzelpreis × Menge
Es wird kein AppliedTradeAllowanceCharge für discountPercent-Positionsrabatte erzeugt. Dadurch bleiben EN-16931-Regeln wie BR-S-08 konsistent.
Im rows-Modus gibt es zusätzlich lineItems.allowanceCharge für weitere Rabatte oder Zuschläge auf Positionsebene, unabhängig von discountPercent. Anders als discountPercent wird hier ein ausgewiesener Positionsrabatt (SpecifiedTradeAllowanceCharge, BG-27) geschrieben, der den Zeilennettobetrag direkt mindert.
Summen#
Automatisch (empfohlen, wenn Preise, Mengen und Steuerprozentsatz vollständig aus der CSV kommen):
totals:
mode: "auto"Manuell (nur sinnvoll, wenn die CSV fachlich geprüfte Summen liefert):
totals:
mode: "manual"
lineTotalAmount:
column: "INVOICE_SUBTOTAL"
allowanceTotalAmount:
value: "0"
taxBasisAmount:
column: "INVOICE_SUBTOTAL"
taxTotalAmount:
column: "INVOICE_TAXTOTAL"
grandTotalAmount:
column: "INVOICE_TOTAL"
totalPrepaidAmount:
value: "0"Im manuellen Modus werden nur die Rechnungssummen aus der CSV übernommen. Die Steueraufschlüsselung je Steuersatz (BG-23) wird weiterhin aus den Positionen abgeleitet: Die Steuerbasis je Satz ist die Summe der Positionsnettobeträge, der Steuerbetrag je Satz wird daraus berechnet. Eine Differenz zwischen der berechneten Steuersumme und taxTotalAmount wird der letzten Steuergruppe zugeschlagen, damit die Summe der Gruppen exakt taxTotalAmount ergibt (BR-CO-14). Bei nur einem Steuersatz entspricht der Steuerbetrag der Gruppe damit genau dem CSV-Wert. Rundungsabweichungen im Cent-Bereich zwischen Steuerbasis × Steuersatz und dem übernommenen Steuerbetrag sind zulässig (BR-CO-17 sieht eine Toleranz vor). Zeilennettobeträge (BT-131) werden auch im manuellen Modus aus Einzelpreis × Menge berechnet; lineTotalAmount muss deren Summe entsprechen (BR-CO-10).
Zahlungsmittel und Bankverbindung#
paymentMeans (ab Version 1.1.0) erzeugt die Zahlungsanweisungen (BG-16) mit Bankverbindung des Verkäufers. Bei einer Überweisung (30 oder 58) verlangt EN 16931 die IBAN des Verkäufers (BR-61), im ZUGFeRD-Profil XRechnung sind Zahlungsanweisungen generell Pflicht (BR-DE-1). Ein vollständiges Praxisbeispiel steht unter Bankverbindung und SEPA-Lastschrift.
paymentMeans:
typeCode:
value: "58" # BT-81: SEPA-Überweisung
information:
value: "SEPA-Überweisung" # BT-82
paymentReference:
column: "INVOICE_NO" # BT-83: Verwendungszweck
sellerAccounts:
- iban:
value: "DE02 1203 0000 0000 2020 51" # BT-84, Leerzeichen werden entfernt
bic:
value: "BYLADEM1001" # BT-86
name:
value: "Muster GmbH" # BT-85Für eine SEPA-Lastschrift (59) kommen Gläubiger-ID, Mandatsreferenz und das Konto des Käufers hinzu:
paymentMeans:
typeCode:
default: "58"
rules:
- when:
column: "PAYMENT_METHOD"
regex: "(?i)^lastschrift$"
value: "59"
directDebit:
creditorId:
value: "DE98ZZZ09999999999" # BT-90
mandateReference:
column: "MANDATE_ID" # BT-89
buyerIban:
column: "CUSTOMER_IBAN" # BT-91
buyerBic:
column: "CUSTOMER_BIC"typeCode akzeptiert den Zahlencode nach UNTDID 4461 oder den Namen (SEPACreditTransfer, SEPADirectDebit, …). directDebit wird nur bei den Codes 59 und 49 ausgewertet. Fehlende Pflichtangaben und IBANs mit falscher Prüfsumme werden als Warnung protokolliert, die Verarbeitung läuft weiter.
Empfehlung#
Die YAML-Datei sollte gemeinsam mit dem CSV-Export versioniert werden. Dadurch bleibt nachvollziehbar, welche CSV-Struktur zu welcher Ausgabe geführt hat.
Praxisbeispiel: columns-Modus mit suffix-Spalten.