☰
金蝶云星辰 OpenAPI 实战:销售退货单闭环核对,自动揪出“超退“和“无源退货“
2026/10/14 14:01:31 网站建设 项目流程

一、背景:退货单据没闭环,账上永远对不平
贸易和批发企业里,退货是日常业务。麻烦的不是退货本身,而是退货单和原始出货单之间没有闭环:有人是超退了(退回来的比当初卖出去的还多),有人是无源退货(找不到对应的出货单,可能是手工开的)。
这两种情况在系统里单看退货单是看不出来的,得拿退货单跟出货单去比对。财务一般怎么做?导两张 Excel,用 VLOOKUP 拉一遍。几千行就是半天,而且每次口径还不一定一致。
我们给客户写的方案是一个核对脚本:把已审核的销售出库单和销售退货单都拉下来,按「客户 + 物料」聚合,算出两组差异——超退和无源退货,直接打印出来给人去查。
二、整体流程
定时/手动触发
│
├─ 1. 取 access_token(本地缓存)
├─ 2. 分页拉已审核销售出库单(客户/物料/数量)
├─ 3. 分页拉已审核销售退货单(同结构)
├─ 4. 按 (客户, 物料) 分别累计数量
├─ 5. 比对:退货 > 出货 → 超退;出货为 0 → 无源退货
└─ 6. 按超出金额/数量降序输出 Top N
接口路径、FormId 和字段名会随版本与授权范围有差异,本文代码里的值请以你所在版本的开放平台文档为准,思路是通用的。
三、代码(可复制)

-- coding: utf-8 --

“”“金蝶云星辰 OpenAPI:销售退货闭环核对
找出两类异常:①超退(退货数量 > 出货数量)②无源退货(找不到对应出货单)
依赖:pip install requests
“””
import collections
import time
import requests

BASE = “https://你的开放平台网关” # 以所在版本文档为准
APP_KEY = “your_app_key”
APP_SECRET = “your_app_secret”
OUT_FORM = “SAL_OUTSTOCK” # 销售出库单,以文档为准
RTN_FORM = “SAL_RETURNSTOCK” # 销售退货单,以文档为准
FIELDS = (“FBillNo,FCustId.FName,FDate,”
“FEntity_FMaterialId.FNumber,FEntity_FQty”)
EPS = 1e-6 # 浮点容差

_tok = {“v”: None, “ts”: 0}

def token():
“”“token 本地缓存,避免每次分页都取”“”
if _tok[“v”] and time.time() - _tok[“ts”] < 3000:
return _tok[“v”]
r = requests.post(BASE + “/oauth2/token”, data={
“grant_type”: “client_credentials”,
“client_id”: APP_KEY,
“client_secret”: APP_SECRET,
}, timeout=20)
r.raise_for_status()
_tok[“v”] = r.json()[“access_token”]
_tok[“ts”] = time.time()
return _tok[“v”]

def fetch_all(form):
“”“分页拉全量,只取已审核的单据”“”
out, start, limit = [], 0, 500
while True:
body = {
“FormId”: form,
“FieldKeys”: FIELDS,
“FilterString”: “FDocumentStatus=‘C’”, # C = 已审核
“OrderString”: “FDate ASC”,
“StartRow”: start,
“Limit”: limit,
}
r = requests.post(BASE + “/api/query”,
headers={“Authorization”: "Bearer " + token()},
json=body, timeout=60)
r.raise_for_status()
rows = r.json()
if not rows:
break
out.extend(rows)
if len(rows) < limit: # 关键:不足一页就结束
break
start += limit
time.sleep(0.3) # 别把接口打爆
return out

def index_by_cust_mat(rows):
“”“按 (客户, 物料) 累计数量。同一组合可能有多行,必须累加”“”
acc = collections.defaultdict(float)
for x in rows:
cust = (x[1] or “”).strip()
mat = (x[3] or “”).strip().upper()
acc[(cust, mat)] += float(x[4] or 0)
return acc

