🤖 第六章 应用实战:采购订单智能识别与ERP自动化填报助手
📄 一句话概括

这是整条链路上唯一"会改账"的一步。前面全在"看"和"搬",只有这一节真的往系统里写字。所以它的要求跟前面完全反过来了:前面求快,这里求稳——填之前要把"不能填"的两类问题分开,填的时候要按依赖顺序走,填完要回挂影像,而且全程不能越权限。

📝 本节和前面几节的关系

6.4.1 送单、6.4.2 取回判档,本节是 RPA 侧三步里的最后一步——也是 6.2 那张流程图里的第 ⑤ 格(填报)与第 ⑥ 格(回挂)。 与前面几章的同行对照:2.4.3 / 4.4.4 / 5.4.3 也做"最后一步落盘",但它们落的是内容(发一篇笔记、发一篇文章)——发错了删掉重发就行。 本章落的是账——采购订单一经保存,就是一份有法律效力的业务凭证(6.1.2 的合规账说过:原始凭证要存 30 年)。"删掉重发"在这里不是一个可选项。

一、本节的定位:唯一"会改账"的一步

先把这一节的分量摆清楚。

环节 动作 出错的后果
6.4.1 送单 把文件传上去 多花点时间
6.4.2 取回 把结果读回来 数据在本地,不改账
6.4.3 填报 往 ERP 里写字并保存 账上多一笔/少一笔

这就是为什么本节的每一步都要"先检查、后动作"。

RPA 在别人眼里是"快"(几秒填完一张单),但它真正的价值在这儿:它不会累、不会漏、不会因为下午四点半而想赶紧弄完。 一张单填 40 个格子,人填到第 30 个手会抖,机器不会。

⚠️ 有一点机器反而不如人:它不懂"这个数看着不对"

人填单时会本能地"觉得不对"——比如单价从 2.5 跳到 25,人会停下来看一眼。 机器不会。 你让它填 25,它就填 25。 所以"业务校验"必须在机器动手之前做完(6.2 把校验单独立成一格,就是为了这个)——机器没有"常识"这道防线,那就必须用规则补上。

二、ERP 长什么样:三种形态,同一套打法

"ERP"不是一个软件,是一大类。 2026 年企业里最常见的是这几种:

形态 例子 RPA 怎么操作
C/S 客户端(桌面软件) 用友 U8 客户端、SAP GUI 桌面软件自动化(1.5.4 那套:找窗口、找控件)
B/S 网页 金蝶云星空、SAP Fiori、用友 U9/NC 的网页端 网页自动化(1.5.3 那套:找元素、填表单)
混合 客户端里嵌了网页控件 两套都要会用

好消息是:主流 ERP 影刀都已经跑通了——SAP(含 Fiori)、Oracle EBS、用友 U8/U9/NC、金蝶云星空/K3/Wise 都有现成的做法。

📝 ERP 自动化最要紧的一个词:非侵入式

RPA 操作 ERP,靠的是"像人一样点界面"——它不需要 ERP 厂商给你开接口、不需要动数据库、更不需要改源码。 这条性质决定了它的落地方式:不用惊动 IT 部门做系统改造,业务部门自己就能推。 但也带来一条约束:界面一变,脚本就可能失效(按钮换位置、字段改名字)。所以"定期回归测试"是这类项目的固定成本——这不是 RPA 的缺陷,是"模拟人操作"这条路线的固有代价。

三、填之前:把"不能填"分成两组

这是本节的核心动作,也是 Demo 要演的东西。

一条数据填不进 ERP,只有两种原因——而这两种的严重程度差了十万八千里:

3.1 A 组:填不进去的(技术故障)

特征:系统会直接拒绝,你根本保存不了。

这一组的"好"在于:它不会造成损失——系统替你守着门,你连错都犯不了。 改完就能填。

3.2 B 组:填进去会出事的(业务错误)

特征:系统照收,事后才炸。

