这是整条链路上唯一"会改账"的一步。前面全在"看"和"搬",只有这一节真的往系统里写字。所以它的要求跟前面完全反过来了:前面求快,这里求稳——填之前要把"不能填"的两类问题分开,填的时候要按依赖顺序走,填完要回挂影像,而且全程不能越权限。
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 都有现成的做法。
RPA 操作 ERP,靠的是"像人一样点界面"——它不需要 ERP 厂商给你开接口、不需要动数据库、更不需要改源码。 这条性质决定了它的落地方式:不用惊动 IT 部门做系统改造,业务部门自己就能推。 但也带来一条约束:界面一变,脚本就可能失效(按钮换位置、字段改名字)。所以"定期回归测试"是这类项目的固定成本——这不是 RPA 的缺陷,是"模拟人操作"这条路线的固有代价。
三、填之前:把"不能填"分成两组
这是本节的核心动作,也是 Demo 要演的东西。
一条数据填不进 ERP,只有两种原因——而这两种的严重程度差了十万八千里:
3.1 A 组:填不进去的(技术故障)
特征:系统会直接拒绝,你根本保存不了。
- 必填项为空(供应商没识别出来);
- 引用了不存在的编码(主数据里没这个物料/供应商)。
这一组的"好"在于:它不会造成损失——系统替你守着门,你连错都犯不了。 改完就能填。
3.2 B 组:填进去会出事的(业务错误)
特征:系统照收,事后才炸。
- 金额超授权额度(本账号只能批 5 万,这张单 15 万)——必须走人工审批;
- 金额前后不符(订单合计 1200,识别出的合计 1356)——不一致就不许猜;
- 税率越界(填了 14%)——不在法定集合里,属于合规问题。
这一组才是真正危险的: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 要的是编码。这一步要查主数据——查不到就是 6.4.2 那个"待人"档;
- 表体行要按原单顺序:行号错乱会让审批的人看不明白(第 1 行和第 3 行对调,人核对时就得多花一倍时间)。
因为ERP 的界面是"单据模型"的投影——表头(供应商、日期、组织)+ 表体(明细行)。这个结构在 6.2 讲"拆表"时就出现过一次(一张采购订单 = 一条主表记录 + 若干条明细记录)。 所以填单的顺序,本质是在按数据模型自顶向下写:先把"这条单是谁的"填对,再把"里面有什么"填进去。
六、别忘了回挂影像
"回挂"指的是:把单据原图的地址写进订单,让这张单和它的来源影像绑在一起。
为什么这一步必须做? 因为6.1.2 的合规账里有两条硬要求:
- 原始凭证保管 30 年(财政部令第 79 号),而且保管期限由内容类型决定、不由载体决定——"我存了电子版"不等于"我可以不存";
- 2020 财会 6 号文:用电子凭证打印件入账的,必须同时保存电子会计凭证——也就是"纸质/打印件"和"电子原件"要一起在。
所以"回挂"不是锦上添花,是留痕。 而2026 年的现实是:数电票覆盖率已超 85%,九部委的无纸化新规 2025 年底试点、2026 年全面推开——"影像与订单必须能对上"这件事,只会越来越硬。
做法:在备注或附件字段里,写进原图的地址/编号(Demo 里的 file_ref)。三年后有人问"这张 250 元的单是从哪来的",你顺着这个引用就能回到那张图。
七、四条合规红线
RPA 填 ERP,最容易出事的地方不是"填错",是"越权"。
四大风险:账号越权、数据泄露(日志里明文记了密码)、审计追踪缺失(分不清是人是机器人操作的)、规则偏离。 对应四条做法:
| 红线 | 做法 |
|---|---|
| 不给机器人共用人的账号 | 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 界面照收,事后才出问题。
预检的价值,就是把这两种【不能填】在点保存之前分清楚。
读结果,五个值得注意的地方:
-
6 条里只有 2 条(
F01、F03)能直接填。 同样是故意加浓的教学样本——真实场景里大部分单是能直接过的。这里把 4 条做成问题件,是为了让你一次看清所有拦点。 -
F02和F04的对比是全场最该记住的一条。F02系统不让你填(界面直接拒绝保存),F04系统让你填、但你不该填(税率 14 界面照收)。 第二种才是真正危险的——因为它不报错。 这就是"预检"存在的全部理由:在点保存之前,把"系统会拦的"和"系统不拦但会出事的"分清楚。 -
F03暴露了F02之外的另一类 A 组问题:供应商和物料都在,主数据却没命中。 注意它的干跑输出照常打出来了——因为F03的master字段在这个 Demo 里是True。 这提醒一件事:Demo 的master是手写的(模拟 6.4.2 主数据对照的结果)。真机上这个值必须来自"拿单上的名字去 ERP 里查一遍"——而查不到就是 A 组。查这一步不能省,省了就变成"填进去被系统拒绝"(那样更糟:你已经填了 40 个格子,最后点保存失败)。 -
B 组按"后果从重到轻"排序,这个顺序是有讲究的。
超授权额度在最前,因为它涉及内控(金额权限是审计最爱查的地方);金额不符其次(钱算错了);税率越界最后(合规问题,但金额小)。 排序的意义在于"人看表的顺序"——异常表是给人看的,最重要的排最上面。 这也是 6.3.2 那张异常表的设计原则。 -
"干跑"这三个字值得单独说。 这一段把要填的每个格子都算出来了,但一个都没点。 这是"填报"这类操作最该有的一道工序——先干跑,再真跑。 干跑能发现的问题:映射写错了(把单价映到数量)、字段取不到值、拼接格式不对(比如日期格式 ERP 不认)。这些错误在干跑里是"一行输出",在真机上就是"填错了 40 个格子"。
30 秒实验
把 Demo 里的 AUTH_LIMIT 从 50000.0 改成 200000.0,再跑一次。
会发生什么:
F06从 B 组消失(15 万没超 20 万的线);- "两组全清的"从
F01、F03变成F01、F03、F06; - B 组从 3 条变 2 条。
结论:同一条数据,会不会出事,取决于"你的权限线画在哪"。
顺着这个思路想一层:那这条线该画在哪?它不是技术参数,是内控参数——是公司"谁能批多少钱"的制度在代码里的投影。 所以这条线绝不能由写脚本的人随手定。 它必须来自公司的授权制度——而且要定期跟人对一遍(人升职了、制度改版了,代码里还是老数)。
再想一层:F06 撤了之后,它去哪了?它没消失,只是"由机器填"变成了"由人审批"。
这就是"分档"的本质:不是"通过/不通过",是"谁来做这个决定"。 6.4.2 的三档(通过/待人/重试)和这里的 A/B 两组,本质都是同一件事:把该人管的交回给人。
常见误区
-
【A 组和 B 组都是"有问题的单",一起处理就行】——不行,两类要分开。 A 组系统会拒绝(改完就能填),B 组系统照收(事后才炸)。混在一起最大的问题是处置顺序乱了:A 组没清,你连"要不要放行"都问不出来(
F02干跑跳过就是这个道理)。 -
【金额不一致就取一个】——绝对不行。 "订单合计 1200、识别合计 1356"差 156 元,取哪个都是替业务做决定。正确做法是停下来交人——因为这两个数哪个对,只有原单说得清。 6.3.1 的提示词里那句"禁止虚构、推断、补全",在代码里就是这条规则。
-
【ERP 自动化要找 IT 开接口】——不用,这是 RPA 的优势。 RPA 靠"像人一样点界面"——不需要厂商开接口、不动数据库、不改源码。代价是界面一变脚本就可能失效,所以"定期回归测试"是这类项目的固定成本,不是意外。
-
【元素定位用坐标最省事】——坐标最脆弱。 窗口一挪、分辨率一改就全乱。 记住顺序:能抓元素就不要截图,能截图就不要点坐标。 RPA 脚本的寿命,取决于它绑定的东西有多稳定。
-
【RPA 用人的账号最方便】——这是审计上最忌讳的一条。 分不清"是张三填的还是机器人填的" —— 一出问题就查不清。 正确做法:独立账号、最小权限、密码放凭证保险箱、日志不记敏感数据。 2026 年超 40% 的企业在 RPA 规模化时撞上审计或合规阻碍,大多就栽在这几条上。
自测三问(点开对答案)
1. **为什么"填不进去的"反而不危险,"填进去会出事的"才危险?** 因为**前者有系统兜着门**:必填项为空、引用了不存在的编码——**ERP 会直接拒绝保存,你连错都犯不了。** 而后者**系统照收不误**:超授权额度的单、税率填 14% 的单,**界面一句提醒都没有,安安静静就存下去了**——**然后在月结、审计或税务比对的时候冒出来。** **这正是 6.1.1 那条"错误延迟暴露"**:**当下不痛、事后很痛。** 所以预检的价值,**就是把这两类"不能填"在点保存之前分清楚**——**"不能填"有两种,处置方式完全不同。** 2. **为什么"干跑"这道工序不能省?** 因为**真机上一次填错就是 40 个格子**,而**干跑里它只是一行输出。** 干跑能提前发现的:**映射写错**(把单价映到数量)、**字段取不到值**、**格式不对**(日期格式 ERP 不认)、**A 组未清就往下走**。 **它的原理和 6.3.1 的"字段契约"是一回事**:**先在纸上把要写的东西写清楚,再动手。** **这类"先干跑后真跑"的工序,在所有"会改数据"的场景里都该有**——它把"错误成本"从"改账"降到了"看一行日志"。 3. **为什么 RPA 必须用独立账号?** 因为**审计要能分清"这件事是谁干的"**。 如果机器人共用人的账号,**日志里所有操作都挂在那个人名下**——**出了问题(比如某张单被改了金额),你分不清是张三改的还是脚本改的。** 2026 年**超 40% 的企业在 RPA 规模化时遇到审计或合规阻碍**,**"审计追踪缺失"就是四大风险之一。** 正确做法:**独立账号 + 最小权限(只给"采购订单录入")+ 密码放凭证保险箱 + 日志不记敏感数据 + 高风险节点留人工**。 还有一条组织要求:**开发、测试、运行三方分立**——**写脚本的人不该同时是往生产系统里跑脚本的人。**- ☐ 下一节(6.5 本章小结)
相关笔记
- RPA获取扣子识别结果 —— 上一步判出来的"通过"档,就是本节要填的那批
- 采购订单智能填报助手整体流程设计 —— 那张七节点流程图,本节落地的是第 ⑤⑥ 格
- 桌面软件自动化 —— C/S 形态 ERP 要用的那一套;网页端则用 1.5.3
- RPA在知乎上发布 —— 同是"最后一步落盘",但本章落的是账,不是内容
- 🤖 Agent+RPA实战 · 索引 —— 全课路线图