1
0
forked from erp-dev/erp
Files
erpnew/docs/CUSTOMER_DEBT_CALCULATION(1).md
2026-05-19 23:41:34 +08:00

222 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 客户欠款统计口径
本文档定义客户欠款的计算方式,已通过 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 的每一行明细上。
示例(张晓鹏 XS202057885 行明细):
```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 1SfJinE 未去重
当前同步代码对 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 |