这一组才是真正危险的:ERP 不会拦它,它会安安静静地存下去——然后在月结、审计、或者税务比对的时候冒出来。

⚠️ 为什么"金额不符"绝对不能猜

"订单合计 1200,识别合计 1356" —— 差 156 元。 最省事的做法是"取一个",但那等于替业务做决定。 正确做法是停下来交人——因为这两个数哪个对,只有原单说得清。 这就是 6.1.1 那条"错误延迟暴露"的威力:金额类的错误,当下不痛、事后很痛。 而 6.3.1 的提示词里那句"禁止虚构、推断、补全",在这里变成了代码里的"不许猜"。

3.3 两组的顺序:先清 A,再看 B

为什么要有顺序? 因为A 组不清,这条单根本进不了系统——你连"要不要放行"这个问题都问不出来。

所以处置顺序是固定的: 1. 先把 A 组清干净(补供应商、补主数据建档); 2. A 组清了,再逐条判 B 组(该审批的送审批,该核对的送人)。

Demo 里的 F02 就是这个顺序的体现:它 A 组未清,干跑时直接"跳过"——根本不会走到"要不要放行"那一步。

四、怎么"点":元素定位的三条路

填报的本质是"找到格子、填进去、点保存"。 难点全在第一步:怎么找到那个格子。

方式 稳定性 说明
抓界面元素(按控件属性/元素属性定位) 最稳,首选 网页找 DOM 元素,桌面找控件句柄——界面挪位置它照样能找到
图像识别(截图找图) 中 元素抓不到时的备用——但分辨率、缩放、主题一变就可能失手
坐标点击(按固定 x/y 点) 最脆弱,尽量别用 窗口一挪、分辨率一改就全乱

判断标准很简单:能抓元素就不要截图,能截图就不要点坐标。

📝 为什么这条顺序是"铁律"

因为RPA 脚本的寿命,取决于它绑定的东西有多稳定。 控件属性(比如"这个输入框的 ID 是 supplier_code")比像素位置稳定得多——前者只在系统改版时变,后者换个显示器就变。 这也是"非侵入式"那条性质的代价在具体环节上的体现:既然靠界面吃饭,那就必须挑界面里最不容易变的东西来抓。

五、填的顺序:字段之间是有依赖的

ERP 的字段不是可以随便乱填的——有些字段填下去会"带动"别的字段。

以采购订单为例,正确的顺序是:

1. 供应商编码   ← 由【供应商名】查主数据得到(必须先查后填)
2. 采购组织     ← 固定值(配置里读)
3. 订单日期     ← 识别结果里的日期,缺省取当天
4. 表体·物料编码 ← 同样是主数据里查出来的
5. 表体·数量 / 单价 / 税率  ← 逐行填
6. 备注         ← 回挂影像(见下一节)

两个必须记住的坑:

📝 为什么"软件里叫"供应商添加",ERP 里叫"表体"

因为ERP 的界面是"单据模型"的投影——表头(供应商、日期、组织)+ 表体(明细行)。这个结构在 6.2 讲"拆表"时就出现过一次(一张采购订单 = 一条主表记录 + 若干条明细记录)。 所以填单的顺序,本质是在按数据模型自顶向下写:先把"这条单是谁的"填对,再把"里面有什么"填进去。

六、别忘了回挂影像

"回挂"指的是:把单据原图的地址写进订单,让这张单和它的来源影像绑在一起。

为什么这一步必须做? 因为6.1.2 的合规账里有两条硬要求:

所以"回挂"不是锦上添花,是留痕。 而2026 年的现实是:数电票覆盖率已超 85%,九部委的无纸化新规 2025 年底试点、2026 年全面推开——"影像与订单必须能对上"这件事,只会越来越硬。

做法:在备注或附件字段里,写进原图的地址/编号(Demo 里的 file_ref)。三年后有人问"这张 250 元的单是从哪来的",你顺着这个引用就能回到那张图。

七、四条合规红线

RPA 填 ERP,最容易出事的地方不是"填错",是"越权"。

