8
n8n 中文网amn8n.com

使用 OCR、GPT-4 和 Salesforce 商机创建自动化发票处理

高级

这是一个Invoice Processing, AI Summarization领域的自动化工作流,包含 23 个节点。主要使用 Code, Salesforce, GoogleDrive, HttpRequest, ExtractFromFile 等节点。 通过 OCR、GPT-4 和 Salesforce 商机创建实现发票处理的自动化

前置要求
  • Salesforce OAuth 凭证
  • Google Drive API 凭证
  • 可能需要目标 API 的认证凭证
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "meta": {
    "instanceId": "9a562c06a632241f66aadd52a495ad98e76b760ef5cfce9c319a4759c47cd94e",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "c87ed7b7-b77d-4236-999e-afefe9064033",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2640,
        -656
      ],
      "parameters": {
        "width": 464,
        "height": 912,
        "content": "# PDF发票提取器(AI)到Salesforce商机"
      },
      "typeVersion": 1
    },
    {
      "id": "e7665458-f2f8-4ad1-b318-1fbe4e3ff464",
      "name": "从文件提取",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -1456,
        -144
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1
    },
    {
      "id": "1a593bd5-922f-4f0d-9ddb-0220ed3be3cb",
      "name": "Google Drive触发器",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        -1952,
        -144
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "list",
          "value": "125tDoA_XgewTZetNg4UhYoYuoomIKLuP",
          "cachedResultUrl": "https://drive.google.com/drive/folders/125tDoA_XgewTZetNg4UhYoYuoomIKLuP",
          "cachedResultName": "Sample"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "CfyGMYYwObyuC3Q6",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ce439fd0-ffcf-4891-a861-61fa62e57a38",
      "name": "向模型发送消息",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        -1264,
        -144
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1",
          "cachedResultName": "GPT-4.1"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "=Role: You are a meticulous extractor. Given raw OCR text that may contain one or many tax invoices, return a normalized JSON array. Do not invent data. If a field is missing, use null (or []).\n\nInput\n- batch_text: {{ $json.text }}\n\nOutput\n- Return only a JSON array. Each element is one invoice with the schema:\n\n[\n  {\n    \"seller\": {\n      \"name\": \"string|null\",\n      \"tax_id\": \"string|null\",\n      \"address\": \"string|null\",\n      \"phone\": \"string|null\",\n      \"email\": \"string|null\",\n      \"bank_account\": \"string|null\",\n      \"bank_name\": \"string|null\",\n      \"bank_branch\": \"string|null\"\n    },\n    \"buyer\": {\n      \"name\": \"string|null\",\n      \"tax_id\": \"string|null\",\n      \"address\": \"string|null\",\n      \"phone\": \"string|null\",\n      \"buyer_representative\": \"string|null\"\n    },\n    \"invoice\": {\n      \"type\": \"string|null\",\n      \"issue_date\": \"YYYY-MM-DD|null\",\n      \"code\": \"string|null\",\n      \"symbol\": \"string|null\",\n      \"number\": \"string|null\",\n      \"payment_method\": \"string|null\",\n      \"currency\": \"string\",\n      \"lookup_url\": \"string|null\",\n      \"lookup_code\": \"string|null\"\n    },\n    \"products\": [\n      {\n        \"name\": \"string\",\n        \"description\": \"string|null\",\n        \"unit\": \"string|null\",\n        \"quantity\": \"number\",\n        \"unit_price\": \"number\",\n        \"discount_percent\": \"number|null\",\n        \"discount_amount\": \"number|null\",\n        \"subtotal_excl_vat\": \"number\",\n        \"tax_rate\": \"number|null\",\n        \"tax_amount\": \"number|null\",\n        \"line_total\": \"number|null\"\n      }\n    ],\n    \"summary\": {\n      \"total_discount\": \"number|null\",\n      \"subtotal_excl_vat\": \"number|null\",\n      \"vat_rate\": \"number|null\",\n      \"vat_amount\": \"number|null\",\n      \"grand_total\": \"number|null\",\n      \"amount_in_words\": \"string|null\"\n    },\n    \"software\": {\n      \"issuer\": \"string|null\",\n      \"issuer_tax_id\": \"string|null\",\n      \"notes\": \"string|null\"\n    },\n    \"parsing\": {\n      \"warnings\": [\"string\"],\n      \"assumptions\": [\"string\"]\n    }\n  }\n]\n\nLocale & normalization rules\n1) Currency & numbers:\n   - Detect currency (e.g., USD, EUR, GBP, VND, ₫, $, €).\n   - Remove thousands separators (., , or space).\n   - Interpret decimals by locale (e.g., “1,234.56” vs “1.234,56”).\n   - Use integers for zero-decimal currencies; otherwise decimals. If uncertain, keep decimal and add a warning.\n\n2) Dates:\n   - Normalize to YYYY-MM-DD from formats like dd/mm/yyyy, mm-dd-yyyy, yyyy.mm.dd, or “dd Month yyyy”.\n   - If ambiguous (e.g., 03/04/2024), infer from locale cues (currency/language). If still uncertain, set null and add a warning.\n\n3) Line wrapping:\n   - Merge wrapped product description lines until quantity/unit/price/tax columns or totals appear.\n\n4) Discounts:\n   - If only percent or amount present, compute the other when possible (round to nearest minor unit).\n   - If discount appears as a negative line, treat as discount_amount.\n\n5) Per-line taxes:\n   - Capture per-line Tax/VAT rate/amount if shown; otherwise set products[*].tax_rate and tax_amount = null and rely on summary vat_rate.\n\n6) Totals cross-check:\n   - sum(products.subtotal_excl_vat) ≈ summary.subtotal_excl_vat (±1 minor unit)\n   - summary.vat_amount ≈ round(subtotal_excl_vat * vat_rate/100)\n   - grand_total = subtotal_excl_vat + vat_amount\n   - If mismatch, keep parsed figures and append a parsing.warnings note.\n\nMulti-invoice splitting (critical)\n- Split batch_text into invoice blocks using anchors; each block = one invoice.\n\nPrimary anchors:\n- “TAX INVOICE”, “INVOICE”, “VAT INVOICE”, or similar header; OR\n- A cluster containing both “Serial/Symbol:” (or “Series:”) and “No./Number:” within ~10 lines.\n\nSecondary boundaries (helpful, not required):\n- “Tax Authority Code / QR / Validation Code” near header\n- “Amount in words:” or “Total amount payable:” near the end\n- Software footers (e.g., “Issued by … software”)\n\nHeuristic:\n- Start a new block when a header reappears or when “Series/Symbol:” and “No./Number:” occur after a prior “Total amount payable”.\n\nExtraction steps (per invoice block)\n1) Seller/Buyer:\n   - Anchors: Company/Organization, Tax ID/VAT No., Address, Phone, Email, Bank Account/IBAN, Bank Name, Branch.\n   - Buyer: Customer/Buyer Name, Tax ID (if any), Address, Phone, Buyer Representative/Contact.\n\n2) Invoice header:\n   - Type, Issue date, Tax authority code (if any), Code (internal), Symbol/Series, Number, Payment method, Currency, Lookup URL/Code (QR/portal).\n\n3) Items table:\n   - Detect headers like: No., Item/Service, Unit, Quantity, Unit Price, Amount, Discount %, Discount Amount, Net before VAT, Tax/VAT, Line Total.\n   - Rebuild wrapped descriptions; keep units as-is if unsure.\n\n4) Summary:\n   - Total discount, Subtotal excl. VAT, VAT rate, VAT amount, Grand total/Total payable, Amount in words, Lookup URL/code (if present).\n\n5) Software/Notes:\n   - E-invoicing platform or software issuer (name & tax ID if shown) and any footer notes.\n\n6) Validate & warn:\n   - Run cross-checks; record anomalies and assumptions (e.g., inferred decimal separator, currency, ambiguous date).\n\nStrict formatting\n- Output only the JSON array (no extra text).\n- All numeric fields must be JSON numbers.\n- Use UTF-8.\n- Do not guess missing data.\n\nAnti-hallucination\n- If a field is not explicitly present or reliably derivable, return null.\n- If per-line VAT isn’t printed, set products[*].tax_rate and tax_amount to null and compute only at summary level.\n- If headers are partially missing due to OCR, infer columns from value patterns (units, prices \\d[\\d.,]*, discounts %, etc.). If uncertain, add a warning.\n"
            },
            {}
          ]
        },
        "jsonOutput": true
      },
      "credentials": {
        "openAiApi": {
          "id": "ciKVFfcmd36noEnL",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "053cdfcf-65f6-4ec6-90fa-090a08da31c8",
      "name": "从Google下载文件",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        -1712,
        -144
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "options": {},
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "id": "CfyGMYYwObyuC3Q6",
          "name": "Google Drive account"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "157f5cdc-de47-4f90-96ea-202636ac5016",
      "name": "更新文件到OneDrive",
      "type": "n8n-nodes-base.microsoftOneDrive",
      "position": [
        -1424,
        32
      ],
      "parameters": {
        "fileName": "={{ $json.name }}",
        "parentId": "folder_id_onedrive",
        "binaryData": true
      },
      "credentials": {
        "microsoftOneDriveOAuth2Api": {
          "id": "bvhIzbOLKeAfHCQc",
          "name": "Microsoft Drive account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "80f186d6-499e-4da2-a49e-a503de546be4",
      "name": "创建商机",
      "type": "n8n-nodes-base.salesforce",
      "position": [
        -656,
        -144
      ],
      "parameters": {
        "name": "={{ $('Message a model').item.json.message.content.invoice.code }}",
        "resource": "opportunity",
        "closeDate": "={{ $('Message a model').item.json.message.content.invoice.issue_date }}",
        "stageName": "Closed Won",
        "additionalFields": {
          "amount": "={{ $('Message a model').item.json.message.content.summary.grand_total }}",
          "accountId": "={{ $json.id }}"
        }
      },
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "LDbxyOy7NMH6fin9",
          "name": "Salesforce account 2"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9f06013f-ff72-4f08-b72e-4b42931f393f",
      "name": "创建或更新账户",
      "type": "n8n-nodes-base.salesforce",
      "position": [
        -928,
        -144
      ],
      "parameters": {
        "name": "={{ $json.message.content.buyer.name }}",
        "resource": "account",
        "operation": "upsert",
        "externalId": "tax_id__c",
        "externalIdValue": "={{ $json.message.content.buyer.tax_id }}",
        "additionalFields": {}
      },
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "LDbxyOy7NMH6fin9",
          "name": "Salesforce account 2"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "70336cbd-460f-4396-964c-196baf054d8f",
      "name": "构建SOQL",
      "type": "n8n-nodes-base.code",
      "position": [
        -352,
        -144
      ],
      "parameters": {
        "jsCode": "//const { pricebook2Id, lines } = $json;\n\nconst pricebook2Id = '01sxxxxxxxxxxxxxxx'; // replace your pricebookID\nconst lines = $('Message a model').first().json.message.content.products; \n\nconst codes = [\n  ...new Set(\n    (lines || [])\n      .map((l) => String(l.name || \"\").trim())\n      .filter(Boolean)\n  ),\n];\n\nif (codes.length === 0) {\n  throw new Error(\"No product codes found in lines[].name\");\n}\n\n// Escape single quotes for SOQL\nconst soqlList = codes\n  .map((c) => `'${c.replace(/'/g, \"\\\\'\")}'`)\n  .join(\",\");\n\nconst soql = `\n  SELECT Id, Product2Id, Product2.ProductCode\n  FROM PricebookEntry\n  WHERE Pricebook2Id = '${pricebook2Id}'\n    AND Product2.ProductCode IN (${soqlList})\n`.trim();\n\nreturn [{ soql, codes }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "021cd4c3-82b4-4589-a523-bea9bd18ae11",
      "name": "查询价格手册条目",
      "type": "n8n-nodes-base.salesforce",
      "position": [
        -64,
        -144
      ],
      "parameters": {
        "query": "={{ $json.soql }}",
        "resource": "search"
      },
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "LDbxyOy7NMH6fin9",
          "name": "Salesforce account 2"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "cafe3e46-af7a-44eb-87b2-9fb34f7cb96b",
      "name": "JavaScript 代码",
      "type": "n8n-nodes-base.code",
      "position": [
        240,
        -144
      ],
      "parameters": {
        "jsCode": "// Input assumptions (no Set node):\n// - This Function runs after your SOQL HTTP node.\n// - The current item ($json) contains:\n//   • records: array of PricebookEntry rows (from SOQL)\n//   • opportunityId: the target Opportunity Id\n//   • lines: your source line array (with name, quantity, unit_price, discount_amount, ...)\n\nconst oppId = $('Create an opportunity').first().json.id;\nif (!oppId) {\n  throw new Error(\"Missing opportunityId in input.\");\n}\n\nconst lines = Array.isArray($('Message a model').first().json.message.content.products) ? $('Message a model').first().json.message.content.products : [];\nif (lines.length === 0) {\n  throw new Error(\"Missing or empty lines array in input.\");\n}\n\nconst pbeRecords = Array.isArray($input.all()) ? $input.all() : [];\n\nif (pbeRecords.length === 0) {\n  throw new Error(\"Missing SOQL result records in input (records[]).\");\n}\n\n\n\n// Build ProductCode → PricebookEntryId map\nconst pbeByCode = new Map();\nfor (const r of pbeRecords) {\n  const code = r?.json.Product2?.ProductCode;\n  const id = r?.json.Id;\n  if (code && id) pbeByCode.set(String(code).trim(), id);\n}\nconsole.log(pbeByCode)\n// Build OpportunityLineItem payloads\nconst records = [];\nfor (const line of lines) {\n  const productCode = String(line.name || \"\").trim();\n  const pbeId = pbeByCode.get(productCode);\n\n  if (!pbeId) {\n    throw new Error(`No PricebookEntry found for ProductCode '${productCode}'.`);\n  }\n\n  const qty = Number(line.quantity ?? 0) || 0;\n  const unitPrice = Number(line.unit_price ?? 0) || 0;\n\n  // Convert total discount to per-unit Discount field\n  const totalDiscount = Number(line.discount_amount ?? 0) || 0;\n  const perUnitDiscount = qty > 0 ? totalDiscount / qty : 0;\n\n  records.push({\n    attributes: { type: \"OpportunityLineItem\" },\n    OpportunityId: oppId,\n    PricebookEntryId: pbeId,\n    Quantity: qty,\n    UnitPrice: unitPrice,\n  });\n}\n\nreturn [{ body: { allOrNone: false, records } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "22eb1f99-b562-4160-bf89-6b0dd890a308",
      "name": "创建商机产品线",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        576,
        -144
      ],
      "parameters": {
        "url": "=https://mydomain.salesforce.com/services/data/v65.0/composite/sobjects",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.body }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "salesforceOAuth2Api"
      },
      "credentials": {
        "salesforceOAuth2Api": {
          "id": "LDbxyOy7NMH6fin9",
          "name": "Salesforce account 2"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "39ef7a5e-6825-4583-b9d2-dbc534bb7c5c",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2048,
        -448
      ],
      "parameters": {
        "width": 256,
        "height": 272,
        "content": "## 1) Google Drive触发器"
      },
      "typeVersion": 1
    },
    {
      "id": "99e69d7e-2ad1-40ef-b476-5ae869e5f927",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1776,
        -448
      ],
      "parameters": {
        "height": 272,
        "content": "## 2) 从Drive下载"
      },
      "typeVersion": 1
    },
    {
      "id": "0830b83e-19f5-489e-a9bd-1eb3e1f7e50b",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1520,
        -448
      ],
      "parameters": {
        "width": 224,
        "height": 272,
        "content": "## 3) 从文件提取"
      },
      "typeVersion": 1
    },
    {
      "id": "edb057fe-1446-4aec-929b-7022c9704ee1",
      "name": "便签说明4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1280,
        -512
      ],
      "parameters": {
        "width": 256,
        "height": 352,
        "content": "## 4) 消息模型(JSON)"
      },
      "typeVersion": 1
    },
    {
      "id": "4f193f7a-2f69-4eb3-9ee6-e15bd50236d3",
      "name": "便签说明5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1008,
        -512
      ],
      "parameters": {
        "height": 352,
        "content": "## 5) Salesforce:更新账户(买家)"
      },
      "typeVersion": 1
    },
    {
      "id": "12811762-358e-4cb7-aec6-92f9f93d47ae",
      "name": "便签 6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        -576
      ],
      "parameters": {
        "width": 304,
        "height": 416,
        "content": "## 6) Salesforce:创建商机"
      },
      "typeVersion": 1
    },
    {
      "id": "0c2e625e-0062-480f-b394-45ea687db779",
      "name": "便签 7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -384
      ],
      "parameters": {
        "width": 288,
        "height": 224,
        "content": "## 7) 代码:构建SOQL(价格手册条目)"
      },
      "typeVersion": 1
    },
    {
      "id": "9b390d82-6233-4d6f-b606-64efadddd654",
      "name": "## 为什么选择 4o 模型?👆",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        -352
      ],
      "parameters": {
        "height": 192,
        "content": "## 8) Salesforce:查询价格手册条目"
      },
      "typeVersion": 1
    },
    {
      "id": "b4ca0bf1-925c-40c6-ba35-208e70a63be1",
      "name": "便签 9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        128,
        -528
      ],
      "parameters": {
        "width": 320,
        "height": 368,
        "content": "## 9) 代码:构建商机产品线载荷"
      },
      "typeVersion": 1
    },
    {
      "id": "0d8dcf4b-77d7-439f-b345-f0fcec555b01",
      "name": "便签10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        -464
      ],
      "parameters": {
        "width": 272,
        "height": 272,
        "content": "## 10) HTTP请求:创建商机产品线"
      },
      "typeVersion": 1
    },
    {
      "id": "73d319ea-e10d-4f10-8132-63976a3cfacf",
      "name": "便签11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1488,
        176
      ],
      "parameters": {
        "height": 272,
        "content": "## 11) 上传到OneDrive"
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "Build SOQL": {
      "main": [
        [
          {
            "node": "Query PricebookEntries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Create or update an account",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Create Opportunity Line Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "Download File From Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create an opportunity": {
      "main": [
        [
          {
            "node": "Build SOQL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query PricebookEntries": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download File From Google": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          },
          {
            "node": "Update File to One Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create or update an account": {
      "main": [
        [
          {
            "node": "Create an opportunity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Opportunity Line Items": {
      "main": [
        []
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 发票处理, AI 摘要总结

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量23
分类2
节点类型9
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Le Nguyen

Le Nguyen

@leeseifer

Salesforce Architect with 10+ years of experience in CRM, integrations, and automation. Skilled in Apex, LWC, REST APIs, and full-stack dev (JavaScript, .NET). I build secure, scalable workflows in n8n—connecting Salesforce, Stripe, and more. Passionate about lead scoring, data sync, and secure field masking. Certified Application Architect with deep expertise in platform, integration, and data architecture.

外部链接
在 n8n.io 查看

分享此工作流