# 客户欠款统计口径 本文档定义客户欠款的计算方式,已通过 4 个客户的 ERP 导出数据交叉验证,全部精确对上。 --- ## 1. 核心公式 ```text 欠款 = AR - 收款 - 现场收款 - 折扣 = (sales.JinE + sale_returns.JinE) - receipt.FkJinE - sales.SfJinE(按 BianHaoID 去重) - (sales.ZkJinE + sale_returns.ZkJinE + receipt.ZkJinE) ``` 各项说明: | 项目 | 数据来源 | 筛选条件 | 取值字段 | 去重规则 | |------|----------|----------|----------|----------| | 销售额 | I_Sale | DanType='成品销售单' AND BianHaoID LIKE 'XS%' | SUM(JinE) | 不去重,每行明细独立 | | 退货抵扣 | I_Sale | DanType='客户退货单' AND BianHaoID LIKE 'XT%' | SUM(JinE) | 不去重,JinE 本身为负数 | | 收款 | F_Skd | BianHaoID LIKE 'SK%' | SUM(FkJinE) | 不去重,每行独立 | | 现场收款 | I_Sale | DanType='成品销售单' AND SfJinE != 0 | SUM(SfJinE) | **必须按 BianHaoID 去重** | | 折扣 | I_Sale + F_Skd | 所有相关行 | SUM(ZkJinE) | 不去重 | --- ## 2. SfJinE 去重规则(关键) `SfJinE` 是**单据头级别字段**,被冗余写到同一 BianHaoID 的每一行明细上。 示例(张晓鹏 XS20205788,5 行明细): ```text SubID=1 JinE=380 SfJinE=22,287 ← 同一个值 SubID=2 JinE=11,668 SfJinE=22,287 ← 重复 SubID=3 JinE=3,864 SfJinE=22,287 ← 重复 SubID=4 JinE=3,206 SfJinE=22,287 ← 重复 SubID=5 JinE=3,169 SfJinE=22,287 ← 重复 ``` 正确做法:每个 BianHaoID 只取一次 SfJinE = 22,287。 错误做法:直接 SUM 所有行 = 22,287 × 5 = 111,435(多算 4 倍)。 --- ## 3. 不参与计算的字段 | 字段 | 所在表 | 原因 | |------|--------|------| | I_Sale.YfJinE | I_Sale | 全部为 0 | | I_Sale.DingJin | I_Sale | 订金标记,实际入账已通过 SK/SfJinE 体现 | | I_Sale.JinET | I_Sale | 信息字段,不参与欠款 | | I_Sale.LjQK | I_Sale | 滚动累欠快照,不是增量 | | I_Sale.DanJiaCB / JinECB / JinE_SL | I_Sale | 全部为 0 | | F_Skd.YfJinE | F_Skd | XS 行与 I_Sale.JinE 重复;XT 行与退货重复 | | F_Skd.LjJinE | F_Skd | 累计金额,不参与求和 | | F_Skd.DjJinE / JyJinE | F_Skd | 订金/结余,不参与 | | refund 桶 | F_Skd | 与 sale_returns.JinE 等值,不重复计入 | --- ## 4. 对应 API 接口 | 桶 | 接口 | 参数 | |----|------|------| | 销售单 | GET /api/v1/i-sale/by-customer | customer_id=KHxxxxx&category=sale | | 销售退货 | GET /api/v1/i-sale/by-customer | customer_id=KHxxxxx&category=sale_return | | 收款单 | GET /api/v1/finance/by-customer | customer_name_b64=xxx&record_types=receipt | 所有接口无分页限制,一次返回全集。 Base URL: `http://43.139.183.222:18080` Auth: `Authorization: your-fixed-authorization-secret` --- ## 5. ERP 对账单滚动累计逻辑 ERP 对账单按时间排序,每行的"结欠金额"是滚动累计: ```text 结欠[n] = 结欠[n-1] + 本行应收金额 - 本行已收金额 ``` - XS 销售单:应收 = JinE,已收 = SfJinE(现场收款,多数为 0) - SK 收款单:应收 = 0(或负数折扣),已收 = FkJinE - XT 退货单:应收 = JinE(负数),已收 = 0 --- ## 6. 折扣与 ERP "应收金额"的关系 ERP 展示的"应收金额"是净应收(JinE - ZkJinE),我们从 I_Sale.JinE 拿到的是毛额。 ```text ERP 视角: 欠款 = Σ(应收_净) - Σ(已收) API 视角: 欠款 = Σ(JinE) - Σ(FkJinE) - Σ(SfJinE去重) - Σ(ZkJinE) ``` 两者数学等价。 --- ## 7. 验证结果(全部精确对上) | 客户 | KhID | API 欠款 | ERP 欠款 | 差额 | |------|------|---:|---:|---:| | 木棉 | KH01078 | 10,718 | 10,718 | 0 | | 腾飞纺织 | KH00311 | 53,521 | 53,521 | 0 | | 金庸 | KH00290 | 82,670 | 82,670 | 0 | | 张晓鹏 | KH00308 | 753,897 | 753,897 | 0 | 各客户 SfJinE 情况: | 客户 | SfJinE(去重后) | 笔数 | 说明 | |------|---:|---:|------| | 木棉 | 0 | 0 | 无现场收款 | | 腾飞纺织 | 0 | 0 | 无现场收款 | | 金庸 | 23,394 | 1 笔 | XS20214770 微信收款 | | 张晓鹏 | 77,132 | 7 笔 | 2020-09 期间支付宝/微信收款 | --- ## 8. 伪代码 ```python def calculate_customer_debt(customer_id: str, customer_name_b64: str) -> float: # 1. 拉销售单 sales = api.get("/api/v1/i-sale/by-customer", customer_id=customer_id, category="sale") # 2. 拉销售退货 sale_returns = api.get("/api/v1/i-sale/by-customer", customer_id=customer_id, category="sale_return") # 3. 拉收款单 receipts = api.get("/api/v1/finance/by-customer", customer_name_b64=customer_name_b64, record_types="receipt") # 4. 计算 AR(每行明细的 JinE 独立求和) ar = sum(r.JinE for r in sales.records) + sum(r.JinE for r in sale_returns.records) # 5. 收款 received_sk = sum(r.FkJinE for r in receipts.receipts) # 6. 现场收款(按 BianHaoID 去重,每单只取一次) seen = set() received_inline = 0.0 for r in sales.records: if r.BianHaoID not in seen: seen.add(r.BianHaoID) received_inline += r.SfJinE # 7. 折扣 discount = (sum(r.ZkJinE for r in sales.records) + sum(r.ZkJinE for r in sale_returns.records) + sum(r.ZkJinE for r in receipts.receipts)) # 8. 欠款 return ar - received_sk - received_inline - discount ``` --- ## 9. 按时间窗口计算 ```python def calculate_debt_as_of(customer_id, customer_name_b64, cutoff_date): # 拉取同上,然后按 RiQi < cutoff_date 过滤 sales_f = [r for r in sales if r.RiQi < cutoff_date] sr_f = [r for r in sale_returns if r.RiQi < cutoff_date] rcp_f = [r for r in receipts if r.RiQi < cutoff_date] ar = sum(r.JinE for r in sales_f) + sum(r.JinE for r in sr_f) received_sk = sum(r.FkJinE for r in rcp_f) seen = set() received_inline = 0.0 for r in sales_f: if r.BianHaoID not in seen: seen.add(r.BianHaoID) received_inline += r.SfJinE discount = (sum(r.ZkJinE for r in sales_f) + sum(r.ZkJinE for r in sr_f) + sum(r.ZkJinE for r in rcp_f)) return ar - received_sk - received_inline - discount ``` --- ## 10. antd-demo 后端待修复的 Bug ### Bug 1:SfJinE 未去重 当前同步代码对 I_Sale 明细行直接求和 SfJinE,导致多行明细的订单被重复计入。 修复:按 BianHaoID 分组,每组只取一次 SfJinE。 ### Bug 2:退货单金额放错列 退货金额被放到"已收金额"(negative_amount),应该放到"本单应收"(positive_amount=0, negative_amount=|JinE|,使得应收为负数)。 ### Bug 3:退货单被冗余写入收款桶 9 条 XT 退货单同时出现在"外部销售退货单"和"收款单"两种类型里,导致收款行数多了 9 条。 修复:同步时 XT 退货只写入退货桶,不写入收款桶。 --- ## 变更记录 | 日期 | 内容 | |------|------| | 2026-05-18 | 初版,木棉/腾飞纺织验证通过 | | 2026-05-18 | 金庸验证发现 SfJinE 必须参与扣减 | | 2026-05-19 | **最终版**:张晓鹏验证发现 SfJinE 必须按 BianHaoID 去重。四个客户全部精确对上 ERP |