⚠️ 2026 现状:超 40% 的企业在 RPA 规模化时撞上审计或合规

四大风险:账号越权、数据泄露(日志里明文记了密码)、审计追踪缺失(分不清是人是机器人操作的)、规则偏离。 对应四条做法:

红线 做法
不给机器人共用人的账号 RPA 用独立账号,权限最小化(只给"采购订单录入",不给"删除""审批")
密码不写进脚本 放凭证保险箱(跟 6.4.1 的 Token 一个道理)
日志不记敏感数据 日志记"做了什么",不记密码、不记完整银行账号
高风险节点留人 金额超限、供应商新建这类必须人工确认(Demo 里的超授权额度就是它)

还有一条组织上的要求:开发、测试、运行应当三方分立(职责分离)——写脚本的人不该同时是往生产系统里跑脚本的人。

这四条看起来是"IT 的事",其实每一任自动化负责人都躲不掉——因为一旦出问题,问的是"谁让机器人干的"。

八、动手 Demo:填报前预检 + 字段映射干跑

这段代码做两件事:把 6 条待填数据分成 A 组(填不进去)与 B 组(填进去会出事),然后在不点保存的前提下,把要填的格子逐个打出来。

# -*- coding: utf-8 -*-
"""6.4.3 Demo:填报前预检 + 字段映射干跑(纯标准库)
把【会填不进去】和【填进去会出事】分开,并在真填之前把映射表跑一遍。
"""

AUTH_LIMIT = 50000.0     # 本账号单张采购订单授权额度上限(元)
TAX_OK = {"0", "1", "3", "6", "9", "13"}

# 待填:单号 / 供应商 / 主数据 / 物料 / 数量 / 单价 / 税率 / 订单合计 / 识别合计
ROWS = [
    {"id": "F01", "sup": "宜昌三峡五金", "master": True,  "item": "M1001", "qty": 100,
     "price": 2.5,  "tax": "13", "order_total": 250.0,   "recog_total": 250.0},
    {"id": "F02", "sup": "",             "master": False, "item": "M2001", "qty": 20,
     "price": 45.0, "tax": "13", "order_total": 900.0,   "recog_total": 900.0},
    {"id": "F03", "sup": "湖北兴发物资", "master": True,  "item": "M2999", "qty": 20,
     "price": 45.0, "tax": "13", "order_total": 900.0,   "recog_total": 900.0},
    {"id": "F04", "sup": "宜昌恒达机电", "master": True,  "item": "M5001", "qty": 15,
     "price": 9.5,  "tax": "14", "order_total": 142.5,   "recog_total": 142.5},
    {"id": "F05", "sup": "宜昌长江轴承", "master": True,  "item": "M3001", "qty": 10,
     "price": 120.0, "tax": "13", "order_total": 1200.0, "recog_total": 1356.0},
    {"id": "F06", "sup": "宜昌恒达机电", "master": True,  "item": "M6001", "qty": 5000,
     "price": 30.0, "tax": "13", "order_total": 150000.0, "recog_total": 150000.0},
]

def inspect(r):
    A, B = [], []
    if not r["sup"]:
        A.append("供应商为空 —— 必填项,单子根本建不起来")
    if not r["master"]:
        A.append("供应商/物料未在主数据中命中 —— 系统会拒绝引用不存在的编码")
    if r["tax"] not in TAX_OK:
        B.append("税率 %s 不在法定集合内 —— 越界税率属于合规问题" % r["tax"])
    if abs(r["order_total"] - r["recog_total"]) > 0.01:
        B.append("订单合计 %.2f 与识别合计 %.2f 不符 —— 金额不一致不允许猜" % (
            r["order_total"], r["recog_total"]))
    if r["order_total"] > AUTH_LIMIT:
        B.append("金额 %.2f 超过本账号授权额度 %.0f —— 必须走人工审批" % (
            r["order_total"], AUTH_LIMIT))
    return A, B

