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

319 lines
15 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
> **状态**: 待审阅
> **前置**: `docs/运输计划管理-需求说明文档.md`、`docs/superpowers/specs/2026-06-21-pl-arrange-transport-refine-design.md`、`openspec/changes/transport-plan-management/specs/transport-plan/spec.md`
> **界面模式**: P06 复杂业务列表 + 弹窗表单;归档弹窗上区编辑、下区只读对照(对齐 `PlPlanArrangeDialog`)
## 目标
在【运输管理】下新增菜单【运输计划归档】,提供与【运输计划】相同结构的列表与检索;操作列仅 **查看 / 编辑 / 归档**。归档通过专用弹窗一次性完成:**填写产品实际数量 → 调整运费 → 执行归档**。原【运输计划】页移除全部归档入口,归档职责集中到本菜单。
---
## 已确认决策(2026-06-25)
| # | 决策 | 说明 |
|---|------|------|
| D1 | **列表展示全部计划** | 默认不过滤 `archiveStatus`;已归档计划仍保留在列表中作为历史可查 |
| D2 | **已归档也可调整** | 已归档行仍显示「查看 / 编辑 / 调整」;「归档」按钮在已归档时文案改为「调整」,打开同一弹窗修改 actualQty 与运费 |
| D2a | **归档页编辑不受归档态限制** | 本菜单内 `PlPlanFormDialog` 对已归档计划可打开;后端 `update_plan` 移除「已归档不可修改」阻断(运输计划页仍通过 UI 隐藏已归档行的编辑入口) |
| D3 | **原页移除归档入口** | `views/pl/plan/index.vue` 去掉「批量归档」;`views/pl/plan/detail.vue` 去掉「归档」按钮;归档只在新区操作 |
| D4 | **弹窗布局对齐安排运输** | 上区:归档表单;下区:完整 `PlPlanViewTabs`(计划概况 / 运输信息含司机·现场链接 / 合同信息) |
| D5 | **归档 API 增强** | 单次请求原子完成:写 `actual_qty` + 同步入库/出库数与金额 + 更新运费 + 改归档/完成状态 |
| D6 | **列表/筛选与运输计划一致** | 列、筛选字段、分页行为与 `/#/pl/plan` 相同;可额外增加「归档状态」筛选项(展开区) |
---
## 非目标(本期不做)
- 跟进状态 12 态完整流转(仍用现有 `draft/processing/completed/cancelled` 简化枚举;归档时将 `follow_status` 置为 `completed`)
- 归档后允许反归档 / 撤销
- 运费结算单已生成后的运费修订流程(见约束:有关联结算且非草稿则阻断改运费)
- OpenSpec 独立 change 归档(可在实现后补 `openspec/changes/` 条目)
---
## 一、页面与路由
### 1.1 新菜单
| 项 | 值 |
|----|-----|
| 菜单名 | 运输计划归档 |
| 路由 path | `plan-archive` |
| Hash | `/#/pl/plan-archive` |
| 组件 | `web-admin/src/views/pl/plan-archive/index.vue` |
| 列表标题 | `运输计划归档`(写入 `App.vue` → `panelTitleBarTextRules`) |
| menuCode | `pl:plan-archive:list` |
| 排序 | 紧接「运输计划」之后,`order_num=10.5` 或 `11`(危化品证上传顺延) |
### 1.2 权限按钮
| 权限码 | 用途 |
|--------|------|
| `pl:plan-archive:list` | 菜单 + 列表 |
| `pl:plan-archive:query` | 查看弹窗 |
| `pl:plan-archive:edit` | 编辑弹窗(含已归档) |
| `pl:plan-archive:archive` | 打开归档/调整弹窗 + 提交(含已归档调整) |
编辑可复用现有 `PlPlanFormDialog` + `pl:plan:edit` 或映射到 `pl:plan-archive:edit`(实现时二选一,推荐归档页独立权限,后端 update 仍走 `/transport/plan/{id}`)。
### 1.3 列表行为(D1 / D2)
- **数据范围**:调用同一 `planApi.list`,默认不传 `archiveStatus`(全部)。
- **筛选**:与运输计划相同 6 项;**展开区增加「归档状态」**(未归档 / 已归档 / 全部)。
- **表格列**:与 `views/pl/plan/index.vue` 一致(计划编号、日期、类型、客户、产品、证书、跟进/运输状态、金额、车牌、司机、跟进人等)。
- **操作列** `min-width` 约 260(3 个 link 按钮),`fixed="right"`。
| 条件 | 查看 | 编辑 | 归档 / 调整 |
|------|------|------|-------------|
| `followStatus !== 'cancelled'` | ✅ | ✅ | ✅(未归档文案「归档」,已归档文案「调整」) |
| `followStatus === 'cancelled'` | ✅ | ❌ | ❌ |
### 1.4 工具栏
| 按钮 | 规则 |
|------|------|
| 已选 N 项 | 保留 |
| 批量归档 | 保留;仅对选中行中未归档且未作废的执行;已归档跳过并汇总提示 |
| 新增 / 删除 / 证书 / 安排 | **不出现** |
### 1.5 原运输计划页改动(D3)
| 文件 | 改动 |
|------|------|
| `views/pl/plan/index.vue` | 删除「批量归档」按钮及 `handleBatchArchive`;删除 `FolderChecked` 导入(若无他用) |
| `views/pl/plan/detail.vue` | 删除「归档」按钮及 `handleArchive` |
---
## 二、归档弹窗(`PlPlanArchiveDialog`)
### 2.1 布局
```
┌──────────────────────────────────────────────────────────────┐
│ 运输计划归档 · {planNo} [×] │
├──────────────────────────────────────────────────────────────┤
│ ▲ 上区:归档操作(max-height ~42vh,可滚动) │
│ [产品实际数量] PlPlanFormProductTable │
│ showActualQty + editableQty(仅 actualQty) │
│ [运费信息] 复用安排运输区块(只读 hasFreight 来自计划)│
│ [归档说明] 可选 remark(写入 plan.remark 或仅展示) │
├──────────────────────────────────────────────────────────────┤
│ ▼ 下区:计划单查看(flex:1,min-height 280px) │
│ PlPlanViewTabs embedded — 三 Tab 与查看/安排弹窗一致 │
└──────────────────────────────────────────────────────────────┘
│ 取消 │ 确认归档 / 确认保存 │
```
**模式切换**:
| 模式 | 标题后缀 | 主按钮 | 提交语义 |
|------|----------|--------|----------|
| 首次归档 | `运输计划归档 · {planNo}` | 确认归档 | 写数量/运费 + 置 `archived` |
| 已归档调整 | `调整归档内容 · {planNo}` | 确认保存 | 仅写数量/运费,**不改变** `archive_status` / `complete_status` |
**尺寸与规范**:`dialog-form-layout`、`align-center`、`width: min(1680px, calc(100vw - 80px))`、`modal-class: order-module-dialog-mask`、`backdrop-filter: none`、表单 `label-position="top"`;底栏 Close / Check 图标间距 6px。
### 2.2 上区 — 产品实际数量
- 使用 `PlPlanFormProductTable`:`show-actual-qty`、`editable-qty`;**计划数量列只读**(不传 editable 到 quantity 或单独 prop `editableActualQtyOnly`)。
- 初始值:`actualQty ?? quantity ?? inboundEstimateQty ?? outboundEstimateQty ?? 0`(与 `usePlPlanViewDetail` 一致)。
- 校验:每行 `actualQty >= 0` 必填;至少一行有有效数量。
- 提交后后端按 `plan_type` 同步:
- `inbound`:`inbound_qty = actual_qty`,`inbound_amount = inbound_qty × inbound_price`(价缺省用 `tax_price`)
- `outbound`:`outbound_qty = actual_qty`,`outbound_amount = outbound_qty × outbound_price`
- `direct`:仅写 `actual_qty`,重算 `tax_subtotal` 若业务需要(按 quantity 单价比例,或保持计划金额不变 — **本期以 actual_qty 落库为主,金额字段按 inbound/outbound 规则**)
### 2.3 上区 — 运费信息
复用 `PlPlanArrangeDialog` 运费区块 + `usePlFreightCalc`:
| 字段 | 规则 |
|------|------|
| `hasFreight` | 来自计划,只读 |
| `distanceKm` | 可编辑;预填 transport / plan |
| `freightType` | `by_weight` / `by_trip` |
| 运输量 | `by_weight` 时默认 **actualQty 汇总**(`sumProductActualQuantities`) |
| `freightPrice` / `freightAmount` | 可编辑;联动计算可手动覆盖 |
| `hasInvoice` | 可编辑 |
`hasFreight=0` 时显示「本计划无运输费用」,跳过运费写入。
### 2.4 下区 — 只读对照
- 完整嵌入 `PlPlanViewTabs`(`embedded`),含:
- 计划概况(含产品 actualQty 展示列)
- 运输信息(车辆、物流、运费、**司机链接复制**、**现场链接复制**、回传、地磅)
- 合同信息
- 提交成功后 `tabsRef.reload()` 刷新下区。
### 2.5 入口与禁用
- `followStatus !== 'cancelled'` 均可打开(**含已归档**)。
- 打开时 `planApi.get` + 若已安排则读 transport 运费预填;已归档时进入「调整」模式。
---
## 三、后端 API
### 3.1 增强归档接口
**方案**:扩展 `POST /transport/plan/{id}/archive`,请求体可选;无 body 时保持向后兼容(仅改状态,供脚本/旧调用),**前端归档弹窗必须带 body**。
**Request `PlanArchiveBody`**:
```json
{
"products": [
{ "id": "计划产品明细ID", "actualQty": 4.7 }
],
"freight": {
"distanceKm": 120.5,
"freightType": "by_weight",
"freightPrice": 50,
"freightAmount": 235,
"hasInvoice": 1
}
}
```
**处理顺序(单事务)**:
1. 校验:计划存在、租户、`follow_status != cancelled`
2. 校验:每个 `products[].id` 属于该计划
3. 更新 `pl_plan_product.actual_qty`;按 `plan_type` 写 `inbound_qty/outbound_qty` 及金额
4. 若 `plan.has_freight == 1` 且 body.freight 存在:
- 若存在 `plan.transport_id` → 更新 `pl_transport` 运费字段 + `transport_qty`
- 更新 `plan.freight_amount`
- **若运输单已纳入非草稿运费结算** → 400 阻断(查 `pl_freight_settlement` 关联)
5. **状态分支**:
- 若 `archive_status != archived`:置 `archive_status=archived`、`complete_status=yes`、`follow_status=completed`
- 若 **已归档**:跳过状态字段,仅更新业务数据(D2)
6. 写 `pl_plan_change`:`change_type=archive`(首次)或 `archive_adjust`(已归档调整)
7. commit
**`update_plan`(编辑弹窗)**:移除 `archive_status == archived` 时的 400 阻断;已归档计划允许通过 PUT 修改计划主表/产品(与归档弹窗互补:表单改计划信息,归档弹窗改 actualQty/运费)。
**Response**:`PlanWithProducts`(与 get 一致)
### 3.2 Schema 新增
文件:`backend/app/schemas/pl/plan.py`
- `PlanArchiveProductItem`:`id`, `actualQty`
- `PlanArchiveFreightBlock`:与 transport arrange 运费字段子集一致
- `PlanArchiveBody`:`products`, `freight?`
### 3.3 Service 抽取
在 `pl_plan_service.py` 新增:
- `apply_archive_product_quantities(plan, products_payload)` — 写 actual + inbound/outbound
- `apply_archive_freight(plan, transport, freight_payload, products)` — 运费同步
便于单测与 `archive_plan` 路由保持薄。
---
## 四、前端文件清单
| 文件 | 动作 |
|------|------|
| `views/pl/plan-archive/index.vue` | **新建** — 列表页 |
| `components/business/pl-plan-archive-dialog/index.vue` | **新建** — 归档弹窗 |
| `composables/usePlPlanList.ts` | **新建(推荐)** — 从 plan/index 抽取 list/search/pagination |
| `views/pl/plan/index.vue` | 删除批量归档;可选改用 composable |
| `views/pl/plan/detail.vue` | 删除归档按钮 |
| `router/modules/pl.ts` | 新增 plan-archive 路由 |
| `api/pl/index.ts` | `planApi.archive(id, body)` 扩展 |
| `utils/router/menu-icon.ts` | `pl:plan-archive:list` 图标 |
| `App.vue` | `panelTitleBarTextRules` 增加「运输计划归档」 |
| `docs/sql/shanxia2.0.sql` | `sys_menu` 插入归档菜单 + 按钮权限 |
| `backend/app/api/pl/plan.py` | 增强 `archive_plan`;`update_plan` 允许已归档编辑 |
| `backend/app/schemas/pl/plan.py` | Archive body schema |
| `backend/app/services/pl_plan_service.py` | archive 业务函数 |
### 4.1 组件复用策略
| 组件 | 用途 |
|------|------|
| `PlPlanViewDialog` | 查看 |
| `PlPlanFormDialog` | 编辑 |
| `PlPlanViewTabs` | 归档弹窗下区 |
| `PlPlanFormProductTable` | 归档弹窗 actualQty(可增 prop `actualQtyEditableOnly`) |
| `usePlFreightCalc` | 归档弹窗运费 |
---
## 五、数据库与菜单 SQL
```sql
-- 菜单(示例 ID,实现时按现有 PL_* 规范)
INSERT INTO sys_menu (...) VALUES
('PL_PLAN_ARCHIVE', '运输计划归档', 'PL_MENU_ROOT', 'C', 'plan-archive', 'pl/plan-archive/index', 'pl:plan-archive:list', ...);
-- 按钮权限(F 类型,parent=PL_PLAN_ARCHIVE)
-- pl:plan-archive:query | edit | archive
```
---
## 六、安全与约束
| 项 | 规则 |
|----|------|
| 已归档 | 可继续「编辑」与「调整」actualQty/运费;不可反归档(`archive_status` 不回退为 `unarchived`) |
| 已作废 | 不可归档 / 不可调整 |
| 运费结算 | 运输单若已在「已提交/审核通过/已确认」结算单中,禁止归档时改运费 |
| 无 transport | `has_freight=1` 但无 `transport_id` 时,仅更新 `plan.freight_amount`,并 warn 日志(或 400 要求先安排运输 — **推荐 400:有运费必须先安排运输**) |
| 租户 | 所有读写 `tenant_no` |
| 事务 | 产品 + 运费 + 归档同一 commit |
---
## 七、验收标准
| # | 场景 | 预期 |
|---|------|------|
| V1 | 打开归档列表 | 展示全部计划;筛选与运输计划一致;多「归档状态」筛选项 |
| V2 | 未归档行 | 查看 / 编辑 / 归档 均可用 |
| V3 | 已归档行 | 查看 / 编辑 / 「调整」均可用 |
| V4 | 打开归档/调整弹窗 | 上区 actualQty、运费可编辑;下区三 Tab 完整 |
| V5 | 首次提交归档 | actual_qty、运费、archive/complete 状态一次成功 |
| V6 | 已归档后再次「调整」 | 仅更新 actual_qty/运费/金额;`archive_status` 保持 `archived` |
| V6a | 已归档「编辑」 | `PlPlanFormDialog` 可保存计划主表/产品变更 |
| V7 | 原运输计划页 | 无批量归档、详情无归档按钮 |
| V8 | 批量归档 | 仅处理未归档行;已归档跳过有提示 |
| V9 | 有运费未安排运输 | 归档 400,提示先安排运输 |
| V10 | 按重量运费 | 运输量默认 actualQty 合计,改 actualQty 后运费量联动 |
| V11 | 下区链接 | 司机/现场链接可复制(与安排运输下区一致) |
| V12 | 权限 | 无 `pl:plan-archive:archive` 不显示归档按钮 |
---
## 八、实施分期
| Phase | 内容 |
|-------|------|
| **P1** | 后端:`PlanArchiveBody` + service + 增强 `archive_plan` |
| **P2** | 前端:`PlPlanArchiveDialog` + `plan-archive/index.vue` + 路由/菜单/API |
| **P3** | 原页移除归档入口;抽取 `usePlPlanList`(可选) |
| **P4** | 联调验收 V1–V12 |
---
## 九、参考路径
| 说明 | 路径 |
|------|------|
| 运输计划列表 | `web-admin/src/views/pl/plan/index.vue` |
| 安排运输弹窗(布局基准) | `web-admin/src/components/business/pl-plan-arrange-dialog/index.vue` |
| 下区三 Tab | `web-admin/src/components/business/pl-plan-view-tabs/index.vue` |
| 产品表 | `web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue` |
| 运费计算 | `web-admin/src/composables/usePlFreightCalc.ts` |
| 现归档 API | `backend/app/api/pl/plan.py` → `archive_plan` |
| 需求 BR-15/32/42 | `docs/运输计划管理-需求说明文档.md` |