{
  "name": "Invoice Generator — Google Sheets to PDF to Email",
  "nodes": [
    {
      "parameters": {},
      "id": "manual-trigger",
      "name": "When clicking Test workflow",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [0, 0]
    },
    {
      "parameters": {
        "rule": {
          "interval": [{ "field": "minutes", "minutesInterval": 15 }]
        }
      },
      "id": "schedule-trigger",
      "name": "Every 15 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 220]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": { "mode": "url", "value": "YOUR_GOOGLE_SHEET_URL_HERE" },
        "sheetName": { "mode": "name", "value": "Invoices" },
        "filtersUI": { "values": [{ "lookupColumn": "Status", "lookupValue": "Draft" }] },
        "options": {}
      },
      "id": "read-invoices",
      "name": "Read Draft Invoices",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [260, 0],
      "credentials": {
        "googleSheetsOAuth2Api": { "id": "{{GOOGLE_CREDENTIAL_ID}}", "name": "Google Sheets account" }
      }
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": { "mode": "url", "value": "YOUR_GOOGLE_SHEET_URL_HERE" },
        "sheetName": { "mode": "name", "value": "Line Items" },
        "options": {}
      },
      "id": "read-items",
      "name": "Read Line Items",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [260, 220],
      "credentials": {
        "googleSheetsOAuth2Api": { "id": "{{GOOGLE_CREDENTIAL_ID}}", "name": "Google Sheets account" }
      }
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": { "mode": "url", "value": "YOUR_GOOGLE_SHEET_URL_HERE" },
        "sheetName": { "mode": "name", "value": "Settings" },
        "options": {}
      },
      "id": "read-settings",
      "name": "Read Settings",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [260, 440],
      "credentials": {
        "googleSheetsOAuth2Api": { "id": "{{GOOGLE_CREDENTIAL_ID}}", "name": "Google Sheets account" }
      }
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// Invoice HTML Generator (Production-Grade)\n// ============================================================\n// Reads: invoices from 'Read Draft Invoices'\n//        line items from 'Read Line Items'\n//        settings from 'Read Settings'\n// Outputs: one item per invoice with invoiceHTML field\n// ============================================================\n\n// Parse settings into a key-value map\nconst settingsRows = $('Read Settings').all();\nconst settings = {};\nfor (const row of settingsRows) {\n  if (row.json['Setting'] && row.json['Value']) {\n    settings[row.json['Setting']] = row.json['Value'];\n  }\n}\n\nconst BIZ_NAME = settings['Business Name'] || 'Your Business';\nconst BIZ_EMAIL = settings['Business Email'] || '';\nconst BIZ_ADDR = settings['Business Address'] || '';\nconst BANK_NAME = settings['Bank Name'] || '';\nconst BANK_ACCT = settings['Bank Account'] || '';\nconst BANK_REF = settings['Bank Reference'] || '';\n\n// Get all line items\nconst allItems = $('Read Line Items').all();\n\n// Process each draft invoice\nconst invoices = $input.all();\nconst results = [];\n\nfor (const inv of invoices) {\n  const d = inv.json;\n  const invoiceNum = d['Invoice #'] || 'INV-000';\n  const taxPct = parseFloat(d['Tax %']) || 0;\n\n  // Find matching line items\n  const items = allItems.filter(i => i.json['Invoice #'] === invoiceNum);\n\n  // Build line items HTML and calculate subtotal\n  let subtotal = 0;\n  let lineRows = '';\n  for (const item of items) {\n    const qty = parseFloat(item.json['Quantity']) || 1;\n    const price = parseFloat(item.json['Unit Price']) || 0;\n    const amount = qty * price;\n    subtotal += amount;\n    const currency = item.json['Currency'] || 'EUR';\n    lineRows += '<tr>'\n      + '<td style=\"padding:10px 16px;border-bottom:1px solid #e8e8e8\">' + item.json['Description'] + '</td>'\n      + '<td style=\"padding:10px 16px;border-bottom:1px solid #e8e8e8;text-align:center\">' + (qty === Math.floor(qty) ? Math.floor(qty) : qty) + '</td>'\n      + '<td style=\"padding:10px 16px;border-bottom:1px solid #e8e8e8;text-align:right\">' + price.toLocaleString('en', {minimumFractionDigits: 2}) + '</td>'\n      + '<td style=\"padding:10px 16px;border-bottom:1px solid #e8e8e8;text-align:right\">' + amount.toLocaleString('en', {minimumFractionDigits: 2}) + '</td>'\n      + '</tr>';\n  }\n\n  const currency = items[0]?.json['Currency'] || 'EUR';\n  const taxAmount = subtotal * taxPct / 100;\n  const total = subtotal + taxAmount;\n\n  const html = '<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head>'\n    + '<body style=\"font-family:Helvetica Neue,Helvetica,Arial,sans-serif;max-width:680px;margin:0 auto;padding:48px 32px;color:#1a1a1a;font-size:14px;line-height:1.5\">'\n    // Header\n    + '<div style=\"display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:40px;padding-bottom:24px;border-bottom:3px solid #1a1a1a\">'\n    + '<div><div style=\"font-size:28px;font-weight:700;letter-spacing:-0.5px\">' + BIZ_NAME + '</div>'\n    + '<div style=\"color:#666;margin-top:4px;font-size:13px\">' + BIZ_ADDR + '</div>'\n    + '<div style=\"color:#666;font-size:13px\">' + BIZ_EMAIL + '</div></div>'\n    + '<div style=\"text-align:right\"><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:1.5px;color:#999;margin-bottom:4px\">Invoice</div>'\n    + '<div style=\"font-size:22px;font-weight:700\">' + invoiceNum + '</div></div></div>'\n    // Client + dates\n    + '<div style=\"display:flex;justify-content:space-between;margin-bottom:36px\">'\n    + '<div><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#999;margin-bottom:6px\">Bill to</div>'\n    + '<div style=\"font-weight:600;font-size:15px\">' + (d['Client Name']||'') + '</div>'\n    + '<div style=\"color:#555;font-size:13px;margin-top:2px\">' + (d['Client Address']||'') + '</div>'\n    + '<div style=\"color:#555;font-size:13px\">' + (d['Client Email']||'') + '</div></div>'\n    + '<div style=\"text-align:right\">'\n    + '<div style=\"margin-bottom:12px\"><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#999\">Issue date</div><div style=\"font-weight:500\">' + (d['Date']||'') + '</div></div>'\n    + '<div style=\"margin-bottom:12px\"><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#999\">Due date</div><div style=\"font-weight:500\">' + (d['Due Date']||'') + '</div></div>'\n    + '<div><div style=\"font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#999\">Terms</div><div style=\"font-weight:500\">' + (d['Payment Terms']||'') + '</div></div>'\n    + '</div></div>'\n    // Table\n    + '<table style=\"width:100%;border-collapse:collapse;margin-bottom:24px\"><thead>'\n    + '<tr style=\"border-bottom:2px solid #1a1a1a\">'\n    + '<th style=\"padding:10px 16px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#666\">Description</th>'\n    + '<th style=\"padding:10px 16px;text-align:center;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#666;width:60px\">Qty</th>'\n    + '<th style=\"padding:10px 16px;text-align:right;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#666;width:100px\">Unit Price</th>'\n    + '<th style=\"padding:10px 16px;text-align:right;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#666;width:100px\">Amount</th>'\n    + '</tr></thead><tbody>' + lineRows + '</tbody></table>'\n    // Totals\n    + '<div style=\"display:flex;justify-content:flex-end;margin-bottom:36px\"><div style=\"width:240px\">'\n    + '<div style=\"display:flex;justify-content:space-between;padding:6px 0;color:#555\"><span>Subtotal</span><span>' + subtotal.toLocaleString('en', {minimumFractionDigits: 2}) + ' ' + currency + '</span></div>'\n    + '<div style=\"display:flex;justify-content:space-between;padding:6px 0;color:#555;border-bottom:1px solid #e8e8e8\"><span>VAT (' + Math.floor(taxPct) + '%)</span><span>' + taxAmount.toLocaleString('en', {minimumFractionDigits: 2}) + ' ' + currency + '</span></div>'\n    + '<div style=\"display:flex;justify-content:space-between;padding:12px 0;font-weight:700;font-size:18px\"><span>Total</span><span>' + total.toLocaleString('en', {minimumFractionDigits: 2}) + ' ' + currency + '</span></div>'\n    + '</div></div>'\n    // Payment details\n    + '<div style=\"background:#f8f8f8;padding:20px;border-radius:6px;margin-bottom:24px\">'\n    + '<div style=\"font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#999;margin-bottom:8px\">Payment details</div>'\n    + '<div style=\"display:flex;gap:40px;font-size:13px\">'\n    + '<div><div style=\"color:#999;font-size:11px;margin-bottom:2px\">Bank</div><div>' + BANK_NAME + '</div></div>'\n    + '<div><div style=\"color:#999;font-size:11px;margin-bottom:2px\">Account</div><div>' + BANK_ACCT + '</div></div>'\n    + '<div><div style=\"color:#999;font-size:11px;margin-bottom:2px\">Reference</div><div>' + BANK_REF + '</div></div>'\n    + '</div></div>'\n    // Notes\n    + (d['Notes'] ? '<div style=\"color:#666;font-size:13px;margin-bottom:24px\">' + d['Notes'] + '</div>' : '')\n    // Footer\n    + '<div style=\"border-top:1px solid #e8e8e8;padding-top:16px;color:#999;font-size:11px;text-align:center\">' + BIZ_NAME + ' &middot; ' + BIZ_EMAIL + '</div>'\n    + '</body></html>';\n\n  results.push({\n    json: {\n      ...d,\n      invoiceNum,\n      total,\n      currency,\n      invoiceHTML: html\n    }\n  });\n}\n\nreturn results;"
      },
      "id": "build-html",
      "name": "Build HTML Invoice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [540, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://html2pdf.fly.dev/api/generate",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [{ "name": "Content-Type", "value": "application/json" }]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\"html\": {{ JSON.stringify($json.invoiceHTML) }}, \"options\": {\"format\": \"A4\", \"margin\": {\"top\": \"10mm\", \"right\": \"10mm\", \"bottom\": \"10mm\", \"left\": \"10mm\"}}}",
        "options": {
          "response": { "response": { "responseFormat": "file" } }
        }
      },
      "id": "to-pdf",
      "name": "Convert to PDF",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [760, 0]
    },
    {
      "parameters": {
        "sendTo": "={{ $('Build HTML Invoice').item.json['Client Email'] }}",
        "subject": "=Invoice {{ $('Build HTML Invoice').item.json.invoiceNum }}",
        "message": "=Hi {{ $('Build HTML Invoice').item.json['Client Name'] }},\n\nPlease find your invoice attached.\n\nAmount due: {{ $('Build HTML Invoice').item.json.total.toLocaleString('en', {minimumFractionDigits: 2}) }} {{ $('Build HTML Invoice').item.json.currency }}\nDue date: {{ $('Build HTML Invoice').item.json['Due Date'] }}\n\nLet me know if you have any questions.\n\nBest regards",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [{ "property": "data" }]
          }
        }
      },
      "id": "send-email",
      "name": "Email Invoice",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [980, 0],
      "credentials": {
        "gmailOAuth2": { "id": "{{GMAIL_CREDENTIAL_ID}}", "name": "Gmail account" }
      }
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": { "mode": "url", "value": "YOUR_GOOGLE_SHEET_URL_HERE" },
        "sheetName": { "mode": "name", "value": "Invoices" },
        "columnToMatchOn": "Invoice #",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Invoice #": "={{ $json[\"invoiceNum\"] }}",
            "Status": "Sent"
          },
          "matchingColumns": ["Invoice #"]
        },
        "options": {}
      },
      "id": "mark-sent",
      "name": "Mark as Sent",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [1200, 0],
      "credentials": {
        "googleSheetsOAuth2Api": { "id": "{{GOOGLE_CREDENTIAL_ID}}", "name": "Google Sheets account" }
      }
    }
  ],
  "connections": {
    "When clicking Test workflow": {
      "main": [[{ "node": "Read Draft Invoices", "type": "main", "index": 0 }]]
    },
    "Every 15 Minutes": {
      "main": [[{ "node": "Read Draft Invoices", "type": "main", "index": 0 }]]
    },
    "Read Draft Invoices": {
      "main": [[{ "node": "Build HTML Invoice", "type": "main", "index": 0 }]]
    },
    "Read Line Items": {
      "main": [[{ "node": "Build HTML Invoice", "type": "main", "index": 0 }]]
    },
    "Read Settings": {
      "main": [[{ "node": "Build HTML Invoice", "type": "main", "index": 0 }]]
    },
    "Build HTML Invoice": {
      "main": [[{ "node": "Convert to PDF", "type": "main", "index": 0 }]]
    },
    "Convert to PDF": {
      "main": [[{ "node": "Email Invoice", "type": "main", "index": 0 }]]
    },
    "Email Invoice": {
      "main": [[{ "node": "Mark as Sent", "type": "main", "index": 0 }]]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
