{
  "name": "Weekly Accounts Receivable Aging Report with n8n (free JSON)",
  "nodes": [
    {
      "id": "Sticky Note",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        -520
      ],
      "parameters": {
        "color": "6",
        "width": 700,
        "height": 180,
        "content": "## Weekly accounts-receivable ageing report from Invoice Ninja\n\nOne landscape PDF that shows who owes you what and how late it is, mailed to yourself every Monday. Add Invoice Ninja, PDFMint and Gmail credentials, then edit **Your company details**.\n\nPDFMint is a community node: install `n8n-nodes-pdfmint` from Settings → Community nodes first."
      },
      "typeVersion": 1
    },
    {
      "id": "Sticky Note1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        -80
      ],
      "parameters": {
        "width": 320,
        "height": 360,
        "content": "### 1 · Schedule\nRuns every Monday at 08:00, before the week's chasing starts."
      },
      "typeVersion": 1
    },
    {
      "id": "Sticky Note2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -200,
        -80
      ],
      "parameters": {
        "width": 320,
        "height": 360,
        "content": "### 2 · Branding\nSet your company name, address, accent colour, currency and the address the report goes to. This is the node where you type your own details."
      },
      "typeVersion": 1
    },
    {
      "id": "Sticky Note3",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        -80
      ],
      "parameters": {
        "width": 320,
        "height": 360,
        "content": "### 3 · Invoice Ninja\nFetches every active invoice with the client record included, so the report can name clients rather than ids."
      },
      "typeVersion": 1
    },
    {
      "id": "Sticky Note4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        520,
        -80
      ],
      "parameters": {
        "width": 320,
        "height": 360,
        "content": "### 4 · Ageing\nKeeps invoices with a balance still owed and buckets each one by days past its due date: not yet due, 1-30, 31-60, 61-90, 90+. Invoices with no due date are left out of the report."
      },
      "typeVersion": 1
    },
    {
      "id": "Sticky Note5",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        -80
      ],
      "parameters": {
        "width": 320,
        "height": 360,
        "content": "### 5 · Guard\nStops cleanly when nothing is outstanding, so no empty report is mailed and no PDF credit is spent."
      },
      "typeVersion": 1
    },
    {
      "id": "Sticky Note6",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1240,
        -80
      ],
      "parameters": {
        "width": 320,
        "height": 360,
        "content": "### 6 · PDF\nRenders the branded ageing table with PDFMint and names the file after the report date."
      },
      "typeVersion": 1
    },
    {
      "id": "Sticky Note7",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        -80
      ],
      "parameters": {
        "width": 320,
        "height": 360,
        "content": "### 7 · Delivery\nEmails the PDF to the address you set in step 2. Swap this node for Slack or Drive if you prefer."
      },
      "typeVersion": 1
    },
    {
      "id": "Every Monday morning",
      "name": "Every Monday morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -480,
        100
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "Your company details",
      "name": "Your company details",
      "type": "n8n-nodes-base.set",
      "position": [
        -120,
        100
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "companyName",
              "type": "string",
              "value": "Your Company Name"
            },
            {
              "id": "a2",
              "name": "companyAddress",
              "type": "string",
              "value": "Your company address"
            },
            {
              "id": "a3",
              "name": "accentColour",
              "type": "string",
              "value": "#0b6b57"
            },
            {
              "id": "a4",
              "name": "currency",
              "type": "string",
              "value": "EUR"
            },
            {
              "id": "a5",
              "name": "reportRecipient",
              "type": "string",
              "value": ""
            },
            {
              "id": "a6",
              "name": "footerNote",
              "type": "string",
              "value": "Balances are as at the run date. Questions about a line? Reply to this email."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "Get open invoices from Invoice Ninja",
      "name": "Get open invoices from Invoice Ninja",
      "type": "n8n-nodes-base.invoiceNinja",
      "position": [
        240,
        100
      ],
      "parameters": {
        "options": {
          "status": "active",
          "include": "client"
        },
        "resource": "invoice",
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 2
    },
    {
      "id": "Build the ageing report",
      "name": "Build the ageing report",
      "type": "n8n-nodes-base.code",
      "position": [
        600,
        100
      ],
      "parameters": {
        "jsCode": "// One accounts-receivable ageing report for the whole ledger: every client who\n// still owes money, split by how late each invoice is.\nconst brand = $('Your company details').first().json;\nconst today = new Date(new Date().toISOString().slice(0, 10));\nconst DAY = 24 * 60 * 60 * 1000;\n\nconst money = (n) => new Intl.NumberFormat('en-IE', {\n  style: 'currency',\n  currency: brand.currency || 'EUR',\n}).format(Number(n) || 0);\n\n// The order an accountant reads them in.\nconst BUCKETS = ['Not yet due', '1-30 days', '31-60 days', '61-90 days', '90+ days'];\nconst bucketFor = (dueDate) => {\n  const days = Math.floor((today - new Date(String(dueDate).slice(0, 10))) / DAY);\n  if (days <= 0) return BUCKETS[0];\n  if (days <= 30) return BUCKETS[1];\n  if (days <= 60) return BUCKETS[2];\n  if (days <= 90) return BUCKETS[3];\n  return BUCKETS[4];\n};\n\n// $input.all() is the documented Code node API. `items` is legacy and hands you\n// undefined entries when the node above returned nothing, which crashes the run\n// on the first property access.\nconst open = $input.all()\n  .map((i) => i.json)\n  .filter(Boolean)\n  // Invoice Ninja keeps paid invoices in the same list. What is still owed is\n  // `balance`, never `amount` - a part-paid invoice carries both.\n  .filter((inv) => Number(inv.balance) > 0 && inv.due_date);\n\nconst byClient = new Map();\nfor (const inv of open) {\n  // `include: client` fills inv.client. Without it there is still an id to group by.\n  const name = inv.client?.display_name || inv.client?.name || `Client ${inv.client_id}`;\n  if (!byClient.has(name)) {\n    byClient.set(name, {\n      name,\n      total: 0,\n      oldestDays: 0,\n      buckets: Object.fromEntries(BUCKETS.map((b) => [b, 0])),\n    });\n  }\n  const row = byClient.get(name);\n  const owed = Number(inv.balance);\n  const late = Math.max(0, Math.floor((today - new Date(String(inv.due_date).slice(0, 10))) / DAY));\n  row.total += owed;\n  row.oldestDays = Math.max(row.oldestDays, late);\n  row.buckets[bucketFor(inv.due_date)] += owed;\n}\n\nconst clients = [...byClient.values()].sort((a, b) => b.total - a.total);\nconst columnTotal = (b) => clients.reduce((sum, c) => sum + c.buckets[b], 0);\nconst grandTotal = clients.reduce((sum, c) => sum + c.total, 0);\nconst overdueTotal = grandTotal - columnTotal(BUCKETS[0]);\nconst asOf = today.toISOString().slice(0, 10);\nconst accent = brand.accentColour || '#0b6b57';\n\nconst esc = (s) => String(s).replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));\nconst cell = (v) => (v ? money(v) : '<span class=\"zero\">-</span>');\n\nconst rows = clients.map((c) => `\n      <tr>\n        <td class=\"name\">${esc(c.name)}${c.oldestDays > 90 ? ' <span class=\"flag\">90+ days</span>' : ''}</td>\n        ${BUCKETS.map((b) => `<td class=\"num\">${cell(c.buckets[b])}</td>`).join('')}\n        <td class=\"num total\">${money(c.total)}</td>\n      </tr>`).join('');\n\nconst html = `<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>Accounts receivable ${asOf}</title>\n<style>\n  @page { size: A4 landscape; margin: 16mm 14mm; }\n  body { font: 12px/1.5 -apple-system, \"Segoe UI\", Helvetica, Arial, sans-serif; color: #1c1c1c; }\n  header { border-bottom: 3px solid ${accent}; padding-bottom: 10px; margin-bottom: 18px; }\n  h1 { font-size: 20px; margin: 0 0 2px; color: ${accent}; }\n  .sub { color: #555; }\n  .cards { display: flex; gap: 10px; margin-bottom: 18px; }\n  .card { flex: 1; border: 1px solid #e3e3e3; border-radius: 6px; padding: 10px 12px; }\n  .card .k { color: #666; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }\n  .card .v { font-size: 18px; font-weight: 600; margin-top: 2px; }\n  table { width: 100%; border-collapse: collapse; }\n  th, td { padding: 7px 8px; border-bottom: 1px solid #ececec; }\n  th { text-align: right; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: #666; }\n  th:first-child, td.name { text-align: left; }\n  td.num { text-align: right; font-variant-numeric: tabular-nums; }\n  td.total, tfoot td { font-weight: 600; }\n  tfoot td { border-top: 2px solid ${accent}; border-bottom: none; }\n  .zero { color: #bbb; }\n  .flag { background: #fdecea; color: #a4291c; border-radius: 3px; padding: 1px 5px; font-size: 10px; }\n  footer { margin-top: 20px; color: #666; font-size: 11px; }\n</style></head>\n<body>\n  <header>\n    <h1>Accounts receivable &mdash; ${esc(brand.companyName || 'Your company')}</h1>\n    <div class=\"sub\">Ageing as at ${asOf} &middot; ${esc(brand.companyAddress || '')}</div>\n  </header>\n  <div class=\"cards\">\n    <div class=\"card\"><div class=\"k\">Outstanding</div><div class=\"v\">${money(grandTotal)}</div></div>\n    <div class=\"card\"><div class=\"k\">Overdue</div><div class=\"v\">${money(overdueTotal)}</div></div>\n    <div class=\"card\"><div class=\"k\">Over 90 days</div><div class=\"v\">${money(columnTotal(BUCKETS[4]))}</div></div>\n    <div class=\"card\"><div class=\"k\">Clients</div><div class=\"v\">${clients.length}</div></div>\n  </div>\n  <table>\n    <thead><tr><th>Client</th>${BUCKETS.map((b) => `<th>${b}</th>`).join('')}<th>Total</th></tr></thead>\n    <tbody>${rows}\n    </tbody>\n    <tfoot><tr><td>Total</td>${BUCKETS.map((b) => `<td class=\"num\">${money(columnTotal(b))}</td>`).join('')}<td class=\"num\">${money(grandTotal)}</td></tr></tfoot>\n  </table>\n  <footer>${esc(brand.footerNote || '')}</footer>\n</body></html>`;\n\nreturn [{\n  json: {\n    html,\n    asOf,\n    clientCount: clients.length,\n    invoiceCount: open.length,\n    grandTotal,\n    grandTotalFormatted: money(grandTotal),\n    overdueTotalFormatted: money(overdueTotal),\n  },\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "Anything outstanding?",
      "name": "Anything outstanding?",
      "type": "n8n-nodes-base.if",
      "position": [
        960,
        100
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.clientCount }}",
              "rightValue": 0
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "Render the ageing report as PDF",
      "name": "Render the ageing report as PDF",
      "type": "n8n-nodes-pdfmint.pdfMint",
      "position": [
        1320,
        100
      ],
      "parameters": {
        "html": "={{ $json.html }}",
        "source": "html",
        "options": {},
        "fileName": "=accounts-receivable-{{ $json.asOf }}.pdf",
        "resource": "document",
        "operation": "pdf"
      },
      "typeVersion": 1
    },
    {
      "id": "Email the report",
      "name": "Email the report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1680,
        100
      ],
      "parameters": {
        "sendTo": "={{ $('Your company details').first().json.reportRecipient }}",
        "message": "=Attached is this week's accounts-receivable ageing report.\n\nOutstanding: {{ $('Build the ageing report').first().json.grandTotalFormatted }}\nOf which overdue: {{ $('Build the ageing report').first().json.overdueTotalFormatted }}\nClients with a balance: {{ $('Build the ageing report').first().json.clientCount }}",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "data"
              }
            ]
          }
        },
        "subject": "=Accounts receivable as at {{ $('Build the ageing report').first().json.asOf }} — {{ $('Build the ageing report').first().json.grandTotalFormatted }} outstanding"
      },
      "typeVersion": 2.1
    },
    {
      "id": "Sticky Note8",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        180,
        -520
      ],
      "parameters": {
        "color": 3,
        "width": 700,
        "height": 380,
        "content": "## Before you turn it on\n\n1. Select your own Invoice Ninja, PDFMint and Gmail credentials, and type your own address in Your company details.\n2. Run it by hand once. A real run reads your invoices, sends the report content to PDFMint to make the PDF, and sends a real email.\n3. Check the totals against Invoice Ninja. Invoices with no due date are left out, invoices from different currencies are added together in one currency, and clients with the same name are merged.\n4. The schedule uses your n8n time zone. Check it so 08:00 means your 08:00.\n5. The report holds client names and amounts. Send it only to people allowed to see them.\n\nSource: based on a template from the n8n Library at n8n.io, documented by LeenLabs.\nNot tested on live APIs."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Every Monday morning": {
      "main": [
        [
          {
            "node": "Your company details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Your company details": {
      "main": [
        [
          {
            "node": "Get open invoices from Invoice Ninja",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anything outstanding?": {
      "main": [
        [
          {
            "node": "Render the ageing report as PDF",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Build the ageing report": {
      "main": [
        [
          {
            "node": "Anything outstanding?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Render the ageing report as PDF": {
      "main": [
        [
          {
            "node": "Email the report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get open invoices from Invoice Ninja": {
      "main": [
        [
          {
            "node": "Build the ageing report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}