资金流动关系分析报告
分析范围:销售出库/销售退货、采购收货/采购退货、零售出库/零售退货 与 结算管理 之间的资金关系。 分析方式:代码只读,未做任何改动。 结论:发现 2 处严重逻辑错误(现场收款重复结算、零售脱离结算体系)、若干轻量问题,并给出详细改动计划。
一、整体资金流
系统分三条资金主线,其中零售主线在结算管理中是断开的。
【供应商侧 · 应付资金流】
采购收货单(ReceiveSheet) totalAmount [+] 结算状态初始化:
采购退单(PurchaseReturn) totalAmount [−] 经销(DISTRIBUTION)供应商 → UN_SETTLE
供应商费用单(SettleFeeSheet) ±(应收/应付) 非经销供应商 → UN_REQUIRE(不参与结算)
供应商预付款单(SettlePreSheet) [−]
│ (按 supplierId 汇总,SUB 取负)
▼
供应商对账单(SettleCheckSheet) totalPayAmount / totalPayedAmount / totalDiscountAmount
│ (从对账单"剩余未付额"生成结算单)
▼
供应商结算单(SettleSheet) 审核通过 → 实付供应商
【客户侧 · 应收资金流】
销售出库单(SaleOutSheet) totalAmount [+] ← 可现场收款(payTypes)
销售退单(SaleReturn) totalAmount [−]
客户费用单(CustomerSettleFeeSheet) ±(应收/应付)
客户预收款单(CustomerSettlePreSheet)[−]
│ (按 customerId 汇总)
▼
客户对账单(CustomerSettleCheckSheet)
│
▼
客户结算单(CustomerSettleSheet) 审核通过 → 向客户收款
【零售侧 · 资金断裂】
零售出库单(RetailOutSheet) totalAmount + payTypes + settleStatus=UN_SETTLE
零售退货单(RetailReturn) totalAmount + payTypes + settleStatus=UN_SETTLE
│
▼
(无去向)→ 不在任何对账单 bizType 枚举中,settle 模块零引用
各单据资金字段与方向
| 单据 | 资金字段 | 方向(对账单内) | 结算状态初始化 |
|---|---|---|---|
| 采购收货单 | totalAmount | + | 经销→UN_SETTLE / 非经销→UN_REQUIRE |
| 采购退单 | totalAmount | − | 同上 |
| 供应商费用单 | totalAmount | ±(收+/付−) | UN_SETTLE |
| 供应商预付款单 | totalAmount | − | UN_SETTLE |
| 销售出库单 | totalAmount、payTypes | + | 恒 UN_SETTLE |
| 销售退单 | totalAmount | − | 恒 UN_SETTLE |
| 客户费用单 | totalAmount | ±(收+/付−) | UN_SETTLE |
| 客户预收款单 | totalAmount | − | UN_SETTLE |
| 零售出库单 | totalAmount、payTypes | (无) | 恒 UN_SETTLE,永不变 |
| 零售退货单 | totalAmount、payTypes | (无) | 恒 UN_SETTLE,永不变 |
结算状态机(settleStatus)
SettleStatus 枚举:UN_SETTLE(0) 未结算、PART_SETTLE(1) 结算中、SETTLED(3) 已结算、UN_REQUIRE(6) 无需结算。
| 阶段 | 操作 | 变化 |
|---|---|---|
| 单据创建 | getInitSettleStatus() |
见上表 |
| 对账单创建 | setBizItemPartSettle |
业务单据 UN_SETTLE → PART_SETTLE |
| 对账单修改/删除 | setBizItemUnSettle |
业务单据 → UN_SETTLE |
| 结算单审核通过 | setSettleAmount 累加已付+优惠 |
对账单 totalPayedAmount/totalDiscountAmount 累加 |
| 结清判定 | 剩余=totalPayAmount−totalPayedAmount−totalDiscountAmount == 0 |
对账单 → SETTLED → 逐行业务单据 → SETTLED |
关键代码:
- 对账单余额判定:
SettleCheckSheetMapper.xml:174、SettleCheckSheetServiceImpl.java:727-764 - 结算单审核通过写回已付:
SettleSheetServiceImpl.java:253-256、CustomerSettleSheetServiceImpl.java:264-267
二、发现的逻辑错误
问题①【严重】销售出库现场收款(payTypes)在对账/结算中未抵扣 → 重复收款
证据链:
- 创建时
SaleOutSheetServiceImpl.create(L918-919):setSettleStatus(UN_SETTLE)+orderPayTypeService.create(sheet.getId(), payTypes)—— 现场收款被单独持久化; getInitSettleStatus(customer)无条件返回UN_SETTLE(SaleOutSheetServiceImpl.java:930)—— 即使全额现收也标记"未结算";- 客户对账单
getBizItem(OUT_SHEET)(CustomerSettleCheckSheetServiceImpl.java:395-399)与getUnCheckBizItems(:610) 均取saleOutSheet.getTotalAmount()全额,不扣 payTypes 已收; - 整个 settle 包对
OrderPayType零引用(grep 确认)。
后果:出库 1000、现场收 300 → 对账单按 1000 计入 → 结算管理再收 1000 → 合计实收 1300,多收 300。
tx_id IS NULL 过滤(SaleOutSheetMapper.xml:301)本意或用于排除现收单据,但实体中无 txId 字段、全代码库无 setter,该条件永不生效,属于半成品。
问题②【严重】零售出库/退货未接入结算管理,状态永远"未结算"
证据链:
CustomerSettleCheckSheetBizType枚举仅OUT_SHEET/SALE_RETURN/SETTLE_FEE_SHEET/SETTLE_PRE_SHEET,无零售类型;- settle 模块对
Retail*零引用(grep 确认); - 零售单据
getInitSettleStatus恒UN_SETTLE(RetailOutSheetServiceImpl.java:773、RetailReturnServiceImpl.java:615),且没有任何代码将其置为 SETTLED。
后果:零售单据在列表永远显示"未结算",无法走结算管理;与《智富ERP使用手册》第 6 章"零售出库单→客户结算"矛盾(手册第 9 章流程图自身也漏掉零售)。
问题③【中】零售退货前端 model 缺字段声明
后端 RetailReturn.approvePass 在 totalAmount>0 时强校验 payTypes 非空(RetailReturnServiceImpl.java:313-318);但前端 app/api/sc/model/retailReturn.ts 只有 memberId/totalAmount,无 settleStatus、payTypes 声明。表单靠 as any 绕过编译,存在类型缺口。
问题④【轻】前端部分模型声明 payTypes 但后端未消费
app/api/sc/model/receiveSheet.ts:68声明payTypes,但ReceiveSheetServiceImpl无orderPayTypeService.create,表单(receiveSheetForm.tsx)也未采集 → 死字段;app/api/sc/model/saleReturn.ts声明payTypes,SaleReturnServiceImpl也未orderPayTypeService.create,表单(saleReturnForm.tsx)未采集 → 死字段(此字段在问题①修复后有望被启用)。
问题⑤【设计确认】非经销供应商不参与结算
getInitSettleStatus(supplier):manageType == DISTRIBUTION(经销) → UN_SETTLE,否则 → UN_REQUIRE(6)。即只有经销供应商走对账/结算。需要业务确认是否为有意设计。
三、详细改动计划
以下计划待确认后实施。所有改动遵循一个核心原则:同一笔应收/应付,现场收付与结算管理两条通道不可叠加,结算只处理"未现场收付的余额"。
P1 现场收款抵扣(修复问题① + 启用问题④的销售退单字段)
1. SaleOutSheetServiceImpl.java
- 在
approvePass(L350)中,于updateAllColumn(sheet, updateOrderWrapper)之前追加结算状态重算:- 查询
orderPayTypeService.findByOrderId(sheet.getId()),汇总已收金额payedAmount; payedAmount >= sheet.getTotalAmount()→sheet.setSettleStatus(SettleStatus.SETTLED)(钱货两清,不进入对账);0 < payedAmount < totalAmount→ 保持UN_SETTLE(余额进对账,不可用 PART_SETTLE,否则会被getApprovedList(UN_SETTLE)排除);payedAmount == 0→ 保持UN_SETTLE。
- 查询
- 说明:
updateAllColumn会把实体上已 set 的 settleStatus 一并持久化。
2. CustomerSettleCheckSheetServiceImpl.java
- 注入
OrderPayTypeService; getBizItem的OUT_SHEET分支(L394-399):totalAmount = saleOutSheet.totalAmount − 已收 payTypes 合计(取与 0 的较大值兜底);getUnCheckBizItems(L590-617):同样改为扣减后的金额;- 新增私有方法
getPayedAmountByOrderId(String orderId):汇总tbl_order_pay_type.pay_amount(或直接给OrderPayTypeService增加sumByOrderId)。
3. SaleReturnServiceImpl.java(销售退单方向)
create:追加orderPayTypeService.create(saleReturn.getId(), vo.getPayTypes()),启用退单现场退款记录;approvePass:同样按 payTypes 与 totalAmount 比较重算 settleStatus(全额退款 → SETTLED);- 对账单侧
SALE_RETURN金额 =totalAmount − 已退 payTypes(同一私有方法复用)。
4. 前端
app/api/sc/model/saleReturn.ts:保留 payTypes(P1 启用后为有效字段)。
5. 约束(写入注释/校验)
- 已进入对账单(PART_SETTLE 或对账单已生成)的销售出库单,禁止再修改 payTypes,避免对账单余额与已收不一致(可在 update 时校验对账单引用)。
P2 零售现收自清(修复问题②,采用方案A)
理由见第四节问题②最优解。
1. RetailOutSheetServiceImpl.approvePass(L308)
- 现有 payTypes 非空校验(L355-360)保留;
- 在
updateAllColumn前重算:payTypes 合计 >= totalAmount→setSettleStatus(SETTLED);否则 →PART_SETTLE(标记"尚有未结",提示业务); - 不再依赖任何对账单/结算单(零售不进结算管理)。
2. RetailReturnServiceImpl.approvePass(L263)
- 同样:全额退款 →
SETTLED,否则 →PART_SETTLE。
3. 前端类型补齐
app/api/sc/model/retailReturn.ts:增加settleStatus?: string; payTypes?: OrderPayTypeVo[];(与 retailOutSheet.ts 对齐)。
4. 手册口径统一
- 建议同步修正《智富ERP使用手册》第 6 章:零售为"现场即时收付、钱货两清",不进入结算管理(与第 9 章流程一致)。
P3 清理死字段(问题④ 采购侧)
app/api/sc/model/receiveSheet.ts:68:删除payTypes声明(后端不消费、表单不采集)。如后续要支持"采购现付",再单独引入。
P4 供应商结算策略解耦(问题⑤,可选增强)
- 将"是否参与结算"从
manageType == DISTRIBUTION硬编码改为供应商主数据的显式字段(如settleStrategy/是否对账结算); getInitSettleStatus(supplier)改为读该字段;- 默认值保持现状(经销=true,非经销=false),不改变现有数据语义,仅解除耦合。
改动影响面
| 改动 | 涉及后端文件 | 涉及前端文件 |
|---|---|---|
| P1 | SaleOutSheetServiceImpl、SaleReturnServiceImpl、CustomerSettleCheckSheetServiceImpl、(可选 OrderPayTypeService) | saleReturn.ts(保留) |
| P2 | RetailOutSheetServiceImpl、RetailReturnServiceImpl | retailReturn.ts |
| P3 | — | receiveSheet.ts |
| P4 | 供应商主数据 + 各单据 getInitSettleStatus | 供应商设置页(可选) |
四、三个待确认问题 —— ERP 逻辑最优解
问题①:现场收款应从结算额中扣除吗?
最优解:扣除。
理由:
- ERP 资金模型要求"一笔应收只对应一次资金回笼"。现收部分已在单据层面完成资金闭环,若再进入对账结算即为重复收款;
- 结算管理(对账/折扣/账期)解决的是赊销赊购场景,应只处理未现场收付的余额;
- 账目平衡验证:出库 1000、现场收 300 → 对账按 700 → 结算收 700 → 合计 1000,与单据金额一致;
- 状态机注意点:部分现收的单据不能标 PART_SETTLE(会被对账
getApprovedList(UN_SETTLE)排除,余额将永远无法结算),而应保持UN_SETTLE;仅全额现收才标SETTLED。
问题②:零售走"现收自清"还是"接入客户对账"?
最优解:方案A —— 现收自清,不接入结算管理。
理由:
- 零售是 POS 场景,交易当下即完成收款(现金/扫码/会员储值),天然"钱货两清",不存在赊销账期;
- 对账/结算解决的是赊销+对账+折扣的账期管理,与零售的资金语义不匹配,接入反而增加多余的结算单据;
- 主体不匹配:零售对应会员(member),客户对账按
customerId聚合,体系不同,硬接入会破坏对账单的数据边界; - 手册第 6 章"零售出库单→客户结算"与第 9 章流程图互相矛盾,且代码从未实现零售结算——正确业务语义应为"零售现收自清",手册以第 9 章为准统一口径;
- 现状下零售单
settleStatus恒为"未结算"是纯展示错误,方案A 通过审核时按 payTypes 置SETTLED即可修正,改动小、风险低。
问题③:非经销供应商 UN_REQUIRE 是否保留?
最优解:保留现有默认,但把判断从硬编码解耦。
理由:
- 经销/代销供应商是账期应付主体,需对账结算;现款采购供应商无需账期,
UN_REQUIRE是合理默认,业务上应保留; - 但
ManageType目前仅DISTRIBUTION一个枚举值,manageType == DISTRIBUTION的硬编码非常脆弱:未设置该字段的供应商会被静默判为"无需结算",一旦出现"非经销但按月结"的供应商即无法结算; - 建议在供应商主数据增加显式"结算方式/是否对账"字段,默认值对齐现状(经销=是、非经销=否),解除
ManageType耦合,把"是否结算"变成业务显式配置而非类型推导。
五、结论
- 必须修复:问题①(重复收款,资金损失)、问题②(零售状态错误+体系断裂)。
- 建议修复:问题③(前端类型缺口)、问题④(死字段清理)。
- 可选增强:问题⑤(结算策略解耦)。
- 本次为纯分析,未改动任何代码;待确认后按 P1→P2→P3→P4 顺序实施。