{
  "name": "E-Rechnungen erkennen und auslesen",
  "nodes": [
    {
      "id": "98e618e0-cb0c-45f2-a2f4-e10d64e76afa",
      "name": "Notiz 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        -520
      ],
      "parameters": {
        "content": "## So richtest du den Ablauf ein\n1. Im Knoten **Neue E-Mail mit Anhang** deine IMAP-Zugangsdaten eintragen.\n2. Den Ordner für E-Rechnungen einmal anlegen und in **Rechnungsdaten auslesen** oben eintragen (Standard: ~/.n8n-files/e-rechnungen).\n3. Mit **Test: Start per Klick** ausprobieren: zwei erfundene E-Rechnungen (CII und UBL) und eine Bestellung, die keine Rechnung ist.\n4. Gelesen werden XML-Dateien (XRechnung). Bei ZUGFeRD steckt die XML-Datei im PDF; die liest dieser Ablauf nicht aus.\n5. Optional: **XML an Lexware Office senden** mit API-Schlüssel (Tarif XL) einrichten und einschalten.",
        "height": 320,
        "width": 560,
        "color": 4
      }
    },
    {
      "id": "6e25e22e-858d-45cc-a7c3-43cbc6b92685",
      "name": "Test: Start per Klick",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        160
      ],
      "parameters": {},
      "notes": "Startet den Ablauf von Hand, zum Testen mit den Beispieldaten."
    },
    {
      "id": "03ec5110-12b5-4b79-a6b7-25e245b8ddfe",
      "name": "Neue E-Mail mit Anhang",
      "type": "n8n-nodes-base.emailReadImap",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "parameters": {
        "mailbox": "INBOX",
        "postProcessAction": "read",
        "format": "simple",
        "downloadAttachments": true,
        "options": {}
      },
      "notes": "Holt neue Mails samt Anhängen per IMAP."
    },
    {
      "id": "561d7907-ee95-4b43-a142-569c6d21fa84",
      "name": "Testdaten: 3 XML-Anhänge",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        160
      ],
      "parameters": {
        "jsCode": "    // Erfundene Beispieldateien: eine XRechnung im Format CII, eine im Format UBL und eine Bestellung (keine Rechnung).\n    const cii = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rsm:CrossIndustryInvoice xmlns:rsm=\"urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100\" xmlns:ram=\"urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100\" xmlns:udt=\"urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100\">\n  <rsm:ExchangedDocument>\n    <ram:ID>RE-2026-5521</ram:ID>\n    <ram:TypeCode>380</ram:TypeCode>\n    <ram:IssueDateTime><udt:DateTimeString format=\"102\">20261103</udt:DateTimeString></ram:IssueDateTime>\n  </rsm:ExchangedDocument>\n  <rsm:SupplyChainTradeTransaction>\n    <ram:ApplicableHeaderTradeAgreement>\n      <ram:SellerTradeParty><ram:Name>Muster Hosting GmbH</ram:Name></ram:SellerTradeParty>\n    </ram:ApplicableHeaderTradeAgreement>\n    <ram:ApplicableHeaderTradeSettlement>\n      <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>\n      <ram:SpecifiedTradePaymentTerms><ram:DueDateDateTime><udt:DateTimeString format=\"102\">20261117</udt:DateTimeString></ram:DueDateDateTime></ram:SpecifiedTradePaymentTerms>\n      <ram:SpecifiedTradeSettlementHeaderMonetarySummation>\n        <ram:GrandTotalAmount>23.80</ram:GrandTotalAmount>\n        <ram:DuePayableAmount>23.80</ram:DuePayableAmount>\n      </ram:SpecifiedTradeSettlementHeaderMonetarySummation>\n    </ram:ApplicableHeaderTradeSettlement>\n  </rsm:SupplyChainTradeTransaction>\n</rsm:CrossIndustryInvoice>`;\n    const ubl = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Invoice xmlns=\"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2\" xmlns:cac=\"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2\" xmlns:cbc=\"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2\">\n  <cbc:ID>88123</cbc:ID>\n  <cbc:IssueDate>2026-10-28</cbc:IssueDate>\n  <cbc:DueDate>2026-11-11</cbc:DueDate>\n  <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>\n  <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>\n  <cac:AccountingSupplierParty><cac:Party><cac:PartyLegalEntity><cbc:RegistrationName>Bürobedarf Beispiel KG</cbc:RegistrationName></cac:PartyLegalEntity></cac:Party></cac:AccountingSupplierParty>\n  <cac:LegalMonetaryTotal>\n    <cbc:TaxInclusiveAmount currencyID=\"EUR\">1234.56</cbc:TaxInclusiveAmount>\n    <cbc:PayableAmount currencyID=\"EUR\">1234.56</cbc:PayableAmount>\n  </cac:LegalMonetaryTotal>\n</Invoice>`;\n    const bestellung = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Bestellung><Nummer>B-77</Nummer><Kunde>Klein Bau</Kunde></Bestellung>`;\n    const datei = (inhalt, name) => ({ data: Buffer.from(inhalt, 'utf8').toString('base64'), mimeType: 'application/xml', fileName: name, fileExtension: 'xml' });\n    return [\n      { json: { from: 'Muster Hosting GmbH <rechnung@example.net>', subject: 'E-Rechnung RE-2026-5521' }, binary: { attachment_0: datei(cii, 'xrechnung.xml') } },\n      { json: { from: 'Bürobedarf Beispiel KG <info@example.com>', subject: 'Ihre Rechnung' }, binary: { attachment_0: datei(ubl, 'invoice_88123.xml') } },\n      { json: { from: 'Klein Bau <einkauf@example.org>', subject: 'Bestellung' }, binary: { attachment_0: datei(bestellung, 'bestellung.xml') } },\n    ];\n"
      },
      "notes": "Zwei erfundene E-Rechnungen (CII und UBL) und eine Bestellung, die keine Rechnung ist."
    },
    {
      "id": "8a238290-62f1-4e94-a906-05bc9cba76bd",
      "name": "Nur XML-Anhänge, einzeln",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        80
      ],
      "parameters": {
        "jsCode": "const ergebnis = [];\nfor (const item of $input.all()) {\n  for (const datei of Object.values(item.binary ?? {})) {\n    // Nur echte XML-Dateien: Endung .xml oder Dateityp application/xml bzw. text/xml (nicht Office-Dateien oder SVG).\n    const istXml = /\\.xml$/i.test(datei.fileName ?? '') || /^(application|text)\\/xml\\b/i.test(datei.mimeType ?? '');\n    if (!istXml) continue;\n    ergebnis.push({ json: { absender: String(item.json.from ?? ''), originalname: datei.fileName ?? 'rechnung.xml' }, binary: { data: datei } });\n  }\n}\nreturn ergebnis;\n"
      },
      "notes": "Nimmt nur XML-Dateien. PDFs (auch ZUGFeRD) bleiben hier liegen."
    },
    {
      "id": "19454a95-957b-4ace-aa26-13c5cbbed579",
      "name": "XML als Text lesen",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        720,
        80
      ],
      "parameters": {
        "operation": "text",
        "binaryPropertyName": "data",
        "destinationKey": "xml",
        "options": {
          "keepSource": "both"
        }
      },
      "notes": "Liest die XML-Datei als Text."
    },
    {
      "id": "bcc8a394-f0aa-4f88-a7d0-13351dc7c319",
      "name": "Rechnungsdaten auslesen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        80
      ],
      "parameters": {
        "jsCode": "// ===== Deine Angaben =====\nconst ORDNER = '/home/node/.n8n-files/e-rechnungen'; // einmal von Hand anlegen\n// ==========================\n// Kleine Helfer: Elemente unabhängig vom Namensraum-Kürzel finden (ram:ID, cbc:ID, ID ...)\nconst block = (xml, name) => {\n  const m = xml.match(new RegExp(`<(?:[\\\\w-]+:)?${name}(?:\\\\s[^>]*)?>([\\\\s\\\\S]*?)</(?:[\\\\w-]+:)?${name}>`));\n  return m ? m[1] : undefined;\n};\nconst pfad = (xml, ...namen) => namen.reduce((rest, n) => (rest === undefined ? undefined : block(rest, n)), xml)?.trim();\nconst sauber = (s) => String(s ?? '').replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue').replace(/ß/g, 'ss').replace(/[^A-Za-z0-9.,_-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 40);\nconst tag102 = (s) => (s && /^\\d{8}$/.test(s) ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}` : s);\n\nreturn $input.all().map((item) => {\n  const xml = String(item.json.xml ?? '');\n  let format = 'unbekannt';\n  let d = {};\n  if (/<(?:[\\w-]+:)?CrossIndustryInvoice[\\s>]/.test(xml)) {\n    format = 'XRechnung (CII)';\n    const kopf = block(xml, 'ExchangedDocument') ?? '';\n    const handel = block(xml, 'SupplyChainTradeTransaction') ?? '';\n    const abrechnung = block(handel, 'ApplicableHeaderTradeSettlement') ?? '';\n    d = {\n      nummer: pfad(kopf, 'ID'),\n      datum: tag102(pfad(kopf, 'IssueDateTime', 'DateTimeString')),\n      faellig: tag102(pfad(abrechnung, 'SpecifiedTradePaymentTerms', 'DueDateDateTime', 'DateTimeString')),\n      verkaeufer: pfad(handel, 'ApplicableHeaderTradeAgreement', 'SellerTradeParty', 'Name'),\n      waehrung: pfad(abrechnung, 'InvoiceCurrencyCode'),\n      betrag: pfad(abrechnung, 'SpecifiedTradeSettlementHeaderMonetarySummation', 'DuePayableAmount'),\n    };\n  } else if (/<(?:[\\w-]+:)?Invoice[\\s>]/.test(xml)) {\n    format = 'XRechnung (UBL)';\n    // Kopfdaten stehen direkt unter <Invoice>, vor den Positionen\n    const kopf = xml.split(/<(?:[\\w-]+:)?InvoiceLine[\\s>]/)[0];\n    d = {\n      nummer: pfad(kopf.replace(/<cac:[\\s\\S]*$/, ''), 'ID'),\n      datum: pfad(kopf, 'IssueDate'),\n      faellig: pfad(kopf, 'DueDate'),\n      verkaeufer: pfad(kopf, 'AccountingSupplierParty', 'Party', 'PartyLegalEntity', 'RegistrationName'),\n      waehrung: pfad(kopf, 'DocumentCurrencyCode'),\n      betrag: pfad(kopf, 'LegalMonetaryTotal', 'PayableAmount'),\n    };\n  }\n  const erkannt = Boolean(d.nummer && d.datum && d.verkaeufer && d.betrag);\n  const betragText = d.betrag ? Number(d.betrag).toFixed(2).replace('.', ',') : '';\n  const dateiname = erkannt\n    ? `${d.datum}_${sauber(d.verkaeufer)}_${sauber(d.nummer)}_${betragText}${d.waehrung ?? ''}.xml`\n    : `PRUEFEN_${sauber(item.json.originalname)}`;\n  return {\n    json: { absender: item.json.absender, originalname: item.json.originalname, format, ...d, erkannt, dateiname, zielpfad: `${ORDNER}/${dateiname}`, listenpfad: `${ORDNER}/eingangsliste.csv` },\n    binary: item.binary,\n  };\n});\n"
      },
      "notes": "Erkennt XRechnung im Format CII oder UBL und liest Nummer, Datum, Fälligkeit, Verkäufer, Währung und Betrag."
    },
    {
      "id": "4aab0ac9-c8ac-4e53-adb5-828aa1616976",
      "name": "E-Rechnung erkannt?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1200,
        80
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "4ce5eb76-115f-4051-a9ad-9dad0a022e4d",
              "leftValue": "={{ $json.erkannt }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "notes": "Nur vollständig gelesene E-Rechnungen werden abgelegt und aufgelistet."
    },
    {
      "id": "c89a6b36-7c6d-4a86-a996-5aa37984e18b",
      "name": "XML umbenannt ablegen",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1440,
        -20
      ],
      "parameters": {
        "operation": "write",
        "fileName": "={{ $json.zielpfad }}",
        "dataPropertyName": "data",
        "options": {}
      },
      "notes": "Legt die XML-Datei unter sprechendem Namen ab."
    },
    {
      "id": "7e7d60ae-5878-4cf2-aeab-f8e48ef2193b",
      "name": "Zeile für die Eingangsliste",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1680,
        -20
      ],
      "parameters": {
        "jsCode": "// Eine Zeile pro Rechnung, Semikolon getrennt\nreturn $input.all().map(({ json: j }) => {\n  const zeile = [j.datum, j.faellig ?? '', j.verkaeufer, j.nummer, String(j.betrag).replace('.', ','), j.waehrung ?? '', j.dateiname].map((w) => `\"${String(w ?? '').replace(/\"/g, '\"\"')}\"`).join(';') + '\\n';\n  return { json: j, binary: { data: { data: Buffer.from(zeile, 'utf8').toString('base64'), mimeType: 'text/csv', fileName: 'eingangsliste.csv', fileExtension: 'csv' } } };\n});\n"
      },
      "notes": "Baut eine Zeile für die Liste aller eingegangenen E-Rechnungen."
    },
    {
      "id": "22ed3834-1e1c-4211-ac84-14fc41399a76",
      "name": "In Eingangsliste eintragen",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1920,
        -20
      ],
      "parameters": {
        "operation": "write",
        "fileName": "={{ $json.listenpfad }}",
        "dataPropertyName": "data",
        "options": {
          "append": true
        }
      },
      "notes": "Hängt die Zeile an die Liste an (CSV-Datei im selben Ordner)."
    },
    {
      "id": "cad25828-94b8-47fc-ab8f-a593e13e0d30",
      "name": "XML an Lexware Office senden",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1680,
        140
      ],
      "disabled": true,
      "parameters": {
        "method": "POST",
        "url": "https://api.lexware.io/v1/files",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "file",
              "inputDataFieldName": "data"
            },
            {
              "name": "type",
              "value": "voucher"
            }
          ]
        },
        "options": {}
      },
      "notes": "Optional, beim Import ausgeschaltet. Lexware nimmt laut Doku auch XML-Dateien an. Schnittstelle nur Tarif XL. Nicht von uns ausgeführt."
    },
    {
      "id": "2b8ad823-2e08-4a9d-abb2-fbefb0e5e4a0",
      "name": "Hinweis an dich: Anhang prüfen",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1440,
        200
      ],
      "disabled": true,
      "parameters": {
        "fromEmail": "Ablauf <deine-adresse@example.com>",
        "toEmail": "deine-adresse@example.com",
        "subject": "={{ 'XML-Anhang ohne erkennbare E-Rechnung: ' + $json.originalname }}",
        "emailFormat": "text",
        "text": "=Von {{ $json.absender }}. Format: {{ $json.format }}. Bitte selbst ansehen.",
        "options": {
          "appendAttribution": false
        }
      },
      "notes": "Sagt dir Bescheid, wenn eine XML-Datei keine lesbare E-Rechnung war. Beim Import ausgeschaltet."
    }
  ],
  "connections": {
    "Test: Start per Klick": {
      "main": [
        [
          {
            "node": "Testdaten: 3 XML-Anhänge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Neue E-Mail mit Anhang": {
      "main": [
        [
          {
            "node": "Nur XML-Anhänge, einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Testdaten: 3 XML-Anhänge": {
      "main": [
        [
          {
            "node": "Nur XML-Anhänge, einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Nur XML-Anhänge, einzeln": {
      "main": [
        [
          {
            "node": "XML als Text lesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "XML als Text lesen": {
      "main": [
        [
          {
            "node": "Rechnungsdaten auslesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rechnungsdaten auslesen": {
      "main": [
        [
          {
            "node": "E-Rechnung erkannt?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "E-Rechnung erkannt?": {
      "main": [
        [
          {
            "node": "XML umbenannt ablegen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Hinweis an dich: Anhang prüfen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "XML umbenannt ablegen": {
      "main": [
        [
          {
            "node": "Zeile für die Eingangsliste",
            "type": "main",
            "index": 0
          },
          {
            "node": "XML an Lexware Office senden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zeile für die Eingangsliste": {
      "main": [
        [
          {
            "node": "In Eingangsliste eintragen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Berlin"
  },
  "pinData": {}
}