print("=" * 78)
print("一、A 组:填不进去的(技术故障,改完就能填)")
print("=" * 78)
a_total = 0
for r in ROWS:
    A, B = inspect(r)
    a_total += len(A)
    if A:
        print()
        print("  [%s]" % r["id"])
        for x in A:
            print("        ✗ %s" % x)
if not a_total:
    print("  (无)")

print()
print("=" * 78)
print("二、B 组:填进去会出事的(按后果从重到轻排)")
print("=" * 78)
ORDER = ["超授权额度", "金额不符", "税率越界"]
b_all = []
for r in ROWS:
    A, B = inspect(r)
    for x in B:
        key = "超授权额度" if "授权额度" in x else ("金额不符" if "金额不一致" in x else "税率越界")
        b_all.append((key, r["id"], x))
b_all.sort(key=lambda t: ORDER.index(t[0]))
for key, rid, msg in b_all:
    print("  [%s] %-8s %s" % (rid, key, msg))
if not b_all:
    print("  (无)")

print()
print("=" * 78)
print("三、字段映射干跑(不点保存,只把要填的格子打出来)")
print("=" * 78)
MAP = [
    ("供应商编码", "master.supplier_code(由供应商名查主数据得到)"),
    ("采购组织",   "config.org_code(固定)"),
    ("订单日期",   "recog.date(缺省取当天)"),
    ("表体·物料编码", "lines[].item_code"),
    ("表体·数量",    "lines[].qty"),
    ("表体·含税单价", "lines[].price"),
    ("表体·税率",    "lines[].tax"),
    ("备注",       "附件引用 file_ref(单据原图地址)"),
]
print("  %-14s ← %s" % ("ERP 字段", "来源"))
for k, v in MAP:
    print("  %-14s ← %s" % (k, v))

print()
print("  干跑逐个打一遍:")
for r in ROWS:
    if inspect(r)[0]:
        print("  [%s] 跳过(A 组未清)" % r["id"])
        continue
    print("  [%s] 供应商=%s|物料=%s|数量=%s|单价=%s|税率=%s|备注=file_ref(%s)" % (
        r["id"], r["sup"], r["item"], r["qty"], r["price"], r["tax"], r["id"]))

print()
print("=" * 78)
print("四、汇总与放行")
print("=" * 78)
clear = [r["id"] for r in ROWS if not any(inspect(r))]
print("  A 组 %d 条(不改就填不进去);B 组 %d 条(按后果从重到轻修)。" % (a_total, len(b_all)))
print("  两组全清的:%s" % ("、".join(clear) if clear else "(无)"))
print()
print("  注意 F02 与 F04 的区别:")
print("  F02 是【系统不让你填】—— 供应商为空、主数据没命中,界面会直接拒绝保存。")
print("  F04 是【系统让你填、但你不该填】—— 税率 14 界面照收,事后才出问题。")
print("  预检的价值,就是把这两种【不能填】在点保存之前分清楚。")

实际运行输出:

==============================================================================
一、A 组:填不进去的(技术故障,改完就能填)
==============================================================================

  [F02]
        ✗ 供应商为空 —— 必填项,单子根本建不起来
        ✗ 供应商/物料未在主数据中命中 —— 系统会拒绝引用不存在的编码

==============================================================================
二、B 组:填进去会出事的(按后果从重到轻排)
==============================================================================
  [F06] 超授权额度    金额 150000.00 超过本账号授权额度 50000 —— 必须走人工审批
  [F05] 金额不符     订单合计 1200.00 与识别合计 1356.00 不符 —— 金额不一致不允许猜
  [F04] 税率越界     税率 14 不在法定集合内 —— 越界税率属于合规问题

