1
0
forked from erp-dev/erp
Files
erpnew/docs/external_finance_sync.md

11 KiB
Raw Blame History

外部财务同步设计

本文档记录 ReceiptOrder 对接 HaoBuYe 外部财务 API 的当前实现口径,以及后续批量同步的预备方案。

1. 当前已实现能力

  • 单客户同步命令:python manage.py sync_external_customer_finance <客户名> --operator-id <Employee.id>
  • 仅同步到 business.ReceiptOrder
  • PaymentOrder 当前不参与外部财务同步
  • 命令完成后,会继续同步该客户的外部 sale / sale_return 业务记录,用于对账单计算

1.0 使用前前置条件

在首次使用前,需要确认以下条件:

  • 已完成数据库迁移,使 ReceiptOrder / PaymentOrder 拥有 is_external_sourceexternal_source_id 字段
  • 已配置外部 API 访问参数:HAOBUYE_API_BASE_URLHAOBUYE_API_AUTHORIZATION
  • 已确定一个本地经办人 Employee.id,用于创建并审批同步产生的 ReceiptOrder

推荐执行:

python manage.py migrate

如果只想确认本次相关迁移,也可以先查看:

python manage.py showmigrations business

本次同步依赖的迁移文件为:

  • business.0028_payment_receipt_external_source_fields
  • business.0029_externalcustomerstatementorder

1.0.1 外部业务记录的落库策略

外部 sale / sale_return 当前不会落到核心 SalesOrder / SalesReturnOrder,而是落到专用模型:

  • business.ExternalCustomerStatementOrder

设计目的:

  • 只服务 build_customer_statement(...) 的对账计算
  • 避免把外部历史业务单塞进当前 ERP 的库存/审批流程
  • 不触发库存出入库
  • 不修改持久化的 CustomerBalance

说明:

  • 对账单计算时,会把这部分外部业务记录纳入 statement records
  • 同时会在 statement 内部临时调整当前余额口径,使“本地收款已同步,但销售历史来自外部”的客户也能得到兼容结果

1.0.2 i-sale 数据实际存放位置

外部 i-sale/by-customer 拉到的 sale / sale_return 数据,当前不会展开成核心 SalesOrder / SalesReturnOrder,而是存到:

  • 模型:business.ExternalCustomerStatementOrder

其中关键字段为:

  • categorysalesale_return
  • external_customer_id:外部客户 ID例如 KH00308
  • external_source_id:外部业务单号,对应 BianHaoID
  • occurred_at:业务日期,对应 RiQi
  • recorded_at:录单时间,对应 KdRiQi
  • total_amount:按同一 BianHaoID 聚合后的金额
  • remarks:由外部备注字段整理后的文本备注

明细与扩展信息使用 JSONField 保存:

  • items_payload:明细快照,按外部单据的多行 I_Sale 记录聚合后保存
  • extra_payload:扩展数据,如 dan_typeraw_count

当前 items_payload 中保留的信息包括:

  • product_id:外部 statement-only 明细不绑定本地产品,固定为 null
  • product_name:优先使用外部 HpName,否则回退为外部 HpID
  • quantity
  • price
  • unit
  • color:来源于外部 i-sale payload 的 YanSe 字段;若源端未下发则为空字符串
  • spec
  • num_of_rolls
  • external_sub_id
  • external_product_id

1.1 字段映射

