Files
shanxia2.0/docs/superpowers/specs/2026-06-25-pl-freight-settlement-design.md
T
2026-06-25 08:06:32 +08:00

450 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运费结算完善 — 设计文档
> **日期**: 2026-06-25
> **状态**: 已确认(2026-06-25)
> **前置**: `docs/运输计划管理-需求说明文档.md`(§10、BR-26~29、BR-40,**BR-27 由本设计 supersede**)、`docs/superpowers/specs/2026-06-25-pl-plan-archive-design.md`
> **界面模式**: P08 页内 Tab + P02 状态驱动列表 + P22 行内审批 + P13 财务确认发放
## 目标
完善【运输管理 → 运费结算】模块:按业务阶段拆分为 **四个 Tab**,支持从未结算计划组单、结算单多级审批(对齐合同管理)、审批完成后财务独立 Tab 登记发放,最终进入已结算归档态。
---
## 已确认决策(2026-06-25)
| # | 决策 | 说明 |
|---|------|------|
| D1 | **四 Tab 独立** | 未结算计划 / 结算单(草稿+审批中)/ 待财务发放 / 已结算 — 四个 Tab 各自独立,不用子筛选合并 |
| D2 | **审批对齐合同管理** | 提交时选审批人 + 顺序/会签/或签;独立审批记录表;**审批按钮仅在「待我审批」时出现在行操作列** |
| D3 | **财务发放独立 Tab** | `approved` 状态的结算单进入 Tab3「待财务发放」;财务在此 Tab 做发放登记(附件+备注),完成后 `confirmed` 进入 Tab4 |
| D4 | **组单以计划展示、运输落库** | Tab1 列表以**运输计划**维度展示(可含多车牌);生成结算单时落库关联 **transport_id**(复用 `pl_freight_settlement_transport`) |
| D5 | **未结算准入:已归档 + 有运费** | Tab1 仅展示 `archive_status=archived` 且 `has_freight=1`、且运输单尚未被非草稿结算单占用的计划 |
| D6 | **Supersede BR-27** | 原「不走工作流、计划专员 C 直接确认」作废;改为合同式多级审批 + 财务发放两阶段 |
---
## 非目标(本期不做)
- 接入通用 Python 工作流引擎(OpenSpec workflow 目标设计尚未落地)
- 结算单驳回后自动拆单/释放部分运输单(驳回整单回到草稿,关联运输单一并释放)
- 邮件/短信通知审批人(仅系统内操作)
- 运费结算报表导出
- 按归属公司隔离数据(延续 BR-51)
---
## 一、页面结构(四 Tab)
### 1.1 路由与菜单
| 项 | 值 |
|----|-----|
| 菜单名 | 运费结算(沿用) |
| Hash | `/#/pl/freight` |
| 组件 | `web-admin/src/views/pl/freight/index.vue`(重构为 Tab 容器 + 子面板) |
| menuCode | `pl:freight:list` |
### 1.2 Tab 定义
```
┌─────────────┬─────────────┬───────────────┬─────────────┐
│ 未结算计划 │ 结算单 │ 待财务发放 │ 已结算 │
│ Tab unsettled│ Tab bills │ Tab disburse │ Tab settled │
└─────────────┴─────────────┴───────────────┴─────────────┘
```
| Tab | `name` | 标题 | 结算状态 filter | 主要角色 |
|-----|--------|------|-----------------|----------|
| 1 | `unsettled` | 未结算计划 | —(查计划,非结算单) | 计划专员 |
| 2 | `bills` | 结算单 | `pending`, `approving` | 计划专员、审批人 |
| 3 | `disburse` | 待财务发放 | `approved` | 财务 |
| 4 | `settled` | 已结算 | `confirmed` | 全员只读 |
Tab 切换不走路由 query;默认激活 Tab1。各 Tab 独立筛选、分页、列表刷新。
### 1.3 权限按钮(新增/调整)
| 权限码 | 用途 |
|--------|------|
| `pl:freight:list` | 菜单 + 四 Tab 列表(已有) |
| `pl:freight:query` | 查看结算单/计划详情 |
| `pl:freight:create` | Tab1 多选组单 |
| `pl:freight:edit` | Tab2 草稿编辑、发票信息 |
| `pl:freight:submit` | Tab2 提交审批 |
| `pl:freight:approve` | Tab2 行内审批(后端校验是否当前节点审批人) |
| `pl:freight:withdraw` | Tab2 撤回审批(提交人) |
| `pl:freight:disburse` | Tab3 财务发放登记 |
| `pl:freight:disburse:list` | Tab3 列表(可选:与 disburse 合并,无 list 则 finance 角色仅见 Tab3 需菜单子权限设计 — **实现时 Tab3/4 仍受 list 控制,按钮用 disburse**) |
---
## 二、Tab1 — 未结算计划
### 2.1 数据范围(D5)
同时满足:
1. `pl_plan.archive_status = 'archived'`
2. `pl_plan.has_freight = 1`
3. `pl_plan.follow_status != 'cancelled'`
4. 计划已关联运输单(`transport_id` 非空)且 `pl_transport.freight_amount > 0`
5. 该 `transport_id` **未**出现在任何 `settlement_status NOT IN ('pending')` 且 `del_flag=0` 的结算单关联中
(若仅在 `pending` 草稿结算单中,视为「占用中」:Tab1 不展示,Tab2 草稿可编辑/删除后释放)
### 2.2 列表列(建议)
| 列 | 来源 |
|----|------|
| 多选 | `type="selection"` |
| 计划编号 | `planNo` |
| 计划日期 | `planDate` |
| 单据类型 | `planType` Tag |
| 客户/供应商 | `customerName` / `supplierName` |
| 车牌号 | `licensePlate` |
| 运输公司 | `transportCompany` |
| 运费(元) | `freightAmount` |
| 归档时间 | `updateTime` 或归档变更记录 |
筛选:计划编号、日期范围、客户、车牌号、单据类型(与运输计划列表 subset 对齐)。
### 2.3 工具栏与操作
| 控件 | 规则 |
|------|------|
| 已选 N 项 | 最前 |
| **生成结算单** | 唯一主色按钮;至少选 1 条;调用 `POST /transport/freight/` |
| 查看 | 行操作,打开计划只读查看(`PlPlanViewDialog` 或跳转 plan detail) |
**组单规则**:
- 一次多选生成 **一张** 结算单
- 后端校验:所有 plan 的 transport 均未锁定;租户一致
- `total_freight = sum(transport.freight_amount)`
- 创建后跳转/激活 **Tab2**,定位到新结算单
---
## 三、Tab2 — 结算单(草稿 + 审批中)
### 3.1 数据范围
`settlement_status IN ('pending', 'approving')`
可选 Tab 内筛选:结算单号、创建时间、提交人、状态(草稿/审批中)。
### 3.2 列表列
| 列 | 说明 |
|----|------|
| 结算单号 | 链接打开查看弹窗 |
| 运费合计 | `totalFreight` |
| 关联计划数 | 聚合 count |
| 车牌摘要 | 多车牌逗号分隔,tooltip 全量 |
| 结算状态 | Tag:`pending` 草稿 / `approving` 审批中 |
| 当前审批人 | `approving` 时展示当前节点审批人姓名 |
| 创建人 / 时间 | |
### 3.3 行操作(操作列 `fixed="right"`,`min-width ≥ 280`)
| 状态 | 查看 | 编辑 | 提交审批 | 撤回 | 审批 |
|------|------|------|----------|------|------|
| `pending` | ✅ | ✅(创建人或有 edit 权限) | ✅ | — | — |
| `approving` | ✅ | ❌ | — | ✅(提交人) | ✅ **仅 `canApprove=true`** |
**约束(D2)**:
- **禁止**在列表顶栏放批量审批
- 「审批」按钮必须带 `<el-icon>`,与规范一致
- `canApprove` 由列表/详情 API 返回(后端 `can_user_approve_settlement`)
### 3.4 草稿编辑弹窗
- 只读:结算单号、合计
- 可编辑:发票号、开票日期、发票金额、备注(`FreightSettlementUpdate` 已有字段)
- 明细区:关联计划/运输表格(只读);**本期不支持**草稿中增删关联行(需删单重建)
- 删除草稿:软删结算单 + 释放 transport 占用(新增 `DELETE /freight/{id}`,仅 `pending`)
### 3.5 提交审批弹窗
复用/抽取合同 `ApprovalDialog` 交互(`web-admin/src/views/crm/contract/components/ApprovalDialog.vue`):
| 字段 | 说明 |
|------|------|
| 审批类型 | 顺序 / 会签 / 或签 |
| 审批人 | 多选远程搜索用户 |
| 提交说明 | 可选 |
提交后:`pending → approving`,创建 `pl_freight_settlement_approval` 记录。
### 3.6 审批弹窗
- 审批意见(必填/选填与合同一致)
- 通过 / 驳回
- 驳回:`approving → pending`,审批记录置 rejected,**释放为可重新编辑草稿**(审批记录保留历史)
### 3.7 查看弹窗
- Hero:结算单号 + 状态 Tag + 运费合计
- Section:发票信息、关联运输/计划明细表
- Section:**审批流时间线**(对齐合同 `ContractDetailDialog` 审批区)
- Section:汇款凭证(`confirmed` 后在 Tab4 查看;Tab2 仅已发放前为空)
---
## 四、Tab3 — 待财务发放
### 4.1 数据范围
`settlement_status = 'approved'`
### 4.2 列表列
| 列 | 说明 |
|----|------|
| 结算单号 | |
| 运费合计 | |
| 审批完成时间 | 最后一节点 `approval_time` |
| 提交人 | |
| 关联计划数 / 车牌摘要 | |
| 状态 | Tag:待发放 |
### 4.3 行操作
| 操作 | 权限 | 说明 |
|------|------|------|
| 查看 | query | 只读详情 + 完整审批流 |
| **发放登记** | disburse | 打开财务确认弹窗 |
**无**审批按钮。
### 4.4 发放登记弹窗(P13 财务确认)
```
┌─────────────────────────────────────────────┐
│ 财务发放登记 · {settlementNo} [×] │
├─────────────────────────────────────────────┤
│ [只读] 结算单号、运费合计、关联明细摘要 │
│ [只读] 审批流时间线(已通过) │
├─────────────────────────────────────────────┤
│ 汇款日期 * │
│ 汇款金额 * │
│ 汇款凭证 * 附件卡片上传(多文件) │
│ 发放备注 textarea │
├─────────────────────────────────────────────┤
│ 取消 │ 确认发放 │
└─────────────────────────────────────────────┘
```
提交逻辑(单事务):
1. 校验 `settlement_status == approved`
2. 写入 `pl_remittance_voucher`(`voucher_urls` JSON 数组、`remittance_date`、`remittance_amount`、`remark`)
3. `settlement_status → confirmed`
4. `update_by` 当前财务用户
---
## 五、Tab4 — 已结算
### 5.1 数据范围
`settlement_status = 'confirmed'`
### 5.2 列表列
在 Tab3 基础上增加:汇款日期、汇款金额、财务确认人、确认时间。
### 5.3 行操作
仅 **查看**(含汇款凭证预览/下载)。
---
## 六、状态机
```
┌──────────┐
组单 │ pending │ 草稿 (Tab2)
───────────────▶│ │
└────┬─────┘
│ submit
▼
┌──────────┐
│approving │ 审批中 (Tab2)
└────┬─────┘
reject │ │ all approved
┌─────┘ └─────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ pending │ │ approved │ 待财务发放 (Tab3)
└──────────┘ └────┬─────┘
│ disburse
▼
┌──────────┐
│confirmed │ 已结算 (Tab4)
└──────────┘
```
### 6.1 字典 `pl_settlement_status`(更新)
| dict_value | dict_label | tag_type | 所在 Tab |
|------------|------------|----------|----------|
| `pending` | 草稿 | info | Tab2 |
| `approving` | 审批中 | warning | Tab2 |
| `approved` | 待发放 | primary | Tab3 |
| `confirmed` | 已结算 | success | Tab4 |
| `rejected` | 已驳回 | danger | 仅审批记录,结算单回到 pending |
移除或废弃旧标签「已提交」「已审核」与 API 中 `submitted` 的直接终态跳转。
### 6.2 运费锁定(与归档模块一致)
以下状态的结算单关联 transport **锁定运费修改**(`transport_has_locked_freight_settlement`):
`approving`, `approved`, `confirmed`
`pending` 草稿不锁定(删单可释放)。
---
## 七、后端设计
### 7.1 新表 `pl_freight_settlement_approval`
| 字段 | 类型 | 说明 |
|------|------|------|
| id | varchar(64) | PK |
| settlement_id | varchar(64) | 结算单 ID |
| step | int | 步骤序号 |
| approver_id | varchar(64) | 审批人 |
| approver_name | varchar(64) | 冗余 |
| approval_type | varchar(20) | sequential / parallel / or |
| approval_result | varchar(20) | pending / approved / rejected |
| approval_opinion | varchar(500) | |
| approval_time | datetime | |
| is_current | tinyint | 当前节点 |
| tenant_no + 公共字段 | | |
索引:`(settlement_id, del_flag)`, `(approver_id, approval_result, is_current)`
### 7.2 Service 层(新建 `pl_freight_settlement_service.py`)
| 函数 | 职责 |
|------|------|
| `list_unsettled_plans` | Tab1 查询 |
| `create_settlement_from_transports` | 组单 + 占用校验 |
| `submit_settlement_approval` | 创建审批链,`pending→approving` |
| `approve_settlement_step` | 单步通过/驳回 + 推进 |
| `withdraw_settlement_approval` | 撤回 |
| `can_user_approve_settlement` | 当前用户是否可审 |
| `disburse_settlement` | 财务发放 + voucher |
| `delete_draft_settlement` | 软删草稿 |
CRUD 参考:`backend/app/crud/crm/contract_approval.py`(勿直接依赖 CRM 表)。
### 7.3 API 清单
| Method | Path | 说明 |
|--------|------|------|
| GET | `/transport/freight/unsettled-plans` | Tab1 分页列表 |
| GET | `/transport/freight/` | Tab2/3/4;增加 `tab` 或 `settlementStatus` 多值 |
| POST | `/transport/freight/` | 组单 `{ planIds }` 或 `{ transportIds }`(**推荐 planIds**,服务端解析 transport) |
| GET | `/transport/freight/{id}` | 详情 + transports + plans 摘要 + `canApprove` |
| PUT | `/transport/freight/{id}` | 更新草稿 |
| DELETE | `/transport/freight/{id}` | 删除草稿 |
| POST | `/transport/freight/{id}/submit-approval` | 提交审批 body 同合同 |
| POST | `/transport/freight/approval/{approvalId}` | 通过/驳回 |
| POST | `/transport/freight/{id}/withdraw-approval` | 撤回 |
| GET | `/transport/freight/{id}/approval-flow` | 审批时间线 |
| POST | `/transport/freight/{id}/disburse` | 财务发放 body: remittanceDate, amount, voucherUrls, remark |
**废弃/替换**:
- `POST /{id}/submit` → 由 `submit-approval` 替代
- `POST /{id}/approve` → 由 `approval/{id}` 替代
- `POST /voucher` → 合并进 `disburse` 或内部调用
列表项返回 `canApprove: boolean`(当前用户 + 当前 settlement)。
### 7.4 审批规则(对齐合同)
| 类型 | 规则 |
|------|------|
| sequential | 按选择顺序,仅 `is_current=1` 且 pending 的一人可审 |
| parallel | 所有 pending 节点需全部通过 |
| or | 任一人通过即通过 |
全部通过后:`settlement_status → approved`,进入 Tab3。
---
## 八、前端文件规划
| 文件 | 动作 |
|------|------|
| `views/pl/freight/index.vue` | 重构为 Tab 容器 |
| `views/pl/freight/components/UnsettledPlanPanel.vue` | Tab1 |
| `views/pl/freight/components/SettlementBillPanel.vue` | Tab2 |
| `views/pl/freight/components/DisbursePanel.vue` | Tab3 |
| `views/pl/freight/components/SettledPanel.vue` | Tab4 |
| `components/business/pl-freight-settlement-view-dialog/index.vue` | 查看弹窗 |
| `components/business/pl-freight-settlement-form-dialog/index.vue` | 草稿编辑 |
| `components/business/pl-freight-approval-dialog/index.vue` | 提交/审批(可 fork 合同 ApprovalDialog) |
| `components/business/pl-freight-disburse-dialog/index.vue` | 财务发放 |
| `api/pl/index.ts` | 扩展 freightApi |
---
## 九、与现有模块衔接
| 模块 | 衔接点 |
|------|--------|
| 运输计划归档 | Tab1 要求已归档;归档弹窗改运费仍受结算锁定约束 |
| 合同管理 | 审批 UX、API 语义对齐 |
| 运输计划列表 | 不新增结算入口,结算统一在 freight 模块 |
| 需求文档 | 实现后更新 §10.1、BR-27、BR-40、附录 C |
---
## 十、验收清单
| # | 场景 | 预期 |
|---|------|------|
| V1 | 四 Tab 切换 | 各自列表/筛选/分页独立正常 |
| V2 | Tab1 仅已归档有运费且未占用计划 | 不符合不出现 |
| V3 | 多选组单 | 生成一张草稿,出现在 Tab2,Tab1 对应行消失 |
| V4 | Tab2 草稿编辑发票 | 保存成功 |
| V5 | 提交审批 | 进入 approving,审批流生成 |
| V6 | 非当前审批人 | Tab2 **无**审批按钮 |
| V7 | 当前审批人 | 行操作列出现审批,通过后流转 |
| V8 | 全部审批通过 | 结算单出现在 Tab3,Tab2 消失 |
| V9 | Tab3 发放登记 | 上传凭证+备注,进入 Tab4 |
| V10 | Tab4 查看 | 只读,含凭证与审批流 |
| V11 | 审批中锁定 | 归档页不可改关联 plan 运费 |
| V12 | 驳回 | 回 Tab2 草稿,可再提交 |
| V13 | 权限 | 无 `disburse` 不见 Tab3 发放按钮 |
---
## 十一、实施阶段建议
| Phase | 范围 |
|-------|------|
| **P1** | 四 Tab 骨架 + Tab1 未结算 + 组单 + Tab4 只读列表 |
| **P2** | Tab2 草稿 CRUD + 查看弹窗 |
| **P3** | 审批表 + 提交/审批/撤回 + `canApprove` 行按钮 |
| **P4** | Tab3 财务发放 + voucher + 状态 confirmed |
| **P5** | 菜单权限 SQL、字典更新、归档锁定联调、文档修订 |
---
## 十二、Open Questions(实现前默认可按本文)
| # | 问题 | 默认 |
|---|------|------|
| Q1 | Tab1 是否必须已归档 | **是**(D5) |
| Q2 | 草稿能否删单 | **是**,仅 pending |
| Q3 | 驳回后审批历史 | **保留**记录,结算单回 pending |
| Q4 | 发放金额是否必须等于运费合计 | **允许差异**,备注必填当不等 |