↓ Zum Hauptinhalt springen
csv2zugferd

docs

YAML-Konfiguration – ZUGFeRD-Mapping mit csv2zugferd

YAML-Mapping für csv2zugferd: CSV-Spalten auf ZUGFeRD-Pflichtfelder abbilden, rows- und columns-Modus konfigurieren und Rechnungsfelder zuweisen.

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üsselBeschreibung
valueFester Wert. Hat Vorrang vor allen anderen Angaben.
columnCSV-Spalte, aus der der Wert gelesen wird.
defaultFallback, wenn value, column und rules keinen Wert liefern.
schemeOptionaler Schema-Qualifier, z. B. für elektronische Adresse oder Steuerregistrierung.
rulesGeordnete Regex-Regeln, mit denen Werte aus anderen CSV-Spalten abgeleitet werden.

Auflösungsreihenfolge:

  1. value, wenn gesetzt
  2. column, wenn gesetzt und der CSV-Wert nicht leer ist
  3. rules von oben nach unten
  4. default, wenn gesetzt
  5. 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.

CodeBedeutungEinsatz
380RechnungStandard
381Gutschrift (kaufmännisch), Stornorechnungvollständiges Storno, Rückerstattung
384RechnungskorrekturTeilkorrektur von Preis, Menge oder Steuersatz
389SelbstfakturierungLeistungsempfänger rechnet ab; Aussteller und Verkäufer sind vertauscht
326, 386, 875–877Teil-, Vorauszahlungs- und Bau-Abschlagsrechnungenwerden 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-26

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

SchemeBedeutung
GermanyVatNumberDeutsche Umsatzsteuer-ID (EAS 9930)
LuxemburgVatNumberLuxemburgische Umsatzsteuer-ID (EAS 9938)
ElectronicMailSmtp oder EME-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.

SchemeBedeutung
FCSteuernummer
VAUmsatzsteuer-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_PRICE und ähnliche Felder werden als Fragment für nummerierte Spalten wie PRODUCT_NAME_0 verwendet.
  • INVOICE_TAX kann im selben Block als globale Rechnungs-Spalte verwendet werden und gilt dann für jede erzeugte Position.
  • sellerAssignedId ist für interne Produktnummern. description bleibt für echte Artikelbeschreibungen reserviert.
  • fixedFields unterstützt vollständiges FieldMapping inklusive rules — 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-85

Fü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.

Häufige Fragen

Welche ZUGFeRD-Felder sind Pflicht?

csv2zugferd benötigt mindestens: Rechnungsnummer, Ausstellungsdatum, Lieferantendaten, Kundendaten, mindestens eine Rechnungsposition mit Menge, Einzelpreis und Steuersatz sowie den Gesamtbetrag.

Kann ich mehrere YAML-Konfigurationen für verschiedene CSV-Formate nutzen?

Ja. csv2zugferd nimmt die Konfigurationsdatei über --config entgegen. Für verschiedene CSV-Quellen (CRM, ERP, Excel-Export) genügt jeweils eine eigene .yml-Datei.