# 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 条费用明细、复制模板行样式和分页。
- 任务 / 订单预填时字段优先级和冲突提示规则。