def check():
o_acc = index_by_cust_mat(fetch_all(OUT_FORM))
r_acc = index_by_cust_mat(fetch_all(RTN_FORM))

over, nosrc = [], [] for key, rq in r_acc.items(): oq = o_acc.get(key, 0.0) if oq <= EPS: nosrc.append((key[0], key[1], rq)) elif rq - oq > EPS: # 浮点比对留容差 over.append((key[0], key[1], oq, rq, rq - oq)) print("出货组合 %d 组,退货组合 %d 组" % (len(o_acc), len(r_acc))) print("\n【超退】%d 条" % len(over)) for c, m, oq, rq, d in sorted(over, key=lambda t: -t[4])[:20]: print(" %s / %s:出货 %.4f,退货 %.4f,超出 %.4f" % (c, m, oq, rq, d)) print("\n【无源退货】%d 条" % len(nosrc)) for c, m, rq in sorted(nosrc, key=lambda t: -t[2])[:20]: print(" %s / %s:退货 %.4f" % (c, m, rq)) return over, nosrc

ifname== “main”:
check()
四、字段表
FieldKeys
含义
要点
FBillNo
单据编号
定位单据,异常时给出源单号
FCustId.FName
客户名称
匹配键之一(单头字段)
FDate
单据日期
排序、按期间筛选
FEntity_FMaterialId.FNumber
物料编码
匹配键之一,明细字段需带 FEntity_ 前缀
FEntity_FQty
明细数量
累计用,不能用单头数量代替
FDocumentStatus
单据状态
过滤条件,C 为已审核
写 FieldKeys 时最容易踩的坑就是前缀:单头字段直接写(如 FCustId.FName),单据体字段要带 FEntity_。混用会直接报字段无效。
五、报错处理表
现象
常见原因
处理
401 / token invalid
密钥错、token 过期
检查 app_secret;token 做缓存但别太久
返回空数组
FilterString 写错、状态值不对
先去掉过滤条件跑一遍,再逐个加回
字段报无效
明细字段漏了 FEntity_ 前缀
用字段查询接口确认本版本可用字段
数量总和与单头对不上
用单头数量代替了明细累加
必须按明细行累加
最后一页数据丢失
只判断了「返回为空」
同时判断 len(rows) < limit
误报大量超退
未过滤未审核单据
FilterString 加 FDocumentStatus=‘C’
少量数量差异误报
浮点直接比较
用 EPS 容差,不要用 ==
429 / 请求限流
分页循环没有间隔
分页之间 sleep(0.3)
六、三个踩坑

  1. 一对多匹配必须"累计",不能"取一条"。 这是这类核对脚本的核心。同一个客户买同一个物料,一个月可能出了 5 次货、退了 3 次货。如果按单据逐条比对,永远对不上。正确做法是先按「客户 + 物料」把两边各自累加成总数,再比总值。数据量大的时候用 defaultdict(float) 建索引,比双层循环快好几个量级。
  2. 明细字段的前缀不一样。 单头字段是 FCustId.FName 这种直接写的,单据体字段必须带 FEntity_,比如 FEntity_FQty。这两个混着写,接口不会给你友好提示,直接返回字段无效。第一次写的时候我在这个上面卡了挺久,后来养成习惯——写 FieldKeys 前先去字段查询接口把本版本的字段名列一遍。
  3. 数量比对不要用 ==。 数量和金额经过多次累加之后会有浮点误差,0.1 + 0.2 != 0.3 是常见现象。用 == 比会报出一堆根本没问题的「差异」。统一用一个容差(比如 1e-6)来判断,误报会少很多。这一点在做金额核对时更明显。
    七、再往前一步
    这个核对逻辑跑稳之后,通常还会往下延伸两个方向:一是把结果推给企业微信,让退货异常每天自动到人;二是按业务员维度汇总,看退货是不是集中在少数人身上。两个方向都不用重写拉数逻辑,在现有结构上加一段就行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询