{
  "name": "Angebot aus Anfrage erstellen",
  "nodes": [
    {
      "id": "306c921c-abe6-4811-a5b0-04dbe8db467e",
      "name": "Notiz 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        -520
      ],
      "parameters": {
        "content": "## So richtest du den Ablauf ein\n1. In **Angebot berechnen** oben deine Preisliste, den Umsatzsteuersatz und deinen Firmennamen eintragen.\n2. Mit **Test: Start per Klick** ausprobieren: Vier Beispiel-Anfragen laufen durch, nichts wird gesendet.\n3. Den Knoten **Anfrageformular** öffnen: n8n zeigt dir die Adresse des Formulars, die du auf deiner Website verlinkst.\n4. Die ausgeschalteten E-Mail-Knoten mit deinen SMTP-Daten einrichten und einschalten.\n5. Optional (Lexware Office, Tarif XL): API-Schlüssel als \"Bearer Auth\" anlegen und **In Lexware Office als Entwurf anlegen** einschalten.\n\nDas Angebot geht immer zuerst an dich zur Prüfung, nie direkt an den Kunden.",
        "height": 320,
        "width": 540,
        "color": 4
      }
    },
    {
      "id": "0d331bc7-7930-419c-a129-0ef0a3a5afd5",
      "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": "d0868e81-bc4d-41e4-a628-aca415217961",
      "name": "Anfrageformular",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.2,
      "position": [
        240,
        0
      ],
      "webhookId": "4091f4f5-5ec5-449e-a0b9-705f3a690adf",
      "parameters": {
        "formTitle": "Angebot anfragen",
        "formDescription": "Sagen Sie uns kurz, was Sie brauchen. Wir melden uns mit einem Angebot.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Name",
              "fieldType": "text",
              "requiredField": true
            },
            {
              "fieldLabel": "Firma",
              "fieldType": "text",
              "requiredField": false
            },
            {
              "fieldLabel": "E-Mail",
              "fieldType": "email",
              "requiredField": true
            },
            {
              "fieldLabel": "Leistung",
              "fieldType": "dropdown",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Beratung (pro Stunde)"
                  },
                  {
                    "option": "Website-Check"
                  },
                  {
                    "option": "Logo-Entwurf"
                  },
                  {
                    "option": "Landingpage"
                  },
                  {
                    "option": "Weiß ich noch nicht"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldLabel": "Menge",
              "fieldType": "number",
              "placeholder": "z. B. Anzahl Stunden oder Stück",
              "requiredField": true
            },
            {
              "fieldLabel": "Nachricht",
              "fieldType": "textarea",
              "requiredField": false
            }
          ]
        },
        "responseMode": "onReceived",
        "options": {
          "appendAttribution": false,
          "respondWithOptions": {
            "values": {
              "respondWith": "text",
              "formSubmittedText": "Danke! Ihre Anfrage ist angekommen. Sie hören in Kürze von uns."
            }
          }
        }
      },
      "notes": "Ein Formular, das n8n selbst bereitstellt. Die Adresse verlinkst du auf deiner Website."
    },
    {
      "id": "c79c69d9-df69-40d8-a4f9-ab32d0091d49",
      "name": "Testdaten: 4 Anfragen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        160
      ],
      "parameters": {
        "jsCode": "// Erfundene Anfragen im selben Aufbau, den das Formular liefert.\nreturn [\n  { Name: 'Lena Hoffmann', Firma: 'Hoffmann Floristik', 'E-Mail': 'lena.hoffmann@example.com', Leistung: 'Website-Check', Menge: 1, Nachricht: 'Unsere Seite lädt langsam.' },\n  { Name: 'Sandra Wolf', Firma: 'Wolf & Partner', 'E-Mail': 's.wolf@example.org', Leistung: 'Beratung (pro Stunde)', Menge: 3, Nachricht: 'Wir wollen unsere Abläufe besprechen.' },\n  { Name: 'Max Berger', Firma: '', 'E-Mail': 'max@example.com', Leistung: 'Weiß ich noch nicht', Menge: '', Nachricht: 'Was kostet eine neue Website?' },\n  // Leistung, die (noch) nicht in der Preisliste steht, z. B. weil das Formular geändert wurde\n  { Name: 'Jana Klein', Firma: 'Klein Bau', 'E-Mail': 'jana.klein@example.net', Leistung: 'Fotoshooting', Menge: 1, Nachricht: 'Geht das auch?' },\n].map((j) => ({ json: { ...j, submittedAt: new Date().toISOString(), formMode: 'test' } }));\n"
      },
      "notes": "Vier erfundene Anfragen: zwei vollständige, eine unvollständige, eine mit einer Leistung, die nicht in der Preisliste steht."
    },
    {
      "id": "81660896-0967-4c25-a79c-281450ed9c30",
      "name": "Anfrage prüfen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        80
      ],
      "parameters": {
        "jsCode": "// Liest die Formularfelder und prüft, ob alles für ein Angebot da ist.\nreturn $input.all().map(({ json: j }) => {\n  const anfrage = {\n    name: String(j['Name'] ?? '').trim(),\n    firma: String(j['Firma'] ?? '').trim(),\n    email: String(j['E-Mail'] ?? '').trim(),\n    leistung: String(j['Leistung'] ?? '').trim(),\n    menge: Number(String(j['Menge'] ?? '').replace(',', '.')) || 0,\n    nachricht: String(j['Nachricht'] ?? '').trim(),\n  };\n  const fehlt = [];\n  if (!anfrage.name) fehlt.push('Ihr Name');\n  if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(anfrage.email)) fehlt.push('Ihre E-Mail-Adresse');\n  if (!anfrage.leistung || anfrage.leistung.startsWith('Weiß ich')) fehlt.push('die gewünschte Leistung');\n  if (anfrage.menge <= 0) fehlt.push('die Menge');\n  return { json: { ...anfrage, vollstaendig: fehlt.length === 0, fehlt } };\n});\n"
      },
      "notes": "Prüft Pflichtangaben: Name, E-Mail, Leistung, Menge."
    },
    {
      "id": "1821ec99-97b0-404f-a1da-b935cf0ecd38",
      "name": "Alles angegeben?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        720,
        80
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "ad7e14e7-18fe-4495-a1dc-ac6d3748b99b",
              "leftValue": "={{ $json.vollstaendig }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "notes": "Vollständig: Angebot rechnen. Sonst: höflich nachfragen."
    },
    {
      "id": "b82188bc-694c-4648-aa4e-248023ee5751",
      "name": "Angebot berechnen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        -20
      ],
      "parameters": {
        "jsCode": "// ===== Deine Angaben: hier anpassen =====\nconst FIRMA = 'Muster Design (Beispiel)';\nconst LAND = 'DE'; // für Lexware: Ländercode des Kunden, DE oder AT\nconst UST_SATZ = 19; // Deutschland 19, Österreich 20\nconst KLEINUNTERNEHMER = false; // true: keine Umsatzsteuer ausweisen\nconst GUELTIG_TAGE = 30;\nconst PREISLISTE = {\n  // Beispielpreise, netto in Euro\n  'Beratung (pro Stunde)': { preis: 90, einheit: 'Stunde' },\n  'Website-Check': { preis: 290, einheit: 'Stück' },\n  'Logo-Entwurf': { preis: 650, einheit: 'Stück' },\n  Landingpage: { preis: 1200, einheit: 'Stück' },\n};\n// =========================================\nconst runden = (z) => Math.round(z * 100) / 100;\nconst heute = new Date();\n// Datum und Uhrzeit in deutscher Zeit (sonst Weltzeit: kurz nach Mitternacht stünde das Datum vom Vortag).\nconst ortszeit = (d) => new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(d);\nconst tag = (d) => ortszeit(d).slice(0, 10);\n// Vorläufige Nummer aus Datum und Uhrzeit, damit sich Nummern nicht wiederholen.\n// Die endgültige Angebotsnummer vergibt dein Buchhaltungsprogramm.\nconst zeit = ortszeit(heute).slice(11, 19).replace(/:/g, '');\nreturn $input.all().map(({ json: a }, i) => {\n  const posten = PREISLISTE[a.leistung];\n  if (!posten) return { json: { ...a, angebot: null, fehler: `\"${a.leistung}\" steht nicht in der Preisliste` } };\n  const netto = runden(posten.preis * a.menge);\n  const satz = KLEINUNTERNEHMER ? 0 : UST_SATZ;\n  const ust = runden((netto * satz) / 100);\n  const gueltigBis = new Date(heute.getTime() + GUELTIG_TAGE * 86400000);\n  const angebot = {\n    nummer: `AN-${tag(heute).replace(/-/g, '')}-${zeit}-${String(i + 1).padStart(2, '0')}`,\n    datum: tag(heute),\n    gueltigBis: tag(gueltigBis),\n    firma: FIRMA,\n    position: { bezeichnung: a.leistung, menge: a.menge, einheit: posten.einheit, einzelpreisNetto: posten.preis, summeNetto: netto },\n    netto,\n    ustSatz: satz,\n    ust,\n    brutto: runden(netto + ust),\n  };\n  // Inhalt für die Lexware-Office-Schnittstelle (POST /v1/quotations), aus der Doku abgeleitet, nicht gegen die echte Schnittstelle getestet\n  const lexware = {\n    voucherDate: `${angebot.datum}T00:00:00.000+01:00`,\n    expirationDate: `${angebot.gueltigBis}T00:00:00.000+01:00`,\n    address: { name: a.firma || a.name, countryCode: LAND },\n    lineItems: [\n      {\n        type: 'custom',\n        name: a.leistung,\n        quantity: a.menge,\n        unitName: posten.einheit,\n        unitPrice: { currency: 'EUR', netAmount: posten.preis, taxRatePercentage: satz },\n      },\n    ],\n    totalPrice: { currency: 'EUR' },\n    // Lexware-Doku: taxType „vatfree“ = steuerfrei; für Kleinunternehmer gewählt, nicht gegen die echte Schnittstelle getestet\n    taxConditions: { taxType: KLEINUNTERNEHMER ? 'vatfree' : 'net' },\n    title: 'Angebot',\n    introduction: `Vielen Dank für Ihre Anfrage. Gern bieten wir Ihnen an:`,\n  };\n  return { json: { ...a, angebot, lexware } };\n});\n"
      },
      "notes": "Rechnet mit deiner Preisliste: Netto, Umsatzsteuer, Brutto, Gültigkeit. Preisliste oben im Code anpassen."
    },
    {
      "id": "a9bca5fe-e99d-42c0-ae97-69f0ae26bf65",
      "name": "Angebot als Text aufbereiten",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        -120
      ],
      "parameters": {
        "jsCode": "const euro = (z) => z.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });\nreturn $input.all().map(({ json: j }) => {\n  const an = j.angebot;\n  if (!an) {\n    // Leistung fehlt in der Preisliste: kein automatisches Angebot, du antwortest selbst.\n    return { json: { ...j, angebotText: `Kein automatisches Angebot möglich: ${j.fehler}. Bitte selbst antworten.\\n\\nAnfrage von ${j.name} <${j.email}>:\\n${j.nachricht}` } };\n  }\n  const p = an.position;\n  const text = [\n    `Angebot ${an.nummer} vom ${an.datum}, gültig bis ${an.gueltigBis}`,\n    `Für: ${j.firma ? `${j.firma}, ` : ''}${j.name} <${j.email}>`,\n    '',\n    `${p.menge} × ${p.bezeichnung} (${p.einheit}) à ${euro(p.einzelpreisNetto)} = ${euro(p.summeNetto)}`,\n    `Netto: ${euro(an.netto)}`,\n    an.ustSatz > 0 ? `Umsatzsteuer ${an.ustSatz} %: ${euro(an.ust)}` : 'Keine Umsatzsteuer (Kleinunternehmer)',\n    `Gesamt: ${euro(an.brutto)}`,\n    '',\n    j.nachricht ? `Nachricht des Kunden: ${j.nachricht}` : '',\n  ].join('\\n');\n  return { json: { ...j, angebotText: text } };\n});\n"
      },
      "notes": "Macht aus den Zahlen einen lesbaren Angebotsentwurf."
    },
    {
      "id": "ffc956af-3544-464a-adb2-2e27c0b23ab5",
      "name": "Preis gefunden?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1200,
        40
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "b7636db5-7918-4377-acfe-4f16f69bbc02",
              "leftValue": "={{ Boolean($json.lexware) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "notes": "Nur Anfragen mit Preis aus der Preisliste gehen an Lexware Office."
    },
    {
      "id": "3bf59783-3ba6-4bbd-aad1-a989c934f235",
      "name": "In Lexware Office als Entwurf anlegen",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1440,
        40
      ],
      "disabled": true,
      "parameters": {
        "method": "POST",
        "url": "https://api.lexware.io/v1/quotations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.lexware) }}",
        "options": {}
      },
      "notes": "Optional, beim Import ausgeschaltet. Legt das Angebot als Entwurf in Lexware Office an. Die Schnittstelle gibt es laut Lexware nur im Tarif XL. Nicht von uns ausgeführt."
    },
    {
      "id": "761d158c-7dda-4735-a5f5-a7bf48b56de1",
      "name": "Entwurf an dich zur Prüfung",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1440,
        -120
      ],
      "disabled": true,
      "parameters": {
        "fromEmail": "Ablauf <deine-adresse@example.com>",
        "toEmail": "deine-adresse@example.com",
        "subject": "={{ $json.angebot ? 'Angebotsentwurf ' + $json.angebot.nummer + ' für ' + $json.name : 'Anfrage ohne automatisches Angebot: ' + $json.name }}",
        "emailFormat": "text",
        "text": "={{ $json.angebotText }}",
        "options": {
          "appendAttribution": false
        }
      },
      "notes": "Schickt den Entwurf an dich. Du prüfst und leitest ihn weiter. Beim Import ausgeschaltet."
    },
    {
      "id": "163916b5-6d93-4c08-ae1d-aa5e001b2c0f",
      "name": "Rückfrage vorbereiten",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        960,
        200
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d5273dd3-441d-4763-a238-7cc7d1897e54",
              "name": "rueckfrageBetreff",
              "value": "Ihre Anfrage: uns fehlt noch eine Angabe",
              "type": "string"
            },
            {
              "id": "37902143-bc7d-4c6b-a060-42a4e33eee56",
              "name": "rueckfrageText",
              "value": "=Guten Tag {{ $json.name || '' }},\n\nvielen Dank für Ihre Anfrage. Damit wir Ihnen ein passendes Angebot schicken können, fehlt uns noch: {{ $json.fehlt.join(', ') }}.\n\nAntworten Sie einfach auf diese E-Mail.\n\nViele Grüße",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "notes": "Schreibt eine freundliche Rückfrage mit den fehlenden Angaben."
    },
    {
      "id": "8019664d-5255-4bcb-af87-bcf3aea5244b",
      "name": "Rückfrage senden",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1200,
        200
      ],
      "disabled": true,
      "parameters": {
        "fromEmail": "Ihr Name <deine-adresse@example.com>",
        "toEmail": "={{ $json.email }}",
        "subject": "={{ $json.rueckfrageBetreff }}",
        "emailFormat": "text",
        "text": "={{ $json.rueckfrageText }}",
        "options": {
          "appendAttribution": false
        }
      },
      "notes": "Sendet die Rückfrage an den Kunden. Beim Import ausgeschaltet."
    }
  ],
  "connections": {
    "Test: Start per Klick": {
      "main": [
        [
          {
            "node": "Testdaten: 4 Anfragen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anfrageformular": {
      "main": [
        [
          {
            "node": "Anfrage prüfen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Testdaten: 4 Anfragen": {
      "main": [
        [
          {
            "node": "Anfrage prüfen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anfrage prüfen": {
      "main": [
        [
          {
            "node": "Alles angegeben?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alles angegeben?": {
      "main": [
        [
          {
            "node": "Angebot berechnen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rückfrage vorbereiten",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Angebot berechnen": {
      "main": [
        [
          {
            "node": "Angebot als Text aufbereiten",
            "type": "main",
            "index": 0
          },
          {
            "node": "Preis gefunden?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preis gefunden?": {
      "main": [
        [
          {
            "node": "In Lexware Office als Entwurf anlegen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Angebot als Text aufbereiten": {
      "main": [
        [
          {
            "node": "Entwurf an dich zur Prüfung",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rückfrage vorbereiten": {
      "main": [
        [
          {
            "node": "Rückfrage senden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Berlin"
  },
  "pinData": {}
}