==============================================================================
三、字段映射干跑(不点保存,只把要填的格子打出来)
==============================================================================
  ERP 字段         ← 来源
  供应商编码          ← master.supplier_code(由供应商名查主数据得到)
  采购组织           ← config.org_code(固定)
  订单日期           ← recog.date(缺省取当天)
  表体·物料编码        ← lines[].item_code
  表体·数量          ← lines[].qty
  表体·含税单价        ← lines[].price
  表体·税率          ← lines[].tax
  备注             ← 附件引用 file_ref(单据原图地址)

  干跑逐个打一遍:
  [F01] 供应商=宜昌三峡五金|物料=M1001|数量=100|单价=2.5|税率=13|备注=file_ref(F01)
  [F02] 跳过(A 组未清)
  [F03] 供应商=湖北兴发物资|物料=M2999|数量=20|单价=45.0|税率=13|备注=file_ref(F03)
  [F04] 供应商=宜昌恒达机电|物料=M5001|数量=15|单价=9.5|税率=14|备注=file_ref(F04)
  [F05] 供应商=宜昌长江轴承|物料=M3001|数量=10|单价=120.0|税率=13|备注=file_ref(F05)
  [F06] 供应商=宜昌恒达机电|物料=M6001|数量=5000|单价=30.0|税率=13|备注=file_ref(F06)

==============================================================================
四、汇总与放行
==============================================================================
  A 组 2 条(不改就填不进去);B 组 3 条(按后果从重到轻修)。
  两组全清的:F01、F03

  注意 F02 与 F04 的区别:
  F02 是【系统不让你填】—— 供应商为空、主数据没命中,界面会直接拒绝保存。
  F04 是【系统让你填、但你不该填】—— 税率 14 界面照收,事后才出问题。
  预检的价值,就是把这两种【不能填】在点保存之前分清楚。

读结果,五个值得注意的地方:

  1. 6 条里只有 2 条(F01、F03)能直接填。 同样是故意加浓的教学样本——真实场景里大部分单是能直接过的。这里把 4 条做成问题件,是为了让你一次看清所有拦点。

  2. F02 和 F04 的对比是全场最该记住的一条。 F02 系统不让你填(界面直接拒绝保存),F04 系统让你填、但你不该填(税率 14 界面照收)。 第二种才是真正危险的——因为它不报错。 这就是"预检"存在的全部理由:在点保存之前,把"系统会拦的"和"系统不拦但会出事的"分清楚。

  3. F03 暴露了 F02 之外的另一类 A 组问题:供应商和物料都在,主数据却没命中。 注意它的干跑输出照常打出来了——因为 F03 的 master 字段在这个 Demo 里是 True。 这提醒一件事:Demo 的 master 是手写的(模拟 6.4.2 主数据对照的结果)。真机上这个值必须来自"拿单上的名字去 ERP 里查一遍"——而查不到就是 A 组。查这一步不能省,省了就变成"填进去被系统拒绝"(那样更糟:你已经填了 40 个格子,最后点保存失败)。

  4. B 组按"后果从重到轻"排序,这个顺序是有讲究的。 超授权额度 在最前,因为它涉及内控(金额权限是审计最爱查的地方);金额不符 其次(钱算错了);税率越界 最后(合规问题,但金额小)。 排序的意义在于"人看表的顺序"——异常表是给人看的,最重要的排最上面。 这也是 6.3.2 那张异常表的设计原则。

  5. "干跑"这三个字值得单独说。 这一段把要填的每个格子都算出来了,但一个都没点。 这是"填报"这类操作最该有的一道工序——先干跑,再真跑。 干跑能发现的问题:映射写错了(把单价映到数量)、字段取不到值、拼接格式不对(比如日期格式 ERP 不认)。这些错误在干跑里是"一行输出",在真机上就是"填错了 40 个格子"。

30 秒实验

把 Demo 里的 AUTH_LIMIT 从 50000.0 改成 200000.0,再跑一次。

会发生什么:

结论:同一条数据,会不会出事,取决于"你的权限线画在哪"。

顺着这个思路想一层:那这条线该画在哪?它不是技术参数,是内控参数——是公司"谁能批多少钱"的制度在代码里的投影。 所以这条线绝不能由写脚本的人随手定。 它必须来自公司的授权制度——而且要定期跟人对一遍(人升职了、制度改版了,代码里还是老数)。

