n8n 实操攻略 9:搭建发票处理机器人(上)

天天

发布于292天前
龙猫也是猫

这个工作流能干什么?

对于经常报销发票的财务人员来说,录入发票信息很麻烦。

这个工作流能自动识别发票图片、提取关键信息并存入表格,最终完成邮件通知。

  • 传统方式:会计要一张张发票看 → 手动输入发票号、日期、金额 → 再核对税率。
  • 现在流程:AI 自动识别 → JSON 清洗 → 直接入 Google Sheets,省去人工输入。系统自动发送邮件(带清晰排版好的 HTML),确保对应人第一时间收到。
https://appstore.lazycat.cloud/#/shop/detail/cloud.lazycat.app.n8n

实操步骤

打开 n8n,新建一个工作流

image.png

先加好所有节点

第一步,我希望通过聊天触发,所以新建一个 on chat message 节点

image.png

第二步,搜索 OpenAI,

image.png

选择这个分析图片的节点:

image.png

第三步,添加一个 Code,对数据进行清洗

image.png

第四步,存入 Google 表格

image.png

选择增加一行新内容

image.png

第 5 步,提取聊天框中的邮箱,加一个 Code

image.png

第 6 步,增加个 if,判断邮箱是否为空

image.png

第 7 步,发送邮件,搜索 Gmail

image.png

选择 send a message

image.png

以上就是所有的工作流节点

image.png

接下来对每个节点,进行详细的配置说明。

1.chat message

双击节点,添加文件上传的属性

image.png

将开关打开

image.png

这时候就可以测试一下,比如我电脑上有这个发票图片:

1605491102752717.jpg

在聊天框中,输入“发送给 你的邮箱@gmail.com”, 点击回形针 选择你的发票图片

image.png

点击发送,可以看到他的输出结果,名称是data0,我们后续会用到

image.png

2.OpenAI 配置

进入官网 openai.com,选择 API 平台

image.png

在右上角 dashboard 里,进入 apikeys,新建一个 key

image.png

起个名字,比如叫 n8n

image.png

注意,你的账户里要先充钱,不然调不通。或者换其他的 API 平台

比如我发现我 OpenAI 没钱了,临时换了用 Gemini,步骤都是一样的

image.png

选择模型,提示词参考下面的


你是一个专业的“发票信息抽取器”。输入是一段发票文字(可能来自 OCR、邮件正文或聊天粘贴),输出是严格有效的 JSON,适合直接写入数据库/Google Sheets。

## 任务
从给定的发票文本中识别并抽取字段,返回单个 JSON 对象,放在一个标记代码块里(json ... )。不要输出任何解释、注释或多余字符。

## 输出要求

仅输出一个 JSON 代码块,不得包含额外文字。

不存在或不确定的值一律填 null。

日期统一为 YYYY-MM-DD(ISO 8601);

金额/数量等数值字段用 数字类型(而非字符串),保留精度但不要添加货币符号或千分位。

税率使用小数(例如 1% → 0.01),并同时提供显示字段(例如 "1%")。

items 数组必须存在;若无行项目,输出空数组 []。

## 字段定义(Schema)

{
  "invoice": {
    "title": "string|null",
    "number": "string|null",
    "issue_date": "YYYY-MM-DD|null",
    "currency": "string|null",
    "buyer": {
      "name": "string|null",
      "tax_id": "string|null",
      "note": "string|null"
    },
    "seller": {
      "name": "string|null",
      "tax_id": "string|null"
    },
    "items": [
      {
        "name": "string|null",
        "spec": "string|null",
        "unit": "string|null",
        "quantity": "number|null",
        "unit_price": "number|null",
        "amount_without_tax": "number|null",
        "tax_rate": "number|null",
        "tax_rate_display": "string|null",
        "tax_amount": "number|null"
      }
    ],
    "totals": {
      "amount_without_tax": "number|null",
      "tax_total": "number|null",
      "amount_with_tax": "number|null"
    },
    "issuer": "string|null"
  }
}

输入类型选 字节类型,名称是上面的 data0
image.png

执行一下,发现右侧输出结果了

image.png

数据清洗

这一步是要把上一步的结果,更加格式化好看一些

输入代码参考:

// n8n Code 节点 - 发票数据解析
// 获取输入数据(处理可能的字符串格式)
const inputData = $input.first().json.choices[0].message.content;

// 如果输入是字符串,先解析为 JSON
let invoiceData;
if (typeof inputData === 'string') {
  // 移除可能的 markdown 代码块标记
  const cleanData = inputData.replace(/^```json\s*/, '').replace(/\s*```$/, '').trim();
  invoiceData = JSON.parse(cleanData);
} else {
  invoiceData = inputData;
}

// 提取发票数据
const invoice = invoiceData.invoice;

