20 KiB
20 KiB
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 手工模式
用户打开手工 Invoice 页面
-> 前端读取发票表单配置和默认值
-> 用户填写 / 选择字段
-> 前端提交 invoice_payload
-> 后端校验、计算金额和税额
-> 后端填充 Excel 模板
-> 调用 M008 内部转换能力生成 PDF
-> PDF 上传 OSS
-> 写入 Invoice 生成记录和审计
-> 返回 PDF URL 和生成摘要
3.2 任务或订单预填模式
后续可以支持从任务或订单进入:
任务详情 / 订单详情
-> 打开 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 | 选择 + 可手填 | 联系人电话 |
| 选择 + 可手填 | 联系人邮箱 | |
| 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 | |
|---|---|---|---|---|---|---|
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 的调试上传接口:
POST /api/reservation/invoices/manual-generations
Authorization: Bearer <access_token>
Content-Type: application/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用于审计和后续配置追溯。
成功响应草案:
{
"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. 建议数据模型
第一阶段建议持久化生成记录,便于审计、排查和后续列表查询。
建议新增表:
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 工作流下,而不是平台文件转换模块:
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. 模板和字段映射
第一阶段建议先使用一个受控模板:
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 条费用明细、复制模板行样式和分页。
- 任务 / 订单预填时字段优先级和冲突提示规则。