Files
zkxw/docs/superpowers/specs/2026-06-17-protocol-invoice-request-design.md
T
2026-06-17 18:16:23 +08:00

437 lines
19 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-17
**状态**:已实现
**需求来源**:客户在「满月待办管理」中新增开票申请;财务在「缴退费财务确认」中处理并回传发票。
**关联页面**:
- `web-admin/src/views/cell-storage/protocol-audit/FullmoonTodoTab.vue`(业务发起 + 查看)
- `web-admin/src/views/cell-storage/payment-confirm/index.vue`(新增 Tab)
- 新建:`web-admin/src/views/cell-storage/payment-confirm/InvoiceRequestTab.vue`
**参考实现**:
- 满月待办办理保险:`FullmoonTodoTab.vue`
- 财务退费确认:`RefundConfirmTab.vue`
- 财务附件上传:`ProtocolFinanceConfirmFields` + `finance-attachments.ts`
- 渠道/体检机构开票信息字段:`biz_channel_invoice` / `biz_checkup_institution_invoice`
---
## 1. 背景与目标
细胞存储「采集协议单管理」→「满月待办管理」目前已有保险办理能力。客户希望对**已财务确认的缴费**发起开票申请,由财务集中开票并上传发票附件,业务侧可查看反馈。
**目标**:
1. 业务人员在满月待办中对当前协议单下**已缴费**明细发起开票申请。
2. 开票金额 = 所选缴费明细**实缴金额之和**(`Σ transfer_amount`),明细行金额只读,合计只读。
3. **多期可合并为一张发票**:一次申请可勾选多期,生成 **1 条开票申请**;财务上传 **1 份**发票附件覆盖该申请下所有期数。
4. 已纳入有效申请(待开票/已开票)的缴费明细不可再次勾选。
4. 财务人员在「缴退费财务确认」新增「开票申请」Tab,处理待开票申请并上传发票附件。
5. 业务人员可查看申请状态及财务回传的发票附件。
**已确认的产品决策**:
| 议题 | 决策 |
|------|------|
| 申请时开票信息(抬头、税号等) | 提供完整字段区块,**全部选填** |
| 财务完成开票 | **仅上传发票附件 + 财务备注**,不填发票号/开票日期等结构化字段 |
| 申请与缴费关系 | **主表 + 明细子表**:一次申请可含 1~N 条缴费明细,**合并开一张发票**;子表保证每条明细不重复纳入 |
| 财务驳回流程 | **本期不做**;仅 `待开票` → `已开票` 两态 |
---
## 2. 现有系统锚点
### 2.1 数据模型
```
biz_collection_protocol(采集协议单)
└── biz_protocol_payment(缴费主表)
└── biz_protocol_payment_detail(缴费明细)
└── biz_protocol_invoice_request_item(开票申请明细,N:1)
└── biz_protocol_invoice_request(开票申请主表,一张发票)
```
### 2.2 页面入口
| 角色 | 菜单路径 | 现状 |
|------|----------|------|
| 业务 | 细胞存储 → 采集协议单管理 → 满月待办管理 | 列表 + 办理保险 |
| 财务 | 细胞存储 → 缴退费财务确认 | Tab:缴费确认 \| 退费确认 |
### 2.3 与需求文档关系
`docs/中科兴皖需求V0.3.md` §2.2 已描述缴费确认、退费确认、满月保险,**未描述开票申请**。本设计为新增能力,不改变既有缴退费状态机。
---
## 3. 状态与流转
### 3.1 开票申请状态(新)
| 状态码 | 含义 | 触发 |
|--------|------|------|
| `dkp` | 待开票 | 业务提交开票申请 |
| `ykp` | 已开票 | 财务上传发票附件并确认 |
### 3.2 与缴费状态关系
- **可申请**:`biz_protocol_payment_detail.pay_status = yjf`(已缴费)
- **不可申请**:`dfj`、`jfqrz`、`tfqrz`、`ytf`,或该明细已出现在任一有效申请的**子表**中(主表 `del_flag=0` 且 `status IN (dkp, ykp)`)
- 缴费状态流转**不因开票申请而改变**;开票是独立子流程
### 3.3 流程图
```
业务(满月待办 · 同一弹窗)
→ 多选 1~N 条可开票缴费明细 + 选填开票信息
→ 一次提交 → 生成 1 条 dkp 申请(含 N 条明细子表)
→ 主表 invoice_amount = 各期实缴之和
财务(缴退费财务确认 · 开票申请 Tab)
→ 待开票列表(一条申请对应一张待开发票)
→ 查看申请内各期明细 + 合计金额
→ 上传 1 份发票附件 + 财务备注 → status=ykp
```
---
## 4. 业务规则
### 4.1 可开票缴费明细条件(后端强校验)
同时满足:
1. `payment_detail.collection_protocol_id` = 当前协议单
2. `pay_status = yjf`
3. 该 `payment_detail_id` 未出现在任何有效申请的子表 `biz_protocol_invoice_request_item` 中
4. `tenant_no` 与当前用户租户一致
### 4.2 开票金额
- **明细行**:各期 `transfer_amount`,申请时只读
- **申请合计**:`invoice_amount` = 所选明细 `transfer_amount` 之和,主表只读展示
- 子表每行写入 `line_amount` 快照;主表 `invoice_amount` = Σ `line_amount`
- 后端校验:主表金额必须等于子表行金额之和,且每行等于对应明细当前 `transfer_amount`
### 4.3 分期、趸交与合并开票
| 场景 | 行为 |
|------|------|
| 趸交(dj) | 1 条 yjf 明细 → 1 条申请、1 行子表 |
| 分期(fq) | 可只开第 1 期;也可**多选多期一次提交**,合并为 **1 条申请、1 张发票** |
| 一次多选 N 期 | 1 条主表申请 + N 行子表;财务待办 **1 条**;发票附件 **1 份** |
| 已开过票的期数 | 不可再勾选(已在其他有效申请的子表中) |
| 剩余期数 | 可另起一次新申请(例如第 1~2 期已开,第 3 期单独再申请) |
弹窗展示:**已选 N 期 · 合计开票金额 ¥xxx**。
### 4.4 申请侧表单字段
**必选**:
- `payment_detail_ids`(多选,至少 1 条;`el-table` 勾选列)
**选填**(写入主表申请):
| 展示标签 | 字段 | 说明 |
|----------|------|------|
| 发票抬头 | `invoice_title` | 对齐渠道开票信息 |
| 税号 | `tax_no` | |
| 单位地址 | `company_address` | |
| 电话 | `company_phone` | |
| 开户银行 | `bank_name` | |
| 银行账号 | `bank_account` | |
| 申请备注 | `apply_remark` | |
弹窗中同步只读展示:客户姓名、协议号、期数、应缴/实缴金额、转账时间、转账方式。
### 4.5 财务处理字段
**必填**:
- `invoice_attachments`(至少 1 个附件,逗号分隔 MinIO URL)
**选填**:
- `finance_remark`
**不提供**:发票号码、开票日期、发票类型等输入项。
### 4.6 退费交叉(本期约定)
- `ytf`(已退费)明细**不可**出现在可开票列表
- 若某期 `ykp` 后发生退费:本期**不实现**发票冲红/作废;由线下处理(范围外)
---
## 5. 数据库设计
### 5.1 主表 `biz_protocol_invoice_request`
一张物理发票对应一条申请主记录。
```sql
CREATE TABLE `biz_protocol_invoice_request` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`collection_protocol_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '采集协议单ID',
`invoice_amount` decimal(18,2) NOT NULL COMMENT '开票合计金额(子表实缴之和)',
`invoice_title` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '发票抬头',
`tax_no` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '税号',
`company_address` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '单位地址',
`company_phone` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
`bank_name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '开户银行',
`bank_account` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '银行账号',
`apply_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '申请备注',
`invoice_attachments` text COLLATE utf8mb4_unicode_ci COMMENT '财务上传发票附件逗号分隔URL',
`finance_remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '财务备注',
`status` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'dkp' COMMENT 'dkp待开票 ykp已开票',
`finance_handle_time` datetime DEFAULT NULL COMMENT '财务处理时间',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_inv_req_protocol` (`collection_protocol_id`,`del_flag`),
KEY `idx_inv_req_status` (`tenant_no`,`status`,`del_flag`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单开票申请主表';
```
### 5.2 子表 `biz_protocol_invoice_request_item`
申请与缴费明细的多对一关联;**每条缴费明细在有效申请中最多出现一次**。
```sql
CREATE TABLE `biz_protocol_invoice_request_item` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`invoice_request_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '开票申请主表ID',
`payment_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '缴费主表ID',
`payment_detail_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '缴费明细ID',
`installment_no` int(11) DEFAULT NULL COMMENT '期数快照,趸交为空',
`line_amount` decimal(18,2) NOT NULL COMMENT '该行开票金额(实缴快照)',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_inv_item_req` (`invoice_request_id`,`del_flag`),
KEY `idx_inv_item_detail` (`payment_detail_id`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单开票申请明细';
```
**唯一性**:创建时校验 `payment_detail_id` 不得存在于任何有效主表(`status IN (dkp,ykp)`, `del_flag=0`)下的有效子表行中。
### 5.3 查找与对账
- 按协议单查历史:主表 `collection_protocol_id`
- 判断某期是否已开票:子表 `payment_detail_id` + 关联主表状态
- 列表展示 `installmentSummary`(如「第1、2、3期」)及 `itemCount`,详情展开子表明细
### 5.4 字段顺序
遵循 `database-conventions`:业务字段 → 公共字段。
---
## 6. 后端 API
路由注册:`app/api/collection_protocol.py`(或独立 `protocol_invoice.py`,与现有缴退费接口同模块)。
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/web/admin/collection-protocols/{protocolId}/invoice-eligible-payments` | 可开票缴费明细列表 |
| GET | `/web/admin/collection-protocols/{protocolId}/invoice-requests` | 某协议单开票申请历史 |
| POST | `/web/admin/collection-protocols/{protocolId}/invoice-requests` | 提交开票申请(`paymentDetailIds[]` → 1 主表 + N 子表) |
| GET | `/web/admin/protocol-invoice-requests` | 财务列表(`status=dkp\|ykp`,分页筛选) |
| GET | `/web/admin/protocol-invoice-requests/pending-count` | 待开票数量(Tab 角标) |
| PUT | `/web/admin/protocol-invoice-requests/{id}/complete` | 财务确认已开票 |
### 6.1 列表筛选(财务)
- `status`:`dkp` / `ykp`
- `keyword`:母亲姓名、身份证号、新生儿姓名、新生儿身份证号(与缴退费财务确认一致)
- `create_time` 范围(可选)
### 6.2 列表返回扩展字段
关联协议单与客户信息,供列表/卡片展示:
- `motherName`、`motherIdCard`、`newbornName`、`protocolNo`
- `invoiceAmount`(合计)、`itemCount`、`installmentSummary`(如「第1、2、3期」)
- `items[]`:`installmentNo`、`lineAmount`、`transferTime`、`transferType` 等
- `status`、`createTime`、`financeHandleTime`
- 申请侧开票信息、发票附件列表(已开票时)
### 6.3 校验清单
- 租户隔离:`tenant_no` 来自当前用户
- 数据权限:`apply_data_scope` 对齐 `list_receipt_confirmed_details`
- 创建:`paymentDetailIds` 非空、无重复;每条明细满足 §4.1;同事务插入 1 主表 + N 子表;金额校验见 §4.2
- 完成:仅 `dkp` 可完成;`invoice_attachments` 非空
### 6.4 `_row_to_dict` 与 Schema
- CRUD `_invoice_request_to_dict` + `_invoice_request_item_to_dict`;详情/列表返回嵌套 `items`
- CreateBody / CompleteBody Pydantic 与前端 `formData` 对齐
- 完成后运行 `python scripts/check_field_consistency.py`
---
## 7. 前端设计
### 7.1 界面模式
| 区域 | 模式 | 参考 |
|------|------|------|
| 满月待办开票 | P06 复杂列表 + 弹窗 | `FullmoonTodoTab.vue` |
| 财务开票 Tab | P21 财务确认双区(待办 + 已办) | `RefundConfirmTab.vue` |
### 7.2 满月待办 `FullmoonTodoTab.vue`
**操作列**(`fixed="right"`,`min-width` 增至约 320px):
- 查看(已有)
- 办理保险(已有)
- **开票申请**(单一入口,**始终可点**;申请与查看均在同一弹窗完成)
> **交互决策(2026-06-17)**:不单独提供「查看开票」按钮。用户只点「开票申请」,弹窗内上半区查看历史、下半区发起新申请。
**统一弹窗「开票申请」**(`dialog-form-layout`,建议 `width` 约 800~900px):
打开时并行请求:
- `GET .../invoice-requests`(历史)
- `GET .../invoice-eligible-payments`(可新开票的缴费明细)
弹窗自上而下分三块:
| 区块 | 内容 | 说明 |
|------|------|------|
| **摘要** | 只读:客户姓名、协议号、新生儿等 | 固定顶部 |
| **申请记录** | `el-table` | 申请时间、**包含期数**(`installmentSummary`)、**合计金额**、状态 Tag、财务处理时间;`ykp` 展示发票附件;可展开查看各行期数/金额 |
| **新增申请** | 表单区 | 有可开票明细时展示;否则展示提示「暂无可开票缴费记录」并隐藏提交 |
**新增申请表单**(`label-position="top"`、`form-grid-2`):
1. 可开票缴费明细(`el-table` **多选**勾选列;已开过票 / 非 `yjf` 的行不可选)
2. 已选汇总(`已选 N 条 · 合计 ¥xxx`,只读)
3. 开票信息 6 字段(均选填)
4. 申请备注
**底栏**:取消 / 提交申请(生成 **1 条**合并申请)
**列表列(可选增强)**:
- 「开票进度」如 `1/3 已开票`,非必须首期实现
### 7.3 缴退费财务确认
**`payment-confirm/index.vue`**:
- Tab 顺序:缴费确认 | 退费确认 | **开票申请**
- 新增 `invoicePendingBadge`,挂载时拉取 `pending-count`
- `<InvoiceRequestTab v-if="mainTab === 'invoice'" @action-done="loadTabPendingCounts" />`
**`InvoiceRequestTab.vue`**(结构对齐 `RefundConfirmTab.vue`):
| 区块 | 内容 |
|------|------|
| 待开票 | 卡片网格 + 关键词筛选 + 分页;操作「处理开票」 |
| 已开票 | 表格 + 筛选 + 分页;操作「查看」 |
**处理弹窗**:
- 只读:客户信息、协议号、**子表明细表格**(期数、各行金额)、**合计金额**、申请时间、开票信息、申请备注
- 可编辑:发票附件(1 份,覆盖该申请下所有期数)、财务备注
- 底栏:取消 / 确认已开票
**查看弹窗**(已开票):只读展示全部信息 + 附件。
### 7.4 UI 规范
- 状态 Tag:`dkp` → `warning` 待开票,`ykp` → `success` 已开票
- 按钮须带 `@element-plus/icons-vue` 图标
- 遮罩无模糊:`modal-class="protocol-dialog-mask"`
- 金额展示:`formatMoneyDisplay` + `¥` 前缀
### 7.5 API 模块
`web-admin/src/api/collection-protocol.ts` 新增对应 `fetch*` 函数。
---
## 8. 权限与菜单
- 业务:复用满月待办列表权限 `cell-storage:fullmoon-todo:list`;若需细粒度可增加 `cell-storage:fullmoon-todo:invoice`
- 财务:缴退费财务确认页面权限下增加开票处理;建议 `cell-storage:payment-confirm:invoice`
- 菜单:不新增一级菜单,仅在现有页面扩展 Tab / 操作
---
## 9. 测试要点
| 场景 | 预期 |
|------|------|
| 趸交已缴费 | 可发起 1 次申请,金额=实缴 |
| 分期 3 期均已缴费 | 可一次多选 3 期 → **1 条**申请、合计金额=三期实缴之和、财务 **1 份**附件 |
| 分期部分已开 | 第 1~2 期已在申请 A 中,则仅第 3 期可勾选并另起申请 B |
| 多期合并 | 子表 3 行、主表 1 行;`installmentSummary`=「第1、2、3期」 |
| 同一明细重复申请 | 第二次提交失败 |
| 未缴费 / 确认中明细 | 不出现在可开票列表 |
| 已退费明细 | 不可申请 |
| 财务无附件提交 | 校验失败 |
| 财务完成后业务查看 | 可见附件与备注 |
| 租户 / 数据权限 | 跨租户不可见 |
---
## 10. 实施文件清单(概要)
| 层级 | 文件 |
|------|------|
| SQL | `docs/sql/protocol_invoice_request.sql`(主表 + 子表) |
| Backend Model | `backend/app/models/models.py` |
| Backend CRUD | `backend/app/crud/collection_protocol.py` 或新文件 |
| Backend API | `backend/app/api/collection_protocol.py` |
| Backend Schema | `backend/app/schemas/collection_protocol.py` |
| Frontend API | `web-admin/src/api/collection-protocol.ts` |
| Frontend 业务 | `FullmoonTodoTab.vue` |
| Frontend 财务 | `InvoiceRequestTab.vue`、`payment-confirm/index.vue` |
完成后执行 `./scripts/update-graph.sh frontend` 与 `backend`(如有新模块)。
---
## 11. 范围外(YAGNI)
- 财务驳回 / 重新申请
- 发票号码、开票日期结构化字段
- 已开票后退费的发票冲红
- 小程序端开票申请
- 消息/短信通知
---
## 12. 审批记录
| 日期 | 决策 |
|------|------|
| 2026-06-17 | 开票信息字段提供但全部选填 |
| 2026-06-17 | 财务仅上传附件 + 备注 |
| 2026-06-17 | 两态流转 dkp/ykp;满月待办单一「开票申请」弹窗(申请+查看) |
| 2026-06-17 | 缴费明细支持多选;一次提交生成 **1 条申请** |
| 2026-06-17 | **多期合并一张发票**:主表+子表;财务 1 份附件;子表防重复纳入 |