// 返回扁平化的 JSON 数据
return [{
  json: {
    title: invoice.title,
    invoiceNumber: invoice.number,
    issueDate: invoice.issue_date,
    currency: invoice.currency,
    issuer: invoice.issuer,
    buyerName: invoice.buyer.name,
    buyerTaxId: invoice.buyer.tax_id,
    buyerNote: invoice.buyer.note,
    sellerName: invoice.seller.name,
    sellerTaxId: invoice.seller.tax_id,
    itemName: invoice.items[0].name,
    itemSpec: invoice.items[0].spec,
    itemUnit: invoice.items[0].unit,
    quantity: invoice.items[0].quantity,
    unitPrice: invoice.items[0].unit_price,
    amountWithoutTax: invoice.items[0].amount_without_tax,
    taxRate: invoice.items[0].tax_rate,
    taxRateDisplay: invoice.items[0].tax_rate_display,
    taxAmount: invoice.items[0].tax_amount,
    subtotal: invoice.totals.amount_without_tax,
    totalTax: invoice.totals.tax_total,
    grandTotal: invoice.totals.amount_with_tax
  }
}];

执行一下,发现报错了

image.png

不要慌,直接用 ai 帮我们解决,我用 Claude,可以这样提问:
这是我在 n8n 中的输入:复制左侧的代码
这是我希望清洗的代码:复制上面的代码
但是遇到了这样的问题:复制报错信息

Claude 直接给我了修正后的代码:

image.png

// n8n Code 节点 - 发票数据解析

// 获取输入数据
const inputItem = $input.first().json;

// 根据实际的数据结构提取文本内容
let textContent;
if (inputItem.content && inputItem.content.parts && inputItem.content.parts[0]) {
  textContent = inputItem.content.parts[0].text;
} else if (inputItem.choices && inputItem.choices[0]) {
  // 兼容其他可能的数据结构
  textContent = inputItem.choices[0].content?.parts?.[0]?.text || 
                 inputItem.choices[0].message?.content;
} else {
  // 如果都不匹配,尝试直接使用
  textContent = inputItem;
}

// 解析JSON数据
let invoiceData;
if (typeof textContent === 'string') {
  // 移除可能的 markdown 代码块标记
  const cleanData = textContent.replace(/^```json\s*/, '').replace(/\s*```$/, '').trim();
  invoiceData = JSON.parse(cleanData);
} else {
  invoiceData = textContent;
}

// 提取发票数据
const invoice = invoiceData.invoice;

// 处理多个商品项目 - 返回每个商品作为独立的记录
const items = [];
for (let i = 0; i < invoice.items.length; i++) {
  const item = invoice.items[i];
  items.push({
    json: {
      // 发票基本信息
      title: invoice.title,
      invoiceNumber: invoice.number,
      issueDate: invoice.issue_date,
      currency: invoice.currency,
      issuer: invoice.issuer,
      
      // 买方信息
      buyerName: invoice.buyer.name,
      buyerTaxId: invoice.buyer.tax_id,
      buyerNote: invoice.buyer.note,
      
      // 卖方信息
      sellerName: invoice.seller.name,
      sellerTaxId: invoice.seller.tax_id,
      
      // 商品明细
      itemIndex: i + 1,  // 商品序号
      itemName: item.name,
      itemSpec: item.spec,
      itemUnit: item.unit,
      quantity: item.quantity,
      unitPrice: item.unit_price,
      amountWithoutTax: item.amount_without_tax,
      taxRate: item.tax_rate,
      taxRateDisplay: item.tax_rate_display,
      taxAmount: item.tax_amount,
      
      // 发票总计
      subtotal: invoice.totals.amount_without_tax,
      totalTax: invoice.totals.tax_total,
      grandTotal: invoice.totals.amount_with_tax
    }
  });
}

// 如果没有商品项,至少返回发票基本信息
if (items.length === 0) {
  return [{
    json: {
      title: invoice.title,
      invoiceNumber: invoice.number,
      issueDate: invoice.issue_date,
      currency: invoice.currency,
      issuer: invoice.issuer,
      buyerName: invoice.buyer.name,
      buyerTaxId: invoice.buyer.tax_id,
      buyerNote: invoice.buyer.note,
      sellerName: invoice.seller.name,
      sellerTaxId: invoice.seller.tax_id,
      subtotal: invoice.totals.amount_without_tax,
      totalTax: invoice.totals.tax_total,
      grandTotal: invoice.totals.amount_with_tax
    }
  }];
}

return items;

我重新复制上面的代码,执行后,发现结果果然正常了

ps: 记住了吗?以后就这样调试

image.png

google sheet

googel 表格的配置,可以参考我前前面的攻略

我在 google 上新建了一个表格:n8n 发票

image.png

表格的列名参考:

发票标题	发票号码	开票日期	购方名称	购方税号	购方备注	销方名称	销方税号	商品名称	规格型号	单位	数量	单价	不含税金额	税率	税率显示	税额	合计不含税金额	合计税额	合计价税合计金额	开票人

image.png

把左侧对应的字段,都拖动列名对应的位置

image.png

点击执行,可以在右侧看到效果了

image.png

在 Google sheets 里,可以看到数据已经进来了

image.png

下面一步,是要把上面的关键信息,发到我们的邮箱里,这里涉及到一些配置信息,可以参考下篇攻略。

评论

0

暂无评论

说点什么呢~
收藏
1
0
0