基于 OCR、Claude AI、Slack 和 Notion DB 的自动化发票付款追踪
高级
这是一个AI Summarization, Multimodal AI领域的自动化工作流,包含 92 个节点。主要使用 If, Code, Wait, Slack, Notion 等节点。 基于 OCR、Claude AI、Slack 和 Notion DB 的自动化发票付款追踪
前置要求
- •Slack Bot Token 或 Webhook URL
- •Notion API Key
- •可能需要目标 API 的认证凭证
- •Anthropic API Key
使用的节点 (92)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"meta": {
"instanceId": "c8523726fb538ba356528013ec3b00a89ffd0cddf2d31a7bcce94063a36b7fec",
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "909b0322-55e4-4c3d-a5a9-9392d5bbf745",
"name": "格式检查",
"type": "n8n-nodes-base.code",
"position": [
-5808,
1744
],
"parameters": {
"jsCode": "const files = $('Slack Trigger').first().json.files || [];\nconst results = [];\n\nfor (const file of files) {\n const mimetype = file.mimetype || \"\";\n const fileType = mimetype.split('/').pop().toLowerCase();\n const fileName = file.name || file.title || \"unknown\";\n\n const isImage = ['jpg', 'jpeg', 'png', 'webp'].includes(fileType);\n const isDocument = ['pdf', 'doc', 'docx', 'txt', 'rtf', 'eml'].includes(fileType);\n const isEmail = fileType === 'eml' || fileName.endsWith('.eml');\n\n results.push({\n json: {\n ...file, // keep Slack file data like url_private_download\n fileType,\n mimetype,\n isImage,\n isDocument,\n isEmail,\n }\n });\n}\n\nreturn results;\n"
},
"typeVersion": 2
},
{
"id": "ca8559e4-b29b-4a1a-b113-e70bba47f23a",
"name": "检查格式",
"type": "n8n-nodes-base.switch",
"position": [
-5584,
1744
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Image",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f5d6e124-c68e-42bb-a9bb-c97d54b09143",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.isImage }}",
"rightValue": "true"
}
]
},
"renameOutput": true
},
{
"outputKey": "Document",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "87759194-16d1-4593-8c9b-016aa7be4e03",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.isDocument }}",
"rightValue": "true"
}
]
},
"renameOutput": true
}
]
},
"options": {
"allMatchingOutputs": true
}
},
"typeVersion": 3.2
},
{
"id": "6543e986-8860-457d-a8de-bb052718e9c6",
"name": "获取文档二进制文件",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5360,
1840
],
"parameters": {
"url": "={{ $json.url_private_download }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "slackApi"
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 4.2
},
{
"id": "af88d22a-5a2d-4471-9ac9-b1cb9531bd35",
"name": "OCR Space 解析1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5136,
1840
],
"parameters": {
"url": "https://api.ocr.space/parse/image",
"method": "POST",
"options": {
"timeout": 100000
},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "filetype",
"value": "PDF"
},
{
"name": "file",
"parameterType": "formBinaryData",
"inputDataFieldName": "data"
},
{
"name": "scale",
"value": "true"
},
{
"name": "OCREngine",
"value": "2"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"id": "dkJjl1msBNIeIT5u",
"name": "OCR Space"
}
},
"retryOnFail": true,
"typeVersion": 4.2,
"waitBetweenTries": 5000
},
{
"id": "ecb68486-32af-41e3-a9a7-86c763205061",
"name": "代码",
"type": "n8n-nodes-base.code",
"position": [
-4688,
1936
],
"parameters": {
"jsCode": "// $input.all() gives you the 3 separate OCR result items\nconst inputItems = $input.all();\n\nreturn inputItems.map((item, index) => {\n const parsedResults = item.json.ParsedResults || [];\n\n // Join all ParsedText per result into one string\n const mergedText = parsedResults\n .map(p => p.ParsedText || \"\")\n .join(\"\\n\\n\")\n .trim();\n\n return {\n json: {\n index: index + 1,\n mergedParsedText: mergedText\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "acb3e33c-59de-4cf3-a008-a47af9673abb",
"name": "检查解析错误3",
"type": "n8n-nodes-base.if",
"position": [
-4912,
1840
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "ddbbbfbb-e05b-4ca9-b607-65ad8ac748bb",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $('OCR Space Parse1').item.json.IsErroredOnProcessing }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "96e61695-6dfd-4cf4-8064-aea9d80284f0",
"name": "基础LLM链",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
-4240,
1888
],
"parameters": {
"text": "=Extract the invoice fields from this OCR text and populate the EXACT schema.\n\nTARGET SCHEMA:\n{\n \"invoice_no\": \"\",\n \"vendor\": \"\",\n \"issue_date\": \"YYYY-MM-DD\",\n \"due_date\": \"YYYY-MM-DD\",\n \"currency\": \"\",\n \"subtotal\": 0,\n \"tax_total\": 0,\n \"discount_total\": 0,\n \"discount_percent\": 0,\n \"amount_total\": 0,\n\n \"amount_due\": 0, // if shown (a.k.a. Balance Due); else 0\n \"paid_amount\": 0, // if shown or implied; else 0\n \"paid_date\": \"\", // if shown; else \"\"\n \"receipt_no\": \"\", // if shown; else \"\"\n \"payment_method\": \"\", // if shown; else \"\"\n\n \"destination_account\": \"\",\n \"payment_ref\": \"\",\n \"doc_says_paid\": false,\n \"status\": \"\",\n \"notes\": \"\",\n \"source_file_id\": \"\",\n \"source_file_name\": \"\",\n \"source_file_url\": \"\",\n \"ingestion_batch\": \"\",\n \"line_items\": [],\n \"attachments\": []\n}\n\nOCR TEXT:\n{{ $json.OCRResult }}\n",
"batching": {},
"messages": {
"messageValues": [
{
"message": "=You are an extraction engine for US invoices. Output EXACTLY the JSON fields of the target schema—no extra keys, no comments, no markdown. Do not invent values.\n\nGeneral rules\n- Locale: US. Normalize dates to YYYY-MM-DD.\n- Numbers: plain decimals (no symbols/commas).\n- currency: infer from symbol/labels when present (“$” ⇒ “USD”), else \"\".\n- If a field is unavailable: use \"\" for strings, 0 for numbers, false for booleans, [] for arrays.\n\nDiscount fields\n- If the doc shows a percent (e.g., “Discount 49%”), set discount_percent to the decimal (0.49) and discount_total to 0 unless a currency discount is also printed.\n- If the doc shows a currency discount (e.g., “Discount $60.27”), set discount_total to that amount; discount_percent is 0 unless the percent is explicitly printed too.\n- If both forms are printed, fill both.\n\nTotals\n- amount_total: prefer the document’s explicit “Amount due/Total/Total amount/Grand total”.\n- If not printed but subtotal, discount_* and tax_total are present, compute:\n amount_total = subtotal − (discount_total OR subtotal × discount_percent) + tax_total.\n- Otherwise leave amount_total = 0.\n\nPartial payments\n- If the doc shows Amount due / Balance due:\n • Set amount_due to that value.\n • If amount_total is known and amount_due < amount_total, set paid_amount = amount_total − amount_due (≥ 0).\n- If the doc shows Amount paid / Payment history:\n • Sum payments and set paid_amount.\n • If amount_due is not shown but amount_total is known, set amount_due = max(amount_total − paid_amount, 0).\n- If a paid date, receipt number, or payment method is explicitly shown, set paid_date, receipt_no, payment_method; otherwise leave them empty.\n- Clamp impossible values (no negatives; when amount_total is known, neither paid_amount nor amount_due may exceed it).\n\nDocument-based status\n- If amount_due == 0 AND the document states PAID (or shows “Amount due $0.00”): status = \"Paid (Unverified)\".\n- Else if paid_amount > 0 AND amount_due > 0: status = \"Partially Paid\".\n- Else: status = \"Unpaid\".\n- Set doc_says_paid = true only if the document itself explicitly says “PAID” or shows “Amount due $0.00”; otherwise false.\n\nLine items\n- line_items: include when clearly listed, as\n [{\"description\": string, \"qty\": number, \"unit_price\": number, \"amount\": number}], else [].\n\nAttachments\n- attachments: [] unless explicit links are present.\n\nReturn ONLY the JSON object matching the target schema.\n"
}
]
},
"promptType": "define"
},
"retryOnFail": false,
"typeVersion": 1.7
},
{
"id": "0aaa9bdf-05b2-4dc1-ad6d-4de8926ef9f1",
"name": "Anthropic 聊天模型4",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
-4240,
2048
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-3-5-haiku-20241022",
"cachedResultName": "Claude Haiku 3.5"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"id": "UBlNqbwvx1EotIn7",
"name": "Anthropic account"
}
},
"typeVersion": 1.3
},
{
"id": "54187b60-dabd-42a7-b5c9-d46199dbed73",
"name": "清理 AI 响应",
"type": "n8n-nodes-base.code",
"position": [
-3888,
1888
],
"parameters": {
"jsCode": "// n8n Code node — Run Once for All Items\n// Repair + Enforce schema + attach Slack file info\n// Adds discount_percent support + derives discount_amount & stable amount_total\n// + Partial payment normalization (amount_due, paid_amount, paid_date, receipt_no, payment_method)\n\nfunction stripCodeFences(s) {\n return String(s || \"\")\n .replace(/```(?:json)?/gi, \"\")\n .replace(/```/g, \"\")\n .trim();\n}\n\nfunction findTopLevelJson(s) {\n const str = String(s || \"\");\n let start = -1, depth = 0;\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n if (start === -1) {\n if (ch === \"{\" || ch === \"[\") { start = i; depth = 1; }\n } else {\n if (ch === \"{\" || ch === \"[\") depth++;\n if (ch === \"}\" || ch === \"]\") depth--;\n if (depth === 0) return str.slice(start, i + 1);\n }\n }\n return null;\n}\n\nfunction safeParseAny(x) {\n if (Array.isArray(x)) return x;\n if (x && typeof x === \"object\" && (x.invoice_no || x.vendor || x.line_items)) return x;\n\n let txt = \"\";\n if (typeof x === \"string\") txt = x;\n else if (typeof x?.text === \"string\") txt = x.text;\n else if (typeof x?.output === \"string\") txt = x.output;\n else if (typeof x?.response === \"string\") txt = x.response;\n else if (typeof x?.content === \"string\") txt = x.content;\n else if (typeof x?.message === \"string\") txt = x.message;\n else txt = JSON.stringify(x ?? \"\");\n\n txt = stripCodeFences(txt);\n\n try { return JSON.parse(txt); } catch {}\n const block = findTopLevelJson(txt);\n if (!block) throw new Error(\"No JSON found in LLM output\");\n return JSON.parse(block);\n}\n\n// coercers\nfunction num(v) {\n if (v === \"\" || v == null || (typeof v === \"number\" && !isFinite(v))) return 0;\n if (typeof v === \"number\") return v;\n let s = String(v).trim();\n if (!s) return 0;\n // keep digits , . -\n s = s.replace(/[^\\d,.\\-]/g, \"\");\n if (s.includes(\",\") && s.includes(\".\")) s = s.replace(/,/g, \"\");\n else if (s.includes(\",\") && !s.includes(\".\")) {\n if ((s.match(/,/g) || []).length === 1 && /\\d,\\d{1,2}$/.test(s)) s = s.replace(\",\", \".\");\n else s = s.replace(/,/g, \"\");\n }\n const n = Number(s);\n return isFinite(n) ? n : 0;\n}\nconst str = (v) => (v == null ? \"\" : String(v).trim());\nconst bool = (v) => v === true;\nconst two = (n) => Math.round((Number(n) || 0) * 100) / 100;\n\n// parse percent from \"49%\", \"49\", 0.49, etc. → 0..1\nfunction pct(v) {\n if (v == null || v === \"\") return 0;\n if (typeof v === \"string\" && v.includes(\"%\")) return Math.min(Math.max(num(v) / 100, 0), 1);\n let n = num(v);\n if (n > 1) n = n / 100;\n if (n < 0) n = 0;\n if (n > 1) n = 1;\n return n;\n}\n\nfunction normalizeOne(obj, fileUrl) {\n const line_items = Array.isArray(obj?.line_items)\n ? obj.line_items.map(x => ({\n description: str(x?.description),\n qty: num(x?.qty),\n unit_price: num(x?.unit_price),\n amount: num(x?.amount)\n }))\n : [];\n\n // base fields from LLM\n const base = {\n invoice_no: str(obj?.invoice_no),\n vendor: str(obj?.vendor),\n issue_date: str(obj?.issue_date),\n due_date: str(obj?.due_date),\n currency: str(obj?.currency),\n subtotal: num(obj?.subtotal),\n tax_total: num(obj?.tax_total),\n\n // discounts (raw)\n discount_total: num(obj?.discount_total), // fixed amount if present\n discount_percent: pct(obj?.discount_percent), // normalized 0..1\n\n amount_total: num(obj?.amount_total),\n\n // payment/receipt extras (raw; normalized below)\n amount_due: num(obj?.amount_due),\n paid_amount: num(obj?.paid_amount),\n paid_date: str(obj?.paid_date),\n receipt_no: str(obj?.receipt_no),\n payment_method: str(obj?.payment_method),\n\n destination_account: str(obj?.destination_account),\n payment_ref: str(obj?.payment_ref),\n doc_says_paid: bool(obj?.doc_says_paid),\n status: str(obj?.status),\n notes: str(obj?.notes),\n source_file_id: str(obj?.source_file_id),\n source_file_name: str(obj?.source_file_name),\n source_file_url: str(obj?.source_file_url),\n ingestion_batch: str(obj?.ingestion_batch),\n line_items,\n attachments: Array.isArray(obj?.attachments) ? obj.attachments.map(str) : []\n };\n\n // ---- discount math ----\n const discount_amount =\n base.discount_total > 0\n ? two(base.discount_total)\n : two(base.subtotal * (base.discount_percent || 0));\n\n // compute fallback amount_total\n const calc_total = two(base.subtotal - discount_amount + base.tax_total);\n\n // choose stable amount_total for pipeline (prefer LLM if sane; else calc)\n const provided = base.amount_total;\n const useProvided =\n provided > 0 && Math.abs(provided - calc_total) <= Math.max(0.01, calc_total * 0.01);\n base.amount_total = useProvided ? two(provided) : calc_total;\n\n // Notion percent wants 0..1\n base.discount_percent_for_notion = +(base.discount_percent || 0).toFixed(4);\n base.discount_amount = discount_amount; // handy for dedup & debug\n base._amount_total_source = useProvided ? \"provided\" : \"calculated\";\n\n // ---- partial / payment normalization ----\n const at = Number(base.amount_total || 0);\n let due = Number(base.amount_due || 0);\n let paid = Number(base.paid_amount || 0);\n\n // Derive missing pieces when amount_total is known\n if (at > 0) {\n if (paid > 0 && due <= 0) due = Math.max(0, two(at - paid));\n if (due > 0 && paid <= 0) paid = Math.max(0, two(at - due));\n // If doc says PAID and due is zero but paid missing, assume fully paid\n if (base.doc_says_paid && due === 0 && paid <= 0) paid = at;\n // Clamp impossible values\n if (paid < 0) paid = 0;\n if (due < 0) due = 0;\n if (paid > at) paid = at;\n if (due > at) due = at;\n if (paid + due > at) due = Math.max(0, two(at - paid));\n } else {\n // amount_total unknown — still clamp non-negatives\n if (paid < 0) paid = 0;\n if (due < 0) due = 0;\n }\n\n base.paid_amount = two(paid);\n base.amount_due = two(due);\n\n // Receipt hints payload for later routing/Notion Receipt DB\n base.receipt_hints = {\n paid_amount: base.paid_amount,\n paid_date: base.paid_date,\n receipt_no: base.receipt_no,\n payment_method: base.payment_method\n };\n\n // Nudge status if inconsistent with amounts\n if (at > 0) {\n if (base.amount_due === 0 && (base.doc_says_paid || base.paid_amount >= at - 0.01)) {\n base.status = \"Paid (Unverified)\";\n } else if (base.paid_amount > 0 && base.amount_due > 0) {\n base.status = \"Partially Paid\";\n } else if (base.paid_amount === 0 && base.amount_due >= at - 0.01) {\n base.status = \"Unpaid\";\n }\n }\n\n // ---- Attach Slack file URL (if available) ----\n if (fileUrl) {\n let fileName = \"\", fileId = \"\";\n try {\n const u = new URL(fileUrl);\n const seg = u.pathname.split(\"/\").filter(Boolean);\n fileName = decodeURIComponent(seg[seg.length - 1] || \"\");\n const m = u.pathname.match(/-(F[A-Z0-9]+)\\//i);\n if (m) fileId = m[1];\n } catch {\n const m1 = fileUrl.match(/\\/download\\/([^?\\/#]+)($|\\?)/i);\n if (m1) fileName = decodeURIComponent(m1[1]);\n const m2 = fileUrl.match(/-(F[A-Z0-9]+)\\//i);\n if (m2) fileId = m2[1];\n }\n\n base.source_file_url = fileUrl;\n if (!base.source_file_name) base.source_file_name = fileName;\n if (!base.source_file_id) base.source_file_id = fileId;\n\n const atSet = new Set(base.attachments.map(String));\n atSet.add(fileUrl);\n base.attachments = Array.from(atSet);\n }\n\n // Guard status values\n const allowedStatus = new Set([\n \"Unpaid\",\"Paid (Unverified)\",\"Paid (Verified)\",\n \"Partially Paid\",\"Overdue\",\"Void/Cancelled\",\"Duplicate\"\n ]);\n if (!allowedStatus.has(base.status)) base.status = \"Unpaid\";\n\n // Basic date sanity\n const dateOk = (s) => /^(\\d{4})-(\\d{2})-(\\d{2})$/.test(s);\n if (base.issue_date && !dateOk(base.issue_date)) base.issue_date = \"\";\n if (base.due_date && !dateOk(base.due_date)) base.due_date = \"\";\n\n return base;\n}\n\n// --- Collect Slack file URLs from either node (PDF or Image lanes) ---\nfunction collectSlackUrls(nodeName) {\n try {\n return $(nodeName).all()\n .map(it =>\n String(\n it.json?.url_private_download ||\n it.json?.file?.url_private_download || // some Slack nodes nest under file\n \"\"\n )\n )\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nconst urlsDoc = collectSlackUrls('Take Binary Files for Document'); // PDF lane\nconst urlsImg = collectSlackUrls('Take Binary Files'); // Image lane\n\n// Pick a URL for item index i; prefer same index, then first available\nfunction pickFileUrl(i) {\n return urlsDoc[i] || urlsImg[i] || urlsDoc[0] || urlsImg[0] || \"\";\n}\n\n// --- Process ALL incoming items; each may yield one or many outputs ---\nconst outputs = [];\n\nfor (const [idx, item] of $input.all().entries()) {\n let parsed = safeParseAny(item.json);\n\n if (parsed && !Array.isArray(parsed) && typeof parsed === \"object\") {\n const maybeArr = parsed.data || parsed.items || parsed.result || parsed.invoices;\n if (Array.isArray(maybeArr)) parsed = maybeArr;\n }\n if (!Array.isArray(parsed)) parsed = [parsed];\n\n const fileUrlForThisItem = pickFileUrl(idx);\n\n for (const obj of parsed) {\n const out = normalizeOne(obj, fileUrlForThisItem);\n outputs.push({ json: out });\n }\n}\n\nreturn outputs;\n"
},
"typeVersion": 2
},
{
"id": "0e729384-88a1-4ee3-9bd1-ccc218462dbc",
"name": "内部检查重复发票",
"type": "n8n-nodes-base.code",
"position": [
-3664,
1888
],
"parameters": {
"jsCode": "// Input: many cleaned invoices (each item.json is like your sample)\n// Output: clusters of duplicates; each cluster picks one \"keep\" and lists \"drop\"\n\nconst input = $input.all().map(i => i.json);\n\n// --- Helpers ---\nconst canonVendor = (v) => {\n if (!v) return \"\";\n return String(v)\n .toLowerCase()\n .replace(/[.,'’\"]/g, \" \")\n .replace(/\\b(pt|tbk|cv|inc|ltd|llc|llp|plc|corp|co|gmbh|sarl|bv)\\b/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n};\nconst canonInv = (s) => {\n if (!s) return \"\";\n return String(s)\n .toUpperCase()\n .replace(/\\s+/g, \"\")\n .replace(/[–—]/g, \"-\")\n .replace(/[^A-Z0-9\\/\\-\\.]/g, \"\")\n .replace(/-+/g, \"-\")\n .trim();\n};\nconst round2 = (n) => Math.round(Number(n || 0) * 100) / 100;\n\n// --- NEW: scoring helpers so PAID wins ---\nfunction parseYmd(s) {\n // \"YYYY-MM-DD\" -> epoch ms; invalid -> 0\n const m = String(s || \"\").match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (!m) return 0;\n return Date.UTC(+m[1], +m[2] - 1, +m[3]);\n}\n\nfunction statusRank(status, docPaid) {\n const s = String(status || \"\").toLowerCase();\n if (docPaid === true || /paid\\s*\\(verified\\)/i.test(status)) return 4;\n if (/paid/i.test(s)) return 3; // Paid (Unverified)\n if (/partially/i.test(s)) return 2; // Partially Paid\n if (/overdue/i.test(s)) return 1; // Overdue still beats Unpaid\n return 0; // Unpaid / others\n}\n\nfunction richness(o) {\n let r = 0;\n if (Array.isArray(o.attachments) && o.attachments.length) r += 2;\n if (o.source_file_id) r += 1;\n if (o.notes) r += 0.5;\n if (Array.isArray(o.line_items)) r += Math.min(2, o.line_items.length * 0.1);\n return r;\n}\n\n// choose one representative to keep\nfunction chooseMaster(arr) {\n let best = null, bestScore = -Infinity, bestWhy = \"\";\n for (const o of arr) {\n const paidRank = statusRank(o.status, o.doc_says_paid); // 0..4\n const newestDate = Math.max(parseYmd(o.due_date), parseYmd(o.issue_date));\n const rich = richness(o);\n\n // Weights: paidness >>> recency > richness\n const score =\n paidRank * 100 + // ensure any \"Paid\" beats \"Unpaid\"\n (newestDate / 8.64e7) * 0.1 + // days since epoch * 0.1\n rich; // small tie-break\n\n const why = `paidRank=${paidRank} doc_says_paid=${!!o.doc_says_paid} status=${o.status || \"\"} newest=${newestDate ? new Date(newestDate).toISOString().slice(0,10) : \"-\"} richness=${rich}`;\n\n if (score > bestScore) {\n best = o; bestScore = score; bestWhy = why;\n }\n }\n best._keep_reason_debug = bestWhy;\n best._keep_score_debug = bestScore;\n return best || arr[0];\n}\n\n// --- Build clusters ---\nconst clustersMap = new Map(); // key -> { reason, list: [] }\n\nfor (const o of input) {\n const vendor_canon = canonVendor(o.vendor);\n const invoice_no_canon = canonInv(o.invoice_no);\n const currency = (o.currency || \"\").toUpperCase().trim();\n const amt = round2(o.amount_total);\n const due = (o.due_date || o.issue_date || \"\").slice(0, 10);\n\n let key, reason;\n if (invoice_no_canon) {\n key = `INV#${vendor_canon}|${invoice_no_canon}`;\n reason = \"SAME_VENDOR + INVOICE_NO\";\n } else {\n key = `FALLBACK#${vendor_canon}|${currency}|${amt}|${due}`;\n reason = \"NO_INVOICE_NO → SAME_VENDOR + CURRENCY + AMOUNT + DATE\";\n }\n\n if (!clustersMap.has(key)) clustersMap.set(key, { reason, list: [] });\n clustersMap.get(key).list.push({\n ...o,\n _vendor_canon: vendor_canon,\n _invoice_no_canon: invoice_no_canon,\n _fallback_key: !invoice_no_canon\n });\n}\n\n// --- Emit only duplicate clusters (>1) ---\nconst results = [];\nfor (const [key, { reason, list }] of clustersMap.entries()) {\n if (list.length <= 1) continue;\n\n const amountSet = Array.from(new Set(list.map(x => round2(x.amount_total))));\n const currencySet = Array.from(new Set(list.map(x => (x.currency || \"\").toUpperCase().trim())));\n\n // annotate if amounts differ\n let reasonDetailed = reason;\n if (reason.startsWith(\"SAME_VENDOR\") && amountSet.length > 1) {\n reasonDetailed += \" (amount differs across copies)\";\n }\n\n // --- NEW: count paid vs unpaid; prefer paid in keep\n const paidCount = list.filter(x => x.doc_says_paid || /^paid/i.test(String(x.status || \"\"))).length;\n if (paidCount > 0) reasonDetailed += \" | keep prefers PAID copy\";\n\n const keep = chooseMaster(list);\n const drop = list.filter(x => x !== keep);\n\n results.push({\n json: {\n isDuplicate: true,\n cluster_key: key,\n reason: reasonDetailed,\n count: list.length,\n amounts: amountSet,\n currencies: currencySet,\n paid_count: paidCount, // debug/metrics\n keep, // should now be one of C/D (paid)\n drop,\n entries: list\n }\n });\n}\n\n// No duplicates? Emit a simple flag.\nif (results.length === 0) {\n return [{ json: { isDuplicate: false, info: \"No duplicate invoices found.\" } }];\n}\n\nreturn results;\n"
},
"typeVersion": 2
},
{
"id": "dbcea768-1818-4a07-932c-3161e06342b5",
"name": "Slack 触发器",
"type": "n8n-nodes-base.slackTrigger",
"position": [
-6032,
1744
],
"webhookId": "d13d3a70-faed-40f9-99fd-00a219c3af7c",
"parameters": {
"options": {},
"trigger": [
"message"
],
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09701BLY2Z",
"cachedResultName": "expenses"
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 1
},
{
"id": "0202318a-016b-4130-930c-bd092f18c492",
"name": "与原始数据合并",
"type": "n8n-nodes-base.code",
"position": [
-3440,
1888
],
"parameters": {
"jsCode": "// n8n Code node — Merge originals with dedup clusters and tag each original\n// Upstream nodes:\n// - \"Cleans AI Response\" -> contains ORIGINAL parsed items (A, B, C)\n// - Input to this node -> contains cluster objects from \"Internal Check Duplicate Invoice\"\n// Output: one item per original, with dedup_* fields added\n\n// ---- helpers ----\nfunction canonVendor(v) {\n return String(v || '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ') // strip punctuation (ASCII-safe)\n .replace(/\\b(pt|tbk|inc|ltd|llc)\\b/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\nfunction canonInvoiceNo(inv) {\n return String(inv || '')\n .toUpperCase()\n .replace(/\\s+/g, '')\n .replace(/[^A-Z0-9\\/\\-.]/g, '');\n}\nfunction clusterKeyFromItem(it) {\n const vendor = canonVendor(it.vendor);\n const inv = canonInvoiceNo(it.invoice_no);\n if (inv) return `INV#${vendor}|${inv}`;\n const currency = String(it.currency || '').toUpperCase();\n const amt = Number(it.amount_total || it.subtotal || 0) || 0;\n const date = String(it.due_date || it.issue_date || '');\n return `FALLBACK#${vendor}|${currency}|${amt}|${date}`;\n}\n// Stable-ish signature to match items across steps\nfunction sig(o) {\n return [\n o.source_file_id || '',\n o.source_file_url || '',\n String(o.vendor || '').trim(),\n String(o.invoice_no || '').trim(),\n String(o.issue_date || '').trim(),\n String(o.due_date || '').trim(),\n String(o.currency || '').trim().toUpperCase(),\n String(Number(o.amount_total ?? o.subtotal ?? 0) || 0)\n ].join('||');\n}\n\n// ---- 1) load originals (A, B, C) ----\nconst originals = $('Cleans AI Response').all().map(i => i.json);\n\n// ---- 2) load and normalize clusters from THIS node input ----\nconst rawIn = $input.all().map(i => i.json);\nconst clusters = [];\nfor (const x of rawIn) {\n if (Array.isArray(x)) {\n for (const c of x) if (c && c.cluster_key) clusters.push(c);\n } else if (x && x.cluster_key) {\n clusters.push(x);\n }\n}\n// If your duplicate checker sometimes emits {clusters:[...]}, support that too\nfor (const x of rawIn) {\n if (x && Array.isArray(x.clusters)) {\n for (const c of x.clusters) if (c && c.cluster_key) clusters.push(c);\n }\n}\n\n// ---- 3) index clusters (by cluster_key) ----\nconst clusterIndex = new Map(); // cluster_key -> { meta, keepSig, dropSigSet, keepUsed }\nfor (const c of clusters) {\n const meta = {\n cluster_key: c.cluster_key,\n reason: c.reason || '',\n count: Number(c.count || (c.entries ? c.entries.length : 1)) || 1,\n amounts: c.amounts || [],\n currencies: c.currencies || []\n };\n const keepSig = c.keep ? sig(c.keep) : null;\n const dropSigSet = new Set((c.drop || []).map(sig));\n clusterIndex.set(c.cluster_key, { meta, keepSig, dropSigSet, keepUsed: false });\n}\n\n// Try to find matching cluster for an original item\nfunction findClusterForOriginal(item) {\n const s = sig(item);\n for (const [ck, rec] of clusterIndex) {\n if ((rec.keepSig && rec.keepSig === s) || rec.dropSigSet.has(s)) return [ck, rec];\n }\n const ck = clusterKeyFromItem(item);\n return [ck, clusterIndex.get(ck) || null];\n}\n\n// ---- 4) merge labels onto originals ----\nconst out = [];\n\nfor (const item of originals) {\n const [ck, rec] = findClusterForOriginal(item);\n\n if (!rec) {\n out.push({\n json: {\n ...item,\n dedup_cluster_key: ck || '',\n dedup_role: 'unique',\n dedup_count: 1,\n dedup_reason: 'no_cluster_match'\n }\n });\n continue;\n }\n\n const { meta, keepSig, dropSigSet } = rec;\n const s = sig(item);\n let role;\n\n if (meta.count <= 1) {\n role = 'unique';\n } else if (keepSig && s === keepSig && !rec.keepUsed) {\n role = 'keep'; // first original that matches the keepSig\n rec.keepUsed = true; // ensure only one \"keep\" per cluster\n } else {\n role = 'drop'; // everything else in that cluster is a duplicate\n }\n\n out.push({\n json: {\n ...item,\n dedup_cluster_key: meta.cluster_key,\n dedup_role: role, // \"keep\" | \"drop\" | \"unique\"\n dedup_count: meta.count,\n dedup_reason: meta.reason,\n dedup_amounts: meta.amounts,\n dedup_currencies: meta.currencies\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "123fc9c7-f6d3-4be5-b02a-d6c65faa5bc9",
"name": "将数据值合并到一个键中",
"type": "n8n-nodes-base.code",
"position": [
-4464,
1888
],
"parameters": {
"jsCode": "// Code node — Run once for all items\n// Preceded by a Merge (Wait for all) so ALL items from both paths arrive here.\n\nconst out = [];\n\nfunction get(v) {\n return typeof v === 'string' ? v.trim() : '';\n}\n\nfor (const it of $input.all()) {\n const j = it.json || {};\n\n // Your two possible sources per item\n const fromPdf = get(j.mergedParsedText); // Error3 (PDF path)\n const fromImage = get(j?.ParsedResults?.[0]?.ParsedText); // Error2 (image path)\n\n const OCRResult = fromPdf || fromImage || '';\n const OCRSource = fromPdf\n ? 'Error3.mergedParsedText'\n : fromImage\n ? 'Error2.ParsedResults[0].ParsedText'\n : 'none';\n\n // Keep any file metadata you already have on the item\n out.push({\n json: {\n ...j,\n OCRResult,\n OCRSource\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "88aae252-885b-4433-af13-88fea42ad24e",
"name": "决定数据去向",
"type": "n8n-nodes-base.if",
"position": [
-3216,
1888
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9cb0bdab-37f9-4536-8b75-de7e3bdd4a76",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.dedup_role }}",
"rightValue": "drop"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "c3b08864-40e6-4c71-b0ad-f46231351a92",
"name": "创建数据库查询",
"type": "n8n-nodes-base.code",
"position": [
-2992,
1312
],
"parameters": {
"jsCode": "// Run ONCE FOR ALL ITEMS\nconst out = [];\n\nfunction windowDates(anchor) {\n if (!anchor) return null;\n const d = new Date(anchor + \"T00:00:00Z\");\n if (isNaN(d)) return null;\n const s = new Date(d); s.setUTCDate(s.getUTCDate() - 30);\n const e = new Date(d); e.setUTCDate(e.getUTCDate() + 30);\n return [s.toISOString().slice(0,10), e.toISOString().slice(0,10)];\n}\n\nfor (const { json: j } of $input.all()) {\n const haveInvoiceNo = !!j?.invoice_no;\n\n // Anchor date:\n // - If we know the invoice number, anchor to invoice-level dates\n // - Otherwise, paid_date is acceptable for finding the invoice by heuristics\n const anchor = haveInvoiceNo\n ? (j?.issue_date || j?.due_date || \"\")\n : (j?.receipt_hints?.paid_date || j?.issue_date || j?.due_date || \"\");\n const dates = windowDates(anchor);\n\n const and = [];\n\n if (haveInvoiceNo) {\n // Prefer canon properties if your DB has them; else fall back to title field\n if (j.invoice_no_canon) {\n and.push({ property: \"Invoice No (Canon)\", rich_text: { equals: String(j.invoice_no_canon) } });\n if (j.vendor_canon) {\n and.push({ property: \"Vendor (Canon)\", rich_text: { equals: String(j.vendor_canon) } });\n }\n } else {\n and.push({ property: \"Invoice No\", title: { equals: String(j.invoice_no) } });\n }\n if (j.currency) {\n and.push({ property: \"Currency\", select: { equals: String(j.currency) } });\n }\n\n // IMPORTANT: no Amount Total filter when Invoice No is present\n // (If you want a belt-and-suspenders check, you can add a WIDE band on amount_total instead,\n // but never use the receipt paid amount.)\n\n } else {\n // Fallback: vendor + currency + invoice total band + date\n if (j.vendor_canon) {\n and.push({ property: \"Vendor (Canon)\", rich_text: { equals: String(j.vendor_canon) } });\n } else if (j.vendor) {\n and.push({ property: \"Vendor\", rich_text: { equals: String(j.vendor) } });\n }\n if (j.currency) {\n and.push({ property: \"Currency\", select: { equals: String(j.currency) } });\n }\n const total = Number(j?.amount_total);\n if (!Number.isNaN(total) && total > 0) {\n const eps = Math.max(5, total * 0.03); // ± $5 or 3%\n and.push(\n { property: \"Amount Total\", number: { greater_than_or_equal_to: +(total - eps).toFixed(2) } },\n { property: \"Amount Total\", number: { less_than_or_equal_to: +(total + eps).toFixed(2) } },\n );\n }\n }\n\n if (dates) {\n const [start, end] = dates;\n and.push({\n or: [\n { property: \"Issue Date\", date: { on_or_after: start, on_or_before: end } },\n { property: \"Due Date\", date: { on_or_after: start, on_or_before: end } },\n ]\n });\n }\n\n const queryBody = { filter: { and }, page_size: haveInvoiceNo ? 10 : 5 };\n out.push({ json: { ...j, queryBody } });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "de2b4a0f-eaba-4d8c-ac76-220f14725f10",
"name": "检查数据库发票",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2768,
1312
],
"parameters": {
"url": "https://api.notion.com/v1/databases/24df7557957380739611da8371404254/query",
"method": "POST",
"options": {},
"jsonBody": "={{ $json.queryBody }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Notion-Version",
"value": "2022-06-28"
}
]
},
"nodeCredentialType": "notionApi"
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 4.2
},
{
"id": "067db3c6-fc83-46a2-8ad4-0b8cec9ab83e",
"name": "合并发票项目",
"type": "n8n-nodes-base.code",
"position": [
-2544,
1312
],
"parameters": {
"jsCode": "// After \"Check DB Invoice\" (HTTP). Mode: Run once for all items. Single output.\n\n// Originals aligned by index (from your query-prep node)\nconst originals = $('Create Query for DB').all().map(i => i.json);\n\n// Toggle if you want partial flows to auto-route (true) or go to manual_review (false)\nconst ENABLE_PARTIAL = true;\n\n// ---------- Notion helpers ----------\nconst getTitle = (p, name) => (p?.properties?.[name]?.title || [])\n .map(t => t.plain_text || '').join('');\nconst getRich = (p, name) => (p?.properties?.[name]?.rich_text || [])\n .map(t => t.plain_text || '').join('');\nconst getSelect = (p, name) => p?.properties?.[name]?.select?.name || '';\nconst getNumber = (p, name) => (\n typeof p?.properties?.[name]?.number === 'number'\n ? p.properties[name].number\n : null\n);\nconst getDate = (p, name) => p?.properties?.[name]?.date?.start || '';\n\nconst isPaidName = (s) => /^paid/i.test(String(s || ''));\n\n// quick equality for “no-change/dup” check\nfunction coreSame(src, db) {\n const eq = (x,y)=> String(x||'') === String(y||'');\n const numEq=(x,y)=> {\n const nx=Number(x??NaN), ny=Number(y??NaN);\n if (Number.isNaN(nx) && Number.isNaN(ny)) return true;\n return Math.abs(nx - ny) < 0.001;\n };\n return (\n eq(src.vendor, db.vendor) &&\n eq(src.invoice_no, db.invoice_no) &&\n eq(src.currency, db.currency) &&\n numEq(src.amount_total, db.amount_total) &&\n eq(src.issue_date, db.issue_date) &&\n eq(src.due_date, db.due_date)\n );\n}\n\nconst out = [];\nconst tolAbs = 0.01; // absolute amount tolerance\nconst tolPct = 0.01; // 1% relative tolerance\n\n$input.all().forEach((it, idx) => {\n const src = originals[idx] || {};\n const http = it.json || {};\n const pages = Array.isArray(http.results) ? http.results : [];\n const manyMatches = pages.length > 1;\n const page = pages[0] || null;\n\n // ---------- DB snapshot ----------\n const db = page ? {\n page_id: page.id,\n invoice_no: getTitle(page, 'Invoice No'),\n vendor: getRich(page, 'Vendor'),\n currency: getSelect(page,'Currency'),\n amount_total:getNumber(page,'Amount Total'),\n issue_date: getDate(page, 'Issue Date'),\n due_date: getDate(page, 'Due Date'),\n status: getSelect(page,'Status'),\n } : null;\n const dbPaid = db ? isPaidName(db.status) : false;\n\n // ---------- mismatches ----------\n const vendorMismatch = !!(db && db.vendor && src.vendor && db.vendor !== src.vendor);\n const currencyMismatch = !!(db && db.currency && src.currency && db.currency !== src.currency);\n\n const inAmt = Number(src.amount_total ?? NaN);\n const dbAmt = Number(db?.amount_total ?? NaN);\n const amountMismatch =\n db && Number.isFinite(inAmt) && Number.isFinite(dbAmt) &&\n (Math.abs(inAmt - dbAmt) > tolAbs) &&\n (Math.abs(inAmt - dbAmt) / Math.max(1, dbAmt) > tolPct);\n\n const fallbackAmbiguous = !src.invoice_no && manyMatches;\n\n // ---------- derive payment signals from AMOUNTS (ignore labels) ----------\n const statusStr = String(src.status || '');\n const totalIn = Number(src.amount_total ?? NaN);\n const paidAmtIn = Number(\n (src?.receipt_hints?.paid_amount ?? src.paid_amount) ?? NaN\n );\n const dueIn = Number(src.amount_due ?? NaN);\n\n // some payment if: positive paid OR “partial” in status OR doc_says_paid flag\n let hasSomePayment =\n (Number.isFinite(paidAmtIn) && paidAmtIn > tolAbs) ||\n /partial/i.test(statusStr) ||\n src.doc_says_paid === true;\n\n // full payment primarily by amounts; labels only if amounts missing\n let isFullPayment =\n (Number.isFinite(dueIn) && dueIn <= tolAbs) ||\n (Number.isFinite(totalIn) && Number.isFinite(paidAmtIn) && (paidAmtIn >= totalIn - tolAbs));\n\n // if due is positive, it cannot be full—even if the doc says “PAID”\n if (Number.isFinite(dueIn) && dueIn > tolAbs) isFullPayment = false;\n\n // keep compatibility names used later in logic\n const incomingPaid = hasSomePayment;\n const partialDetected = hasSomePayment && !isFullPayment;\n\n // ---------- decide route ----------\n let next_action = 'archive';\n let reason = '';\n\n if (!page) {\n // No DB page found → choose create_* route\n if (hasSomePayment) {\n if (isFullPayment) {\n next_action = 'create_paid';\n reason = 'no_db_match_paid';\n } else {\n next_action = ENABLE_PARTIAL ? 'create_partial' : 'manual_review';\n reason = ENABLE_PARTIAL ? 'no_db_match_partial_payment' : 'partial_needs_review';\n }\n } else {\n next_action = 'create_unpaid';\n reason = 'no_db_match_unpaid';\n }\n } else {\n // Have a DB page\n if (manyMatches || vendorMismatch || currencyMismatch || fallbackAmbiguous) {\n next_action = 'manual_review';\n reason = manyMatches\n ? 'multiple_db_matches'\n : vendorMismatch\n ? 'vendor_mismatch'\n : currencyMismatch\n ? 'currency_mismatch'\n : 'fallback_ambiguous';\n } else if (hasSomePayment) {\n // There is some payment in the incoming doc\n if (isFullPayment) {\n // mark paid if DB not yet paid; else archive duplicate payment\n if (!dbPaid) {\n next_action = 'update_mark_paid';\n reason = 'mark_paid';\n } else {\n next_action = 'archive';\n reason = 'duplicate_payment_already_paid';\n }\n } else {\n // partial payment path\n if (ENABLE_PARTIAL) {\n next_action = 'update_partial';\n reason = 'partial_payment_detected';\n } else {\n next_action = 'manual_review';\n reason = 'partial_needs_review';\n }\n }\n } else if (amountMismatch) {\n next_action = 'manual_review';\n reason = 'amount_mismatch';\n } else {\n // no payment transition\n const same = coreSame(src, db);\n next_action = 'archive';\n reason = same ? 'duplicate_no_change' : 'no_payment_transition';\n }\n }\n\n out.push({\n json: {\n ...src,\n db_found: !!page,\n db_page_id: db?.page_id || '',\n db_status: db?.status || '',\n next_action,\n reason,\n _checks: {\n manyMatches,\n vendorMismatch,\n currencyMismatch,\n amountMismatch,\n fallbackAmbiguous,\n hasSomePayment,\n isFullPayment,\n dbPaid\n }\n }\n });\n});\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "1e7efdbb-f58c-4cfa-9a74-22cda80b58c7",
"name": "发送至源文件发票1",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
736
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "3b8e7c65-8d40-4636-bfb0-8091365e584e",
"name": "发送前准备行项目",
"type": "n8n-nodes-base.code",
"position": [
-2320,
1312
],
"parameters": {
"jsCode": "// n8n Code node — prepare line items + compute ONE field for Notion (\"Paid Amount\")\nconst out = [];\n\nfunction two(n){ return Math.round((Number(n)||0)*100)/100; }\nfunction chunk(str, size=1800){ const a=[]; for(let i=0;i<str.length;i+=size)a.push(str.slice(i,i+size)); return a; }\nfunction num(x){ if(x===null||x===undefined||x===\"\") return NaN; const n=Number(String(x).replace(/[^0-9.-]/g,\"\")); return isNaN(n)?NaN:n; }\nfunction clamp(n,min,max){ return Math.max(min, Math.min(max, n)); }\nfunction approxEq(a,b,eps=0.01){ return Math.abs(a-b)<=eps; }\n\nfor (const item of $input.all()) {\n const j = item.json || {};\n const ccy = (j.currency || '').toUpperCase();\n const items = Array.isArray(j.line_items) ? j.line_items : [];\n\n // Build line-items text (unchanged)\n const lines = items.map(li => {\n const desc=(li?.description||'').trim();\n const qty = two(li?.qty);\n const up = two(li?.unit_price);\n const amt = two(li?.amount || qty*up);\n const tail = ccy ? ` ${ccy}` : '';\n return `${qty} × ${desc} @ ${up}${tail} = ${amt}${tail}`;\n });\n const text = lines.join('\\n') || '';\n const rich = chunk(text).map(s => ({ type:'text', text:{ content:s }}));\n\n // Minimal compute for Paid Amount\n const total = num(j.amount_total);\n const due = num(j.amount_due);\n const delta = num(j?.receipt_hints?.paid_amount ?? j.paid_amount); // new receipt (e.g., 200)\n const prev = num(j.db_paid_amount_prev ?? j.invoice?.paid_amount ?? 0); // previous paid if you pass it\n\n // Prefer set-to from doc (Total - Due); fallback to prev + delta\n let newPaid = Number.isFinite(total) && Number.isFinite(due)\n ? two(clamp(total - due, 0, total))\n : (Number.isFinite(delta) ? two(clamp(prev + delta, 0, Number.isFinite(total)? total : Infinity)) : NaN);\n\n // If still NaN, don't emit the field to avoid bad writes\n const outJson = {\n ...j,\n line_items_text: text,\n line_items_rich_text: rich,\n };\n if (Number.isFinite(newPaid)) outJson.notion_paid_amount = newPaid;\n\n // (Optional tiny extra: status, only if you want it — safe to ignore)\n if (Number.isFinite(total) && Number.isFinite(newPaid)) {\n outJson.notion_status =\n approxEq(newPaid, 0) ? \"Unpaid\" :\n approxEq(newPaid, total) ? \"Paid (Verified)\" :\n \"Partially Paid\";\n }\n\n out.push({ json: outJson });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "fa73399a-c133-48b3-87fe-922961f4d448",
"name": "决定去向",
"type": "n8n-nodes-base.switch",
"position": [
-2096,
1232
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Create Unpaid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "18f4cc4b-ad0f-4782-a59d-a66dbffc6475",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "create_unpaid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Create Paid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d8bb0418-0798-428a-b379-8cf8accf5ea9",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "create_paid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Update to Paid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4009916c-000e-435d-ada3-6a7d1fbc595c",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "update_mark_paid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Archive",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "172dad2a-7a92-493d-977d-56c568d5c360",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "archive"
}
]
},
"renameOutput": true
},
{
"outputKey": "New Create Partial",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "a14d6d5c-6eac-49e8-8380-f85cf565a542",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "create_partial"
}
]
},
"renameOutput": true
},
{
"outputKey": "Update Partial Payment",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b9d0f364-e7a4-48b8-94fc-6053b51f6e14",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "update_partial"
}
]
},
"renameOutput": true
},
{
"outputKey": "Manual Review",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4d8986d1-3896-42bf-9d6f-9adb48ac3987",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.next_action }}",
"rightValue": "manual_review"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "f90f09ea-6016-429b-915f-1565bcff1d97",
"name": "创建新未付发票",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
736
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Decide Fate').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Decide Fate').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Decide Fate').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Decide Fate').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Decide Fate').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Decide Fate').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Decide Fate').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "8f25494a-6b7c-45e5-99fc-7ab5e2e0d085",
"name": "将收据添加到现金流",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1312
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $json.invoice_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.db_page_id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $json.payment_method }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.line_items_rich_text[0].text.content }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "35db92f5-faf6-4ecd-a148-7bbf09bf620b",
"name": "更新发票为全额支付",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1312
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Prepare Line Items before send').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Receipt|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "Paid"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5487835c-5655-476b-bbad-f368653150d1",
"name": "发送至源文件发票2",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
928
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "a09de3d7-f79a-4027-ab49-a4e2376d3f82",
"name": "将收据添加到已支付现金流1",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
928
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $('Prepare Line Items before send').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.receipt_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "54b5f427-4c28-4b83-a04d-40bc14df309d",
"name": "创建新全额支付发票",
"type": "n8n-nodes-base.notion",
"position": [
-1424,
928
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Decide Fate').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Decide Fate').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Decide Fate').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Decide Fate').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Decide Fate').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Decide Fate').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Decide Fate').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice 2').item.json.id }}"
]
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "48bc9ab1-03bb-40a6-844d-a4fff203fcd5",
"name": "发送至归档源文件发票",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1120
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "239f7557-9573-80d6-a8fb-fc710833e5db",
"cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
"cachedResultName": "Archived Source File (Duplicate)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "FILE URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "d165164c-bcf8-4ef3-b434-9416865c6b26",
"name": "归档发票",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1120
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "254f7557-9573-8012-b9b3-e7811bfac12c",
"cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
"cachedResultName": "Archived Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Decide Fate').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Decide Fate').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Decide Fate').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Decide Fate').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Decide Fate').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Decide Fate').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Decide Fate').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Decide Fate').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "4157135a-0645-4f12-9315-c6b2299520b6",
"name": "等待",
"type": "n8n-nodes-base.wait",
"position": [
-1600,
2544
],
"webhookId": "9d0e00b6-5ee8-45c9-8597-34db73f701ae",
"parameters": {
"resume": "webhook",
"options": {},
"resumeUnit": "days",
"resumeAmount": 2,
"limitWaitTime": true
},
"typeVersion": 1.1
},
{
"id": "da9cdafe-01c8-49c8-afaa-29cb46f67aef",
"name": "发送消息",
"type": "n8n-nodes-base.slack",
"position": [
-1824,
2544
],
"webhookId": "2df2d5da-990f-4499-9131-0bbf3595e9bc",
"parameters": {
"text": "Test Text",
"select": "channel",
"blocksUi": "={\n\t\"blocks\": [\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"You have a new invoice to review:\\n*{{ $('Prepare Line Items before send').item.json.invoice_no || '—' }}*\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Type:*\\nInvoice\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*When:*\\n{{ $('Prepare Line Items before send').item.json.issue_date || '—' }} → {{ $('Prepare Line Items before send').item.json.due_date || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Vendor:*\\n{{ $('Prepare Line Items before send').item.json.vendor || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Currency:*\\n{{ $('Prepare Line Items before send').item.json.currency || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Invoice No:*\\n{{ $('Prepare Line Items before send').item.json.invoice_no || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Status (detected):*\\n{{ $('Prepare Line Items before send').item.json.status || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Amount Total:*\\n{{ $('Prepare Line Items before send').item.json.amount_total || 0 }} {{ $('Prepare Line Items before send').item.json.currency || '' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Paid / Due:*\\n{{ $('Prepare Line Items before send').item.json.paid_amount || 0 }} / {{ $('Prepare Line Items before send').item.json.amount_due || 0 }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Payment Method:*\\n{{ $('Prepare Line Items before send').item.json.payment_method || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Reason:*\\n{{ $('Prepare Line Items before send').item.json.reason || '—' }}\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"type\": \"image\",\n\t\t\t\"title\": {\n\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\"text\": \"image1\",\n\t\t\t\t\"emoji\": true\n\t\t\t},\n\t\t\t\"image_url\": \"{{ $json.source_file_url }}\",\n\t\t\t\"alt_text\": \"image1\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"divider\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"actions\",\n\t\t\t\"block_id\": \"review_actions\",\n\t\t\t\"elements\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"approve\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"✅ Approve Paid\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"primary\",\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=approve&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"unpaid\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"📝 Approve Unpaid\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"danger\",\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=unpaid&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"archive\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"❌ Archive\"\n\t\t\t\t\t},\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=archive&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C09B5P66Q4A",
"cachedResultName": "required-invoice-review"
},
"messageType": "block",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "e2179126-0881-4701-9d4a-62d335a74abd",
"name": "发送至归档源文件重复发票",
"type": "n8n-nodes-base.notion",
"position": [
-2768,
2032
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "239f7557-9573-80d6-a8fb-fc710833e5db",
"cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
"cachedResultName": "Archived Source File (Duplicate)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "FILE URL|url",
"urlValue": "={{ $json.source_file_url }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "cca27d76-da70-42fc-b6b2-5c5330490d2b",
"name": "归档重复发票",
"type": "n8n-nodes-base.notion",
"position": [
-2544,
2032
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "254f7557-9573-8012-b9b3-e7811bfac12c",
"cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
"cachedResultName": "Archived Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Archive Duplicate Items').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Prepare Archive Duplicate Items').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Prepare Archive Duplicate Items').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Prepare Archive Duplicate Items').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.line_items_text }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Prepare Archive Duplicate Items').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Prepare Archive Duplicate Items').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "e4d849f2-4c10-4e93-9133-b78e861687f2",
"name": "准备归档重复项目",
"type": "n8n-nodes-base.code",
"position": [
-2992,
2032
],
"parameters": {
"jsCode": "// Build \"summary items\" text from line_items (one output per input)\n\nfunction toNum(v) {\n const n = Number(v);\n return Number.isFinite(n) ? n : 0;\n}\nfunction clean(s) {\n return String(s ?? \"\").trim();\n}\nfunction fmt(n) {\n // 2dp but strip trailing .00 if integer\n const f = (Math.round(n * 100) / 100).toFixed(2);\n return f.endsWith(\".00\") ? String(Math.round(n)) : f;\n}\n\nconst out = [];\n\nfor (const { json: rec } of $input.all()) {\n const currency = clean(rec.currency).toUpperCase(); // e.g., \"USD\"\n const lis = Array.isArray(rec.line_items) ? rec.line_items : [];\n\n // Group by description + unit_price to collapse duplicates\n const groups = new Map();\n for (const li of lis) {\n const desc = clean(li?.description) || \"Item\";\n const unit = toNum(li?.unit_price);\n const qty = toNum(li?.qty);\n const amt = toNum(li?.amount) || qty * unit;\n\n const key = `${desc}||${unit}`;\n if (!groups.has(key)) groups.set(key, { desc, unit, qty: 0, amount: 0 });\n const g = groups.get(key);\n g.qty += qty;\n g.amount += amt;\n }\n\n // Render lines\n const lines = [];\n const rich = [];\n for (const g of groups.values()) {\n const qtyS = fmt(g.qty);\n const unitS = fmt(g.unit);\n const amtS = fmt(g.amount);\n const curS = currency ? ` ${currency}` : \"\";\n const line = `${qtyS} × ${g.desc} @ ${unitS}${curS} = ${amtS}${curS}`;\n lines.push(line);\n rich.push({ type: \"text\", text: { content: line } });\n }\n\n const summary = lines.join(\"\\n\");\n\n out.push({\n json: {\n ...rec,\n summary_items: summary, // <-- your \"Summary Items\" field\n line_items_text: summary, // keep old name if you need it\n line_items_rich_text: rich // Notion rich_text-friendly\n }\n });\n}\n\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "237841d5-2268-4efe-9ae2-25e904dbd48a",
"name": "发送至源文件发票3",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2288
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "010957b9-c7a4-4432-ac02-6ea1400f0418",
"name": "将收据添加到已支付现金流2",
"type": "n8n-nodes-base.notion",
"position": [
-480,
2288
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $('Take Invoice Details 1').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 1').item.json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $('Take Invoice Details 1').item.json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.receipt_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5b13d0d1-28a9-47df-82a2-5b83d338c413",
"name": "审批检查",
"type": "n8n-nodes-base.switch",
"position": [
-1376,
2528
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Paid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cf7a1e2d-9984-4052-9c1f-691f7a0ea50c",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.choice }}",
"rightValue": "approve"
}
]
},
"renameOutput": true
},
{
"outputKey": "Unpaid",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "78953ded-6256-4f3a-86c5-1189b02b20e7",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.choice }}",
"rightValue": "unpaid"
}
]
},
"renameOutput": true
},
{
"outputKey": "Archive",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "3f329867-45ba-4287-b5f7-632c52567c79",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.query.choice }}",
"rightValue": "archive"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "0163f077-3850-4609-9def-1483cd5f720b",
"name": "将收据添加到现金流1",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2096
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $json.invoice_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.db_page_id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $json.payment_method }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.line_items_rich_text[0].text.content }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "557a172b-f58a-4f82-a8b8-ddc3865bbe20",
"name": "更新发票为全额支付1",
"type": "n8n-nodes-base.notion",
"position": [
-480,
2096
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Take Invoice Details 1').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Receipt|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "Paid"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5e962f4f-f35a-46f6-a80f-bdd66553c3ed",
"name": "创建新支付发票3",
"type": "n8n-nodes-base.notion",
"position": [
-256,
2288
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 1').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Take Invoice Details 1').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Take Invoice Details 1').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Take Invoice Details 1').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Take Invoice Details 1').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Take Invoice Details 1').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice 3').item.json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "5fcebb58-8279-4d89-981f-27d34f2c2747",
"name": "获取发票详情2",
"type": "n8n-nodes-base.code",
"position": [
-1152,
2576
],
"parameters": {
"jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || ''; // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n : undefined;\n\n// soft fallback(s)\nif (!picked) {\n if (prepared.length === 1) {\n picked = prepared[0];\n } else {\n const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n const invC = canon(inv);\n picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n ...picked,\n review: {\n choice, // approve | unpaid | archive\n inv, // invoice_no from the button\n source: 'slack_button'\n }\n};\n\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "d6375c43-4c99-41fe-9215-eec2ba228e17",
"name": "获取发票详情1",
"type": "n8n-nodes-base.code",
"position": [
-1152,
2192
],
"parameters": {
"jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || ''; // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n : undefined;\n\n// soft fallback(s)\nif (!picked) {\n if (prepared.length === 1) {\n picked = prepared[0];\n } else {\n const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n const invC = canon(inv);\n picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n ...picked,\n review: {\n choice, // approve | unpaid | archive\n inv, // invoice_no from the button\n source: 'slack_button'\n }\n};\n\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "a9a19c40-4875-4c02-9c2b-3f65124ebdec",
"name": "更新发票为全额支付",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2480
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Take Invoice Details 2').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "=Unpaid"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "0af156a4-3324-4a1f-9f63-7a181adb4485",
"name": "发送至源文件发票",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2672
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "a1eaf719-3843-424f-b758-b58b32cdc30c",
"name": "创建新支付发票",
"type": "n8n-nodes-base.notion",
"position": [
-480,
2672
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 2').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Take Invoice Details 2').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Take Invoice Details 2').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Take Invoice Details 2').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.notes }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Take Invoice Details 2').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Take Invoice Details 2').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice ').item.json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 2').item.json.paid_amount }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "2d7330e7-e3b9-4afc-9217-9dcbdbd4a3e9",
"name": "归档发票1",
"type": "n8n-nodes-base.notion",
"position": [
-704,
2864
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "254f7557-9573-8012-b9b3-e7811bfac12c",
"cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
"cachedResultName": "Archived Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Take Invoice Details 3').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Take Invoice Details 3').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Take Invoice Details 3').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Take Invoice Details 3').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.notes || $('Take Invoice Details 3').item.json.reason}}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Take Invoice Details 3').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Take Invoice Details 3').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Take Invoice Details 3').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "6342d55e-4e82-4755-8826-3d4e5b7f9be7",
"name": "获取发票详情3",
"type": "n8n-nodes-base.code",
"position": [
-1152,
2864
],
"parameters": {
"jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || ''; // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n : undefined;\n\n// soft fallback(s)\nif (!picked) {\n if (prepared.length === 1) {\n picked = prepared[0];\n } else {\n const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n const invC = canon(inv);\n picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n ...picked,\n review: {\n choice, // approve | unpaid | archive\n inv, // invoice_no from the button\n source: 'slack_button'\n }\n};\n\nreturn [{ json: out }];\n"
},
"typeVersion": 2
},
{
"id": "c6f37078-96cb-4e97-b678-ebb688afb413",
"name": "检查数据库存在1",
"type": "n8n-nodes-base.if",
"position": [
-928,
2192
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "95b89f55-ca6f-46ce-bc0c-8ea48b18c013",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.db_found }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "2b1a1c57-bad7-4629-a969-aced98962828",
"name": "检查数据库存在2",
"type": "n8n-nodes-base.if",
"position": [
-928,
2576
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "95b89f55-ca6f-46ce-bc0c-8ea48b18c013",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.db_found }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ed421b2f-4a7e-4098-9d09-96965813f3e4",
"name": "发送至源文件发票4",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1504
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-8062-b50d-fe9db34ec410",
"cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
"cachedResultName": "Source File"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "File URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "3f33a49f-f4d5-4483-b01b-ab7b05f1edef",
"name": "将收据添加到部分支付现金流",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1504
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $('Prepare Line Items before send').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes || \"Partial Paid\" }}"
},
{
"key": "Timestamp|date",
"date": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.receipt_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "a988e2c7-ad51-4df3-b4d6-9d5cd6e123af",
"name": "创建新部分支付发票",
"type": "n8n-nodes-base.notion",
"position": [
-1424,
1504
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "24df7557-9573-8073-9611-da8371404254",
"cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
"cachedResultName": "Clearflow Invoice (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Attachments|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.attachments[0] }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
},
{
"key": "Destination Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.destination_account }}"
},
{
"key": "Discount Percent|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.discount_percent_for_notion }}"
},
{
"key": "Due Date|date",
"date": "={{ $('Prepare Line Items before send').item.json.due_date }}"
},
{
"key": "Invoice No|title",
"title": "={{ $('Prepare Line Items before send').item.json.invoice_no }}"
},
{
"key": "Issue Date|date",
"date": "={{ $('Prepare Line Items before send').item.json.issue_date }}"
},
{
"key": "Line Items|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.line_items_rich_text[0].text.content }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
},
{
"key": "Payment Ref|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_ref }}"
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.destination_account }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.status }}"
},
{
"key": "Subtotal|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.subtotal }}"
},
{
"key": "Tax Total|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
},
{
"key": "Vendor|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
},
{
"key": "=Source File|relation",
"relationValue": [
"={{ $('Send to Source File Invoice 4').item.json.id }}"
]
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "8e8ac821-5cd5-4bd2-b78d-203fd5cf57fb",
"name": "将收据添加到现金流2",
"type": "n8n-nodes-base.notion",
"position": [
-1872,
1696
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "238f7557-9573-80a2-96c8-e638a56411d2",
"cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
"cachedResultName": "Cashflow (Ledger)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Cashflow ID|title",
"title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
},
{
"key": "Amount (Net)|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Currency|select",
"selectValue": "={{ $json.currency }}"
},
{
"key": "Fee|number",
"numberValue": "={{ $json.tax_total }}"
},
{
"key": "Gross Amount|number",
"numberValue": "={{ $json.paid_amount }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.notes }}"
},
{
"key": "Timestamp|date",
"date": "={{ $json.paid_date }}"
},
{
"key": "Timestamp (ISO)|rich_text",
"textContent": "={{ $json.paid_date }}"
},
{
"key": "Type|select",
"selectValue": "=Expense"
},
{
"key": "Vendor / Recipient|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Vendor Ref ID|rich_text",
"textContent": "={{ $json.invoice_no }}"
},
{
"key": "Source File|relation",
"relationValue": [
"={{ $json.db_page_id }}"
]
},
{
"key": "Recipient Account|rich_text",
"textContent": "={{ $json.vendor }}"
},
{
"key": "Source Account|rich_text",
"textContent": "={{ $json.payment_method }}"
},
{
"key": "Notes|rich_text",
"textContent": "={{ $json.line_items_rich_text[0].text.content }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "25951d6e-c27d-48b0-8bef-1b06bb931c30",
"name": "更新发票为部分或全额支付",
"type": "n8n-nodes-base.notion",
"position": [
-1648,
1696
],
"parameters": {
"pageId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Prepare Line Items before send').item.json.db_page_id }}"
},
"options": {},
"resource": "databasePage",
"operation": "update",
"propertiesUi": {
"propertyValues": [
{
"key": "Receipt|relation",
"relationValue": [
"={{ $json.id }}"
]
},
{
"key": "Paid Amount|number",
"numberValue": "={{ $('Prepare Line Items before send').item.json.notion_paid_amount }}"
},
{
"key": "Status|select",
"selectValue": "={{ $('Prepare Line Items before send').item.json.notion_status }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "1ed7bced-88d4-4210-b323-c64bf1c1fece",
"name": "检查解析错误2",
"type": "n8n-nodes-base.if",
"position": [
-4688,
1552
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "ddbbbfbb-e05b-4ca9-b607-65ad8ac748bb",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.IsErroredOnProcessing }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "1c81ef35-2d9b-435b-90ae-979dbebe8ace",
"name": "OCR Space 解析",
"type": "n8n-nodes-base.httpRequest",
"position": [
-4912,
1552
],
"parameters": {
"url": "https://api.ocr.space/parse/image",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "multipart-form-data",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "filetype",
"value": "jpg"
},
{
"name": "file",
"parameterType": "formBinaryData",
"inputDataFieldName": "data"
},
{
"name": "scale",
"value": "true"
},
{
"name": "OCREngine",
"value": "2"
}
]
},
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"id": "dkJjl1msBNIeIT5u",
"name": "OCR Space"
}
},
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "7dde97e8-b6cd-4527-a948-36d62ca9d235",
"name": "获取二进制文件",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5136,
1552
],
"parameters": {
"url": "={{ $json.url_private_download }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "slackApi"
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 4.2
},
{
"id": "d2da0e22-0a2f-4a66-8651-7a0bd73f3452",
"name": "停止并报错",
"type": "n8n-nodes-base.stopAndError",
"position": [
-4464,
1504
],
"parameters": {
"errorMessage": "=OCR Server Parsing Failed Reason:{{ $json.ErrorMessage[0] }}"
},
"typeVersion": 1
},
{
"id": "6376b9b6-40c7-4f01-8a11-8f5bee34dfd4",
"name": "停止和错误1",
"type": "n8n-nodes-base.stopAndError",
"position": [
-4688,
1744
],
"parameters": {
"errorMessage": "=OCR Server Parsing Failed Reason:{{ $json.ErrorMessage[0] }}"
},
"typeVersion": 1
},
{
"id": "17f5f3e0-7aa0-45c2-9c84-d840321ef94a",
"name": "聚合1",
"type": "n8n-nodes-base.aggregate",
"position": [
-2320,
2032
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "d707c4fc-d2a5-496d-9d6b-8c71f627ad21",
"name": "发送重复通知",
"type": "n8n-nodes-base.slack",
"position": [
-2096,
2032
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟥 Internal Duplicate Found:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0971ANKZM1",
"cachedResultName": "duplicate-alert"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "548ea7e3-8e51-4a56-891e-1db51e466b2d",
"name": "聚合",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
736
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "893731ba-84cd-4189-9c3d-c1a73c158979",
"name": "聚合2",
"type": "n8n-nodes-base.aggregate",
"position": [
-1200,
928
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "2d9bf727-1b2f-4a1e-bc54-61c686f63d01",
"name": "通知带收据的新发票",
"type": "n8n-nodes-base.slack",
"position": [
-976,
928
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Paid Invoice Created:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "cb2e3474-2c42-4804-a1a0-aea65c45aece",
"name": "通知新发票",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
736
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟨 Invoice Created:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "6db4ac23-65d8-4e5a-b1ae-84777f7c6a0b",
"name": "聚合3",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
1312
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "7766fd88-d419-47ee-b545-e6d6d574045f",
"name": "通知更新发票",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
1312
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Invoice Updated to Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "ec4398cb-c999-43bb-9d71-76a9ec6e2747",
"name": "聚合4",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
1120
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "8976661d-c12c-4b3b-92c9-337fb7b3be3f",
"name": "聚合5",
"type": "n8n-nodes-base.aggregate",
"position": [
-1200,
1504
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "20260742-e05b-4ce1-b2ca-40db32c1e66b",
"name": "聚合6",
"type": "n8n-nodes-base.aggregate",
"position": [
-1424,
1696
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "75d16382-b71a-413c-abb7-5f6ee529b268",
"name": "通知部分支付更新发票",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
1696
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟡 Updated Invoice Partial Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "be11d35c-a96d-432d-bf60-747e9242b8b0",
"name": "通知创建部分支付发票",
"type": "n8n-nodes-base.slack",
"position": [
-976,
1504
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟡 Created New Invoice Partial Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "cb059bc5-42c4-4eee-a4c5-f70013b81d1f",
"name": "通知发票已归档",
"type": "n8n-nodes-base.slack",
"position": [
-1200,
1120
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟥 Database Duplicate Found:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "29cc18f8-253c-4763-a2ae-e9f2668b4e8c",
"name": "聚合8",
"type": "n8n-nodes-base.aggregate",
"position": [
-480,
2864
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "852ca201-dfe6-4d5b-bf6d-9d06954f0604",
"name": "通知发票已归档2",
"type": "n8n-nodes-base.slack",
"position": [
-256,
2864
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟥 Manual Review Archived:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "2e9ab2da-f324-4864-8617-4ebf6d6873ab",
"name": "发送至归档源文件发票",
"type": "n8n-nodes-base.notion",
"position": [
-928,
2864
],
"parameters": {
"title": "={{ $json.source_file_id }}",
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "239f7557-9573-80d6-a8fb-fc710833e5db",
"cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
"cachedResultName": "Archived Source File (Duplicate)"
},
"propertiesUi": {
"propertyValues": [
{
"key": "File ID|title",
"title": "={{ $json.source_file_id }}"
},
{
"key": "Filename|rich_text",
"textContent": "={{ $json.source_file_name }}"
},
{
"key": "FILE URL|url",
"urlValue": "={{ $json.source_file_url }}"
},
{
"key": "Summary|rich_text",
"text": {
"text": [
{
"text": "={{ $json.line_items_rich_text[0].text.content }}",
"annotationUi": {}
}
]
},
"richText": true
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
},
{
"id": "ff53fb86-f773-4690-b874-be81d77259ab",
"name": "聚合7",
"type": "n8n-nodes-base.aggregate",
"position": [
-256,
2672
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "38fd6b37-35cd-43f4-a6a1-caf4c20f0848",
"name": "通知新发票1",
"type": "n8n-nodes-base.slack",
"position": [
-32,
2672
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟨 Manual Review Invoice Created Unpaid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "30d801f2-7ae0-4f43-882d-2df58867102e",
"name": "聚合9",
"type": "n8n-nodes-base.aggregate",
"position": [
-480,
2480
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "3fbead5e-2aa0-4222-a2ff-2a03901ffbe0",
"name": "通知新发票2",
"type": "n8n-nodes-base.slack",
"position": [
-256,
2480
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟨 Manual Review Invoice Updated Unpaid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "9706d716-8bfc-433e-9d09-8a3a479a5ecc",
"name": "聚合10",
"type": "n8n-nodes-base.aggregate",
"position": [
-32,
2288
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "2731b665-b116-459e-9a90-1d28d63d1c85",
"name": "通知带收据的新发票1",
"type": "n8n-nodes-base.slack",
"position": [
192,
2288
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Manual Review Invoice Created Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "ffec1ef1-51d6-4870-b4e6-86af6c95ed87",
"name": "聚合11",
"type": "n8n-nodes-base.aggregate",
"position": [
-256,
2096
],
"parameters": {
"include": "specifiedFields",
"options": {},
"aggregate": "aggregateAllItemData",
"fieldsToInclude": "name, url"
},
"typeVersion": 1
},
{
"id": "b206f8e8-74f5-4502-8dc1-ef154867890f",
"name": "通知带收据的新发票2",
"type": "n8n-nodes-base.slack",
"position": [
-32,
2096
],
"webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
"parameters": {
"text": "=🟩 Manual Review Invoice Updated Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": "C0976KA2QTC",
"cachedResultName": "notification"
},
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"credentials": {
"slackApi": {
"id": "muT22qcO4YUdHAGU",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "b4ea52bb-acc9-42d9-a61b-0f89bcfabba0",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-6112,
1440
],
"parameters": {
"width": 672,
"height": 704,
"content": "# 🔤 输入与格式检测"
},
"typeVersion": 1
},
{
"id": "4a5b2a7d-80d5-4897-884c-00d0451daee0",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5376,
1440
],
"parameters": {
"color": 2,
"width": 1056,
"height": 704,
"content": "# 👨💻 解析数据"
},
"typeVersion": 1
},
{
"id": "c3f62378-ecf0-4bd2-88d1-63e5268ef615",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4272,
1440
],
"parameters": {
"color": 3,
"width": 1200,
"height": 704,
"content": "# 🤖 AI 格式化与重复检测"
},
"typeVersion": 1
},
{
"id": "3f9110e8-9778-48e4-ab48-286bdfefc291",
"name": "便签3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3040,
1888
],
"parameters": {
"color": 7,
"width": 1120,
"height": 384,
"content": "# ❌ 归档重复项"
},
"typeVersion": 1
},
{
"id": "f32354a7-d05f-49e3-b60d-5d48c7f02126",
"name": "便签4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3024,
1200
],
"parameters": {
"color": 6,
"width": 1040,
"height": 384,
"content": "# 🟢 检查数据库中的发票"
},
"typeVersion": 1
},
{
"id": "4996b730-0f80-4a37-a702-fc8588934ece",
"name": "便签5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1936,
672
],
"parameters": {
"color": 4,
"width": 1136,
"height": 1200,
"content": "# 🟢 发送数据与通知"
},
"typeVersion": 1
},
{
"id": "49782403-dd3c-4457-97ca-bf49c393c395",
"name": "便签7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1888,
2016
],
"parameters": {
"color": 5,
"width": 2384,
"height": 1072,
"content": "# 👤 人工审核与发送数据"
},
"typeVersion": 1
},
{
"id": "ff8438b9-5804-473b-bd6b-c9840be62317",
"name": "错误触发器",
"type": "n8n-nodes-base.errorTrigger",
"position": [
-6048,
2192
],
"parameters": {},
"typeVersion": 1
},
{
"id": "fdaacf56-2efe-4272-80fc-365951cd7d48",
"name": "创建数据库页面",
"type": "n8n-nodes-base.notion",
"position": [
-5840,
2192
],
"parameters": {
"options": {},
"resource": "databasePage",
"databaseId": {
"__rl": true,
"mode": "list",
"value": "258f7557-9573-8013-bb51-d528ba507e68",
"cachedResultUrl": "https://www.notion.so/258f755795738013bb51d528ba507e68",
"cachedResultName": "ERROR CHECK"
},
"propertiesUi": {
"propertyValues": [
{
"key": "Message|rich_text",
"textContent": "={{ $json.execution.error.message }}"
},
{
"key": "Workflow|title",
"title": "={{ $json.workflow.name }}"
}
]
}
},
"credentials": {
"notionApi": {
"id": "FRdXwx5ANpHG8ZPX",
"name": "Notion account"
}
},
"typeVersion": 2.2
}
],
"pinData": {},
"connections": {
"Code": {
"main": [
[
{
"node": "Merge Data Value into One Key",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Approval Check",
"type": "main",
"index": 0
}
]
]
},
"Aggregate": {
"main": [
[
{
"node": "Notify New Invoice",
"type": "main",
"index": 0
}
]
]
},
"Aggregate1": {
"main": [
[
{
"node": "Send Duplicate Notification",
"type": "main",
"index": 0
}
]
]
},
"Aggregate2": {
"main": [
[
{
"node": "Notify New Invoice with Receipt",
"type": "main",
"index": 0
}
]
]
},
"Aggregate3": {
"main": [
[
{
"node": "Notify Update Invoice",
"type": "main",
"index": 0
}
]
]
},
"Aggregate4": {
"main": [
[
{
"node": "Notify Invoice Archived",
"type": "main",
"index": 0
}
]
]
},
"Aggregate5": {
"main": [
[
{
"node": "Notify Create Invoice Partial Paid",
"type": "main",
"index": 0
}
]
]
},
"Aggregate6": {
"main": [
[
{
"node": "Notify Update Invoice Partial Paid",
"type": "main",
"index": 0
}
]
]
},
"Aggregate7": {
"main": [
[
{
"node": "Notify New Invoice1",
"type": "main",
"index": 0
}
]
]
},
"Aggregate8": {
"main": [
[
{
"node": "Notify Invoice Archived2",
"type": "main",
"index": 0
}
]
]
},
"Aggregate9": {
"main": [
[
{
"node": "Notify New Invoice2",
"type": "main",
"index": 0
}
]
]
},
"Aggregate10": {
"main": [
[
{
"node": "Notify New Invoice with Receipt1",
"type": "main",
"index": 0
}
]
]
},
"Aggregate11": {
"main": [
[
{
"node": "Notify New Invoice with Receipt2",
"type": "main",
"index": 0
}
]
]
},
"Decide Fate": {
"main": [
[
{
"node": "Send to Source File Invoice 1",
"type": "main",
"index": 0
}
],
[
{
"node": "Send to Source File Invoice 2",
"type": "main",
"index": 0
}
],
[
{
"node": "Add Receipt into Cashflow",
"type": "main",
"index": 0
}
],
[
{
"node": "Send to Archive Source File Invoice",
"type": "main",
"index": 0
}
],
[
{
"node": "Send to Source File Invoice 4",
"type": "main",
"index": 0
}
],
[
{
"node": "Add Receipt into Cashflow2",
"type": "main",
"index": 0
}
],
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
},
"Check Format": {
"main": [
[
{
"node": "Take Binary Files",
"type": "main",
"index": 0
}
],
[
{
"node": "Take Binary Files for Document",
"type": "main",
"index": 0
}
]
]
},
"Format Check": {
"main": [
[
{
"node": "Check Format",
"type": "main",
"index": 0
}
]
]
},
"Error Trigger": {
"main": [
[
{
"node": "Create a database page",
"type": "main",
"index": 0
}
]
]
},
"Slack Trigger": {
"main": [
[
{
"node": "Format Check",
"type": "main",
"index": 0
}
]
]
},
"Approval Check": {
"main": [
[
{
"node": "Take Invoice Details 1",
"type": "main",
"index": 0
}
],
[
{
"node": "Take Invoice Details 2",
"type": "main",
"index": 0
}
],
[
{
"node": "Take Invoice Details 3",
"type": "main",
"index": 0
}
]
]
},
"Send a message": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Archive Invoice": {
"main": [
[
{
"node": "Aggregate4",
"type": "main",
"index": 0
}
]
]
},
"Basic LLM Chain": {
"main": [
[
{
"node": "Cleans AI Response",
"type": "main",
"index": 0
}
]
]
},
"OCR Space Parse": {
"main": [
[
{
"node": "Check parsing Error2",
"type": "main",
"index": 0
}
]
]
},
"Check DB Exist 1": {
"main": [
[
{
"node": "Add Receipt into Cashflow1",
"type": "main",
"index": 0
}
],
[
{
"node": "Send to Source File Invoice 3",
"type": "main",
"index": 0
}
]
]
},
"Check DB Exist 2": {
"main": [
[
{
"node": "Update Invoice to Paid Fully ",
"type": "main",
"index": 0
}
],
[
{
"node": "Send to Source File Invoice ",
"type": "main",
"index": 0
}
]
]
},
"Check DB Invoice": {
"main": [
[
{
"node": "Merge Items Invoice",
"type": "main",
"index": 0
}
]
]
},
"OCR Space Parse1": {
"main": [
[
{
"node": "Check parsing Error3",
"type": "main",
"index": 0
}
]
]
},
"Archive Invoice 1": {
"main": [
[
{
"node": "Aggregate8",
"type": "main",
"index": 0
}
]
]
},
"Take Binary Files": {
"main": [
[
{
"node": "OCR Space Parse",
"type": "main",
"index": 0
}
]
]
},
"Cleans AI Response": {
"main": [
[
{
"node": "Internal Check Duplicate Invoice",
"type": "main",
"index": 0
}
]
]
},
"Create Query for DB": {
"main": [
[
{
"node": "Check DB Invoice",
"type": "main",
"index": 0
}
]
]
},
"Merge Items Invoice": {
"main": [
[
{
"node": "Prepare Line Items before send",
"type": "main",
"index": 0
}
]
]
},
"Check parsing Error2": {
"main": [
[
{
"node": "Stop and Error",
"type": "main",
"index": 0
}
],
[
{
"node": "Merge Data Value into One Key",
"type": "main",
"index": 0
}
]
]
},
"Check parsing Error3": {
"main": [
[
{
"node": "Stop and Error1",
"type": "main",
"index": 0
}
],
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Decide Fate for Data": {
"main": [
[
{
"node": "Create Query for DB",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Archive Duplicate Items",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat Model4": {
"ai_languageModel": [
[
{
"node": "Basic LLM Chain",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Take Invoice Details 1": {
"main": [
[
{
"node": "Check DB Exist 1",
"type": "main",
"index": 0
}
]
]
},
"Take Invoice Details 2": {
"main": [
[
{
"node": "Check DB Exist 2",
"type": "main",
"index": 0
}
]
]
},
"Take Invoice Details 3": {
"main": [
[
{
"node": "Send to Archive Source File Invoice ",
"type": "main",
"index": 0
}
]
]
},
"Create new Invoice Paid ": {
"main": [
[
{
"node": "Aggregate7",
"type": "main",
"index": 0
}
]
]
},
"Merge with Original Data": {
"main": [
[
{
"node": "Decide Fate for Data",
"type": "main",
"index": 0
}
]
]
},
"Add Receipt into Cashflow": {
"main": [
[
{
"node": "Update Invoice to Paid Fully",
"type": "main",
"index": 0
}
]
]
},
"Archive Invoice Duplicate": {
"main": [
[
{
"node": "Aggregate1",
"type": "main",
"index": 0
}
]
]
},
"Create new Invoice Paid 3": {
"main": [
[
{
"node": "Aggregate10",
"type": "main",
"index": 0
}
]
]
},
"Create new Invoice Unpaid": {
"main": [
[
{
"node": "Aggregate",
"type": "main",
"index": 0
}
]
]
},
"Add Receipt into Cashflow1": {
"main": [
[
{
"node": "Update Invoice to Paid Fully 1",
"type": "main",
"index": 0
}
]
]
},
"Add Receipt into Cashflow2": {
"main": [
[
{
"node": "Update Invoice to Paid Partial or Fully",
"type": "main",
"index": 0
}
]
]
},
"Create new Invoice Paid Full": {
"main": [
[
{
"node": "Aggregate2",
"type": "main",
"index": 0
}
]
]
},
"Send to Source File Invoice ": {
"main": [
[
{
"node": "Create new Invoice Paid ",
"type": "main",
"index": 0
}
]
]
},
"Update Invoice to Paid Fully": {
"main": [
[
{
"node": "Aggregate3",
"type": "main",
"index": 0
}
]
]
},
"Merge Data Value into One Key": {
"main": [
[
{
"node": "Basic LLM Chain",
"type": "main",
"index": 0
}
]
]
},
"Send to Source File Invoice 1": {
"main": [
[
{
"node": "Create new Invoice Unpaid",
"type": "main",
"index": 0
}
]
]
},
"Send to Source File Invoice 2": {
"main": [
[
{
"node": "Add Receipt into Cashflow Paid 1",
"type": "main",
"index": 0
}
]
]
},
"Send to Source File Invoice 3": {
"main": [
[
{
"node": "Add Receipt into Cashflow Paid 2",
"type": "main",
"index": 0
}
]
]
},
"Send to Source File Invoice 4": {
"main": [
[
{
"node": "Add Receipt into Cashflow Paid Partially",
"type": "main",
"index": 0
}
]
]
},
"Update Invoice to Paid Fully ": {
"main": [
[
{
"node": "Aggregate9",
"type": "main",
"index": 0
}
]
]
},
"Prepare Line Items before send": {
"main": [
[
{
"node": "Decide Fate",
"type": "main",
"index": 0
}
]
]
},
"Take Binary Files for Document": {
"main": [
[
{
"node": "OCR Space Parse1",
"type": "main",
"index": 0
}
]
]
},
"Update Invoice to Paid Fully 1": {
"main": [
[
{
"node": "Aggregate11",
"type": "main",
"index": 0
}
]
]
},
"Prepare Archive Duplicate Items": {
"main": [
[
{
"node": "Send to Archive Source File Invoice Duplicate",
"type": "main",
"index": 0
}
]
]
},
"Add Receipt into Cashflow Paid 1": {
"main": [
[
{
"node": "Create new Invoice Paid Full",
"type": "main",
"index": 0
}
]
]
},
"Add Receipt into Cashflow Paid 2": {
"main": [
[
{
"node": "Create new Invoice Paid 3",
"type": "main",
"index": 0
}
]
]
},
"Internal Check Duplicate Invoice": {
"main": [
[
{
"node": "Merge with Original Data",
"type": "main",
"index": 0
}
]
]
},
"Create new Invoice Paid Partially": {
"main": [
[
{
"node": "Aggregate5",
"type": "main",
"index": 0
}
]
]
},
"Send to Archive Source File Invoice": {
"main": [
[
{
"node": "Archive Invoice",
"type": "main",
"index": 0
}
]
]
},
"Send to Archive Source File Invoice ": {
"main": [
[
{
"node": "Archive Invoice 1",
"type": "main",
"index": 0
}
]
]
},
"Update Invoice to Paid Partial or Fully": {
"main": [
[
{
"node": "Aggregate6",
"type": "main",
"index": 0
}
]
]
},
"Add Receipt into Cashflow Paid Partially": {
"main": [
[
{
"node": "Create new Invoice Paid Partially",
"type": "main",
"index": 0
}
]
]
},
"Send to Archive Source File Invoice Duplicate": {
"main": [
[
{
"node": "Archive Invoice Duplicate",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - AI 摘要总结, 多模态 AI
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
If
Set
Code
+20
52 节点Daniel Lianes
杂项
使用 OpenAI、QuickChart 和 Google Drive 将电子表格数据转换为智能图表
使用 OpenAI、QuickChart 和 Google Drive 将电子表格数据转换为智能图表
If
Set
Code
+18
82 节点LeeWei
内容创作
基于AI的会议研究与每日议程(Google日历、Attio CRM和Slack)
基于AI的会议研究与每日议程:使用Google日历、Attio CRM和Slack
If
Set
Code
+15
30 节点Harry Siggins
AI 摘要总结
Gitlab 代码审查模板
使用Gemini AI和JIRA上下文自动化GitLab合并请求代码审查
If
Set
Code
+10
41 节点Evgeny Agronsky
AI 摘要总结
Google Meet AI助手
使用Google Meet、AssemblyAI和Claude AI自动化会议摘要与行动项
If
Set
Code
+12
25 节点Dominic
内容创作
PDF 转订单
使用AI将PDF采购订单自动化转换为Adobe Commerce销售订单
If
Set
Code
+19
96 节点JKingma
文档提取