forked from erp-dev/erp
9.6 KiB
9.6 KiB
外部财务同步设计
本文档记录 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_source与external_source_id字段 - 已配置外部 API 访问参数:
HAOBUYE_API_BASE_URL、HAOBUYE_API_AUTHORIZATION - 已确定一个本地经办人
Employee.id,用于创建并审批同步产生的ReceiptOrder
推荐执行:
python manage.py migrate
如果只想确认本次相关迁移,也可以先查看:
python manage.py showmigrations business
本次同步依赖的迁移文件为:
business.0028_payment_receipt_external_source_fieldsbusiness.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
其中关键字段为:
category:sale或sale_returnexternal_customer_id:外部客户 ID,例如KH00308external_source_id:外部业务单号,对应BianHaoIDoccurred_at:业务日期,对应RiQirecorded_at:录单时间,对应KdRiQitotal_amount:按同一BianHaoID聚合后的金额remarks:由外部备注字段整理后的文本备注
明细与扩展信息使用 JSONField 保存:
items_payload:明细快照,按外部单据的多行I_Sale记录聚合后保存extra_payload:扩展数据,如dan_type、raw_count等
当前 items_payload 中保留的信息包括:
product_id:若能通过HpID -> Product.human_id匹配到本地产品,则写入本地产品 ID;否则为nullproduct_name:优先本地产品名,否则回退为外部HpIDquantitypriceunitspecnum_of_rollsexternal_sub_idexternal_product_id
1.1 字段映射
收款(SK%)
BianHaoID->ReceiptOrder.external_source_idFkJinE->ReceiptOrder.amountZkJinE->ReceiptOrder.discount_amountJieSunFS->ReceiptOrder.markupRiQi(回退KdRiQi)->ReceiptOrder.receipt_date
退款(XT%)
BianHaoID->ReceiptOrder.external_source_idYfJinE->ReceiptOrder.amountdiscount_amount固定为0JieSunFS->ReceiptOrder.markupRiQi(回退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_returnexternal_source_id = BianHaoID
1.3 外部业务单来源字段
命令在同步收款/退款后,会继续调用:
GET /api/v1/i-sale/by-customer?customer_id=...&category=saleGET /api/v1/i-sale/by-customer?customer_id=...&category=sale_return
当前聚合策略:
- 按
BianHaoID聚合同一张外部业务单 RiQi-> statementoccurred_atKdRiQi-> statementrecorded_atJinE聚合为外部业务单金额HpID优先匹配本地Product.human_id- 若未匹配到本地产品,仍保留
HpID作为对账单 item 展示标识,不阻塞同步
1.4 已同步客户如何补齐 i-sale 业务依据
对于“已经同步过收款/退款,但当时还没有 i-sale 接口”的客户,不需要删除任何旧数据,也不需要回滚收款单。
正确做法是:
- 先执行数据库迁移
- 重新执行同一个单客户同步命令
例如:
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_return到ExternalCustomerStatementOrder
常用示例:
# 正式同步
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_nameexternal_customer_idreceipts_seenrefunds_seensales_seensale_returns_seencreated_countskipped_existing_countskipped_zero_settlement_countexternal_business_created_countexternal_business_skipped_existing_countdry_run_countcreated_receipt_idscreated_external_business_idsskipped_external_ids
对于已同步过财务数据、再次补齐 i-sale 的客户,常见 summary 形态是:
created_count = 0skipped_existing_count > 0external_business_created_count > 0
这表示:
- 收款/退款因为已存在而被跳过
- 但外部业务依据(statement-only)被成功补录
也可通过环境变量预配置:
HAOBUYE_API_BASE_URLHAOBUYE_API_AUTHORIZATIONHAOBUYE_API_TIMEOUT_SECONDSHAOBUYE_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 批量模式预期流程
- 调用外部
GET /api/v1/customers?cursor_id=... - 拿到最多
10个客户 - 对每个客户执行
sync_customer_finance(...) - 整页处理完毕后再推进
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),而不是新增第二套退款模型