再想一层:F06 撤了之后,它去哪了?它没消失,只是"由机器填"变成了"由人审批"。 这就是"分档"的本质:不是"通过/不通过",是"谁来做这个决定"。 6.4.2 的三档(通过/待人/重试)和这里的 A/B 两组,本质都是同一件事:把该人管的交回给人。

常见误区

  1. 【A 组和 B 组都是"有问题的单",一起处理就行】——不行,两类要分开。 A 组系统会拒绝(改完就能填),B 组系统照收(事后才炸)。混在一起最大的问题是处置顺序乱了:A 组没清,你连"要不要放行"都问不出来(F02 干跑跳过就是这个道理)。

  2. 【金额不一致就取一个】——绝对不行。 "订单合计 1200、识别合计 1356"差 156 元,取哪个都是替业务做决定。正确做法是停下来交人——因为这两个数哪个对,只有原单说得清。 6.3.1 的提示词里那句"禁止虚构、推断、补全",在代码里就是这条规则。

  3. 【ERP 自动化要找 IT 开接口】——不用,这是 RPA 的优势。 RPA 靠"像人一样点界面"——不需要厂商开接口、不动数据库、不改源码。代价是界面一变脚本就可能失效,所以"定期回归测试"是这类项目的固定成本,不是意外。

  4. 【元素定位用坐标最省事】——坐标最脆弱。 窗口一挪、分辨率一改就全乱。 记住顺序:能抓元素就不要截图,能截图就不要点坐标。 RPA 脚本的寿命,取决于它绑定的东西有多稳定。

  5. 【RPA 用人的账号最方便】——这是审计上最忌讳的一条。 分不清"是张三填的还是机器人填的" —— 一出问题就查不清。 正确做法:独立账号、最小权限、密码放凭证保险箱、日志不记敏感数据。 2026 年超 40% 的企业在 RPA 规模化时撞上审计或合规阻碍,大多就栽在这几条上。

自测三问(点开对答案) 1. **为什么"填不进去的"反而不危险,"填进去会出事的"才危险?** 因为**前者有系统兜着门**:必填项为空、引用了不存在的编码——**ERP 会直接拒绝保存,你连错都犯不了。** 而后者**系统照收不误**:超授权额度的单、税率填 14% 的单,**界面一句提醒都没有,安安静静就存下去了**——**然后在月结、审计或税务比对的时候冒出来。** **这正是 6.1.1 那条"错误延迟暴露"**:**当下不痛、事后很痛。** 所以预检的价值,**就是把这两类"不能填"在点保存之前分清楚**——**"不能填"有两种,处置方式完全不同。** 2. **为什么"干跑"这道工序不能省?** 因为**真机上一次填错就是 40 个格子**,而**干跑里它只是一行输出。** 干跑能提前发现的:**映射写错**(把单价映到数量)、**字段取不到值**、**格式不对**(日期格式 ERP 不认)、**A 组未清就往下走**。 **它的原理和 6.3.1 的"字段契约"是一回事**:**先在纸上把要写的东西写清楚,再动手。** **这类"先干跑后真跑"的工序,在所有"会改数据"的场景里都该有**——它把"错误成本"从"改账"降到了"看一行日志"。 3. **为什么 RPA 必须用独立账号?** 因为**审计要能分清"这件事是谁干的"**。 如果机器人共用人的账号,**日志里所有操作都挂在那个人名下**——**出了问题(比如某张单被改了金额),你分不清是张三改的还是脚本改的。** 2026 年**超 40% 的企业在 RPA 规模化时遇到审计或合规阻碍**,**"审计追踪缺失"就是四大风险之一。** 正确做法:**独立账号 + 最小权限(只给"采购订单录入")+ 密码放凭证保险箱 + 日志不记敏感数据 + 高风险节点留人工**。 还有一条组织要求:**开发、测试、运行三方分立**——**写脚本的人不该同时是往生产系统里跑脚本的人。**

相关笔记