收款(SK%

  • BianHaoID -> ReceiptOrder.external_source_id
  • FkJinE -> ReceiptOrder.amount
  • ZkJinE -> ReceiptOrder.discount_amount
  • JieSunFS -> ReceiptOrder.markup
  • RiQi(回退 KdRiQi-> ReceiptOrder.receipt_date

退款(XT%

  • BianHaoID -> ReceiptOrder.external_source_id
  • YfJinE -> ReceiptOrder.amount
  • discount_amount 固定为 0
  • JieSunFS -> ReceiptOrder.markup
  • RiQi(回退 KdRiQi-> ReceiptOrder.receipt_date

说明:

  • XT% 当前按负金额 ReceiptOrder 落库,表示客户退款。
  • 外部 refund 若结算金额为 0,当前会跳过并记入 summary不落库。
  • 为兼容外部“纯折扣收款”(如 FkJinE=0, ZkJinE>0),同步层直接创建 ReceiptOrder 模型并复用审批逻辑,不走普通创建 API 的 amount != 0 限制。

1.2 幂等规则

  • 幂等键:merchant + external_source_id + is_external_source=True
  • 若本地已存在同一 external_source_id 且核心字段一致,则跳过
  • 若同一 external_source_id 已存在但金额/日期/客户不一致,则报冲突错误,避免静默脏写

外部业务记录statement-only使用单独幂等键

  • merchant + category + external_source_id

其中:

  • category = sale | sale_return
  • external_source_id = BianHaoID

1.3 外部业务单来源字段

命令在同步收款/退款后,会继续调用:

  • GET /api/v1/i-sale/by-customer?customer_id=...&category=sale
  • GET /api/v1/i-sale/by-customer?customer_id=...&category=sale_return

当前聚合策略:

  • BianHaoID 聚合同一张外部业务单
  • RiQi -> statement occurred_at
  • KdRiQi -> statement recorded_at
  • JinE 聚合为外部业务单金额
  • 外部业务单仅作为对账依据保存,不再通过 HpID 绑定本地 Product,避免外部历史数据误入库存/审批产品体系

1.3.1 i-sale → 对账单 item 完整字段映射

下表列出 i-sale/by-customer 返回的外部字段到对账单 API records[].items[] 内各字段的映射关系。

外部字段 (I_Sale) item 字段 前端取值路径 类型 说明
DanJia price records[].items[].price 字符串 不含税单价,两位小数
ShuLiang quantity records[].items[].quantity 字符串 总数量,两位小数
JianShu num_of_rolls records[].items[].num_of_rolls 整数 条数(件数)
HpName product_name records[].items[].product_name 字符串 品名;空时回退为 HpID
YanSe color records[].items[].color 字符串 颜色;源端未下发则空串
SeHao spec records[].items[].spec 字符串 幅宽/规格;源端未下发则空串
HpID external_product_id records[].items[].external_product_id 字符串 外部货品 ID
JiJiaDW unit records[].items[].unit 字符串 计价单位
SubID external_sub_id records[].items[].external_sub_id 整数/null 外部明细行 ID

注意事项:

  • pricequantity 均为字符串类型,不是 number前端使用时需注意类型转换。
  • 所有 item 字段位于 records[].items[] 数组内,不在 record 顶层。
  • product_id 固定为 null(外部业务单不绑定本地产品)。
  • quantity_of_rolls 固定为空数组 [](外部数据无逐条明细)。
  • 同一 BianHaoID 下可能有多条明细行(不同产品/单价),items 数组会有多个元素。

1.4 已同步客户如何补齐 i-sale 业务依据

对于“已经同步过收款/退款,但当时还没有 i-sale 接口”的客户,不需要删除任何旧数据,也不需要回滚收款单。

正确做法是:

  1. 先执行数据库迁移
  2. 重新执行同一个单客户同步命令

例如:

python manage.py migrate
python manage.py sync_external_customer_finance "张晓鹏" --operator-id 12 --allow-create-customer

原因:

  • 已存在的 ReceiptOrder 会按既有幂等键跳过,不会重复插入
  • 缺失的 ExternalCustomerStatementOrder 会被补齐
  • 已存在的外部业务来源单,也会按 merchant + category + external_source_id 跳过

因此,补齐老客户时不需要先删除收款/退款数据;“先删再重灌”既不科学,也会增加误删风险。

建议:

  • 如果只是想先确认将会补哪些业务依据,可先用 --dry-run
  • 如果外部同一 BianHaoID 的核心字段与本地已存快照不一致,命令会报冲突错误,而不是静默覆盖
  • 真正需要人工处理的场景,应优先核对外部数据是否修订过,而不是直接删除本地记录

2. 当前命令参数

python manage.py sync_external_customer_finance "张晓鹏" --operator-id 12 --allow-create-customer

参数说明:

  • customer_name:外部客户名称,精确匹配
  • --operator-id:本地经办人 Employee.id。若不传,则回退使用 HAOBUYE_FINANCE_SYNC_OPERATOR_ID
  • --allow-create-customer:本地没有该客户时自动创建;默认关闭
  • --dry-run:只校验,不写入;默认关闭

命令行为说明:

  • 如果既没有传 --operator-id,也没有配置 HAOBUYE_FINANCE_SYNC_OPERATOR_ID,命令会直接报错退出
  • 如果客户不存在且未传 --allow-create-customer,命令会直接报错退出
  • 如果外部返回 status=not_found,命令不会写入任何数据,而是返回一份空结果 summary
  • 如果本地已存在同一个 external_source_id 且字段一致,会跳过,不会重复插入
  • 如果本地已存在同一个 external_source_id 但核心字段不一致,会报冲突错误,避免静默覆盖
  • 收款/退款同步完成后,命令会继续同步外部 sale / sale_returnExternalCustomerStatementOrder

常用示例:

# 正式同步
python manage.py sync_external_customer_finance "张晓鹏" --operator-id 12 --allow-create-customer

# 只做拉取和校验,不写入
python manage.py sync_external_customer_finance "张晓鹏" --operator-id 12 --allow-create-customer --dry-run

# 使用环境变量里的默认 operator
python manage.py sync_external_customer_finance "张晓鹏" --allow-create-customer

命令成功后会输出 summary典型字段包括

  • customer_name
  • external_customer_id
  • receipts_seen
  • refunds_seen
  • sales_seen
  • sale_returns_seen
  • created_count
  • skipped_existing_count
  • skipped_zero_settlement_count
  • external_business_created_count
  • external_business_skipped_existing_count
  • dry_run_count
  • created_receipt_ids
  • created_external_business_ids
  • skipped_external_ids

对于已同步过财务数据、再次补齐 i-sale 的客户,常见 summary 形态是:

  • created_count = 0
  • skipped_existing_count > 0
  • external_business_created_count > 0

这表示:

  • 收款/退款因为已存在而被跳过
  • 但外部业务依据statement-only被成功补录

也可通过环境变量预配置:

  • HAOBUYE_API_BASE_URL
  • HAOBUYE_API_AUTHORIZATION
  • HAOBUYE_API_TIMEOUT_SECONDS
  • HAOBUYE_FINANCE_SYNC_OPERATOR_ID

推荐最小配置示例:

HAOBUYE_API_BASE_URL=http://43.139.183.222:18080
HAOBUYE_API_AUTHORIZATION=your-fixed-authorization-secret
HAOBUYE_API_TIMEOUT_SECONDS=30
HAOBUYE_FINANCE_SYNC_OPERATOR_ID=12

3. 已预留的批量能力

代码中已实现批量入口:

  • sync_customer_finance_batch(...)

设计原则:

  • 批量模式只负责“枚举客户”
  • 单客户同步逻辑完全复用 sync_customer_finance(...)
  • 当前尚未暴露为 command / celery / cron

3.1 批量模式预期流程

  1. 调用外部 GET /api/v1/customers?cursor_id=...
  2. 拿到最多 10 个客户
  3. 对每个客户执行 sync_customer_finance(...)
  4. 整页处理完毕后再推进 next_cursor_id

3.2 后续建议

  • 若进入定时任务阶段,建议将外部 cursor_id 持久化到 api_v1.DataSync
  • 若需要失败补偿,建议单独增加 finance failure model记录 customer_name / external_source_id / run_date / error
  • 若前端后续需要区分“收款/退款”,建议在 ReceiptOrder 列表 API 上增加只读过滤参数(例如 amount_sign=positive|negative),而不是新增第二套退款模型