{
  "name": "KI-Starter (gratis) – Belege-Eingang (Ordner) Lite",
  "nodes": [
    {
      "name": "Notiz: Start",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -460,
        -440
      ],
      "parameters": {
        "content": "# 📥 Belege-Eingang (Ordner) – Lite\n**KI-Starter (gratis) v1.0.1** – präsentiert von Nova Tessin.\n_Nova Tessin ist eine KI-generierte virtuelle Persona; die Workflows hat die Redaktion gebaut und getestet._\n\nLiest alle PDFs aus `Eingang/`, sucht das **Belegdatum** (Dateiname oder PDF-Text) und legt eine Kopie unter\n`Belege/JJJJ/MM/JJJJ-MM-TT_Originalname.pdf` ab. Ohne Datum → `Belege/_Pruefen/`.\n\n### Einrichtung\n1. Ordner anlegen: `vorlagen/ordner-anlegen-lite.ps1` (Windows) bzw. `.sh` – **n8n legt keine Ordner selbst an.**\n2. Im Node **Einstellungen** den `basisOrdner` eintragen.\n3. PDFs in `Eingang/` legen und **Workflow testen** (manueller Start).\n4. Ergebnis im Node **Ergebnis** ansehen, abgelegte Dateien prüfen, Eingang von Hand leeren.\n\n⚠️ n8n darf nur auf freigegebene Ordner zugreifen (`N8N_RESTRICT_FILE_ACCESS_TO`, Standard `~/.n8n-files`). Details: README.md.",
        "width": 560,
        "height": 560,
        "color": 4
      },
      "id": "a7bb636f-8a62-4db1-aa8d-42195ac2afd3"
    },
    {
      "name": "Notiz: Grenzen",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        760,
        -440
      ],
      "parameters": {
        "content": "## Was die Lite-Version (bewusst) nicht macht\n- **Kein** Lieferant/Betrag im Dateinamen, **keine** Belegliste (CSV), **keine** Kategorien.\n- **Keine** Duplikaterkennung: Jeder Lauf verarbeitet alle PDFs im Eingang erneut und **überschreibt** die gleichnamige Ablage-Kopie (gleicher Name, gleicher Inhalt). Deshalb nach dem Prüfen den Eingang leeren.\n- **Nur manueller Start** (kein Zeitplan).\n- Gescannte Bilder ohne Textebene werden nicht gelesen (keine Texterkennung).\n- Die Originale werden nicht gelöscht.\n\nArbeitshilfe, keine Steuer- oder Buchhaltungsberatung.\n\n_Hinweis in eigener Sache (Werbung): Der kostenpflichtige Papierkram-Autopilot ergänzt u. a. Lieferant/Betrag, Belegliste, Duplikat-Schutz, Zeitplan, Monatsübersicht und Rechnungs-Erinnerung._",
        "width": 600,
        "height": 400,
        "color": 6
      },
      "id": "e6ced8ee-ea81-427c-aa81-5ecf4356c428"
    },
    {
      "name": "Manuell starten",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -420,
        220
      ],
      "parameters": {},
      "id": "af91c044-d8b8-4dfd-aa11-f53bea1c19c7"
    },
    {
      "name": "Einstellungen",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -200,
        220
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "2f3edc27-fa53-4ce3-ad98-0b5b2ce34f8f",
              "name": "basisOrdner",
              "value": "/home/node/.n8n-files/Papierkram",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "notes": "Pfad zu deinem Papierkram-Ordner. Docker-Standard: /home/node/.n8n-files/Papierkram – Windows z. B. C:/Users/DU/.n8n-files/Papierkram (Schrägstriche / verwenden).",
      "notesInFlow": true,
      "id": "5ec25081-4ced-4472-af94-425d8e867c06"
    },
    {
      "name": "Eingang lesen",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        20,
        220
      ],
      "parameters": {
        "fileSelector": "={{ $('Einstellungen').first().json.basisOrdner.replace(/\\\\/g, '/').replace(/\\/+$/, '') + '/Eingang/*.{pdf,PDF,Pdf}' }}",
        "options": {}
      },
      "executeOnce": true,
      "onError": "continueRegularOutput",
      "id": "8c8bcf30-2532-423b-ab9c-17e4e2bc9306"
    },
    {
      "name": "PDF-Text auslesen",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        240,
        220
      ],
      "parameters": {
        "operation": "pdf",
        "binaryPropertyName": "data",
        "options": {
          "joinPages": true,
          "keepSource": "binary"
        }
      },
      "onError": "continueRegularOutput",
      "id": "bacd8a46-5ab1-4961-ac5d-18e6e5ec9e43"
    },
    {
      "name": "Belege umbenennen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        220
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// ===== Code-Node \"Belege umbenennen\" (Modus: Run Once for All Items) =====\n// KI-Starter (gratis) – Belege-Eingang Lite.\n// Bestimmt fuer jedes PDF das Belegdatum und den Zielpfad:\n//   Belege/JJJJ/MM/JJJJ-MM-TT_<Originalname>.pdf\n// Kein Datum gefunden -> Belege/_Pruefen/<Originalname>.pdf\n// Lite-Version: keine Lieferanten-/Betragserkennung, keine Belegliste, keine Duplikaterkennung.\n\nfunction pad(n) { return String(n).padStart(2, '0'); }\n\nfunction gueltigesDatum(j, m, t) {\n  const d = new Date(Date.UTC(j, m - 1, t));\n  return d.getUTCFullYear() === j && d.getUTCMonth() === m - 1 && d.getUTCDate() === t && j >= 2000 && j <= 2100;\n}\n\n// Sucht ein Datum im PDF-Text. Zeilen mit \"Rechnungsdatum\", \"Datum\" o. ae. haben Vorrang.\nfunction findeDatum(text) {\n  const muster = [\n    { re: /\\b(\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})\\b/g, f: (m) => [+m[3], +m[2], +m[1]] },\n    { re: /\\b(\\d{4})-(\\d{2})-(\\d{2})\\b/g, f: (m) => [+m[1], +m[2], +m[3]] },\n  ];\n  const schluessel = /(rechnungsdatum|belegdatum|leistungsdatum|datum|date)/i;\n  const kandidaten = [];\n  text.split(/\\r?\\n/).forEach((zeile, idx) => {\n    for (const mu of muster) {\n      mu.re.lastIndex = 0;\n      let m;\n      while ((m = mu.re.exec(zeile)) !== null) {\n        const [j, mo, t] = mu.f(m);\n        if (gueltigesDatum(j, mo, t)) kandidaten.push({ datum: `${j}-${pad(mo)}-${pad(t)}`, prio: schluessel.test(zeile) ? 0 : 1, idx });\n      }\n    }\n  });\n  kandidaten.sort((a, b) => a.prio - b.prio || a.idx - b.idx);\n  return kandidaten.length ? kandidaten[0].datum : null;\n}\n\nfunction ersetzeUmlaute(s) {\n  return s.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/Ä/g, 'Ae').replace(/Ö/g, 'Oe').replace(/Ü/g, 'Ue').replace(/ß/g, 'ss');\n}\n\n// Dateinamen-tauglicher Teil: nur Buchstaben, Ziffern, Komma, Unterstrich, Bindestrich\nfunction dateinamenTeil(s) {\n  return ersetzeUmlaute(String(s).normalize('NFC'))\n    .replace(/&/g, 'und')\n    .replace(/[^A-Za-z0-9,_-]+/g, '-')\n    .replace(/-+/g, '-')\n    .replace(/^[-_]+|[-_]+$/g, '')\n    .slice(0, 60) || 'Beleg';\n}\n\nconst ergebnis = [];\nconst belegteZiele = new Set();\n\nfor (const item of $input.all()) {\n  const bin = item.binary && (item.binary.data || Object.values(item.binary)[0]);\n  if (!bin) continue; // z. B. leerer Eingang\n  const original = bin.fileName || 'unbenannt.pdf';\n  const ohneEndung = original.replace(/\\.pdf$/i, '');\n  const text = String(item.json.text || '');\n\n  let datum = null;\n  let quelle = 'PDF-Text';\n  let namensTeil = ohneEndung;\n  // 1. Dateiname beginnt schon mit JJJJ-MM-TT -> Datum uebernehmen, nicht doppelt voranstellen\n  const m = ohneEndung.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:[_ -]+(.*))?$/);\n  if (m && gueltigesDatum(+m[1], +m[2], +m[3])) {\n    datum = `${m[1]}-${m[2]}-${m[3]}`;\n    namensTeil = m[4] || 'Beleg';\n    quelle = 'Dateiname';\n  }\n  // 2. Sonst Datum aus dem PDF-Text\n  if (!datum) datum = findeDatum(text);\n\n  let ordner, dateiname;\n  if (datum) {\n    ordner = `Belege/${datum.slice(0, 4)}/${datum.slice(5, 7)}`;\n    dateiname = `${datum}_${dateinamenTeil(namensTeil)}.pdf`;\n  } else {\n    ordner = 'Belege/_Pruefen';\n    dateiname = `${dateinamenTeil(ohneEndung)}.pdf`;\n    quelle = 'kein Datum gefunden';\n  }\n  // Namenskollision innerhalb eines Laufs vermeiden (_2, _3, ...)\n  let ziel = `${ordner}/${dateiname}`;\n  let n = 2;\n  while (belegteZiele.has(ziel.toLowerCase())) {\n    ziel = `${ordner}/${dateiname.replace(/\\.pdf$/i, '')}_${n}.pdf`;\n    n++;\n  }\n  belegteZiele.add(ziel.toLowerCase());\n\n  ergebnis.push({\n    json: {\n      Original_Datei: original,\n      Datum: datum || '',\n      Quelle: quelle,\n      Status: datum ? 'abgelegt' : 'pruefen',\n      Ablage_Datei: ziel,\n    },\n    binary: { data: bin },\n  });\n}\n\nreturn ergebnis;\n"
      },
      "id": "57983d23-9ef5-4a4e-a5f3-2dff9e91ae7c"
    },
    {
      "name": "Beleg ablegen",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        680,
        220
      ],
      "parameters": {
        "operation": "write",
        "fileName": "={{ $('Einstellungen').first().json.basisOrdner.replace(/\\\\/g, '/').replace(/\\/+$/, '') + '/' + $json.Ablage_Datei }}",
        "dataPropertyName": "data",
        "options": {}
      },
      "onError": "continueRegularOutput",
      "id": "a1c6bab4-fdfc-4b89-a8df-f639bcbb5544"
    },
    {
      "name": "Ergebnis",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        220
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// ===== Code-Node \"Ergebnis\" (Modus: Run Once for All Items) =====\n// Fasst den Lauf zusammen: wie viele Belege abgelegt wurden, wie viele in _Pruefen\n// gelandet sind und welche nicht gespeichert werden konnten (z. B. Zielordner fehlt).\n\nconst geplant = $('Belege umbenennen').all().map((i) => i.json);\nconst geschrieben = $input.all(); // Ausgabe von \"Beleg ablegen\" (gleiche Reihenfolge)\nconst ok = [];\nconst fehler = [];\ngeplant.forEach((r, i) => {\n  const out = geschrieben[i] && geschrieben[i].json;\n  if (!out || out.error) {\n    const ordner = String(r.Ablage_Datei).split('/').slice(0, -1).join('/');\n    fehler.push(`${r.Original_Datei} -> ${r.Ablage_Datei} (${out && out.error ? out.error : 'unbekannter Fehler'}; existiert der Ordner ${ordner}?)`);\n  } else {\n    ok.push(r);\n  }\n});\n\nreturn [{\n  json: {\n    abgelegt: ok.filter((r) => r.Status === 'abgelegt').length,\n    pruefen: ok.filter((r) => r.Status === 'pruefen').length,\n    fehler_anzahl: fehler.length,\n    fehler_text: fehler.join('\\n'),\n    dateien: ok.map((r) => `${r.Original_Datei} -> ${r.Ablage_Datei}`),\n    hinweis: 'Die Originale bleiben im Eingang. Bitte Ergebnis prüfen und den Eingang danach von Hand leeren.',\n  },\n}];\n"
      },
      "id": "36faece9-41b6-4313-adc4-03c39d0e6b3c"
    },
    {
      "name": "Ablagefehler?",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        1120,
        220
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "59e23640-ca14-4df9-a338-c76f08d55c89",
              "leftValue": "={{ $json.fehler_anzahl }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "fbdb8a82-5ecf-4eb7-a815-d2fd7daa11c1"
    },
    {
      "name": "Fehler melden",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1340,
        220
      ],
      "parameters": {
        "errorType": "errorMessage",
        "errorMessage": "={{ 'Belege-Eingang Lite: ' + $('Ergebnis').first().json.fehler_anzahl + ' Beleg(e) konnten nicht abgelegt werden. Meist fehlt der Zielordner (Belege/JJJJ/MM) – bitte mit vorlagen/ordner-anlegen-lite anlegen und erneut starten. Die Originale bleiben im Eingang. Details: ' + $('Ergebnis').first().json.fehler_text }}"
      },
      "id": "216e3333-3ca6-4679-a452-86f76d283e0d"
    }
  ],
  "connections": {
    "Manuell starten": {
      "main": [
        [
          {
            "node": "Einstellungen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Einstellungen": {
      "main": [
        [
          {
            "node": "Eingang lesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Eingang lesen": {
      "main": [
        [
          {
            "node": "PDF-Text auslesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PDF-Text auslesen": {
      "main": [
        [
          {
            "node": "Belege umbenennen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Belege umbenennen": {
      "main": [
        [
          {
            "node": "Beleg ablegen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Beleg ablegen": {
      "main": [
        [
          {
            "node": "Ergebnis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ergebnis": {
      "main": [
        [
          {
            "node": "Ablagefehler?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ablagefehler?": {
      "main": [
        [
          {
            "node": "Fehler melden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromSameOwner"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false,
    "kiStarterGratis": "1.0.1"
  },
  "tags": []
}
