资金流动关系分析报告

分析范围:销售出库/销售退货、采购收货/采购退货、零售出库/零售退货 与 结算管理 之间的资金关系。 分析方式:代码只读,未做任何改动。 结论:发现 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:174SettleCheckSheetServiceImpl.java:727-764
  • 结算单审核通过写回已付:SettleSheetServiceImpl.java:253-256CustomerSettleSheetServiceImpl.java:264-267

二、发现的逻辑错误

问题①【严重】销售出库现场收款(payTypes)在对账/结算中未抵扣 → 重复收款

证据链:

  1. 创建时 SaleOutSheetServiceImpl.create(L918-919):setSettleStatus(UN_SETTLE) + orderPayTypeService.create(sheet.getId(), payTypes) —— 现场收款被单独持久化;
  2. getInitSettleStatus(customer) 无条件返回 UN_SETTLE(SaleOutSheetServiceImpl.java:930)—— 即使全额现收也标记"未结算";
  3. 客户对账单 getBizItem(OUT_SHEET)(CustomerSettleCheckSheetServiceImpl.java:395-399)与 getUnCheckBizItems(:610) 均取 saleOutSheet.getTotalAmount() 全额,不扣 payTypes 已收;
  4. 整个 settle 包对 OrderPayType 零引用(grep 确认)。

后果:出库 1000、现场收 300 → 对账单按 1000 计入 → 结算管理再收 1000 → 合计实收 1300,多收 300tx_id IS NULL 过滤(SaleOutSheetMapper.xml:301)本意或用于排除现收单据,但实体中无 txId 字段、全代码库无 setter,该条件永不生效,属于半成品。

问题②【严重】零售出库/退货未接入结算管理,状态永远"未结算"

证据链:

  1. CustomerSettleCheckSheetBizType 枚举仅 OUT_SHEET/SALE_RETURN/SETTLE_FEE_SHEET/SETTLE_PRE_SHEET,无零售类型;
  2. settle 模块对 Retail* 零引用(grep 确认);
  3. 零售单据 getInitSettleStatusUN_SETTLE(RetailOutSheetServiceImpl.java:773、RetailReturnServiceImpl.java:615),且没有任何代码将其置为 SETTLED。

后果:零售单据在列表永远显示"未结算",无法走结算管理;与《智富ERP使用手册》第 6 章"零售出库单→客户结算"矛盾(手册第 9 章流程图自身也漏掉零售)。

问题③【中】零售退货前端 model 缺字段声明

后端 RetailReturn.approvePasstotalAmount>0 时强校验 payTypes 非空(RetailReturnServiceImpl.java:313-318);但前端 app/api/sc/model/retailReturn.ts 只有 memberId/totalAmountsettleStatuspayTypes 声明。表单靠 as any 绕过编译,存在类型缺口。

问题④【轻】前端部分模型声明 payTypes 但后端未消费

  • app/api/sc/model/receiveSheet.ts:68 声明 payTypes,但 ReceiveSheetServiceImplorderPayTypeService.create,表单(receiveSheetForm.tsx)也未采集 → 死字段;
  • app/api/sc/model/saleReturn.ts 声明 payTypesSaleReturnServiceImpl 也未 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
  • getBizItemOUT_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 合计 >= totalAmountsetSettleStatus(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 逻辑最优解

问题①:现场收款应从结算额中扣除吗?

最优解:扣除。

理由:

  1. ERP 资金模型要求"一笔应收只对应一次资金回笼"。现收部分已在单据层面完成资金闭环,若再进入对账结算即为重复收款;
  2. 结算管理(对账/折扣/账期)解决的是赊销赊购场景,应只处理未现场收付的余额;
  3. 账目平衡验证:出库 1000、现场收 300 → 对账按 700 → 结算收 700 → 合计 1000,与单据金额一致;
  4. 状态机注意点:部分现收的单据不能标 PART_SETTLE(会被对账 getApprovedList(UN_SETTLE) 排除,余额将永远无法结算),而应保持 UN_SETTLE;仅全额现收才标 SETTLED

问题②:零售走"现收自清"还是"接入客户对账"?

最优解:方案A —— 现收自清,不接入结算管理。

理由:

  1. 零售是 POS 场景,交易当下即完成收款(现金/扫码/会员储值),天然"钱货两清",不存在赊销账期;
  2. 对账/结算解决的是赊销+对账+折扣的账期管理,与零售的资金语义不匹配,接入反而增加多余的结算单据;
  3. 主体不匹配:零售对应会员(member),客户对账按 customerId 聚合,体系不同,硬接入会破坏对账单的数据边界;
  4. 手册第 6 章"零售出库单→客户结算"与第 9 章流程图互相矛盾,且代码从未实现零售结算——正确业务语义应为"零售现收自清",手册以第 9 章为准统一口径;
  5. 现状下零售单 settleStatus 恒为"未结算"是纯展示错误,方案A 通过审核时按 payTypes 置 SETTLED 即可修正,改动小、风险低。

问题③:非经销供应商 UN_REQUIRE 是否保留?

最优解:保留现有默认,但把判断从硬编码解耦。

理由:

  1. 经销/代销供应商是账期应付主体,需对账结算;现款采购供应商无需账期,UN_REQUIRE 是合理默认,业务上应保留;
  2. ManageType 目前仅 DISTRIBUTION 一个枚举值,manageType == DISTRIBUTION 的硬编码非常脆弱:未设置该字段的供应商会被静默判为"无需结算",一旦出现"非经销但按月结"的供应商即无法结算;
  3. 建议在供应商主数据增加显式"结算方式/是否对账"字段,默认值对齐现状(经销=是、非经销=否),解除 ManageType 耦合,把"是否结算"变成业务显式配置而非类型推导。

五、结论

  • 必须修复:问题①(重复收款,资金损失)、问题②(零售状态错误+体系断裂)。
  • 建议修复:问题③(前端类型缺口)、问题④(死字段清理)。
  • 可选增强:问题⑤(结算策略解耦)。
  • 本次为纯分析,未改动任何代码;待确认后按 P1→P2→P3→P4 顺序实施。