--- schema: "hl-changelog/v2" ticket: "8127" title: "应付款付款支持冲抵供应商预付款(差额付款 + 占用/付讫/回冲状态机)" consumer: "admin" author: "yst(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "not_required" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "d0a264ec33214617f5ee3ca500b0f9ebd76b4bb6" target_release: "v2.1" verified_at: "2026-09-22" status_note: "backend_status: deployed - hl-order-service-v3 已部署测试服(dev-v3,含 finance 同进程),E2E 全链路 PASS(改单加冲抵→审批→出纳差额付款,往来净额/预付余额/冲抵明细 SETTLED 全对,2026-09-22 取证); gateway_status: not_required - 零网关改动,/admin/finance/** 走 hl-gateway 既有通配路由; frontend_status: pending - 前端适配情况未知,后端不代填。 前端核验(2026-09-22): 已交付 hl-admin d0a264ec——payable.js 加 getOffsettablePrepayments+PREPAY_OFFSET_STATUS meta+4 写口 JSDoc(单笔 create 前端无调用方仅补文档);新共享组件 PrepayOffsetPicker(勾选+金额≤availableAmount+598812/598813 前置校验);PaymentDetailPanel 详情冲抵金额/实付金额+offsets 状态机明细卡、编辑草稿冲抵区块(回显仅 OCCUPYING,整体置换恒传,空数组清占用);GroupPayPanel 按供应商分组上送、SupplierPayPanel 单元素分组;出纳页零改动(#7815 锁 actualAmount 槽位);598812-598817 透 message;原型 pay-adv 冲抵记录子 tab 契约无列表读口,未造数留痕待后端;Vitest 23/23+checkpoint 全量 13 项全绿。" updated_at: "2026-09-22" base: "dev-v3" --- # 财务:应付款付款支持冲抵供应商预付款(Epic #8127) > 应付款付款时可用「该供应商已付讫且有余额的预付款」冲抵,出纳只付差额现金。涉及 4 个建单/改单接口入参新增、付款单详情出参新增、新增 1 个可冲抵预付查询接口。 ## ① 接口背景 供应商常有预付(先打款后结算)。此前预付款与应付款是两条独立线,应付款付款只能全额现金支出,无法用已预付的余额抵扣,资金占用高。本变更让应付款付款单可勾选「该供应商的预付款」做冲抵:冲抵部分无现金流出,出纳仅按「应付金额 − 冲抵金额 = 实付金额」付差额。 三个口径决策(后端已定): - **冲抵算 paid**:冲抵部分随现金一并计入应付台账已付,清偿方式不影响应付债务的付讫认定。 - **占用时点=建单时**:建单/改单(PENDING 草稿)即扣减预付可用余额并落「占用中」明细,驳回/删改草稿回冲,付讫转「已冲抵」。 - **往来差额归零**:付讫时补一对抵销分录,付款单文档净额=0、预付单文档净额=剩余可用余额,供应商往来对账自清。 ## ② 变更清单 | 类型 | 接口 | 变更 | |---|---|---| | 修改 | `POST /admin/finance/payments` 创建付款单 | 入参新增 `prepayOffsets[]` | | 修改 | `POST /admin/finance/payments/batch` 按订单批量创建 | 入参新增 `prepayOffsets[]`(按供应商分组) | | 修改 | `POST /admin/finance/payments/batch-by-supplier` 按供应商合并创建 | 入参新增 `prepayOffsets[]`(按供应商分组) | | 修改 | `PUT /admin/finance/payments/{id}` 编辑草稿 | 入参新增 `prepayOffsets[]`(整体置换语义) | | 修改 | `GET /admin/finance/payments/{id}` 付款单详情 | 出参新增 `offsets[]`;`prepayOffsetAmount`/`actualPayAmount` 由恒 0 真值化 | | 新增 | `GET /admin/finance/payments/offsettable-prepays` 可冲抵预付查询 | 新接口 | ## ③ 接口详情 ### 3.1 新增:可冲抵预付查询 ``` GET /admin/finance/payments/offsettable-prepays?supplierId={supplierId} ``` 供付款申请页勾选「用哪笔预付冲抵」。返回该供应商下**已付讫(PAID)且可用余额 > 0** 的预付款,按付款日期升序。 ### 3.2 创建/编辑付款单(冲抵) 在既有入参基础上加 `prepayOffsets`,提交即占用预付余额;不冲抵则该字段不传或传空数组(行为与旧版完全一致)。 ### 3.3 付款单详情 详情返回本单的冲抵明细 `offsets[]` 及冲抵状态;`actualPayAmount`(实付)为出纳真正付现的金额。 ## ④ 入参 ### 4.1 `prepayOffsets[]`(创建/编辑付款单新增,可空) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `prepayId` | Long(string) | 是 | 预付款申请单 ID(须为本供应商 PAID 且余额充足) | | `offsetAmount` | number | 是 | 该笔预付冲抵金额(>0,≤预付可用余额) | > 批量接口(按订单批量/按供应商合并)按供应商分组传:`prepayOffsets: [{ supplierId, offsets: [{ prepayId, offsetAmount }] }]`。 > 编辑草稿为**整体置换语义**:传入的 `prepayOffsets` 全量替换旧占用(先回冲旧占用再按新入参重占)。 ### 4.2 可冲抵预付查询入参 | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `supplierId` | Long(string) | 是 | 供应商 ID(查询其可冲抵预付) | ## ⑤ 出参 ### 5.1 付款单详情 `GET /admin/finance/payments/{id}`(新增/真值化字段) | 字段 | 类型 | 说明 | |---|---|---| | `amount` | number | 应付金额(= Σ明细行,不随冲抵变化) | | `prepayOffsetAmount` | number | **冲抵金额合计**(= Σoffsets.offsetAmount;原恒 0,本期真值化) | | `actualPayAmount` | number | **实付金额 = amount − prepayOffsetAmount**(出纳按此现金付款;原恒等于 amount) | | `offsets` | array | **本单冲抵明细列表**(新增,无冲抵时为 `[]`) | `offsets[]` 元素: | 字段 | 类型 | 说明 | |---|---|---| | `offsetId` | Long(string) | 冲抵明细 ID | | `prepayId` | Long(string) | 预付款申请单 ID | | `prepayNo` | string | 预付款单号(YF- 前缀) | | `offsetAmount` | number | 该笔冲抵金额 | | `status` | string | 冲抵状态:`OCCUPYING` 占用中 / `SETTLED` 已冲抵(付讫)/ `CANCELLED` 已取消(驳回/删改草稿回冲) | | `settleTime` | string(datetime) | 付讫冲抵时间(仅 SETTLED 有值,否则 null) | ### 5.2 可冲抵预付查询 `offsettable-prepays` 出参(数组) | 字段 | 类型 | 说明 | |---|---|---| | `prepayId` | Long(string) | 预付款申请单 ID | | `prepayNo` | string | 预付款单号 | | `amount` | number | 预付本金 | | `availableAmount` | number | 可用余额(可被冲抵的上限) | | `payDate` | string(date) | 付款日期 | | `supplierId` | Long(string) | 供应商 ID | | `supplierName` | string | 供应商名称 | ## ⑥ 枚举/数据字典 **冲抵明细状态 `offsets[].status`**: | 值 | 含义 | |---|---| | `OCCUPYING` | 占用中(建单~付讫前) | | `SETTLED` | 已冲抵(付讫,终态) | | `CANCELLED` | 已取消(驳回/删草稿/改草稿回冲,留痕) | `paymentType` 仍走既有 `fin_payment_type` 字典标签(住宿/门票·游玩/餐食/车辆/导游/摄影/保险/其他支出/退款/其他应付),本次未新增。 ## ⑦ 错误码 | 码 | 语义 | |---|---| | 598812 | 冲抵入参非法(金额≤0 / 同一预付单重复传) | | 598813 | 冲抵合计超应付金额(Σoffset > amount) | | 598814 | 预付单不可用(不存在 / 非已付讫 / 可用余额不足 / 金额非法) | | 598815 | 预付单冲抵单位与付款供应商不一致(跨供应商拦截) | | 598816 | 预付可冲抵余额并发不足(CAS 扣减失败,整批回滚) | | 598817 | 冲抵回冲账实不符(回冲 CAS 失败,fail-fast) | ## ⑧ 示例 ### 8.1 典型:创建付款单并冲抵预付 请求 `POST /admin/finance/payments`: ```json { "supplierId": "2096854417461403650", "payeeAccountId": "2096854417490763777", "amount": 800.00, "paymentType": "住宿", "reason": "9月羊和远方住宿结算", "orderId": "2100482290093039617", "prepayOffsets": [ { "prepayId": "2100473583917580290", "offsetAmount": 500.00 } ] } ``` 响应(`data.paymentId` 为新单 ID)。此时预付 `available_amount` 1000→500,`prepayOffsetAmount=500`、`actualPayAmount=300`,offset 行 `OCCUPYING`。 详情出参(付讫后): ```json { "code": 200, "data": { "id": "2100485199803359234", "paymentNo": "FK-202609170057", "amount": 800.00, "prepayOffsetAmount": 500.00, "actualPayAmount": 300.00, "status": "PAID", "offsets": [ { "offsetId": "2102204...", "prepayId": "2100473583917580290", "prepayNo": "YF-202609170002", "offsetAmount": 500.00, "status": "SETTLED", "settleTime": "2026-09-22 09:15:35" } ] } } ``` ### 8.2 边界:可冲抵预付查询 请求 `GET /admin/finance/payments/offsettable-prepays?supplierId=2096854417461403650`: ```json { "code": 200, "data": [ { "prepayId": "2100473583917580290", "prepayNo": "YF-202609170002", "amount": 1000.00, "availableAmount": 500.00, "payDate": "2026-09-17", "supplierId": "2096854417461403650", "supplierName": "呼伦贝尔羊和远方牧业有限公司" } ] } ``` (该预付已被冲抵 500,故 `availableAmount` 由 1000 降为 500。) ### 8.3 异常:冲抵超应付 / 跨供应商 ```json { "code": 598813, "message": "冲抵合计超应付金额", "success": false } { "code": 598815, "message": "预付单冲抵单位与付款供应商不一致", "success": false } { "code": 598814, "message": "预付单不可用(不存在/非已付讫/余额不足)", "success": false } ``` ## ⑨ 业务边界 - **可冲抵资格**:预付单须 `status=PAID`(钱真出了)且 `availableAmount>0` 且其「冲抵单位」= 付款供应商;未付讫(APPROVED 未出钱)不可勾选。 - **出纳只付差额**:付款金额锁死 = `actualPayAmount`,冲抵部分无现金流出、无资金流水。 - **回冲时机**:仅 PENDING 草稿可编辑/删除;驳回(REJECTED)、删除、改草稿均回冲预付余额并置 offset 行 CANCELLED。**付讫(PAID)后无回冲**——已消耗预付不回退(供应商退回走退回形态,只退现金)。 - **台账外手工单**:无台账锚点的手工付款单同样支持冲抵,行级/头表回写跳过(WARN),不影响付款主流程。 ## ⑩ 修改前后对比 | 项 | 修改前 | 修改后 | |---|---|---| | 付款方式 | 只能全额现金 | 可勾选预付冲抵,出纳付差额 | | `prepayOffsetAmount` | 恒 0 | 真值化 = Σ冲抵 | | `actualPayAmount` | 恒等于 amount | = amount − 冲抵 | | 详情 `offsets` | 无 | 返回冲抵明细及状态机 | ## ⑪ 影响评估 / 回滚 - **向后兼容**:不冲抵(不传 `prepayOffsets` 或传空)时行为与旧版完全一致(offset=0、actualPay=amount),旧前端不感知。 - **回滚**:代码回滚即恢复全额现金逻辑;已产生的冲抵数据(fin_prepay_offset)保留不影响。 ## ⑫ 注意事项 - 列表行 `PaymentRowRespVO` 暂未加 `prepayOffsetAmount`,列表「实付」列如需区分冲抵单,另提需求(详情已全量返回)。 - 冲抵不改变应付台账 `applied` 占用口径(建单占全额,付讫全额转 paid)。 ## ⑬ 关联 / 联系人 - Issue:https://git.1814.love:8443/wx/HL/issues/8127 - PR:#8134(地基)/ #8144(建单链路)/ #8145(付讫+回冲)/ #8146(收尾) - 负责人:yst