Files
th-hotel-simple/docs/project/requirements/M009-manual-invoice-generation-v1.md
2026-07-17 16:03:01 +07:00

405 lines
20 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.

# M009 Manual Invoice 手工开票生成 V1
| 项目 | 内容 |
| --- | --- |
| 文档状态 | CP3 前端 V1 已完成,后续继续推进订单 / 任务预填、客户目录接口和历史查询 |
| 适用范围 | 无订单 / 无任务场景下的手工 Invoice 创建,以及后续从任务或订单预填 Invoice |
| 当前目标 | 第一阶段已基于现有 `invoice.html` 原型落地 Manual Invoice Mode前端页面已接入正式业务接口并按原型三段式结构展示 |
| 依赖能力 | M008 Excel 转 PDF 平台转换能力、阿里云 OSS、登录权限和酒店上下文 |
## 1. 背景
当前系统的主业务数据来自 AgentBus、SuperAgent、订单和任务。但实际运营中可能出现极端情况
- 邮件还没有被 SuperAgent 正确识别成订单 / 任务。
- 订单或任务尚未入库,用户仍需要先出一张 Proforma Invoice。
- 历史、线下、电话或临时沟通产生的开票需求没有可关联的任务。
- SuperAgent 或主链路故障时,需要人工兜底生成 PDF。
因此第一阶段需要支持一个独立的 `Manual Invoice Mode`:用户可以不选择订单、不选择任务,直接填写或选择 Invoice 字段,由后端生成 Excel再通过 LibreOffice 转为 PDF 并上传 OSS。
## 2. 第一阶段定位
第一阶段不是完整财务发票系统,而是酒店预订业务中的 Proforma Invoice 生成能力。
第一阶段应做到:
- 页面可从独立入口进入,例如 `/reservation/invoices/new``/invoices/new`
- 不要求 `task_id``order_id`
- 用户手工填写 / 选择字段后,后端生成 PDF。
- 后端重新计算金额、税额和合计,不信任前端提交的计算结果。
- 酒店、法人、税号、银行账户、Logo 和固定文案优先来自模板或酒店发票配置。
- 生成行为必须有权限、酒店隔离和审计记录。
第一阶段不做:
- 不接真实 OPERA / OHIP。
- 不要求必须挂靠订单或任务。
- 不做应收账款、收款核销、发票税务编号或财务系统入账。
- 不把 SuperAgent AI 原始 JSON 直接渲染成 Invoice 表单。
- 不允许前端直接调用 M008 调试上传接口来伪造业务开票。
## 3. 与现有流程关系
### 3.1 手工模式
```text
用户打开手工 Invoice 页面
-> 前端读取发票表单配置和默认值
-> 用户填写 / 选择字段
-> 前端提交 invoice_payload
-> 后端校验、计算金额和税额
-> 后端填充 Excel 模板
-> 调用 M008 内部转换能力生成 PDF
-> PDF 上传 OSS
-> 写入 Invoice 生成记录和审计
-> 返回 PDF URL 和生成摘要
```
### 3.2 任务或订单预填模式
后续可以支持从任务或订单进入:
```text
任务详情 / 订单详情
-> 打开 Invoice 页面并带 task_id 或 order_id
-> 后端按 task_id / order_id 查询可用上下文
-> 返回同一套 invoice_payload 默认值
-> 用户确认或修改
-> 后续生成流程与手工模式相同
```
中文说明:
- `task_id``order_id` 只是可选上下文,不是第一阶段生成 Invoice 的必要条件。
- 如果从任务进入,应优先使用任务详情 `fields[]``draft_payload_json``confirmed_payload_json` 中已确认的数据。
- 如果从订单进入,应由用户选择具体任务或由页面显式展示“仅使用订单摘要生成”,避免一个订单多任务时字段来源不清。
## 4. 字段来源和控件分类
基于现有 `invoice.html` 原型和 Excel 模板,字段分为四类。
| 分类 | 中文说明 | 示例字段 | 第一阶段控件建议 |
| --- | --- | --- | --- |
| 用户输入字段 | 每张 Invoice 都可能不同,需要用户填写或修改 | Group Name、Arrival Date、Departure Date、Due Date、Room Rate Note、明细 Description | `input` / `date` / `textarea` |
| 选择 + 可手填字段 | 有标准目录,但业务上允许人工覆盖 | Company、Attention、Address、Tel、Email、Booking Date、Room Type、Room Rate、Extra Bed | `select + Manual` 或后续 combobox |
| 模板 / 配置字段 | 不是每张 Invoice 的业务输入,随酒店、法人或模板版本变化 | 酒店名称、Tax ID、法人公司、银行账号、Logo、固定付款文案、页脚 | 第一阶段可在模板或酒店发票配置中维护 |
| 系统计算字段 | 不应由用户手填,后端或 Excel 公式计算 | Amount、Sub-Total、VAT、Total、Night(s) 汇总、Room(s) 汇总 | 前端只读预览,后端重新计算 |
### 4.1 Basic Information
| 字段 | 控件 | 后续来源 |
| --- | --- | --- |
| Company | 选择 + 可手填 | 客户 / 旅行社目录,第一阶段可使用后端配置或静态种子 |
| Attention | 选择 + 可手填 | Company 下联系人目录 |
| Address | 选择 + 可手填 | 联系人或公司账单地址 |
| Tel | 选择 + 可手填 | 联系人电话 |
| Email | 选择 + 可手填 | 联系人邮箱 |
| Booking Date | 选择 + 可手填 | 默认使用当前酒店本地日期,也可用户调整 |
### 4.1.1 Recipient Directory 第一阶段种子数据
`Company``Attention``Address``Tel``Email` 不是五个互相独立的普通字段,而是一组收件方 / 旅行社联系人档案。第一阶段页面可以允许 Manual 覆盖,但默认选择逻辑必须保持关联关系:
- 选择 `Company` 后,刷新该公司下可选 `Attention` 列表。
- 选择 `Attention` 后,同步带出该联系人的 `Address``Tel``Email`
- 公司只有一个联系人时,可以默认选中该联系人并自动带出地址、电话和邮箱。
- `Address` 可在公司级和联系人级之间复用;如果同一公司多个联系人共用地址,不应在每个联系人手工重复维护多份无关数据。
- 用户选择 `Manual` 时,才允许脱离目录手工填写;提交时仍保存最终文本值,目录 code 仅用于审计和后续追溯。
第一阶段确认的种子数据:
| company_code | Company | contact_id | Attention | Address | Tel | Email |
| --- | --- | --- | --- | --- | --- | --- |
| `LIAN_TAI` | `LIAN TAI TRAVEL (THAILAND) CO., LTD.` | `LIAN_TAI_KHUN_ANN` | `Khun Ann` | `2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240` | `061-397-2675` | `op.liantaitravel@gmail.com` |
| `QBD` | `Q.B.D. TRAVEL GROUP CO., LTD` | `QBD_JITDANUN_PANAPHUCHONG` | `Jitdanun Panaphuchong` | `2/90 Rajpattana,Rajpattana,Sapansoong, Bangkok, TH, 10240` | `089-032 0176` | `op.qbdtravel@gmail.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_WICHIENPRAKARN` | `Wichienprakarn, Nuanphae, Khun.` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `066 124 - 7297` | `HI219@hanatour.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_PHONGBUPPA` | `Phongbuppa, Buppachat` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `096 051 3587` | `HI223@hanatour.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_KANG_SUNG_HWA` | `Kang,Sung Hwa` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 051 804 0707` | `k8040707@nave.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_MINYOUNG_KIM` | `Minyoung Kim` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 6638 3345` | `mykim1220@hanayour.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_BOLAM_JO` | `Bolam Jo` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 4182 4615` | `melissa0609@hanatour.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_HWANG_SEONGSEOP` | `Hwang Seongseop` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 3167 8648` | `hwangpilot59@naver.com` |
| `HANATOUR` | `HANATOUR TD CO., LTD.` | `HANATOUR_SONG_SAE_HWA` | `Song Sae Hwa` | `HanaTour Bldg, 41,Insadong 5-gil, Jongno-gu, KR` | `82 010 2400 6801` | `sh-tour2016@naver.com` |
中文说明:
- 以上数据第一阶段可以作为后端种子配置、数据库初始化数据或前端原型静态数据,但正式业务接口生成 PDF 时应以请求中的最终文本值为准。
- 后续如果接客户 / 联系人维护后台,`company_code``contact_id` 应保持稳定,不使用 Company 或 Attention 展示文案做业务主键。
- Booking Date 虽然展示在 Basic Information 区域,但它不是联系人档案的一部分,应独立保存在 `invoice_payload.document.booking_date`
### 4.2 Reservation
| 字段 | 控件 | 后续来源 |
| --- | --- | --- |
| Group Name / Group Code | 输入框 | 手工填写;任务预填时来自 `case_keys.group_code` 或订单 `group_code` |
| Arrival Date | 日期输入 | 手工填写;任务预填时来自任务字段 |
| Departure Date | 日期输入 | 手工填写;任务预填时来自任务字段 |
| Due Date | 日期输入 | 手工填写;可按酒店付款规则默认生成 |
| Room Rate | 选择 + 可手填 | 第一阶段可手填;后续接 Rate Code / 房价配置 |
| Room Rate Note | 输入框 | 例如 `includingBF` |
| Extra bed | 选择 + 可手填 | 第一阶段可手填;后续接加床价格配置 |
| No. of Room(s) | 输入框 / 只读派生 | 可由第一条明细同步,也允许手工覆盖 |
| No. of Night(s) | 输入框 / 日期派生 | 可由 Arrival / Departure 派生,也允许手工覆盖 |
### 4.3 Description & Amount
| 字段 | 控件 | 后续来源 |
| --- | --- | --- |
| Description | 输入框 | 手工填写;默认可使用 Group Name 或 Booking No |
| Room Types | 选择 + 可手填 | 第一阶段可手填;后续接 PMS 房型目录 |
| Quantity(s) | 数字输入 | 后端要求正数 |
| Rate | 数字输入 | 后端要求正数 |
| Night(s) | 数字输入 | 后端要求正数 |
| Amount | 只读 | `quantity * rate * nights` |
| Sub-Total | 只读 | 后端按 VAT 规则计算 |
| VAT 7% | 只读 | 后端按配置税率计算 |
| Total amount | 只读 | 明细金额合计 |
## 5. 建议请求模型
第一阶段建议新增业务接口,不复用 M008 的调试上传接口:
```text
POST /api/reservation/invoices/manual-generations
Authorization: Bearer <access_token>
Content-Type: application/json
```
请求体草案:
```json
{
"hotel_id": "HOTEL-TEST",
"source_type": "MANUAL",
"task_id": null,
"order_id": null,
"template_code": "PROFORMA_INVOICE_V1",
"invoice_payload": {
"document": {
"invoice_date": "2026-07-17",
"booking_date": "2026-07-12",
"due_date": "2026-07-22"
},
"recipient": {
"company_code": "LIAN_TAI",
"contact_id": "LIAN_TAI_KHUN_ANN",
"company": "LIAN TAI TRAVEL (THAILAND) CO., LTD.",
"attention": "Khun Ann",
"address": "2/86 Rajpattana Road, Rajpattana, Sapansoong, Bangkok, TH, 10240",
"telephone": "061-397-2675",
"email": "op.liantaitravel@gmail.com"
},
"booking": {
"group_name": "GRP-DEMO-0802",
"arrival_date": "2026-08-02",
"departure_date": "2026-08-05",
"room_rate_note": "includingBF",
"extra_bed_rate": 1200
},
"charges": [
{
"description": "GRP-DEMO-0802",
"room_type": "Deluxe Room",
"quantity": 2,
"rate": 3000,
"nights": 3
}
]
}
}
```
中文说明:
- `source_type=MANUAL` 表示无订单 / 无任务的手工开票。
- `task_id``order_id` 第一阶段允许为空。
- 如果同时传入 `task_id``order_id`,后端会校验该任务必须挂靠在该订单下;不一致时返回 `RESERVATION_INVOICE_CONTEXT_MISMATCH`
- `invoice_payload` 中的日期是酒店本地业务日期,使用 `yyyy-MM-dd`
- 前端可以传金额预览,但后端不得信任;最终金额以服务端计算和模板公式为准。
- 客户目录选择码和手填文本可以同时传;后端第一阶段以文本值生成 PDF`company_code``contact_id` 用于审计和后续配置追溯。
成功响应草案:
```json
{
"invoice_generation_id": "2080000000000000001",
"generation_status": "SUCCEEDED",
"source_type": "MANUAL",
"hotel_id": "HOTEL-TEST",
"template_code": "PROFORMA_INVOICE_V1",
"pdf_url": "https://oss.example.test/reservation-invoices/HOTEL-TEST/2026-07-17/.../proforma-invoice.pdf",
"pdf_object_key": "reservation-invoices/HOTEL-TEST/2026-07-17/.../proforma-invoice.pdf",
"generated_excel_object_key": "reservation-invoices/HOTEL-TEST/2026-07-17/.../proforma-invoice.xlsx",
"totals": {
"subtotal": 16822.43,
"vat": 1177.57,
"total": 18000.00,
"currency": "THB"
},
"created_at": "2026-07-17T03:30:00Z"
}
```
## 6. 建议数据模型
第一阶段建议持久化生成记录,便于审计、排查和后续列表查询。
建议新增表:
```text
workflow_reservation_invoice_generation
```
核心字段:
| 字段 | 中文含义 |
| --- | --- |
| `id` | Invoice 生成记录 ID |
| `hotel_id` | 酒店 ID |
| `source_type` | 来源类型:`MANUAL``TASK``ORDER` |
| `order_id` | 可选订单 ID |
| `task_id` | 可选任务 ID |
| `source_message_id` | 可选来源邮件 ID |
| `template_code` | 模板编码 |
| `template_version` | 模板版本 |
| `invoice_payload_json` | 用户提交并经后端归一化后的业务字段 |
| `calculated_totals_json` | 后端计算后的金额、税额和合计 |
| `generated_excel_object_key` | 生成后的 Excel OSS 对象 Key |
| `pdf_object_key` | PDF OSS 对象 Key |
| `pdf_url` | PDF 访问 URL |
| `generation_status` | 生成状态 |
| `safe_error_code` | 安全错误码 |
| `safe_error_summary` | 安全错误摘要 |
| `created_by` | 创建人 |
| `created_at` / `updated_at` | UTC 时间点 |
中文说明:
- `generated_excel_object_key``pdf_object_key``pdf_url` 不只在成功记录中出现;如果流程在后续步骤失败,后端也会保留已经成功上传的对象定位,避免 OSS 生成物变成不可追踪的孤立文件。
- `SUCCEEDED` 只在模板填充、Excel 上传、PDF 转换、PDF 上传和业务审计均完成后写入;如果业务审计失败,不会先标记成功再覆盖为失败。
建议状态:
| 状态 | 中文含义 |
| --- | --- |
| `PENDING` | 已接收,等待生成 |
| `RUNNING` | 正在填模板或转换 |
| `SUCCEEDED` | 生成成功 |
| `FAILED` | 生成失败 |
第一阶段也可以同步生成并直接返回,但仍建议落库,因为这属于业务文档生成,不是一次性调试转换。
## 7. 后端模块边界
建议放在 Reservation 工作流下,而不是平台文件转换模块:
```text
server/src/main/java/cn/nianxx/thhotel/workflows/reservation/invoice
├── control
├── service
│ └── impl
├── domain
├── mapper
├── repository
└── common
├── dto
├── request
├── result
└── enums
```
中文说明:
- `reservation.invoice` 负责业务字段校验、模板字段映射、发票生成记录、权限和审计。
- `platform.documentconversion` 继续作为底层文件转换能力,负责 Excel 到 PDF。
- `integrations.storage.aliyunoss` 继续作为 OSS 能力。
- 前端不能直接调用 `POST /api/system/document-conversions/excel-to-pdf` 来完成业务开票;那是调试 / 后台工具接口。
## 8. 模板和字段映射
第一阶段建议先使用一个受控模板:
```text
server/src/main/resources/templates/reservation-invoice/proforma-invoice-v1.xlsx
```
后续可迁移到 OSS + 数据库模板版本管理。
模板字段映射原则:
- 模板固定文案、Logo、银行信息、税号可以先保留在模板内。
- 当前 `PROFORMA_INVOICE_V1` 使用 A4 纵向单页模板,打印区域固定为 `A1:H52`,后端渲染时会再次写入纸张、缩放和打印区域,避免 LibreOffice 转 PDF 时分页漂移。
- 可动态变更但不属于单张 Invoice 的配置,后续迁移到酒店发票配置。
- 用户输入字段通过后端模板填充器写入指定单元格。
- 第一版明细行固定支持 1 到 10 行;超过模板默认行数时,后续 checkpoint 再做复制行样式和公式。
- 金额公式由后端统一写入或由模板公式统一生成,不复制历史案例中不一致的公式。
## 9. 权限、安全和审计
第一阶段建议:
- 接口分类:`FRONTEND_USER`,不是 `FRONTEND_DEBUG`
- 必须 Bearer 登录。
- 权限码建议:`RESERVATION_INVOICE_GENERATE`
- 必须校验用户对 `hotel_id` 的访问权。
- 如果传入 `task_id``order_id`,必须反查对象所属酒店并校验与 `hotel_id` 一致;如果两者同时传入,必须校验任务所属订单与 `order_id` 一致。
- 写入业务审计,记录创建人、酒店、来源类型、可选订单 / 任务、模板版本和生成结果。
- 日志不得输出完整 `invoice_payload_json`、客户邮箱、电话、OSS 签名 URL 或 PDF 内容。
## 10. Checkpoint 规划
### CP1文档和页面方向确认
- 落地本文档。
- 明确当前 `invoice.html` 原型可作为前端交互和控件参考。
- 确认第一阶段是手工模式,不强依赖订单和任务。
### CP2后端手工生成接口
- 已完成:新增业务接口 `POST /api/reservation/invoices/manual-generations`
- 已完成:新增生成记录表 `workflow_reservation_invoice_generation` 和业务审计。
- 已完成:根据请求 payload 填充受控 Excel 模板 `templates/reservation-invoice/proforma-invoice-v1.xlsx`
- 已完成:模板切换为接近酒店历史样张的 A4 单页版式,后端渲染器固定 `A1:H52` 打印区域,避免生成 PDF 被拆成多页。
- 已完成:复用平台 Excel 转 PDF Adapter 生成 PDF并上传阿里云 OSS。
- 已完成:返回 PDF URL、对象 Key、金额摘要和生成状态。
- 已完成:失败记录保留已上传成功的 Excel / PDF object key业务审计完成后才标记 `SUCCEEDED`
- 第一版限制:只支持 `source_type=MANUAL``template_code=PROFORMA_INVOICE_V1`、最多 10 条费用明细;暂不提供历史列表、详情查询、任务 / 订单预填和客户联系人目录查询接口。
### CP3前端 Manual Invoice 页面(已完成 V1
- 已完成:基于当前 `invoice.html` 原型改造成前端项目内 `/reservation/invoices/new` 页面。
- 已完成:页面按 `01 Basic Information``02 Please refer to the following reservation``03 Description&Amount` 三段式结构展示;标题、关键输入占位符和主要操作按钮已接入中 / 英 / 泰 i18n其余原型字段标签后续按需求统一处理。
- 已完成:字段控件按“输入 / 选择 + 可手填 / 只读计算 / 模板配置”实现,第一条费用明细和预订摘要区保持联动。
- 已完成:提交到后端业务接口,不调用调试上传接口。
- 已完成:展示 PDF 生成结果、错误提示和下载 / 预览入口。
### CP4任务 / 订单预填
- 从任务详情进入时,使用任务详情 `fields[]`、草稿或确认 payload 预填。
- 从订单详情进入时,要求用户选择具体任务或确认仅使用订单摘要。
- 保持用户可以修改字段后再生成 PDF。
### CP5配置化和历史查询
- 客户 / 联系人目录、房型、Rate、Extra Bed、银行账户和模板版本逐步配置化。
- 增加 Invoice 生成历史列表和详情。
- 明确 OSS 生命周期、重新生成、作废和审计策略。
## 11. 当前待确认事项
已确认并落地:
- 第一阶段接口路径采用 `POST /api/reservation/invoices/manual-generations`
- PDF 生成记录第一阶段必须落库。
- 生成后的 Excel 同步上传 OSS 并保存对象 Key便于排查。
- Invoice Date 第一阶段允许前端提交,后端按酒店本地业务日期字段处理。
- 发票模板初始文件使用当前 `Invoice Templete.xlsx` 改造成受控模板资源。
- VAT 第一阶段固定按 7% 含税反算。
仍待后续确认:
- 客户 / 联系人目录是否进入后端配置 / 数据库维护,还是继续由前端临时静态维护。
- 是否需要 Invoice 历史列表、详情、重新生成和作废接口。
- 是否需要支持超过 10 条费用明细、复制模板行样式和分页。
- 任务 / 订单预填时字段优先级和冲突提示规则。