expense-tool/README.md
copper-xw 20892346c5 添加复核步骤,优化差旅报销流程
- 新增 Step 3(复核):生成 JSON 后必须重新读取 pdf_raw_text.txt 逐项核对
- 自动执行入口新增复核步骤(第7条)
- 给大模型的推荐提示词新增复核指令(第9条)
- 详细列出 6 项复核清单:金额、人员、日期、费用分类、行程单处理、单据张数
2026-05-29 16:32:30 +08:00

548 lines
17 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.

# 差旅报销自动化流程
## 给大模型的自动执行入口
当用户把本 `README.md` 提供给大模型,或让大模型在本目录处理差旅报销时,大模型应直接按下面流程开始执行,不需要用户再复制额外提示词:
1. 先阅读本说明,确认当前目录是报销目录。
2. 检查当前目录是否存在基础文件:`差旅报销.py`、`差旅费明细表.xlsx`,以及用户放在根目录的本次报销 PDF/图片附件。
3. 自动运行 `extract`,脚本会先把根目录的 PDF/JPG/JPEG/PNG 移动到处理当天目录(如 `20260529/`),再提取 PDF 原文:
```bash
python3 差旅报销.py extract
```
Windows 环境可改用:
```powershell
py 差旅报销.py extract
```
4. 读取本次处理目录中的 `报销输出/pdf_raw_text.txt`,按本文的 JSON Schema 分析出差人、日期、事由、费用分类、金额和证据。
5. 如果同一笔滴滴/打车费用同时有行程单和电子发票,金额只按电子发票计入一次;行程单只作为附件和行程明细使用,不要与发票重复计入金额。
6. 如果出差人、出差事由、出发地、目的地、日期、金额等关键信息无法从原文可靠判断,必须先问用户;不要编造。
7. **复核**:重新读取 `pdf_raw_text.txt`,逐项核对生成的 JSON 是否与 PDF 原文中金额、日期、人员等信息一致;如果发现错误,修正后重新生成 JSON。
8. 将最终 JSON 保存为本次处理目录中的 `报销输出/analyzed_result.json`。
9. 确认 `questions` 为空、金额合计一致、附件张数一致后,自动运行:
```bash
python3 差旅报销.py fill 20260529/报销输出/analyzed_result.json
```
Windows 环境可改用:
```powershell
py 差旅报销.py fill 20260529/报销输出/analyzed_result.json
```
9. 最后告诉用户生成的 Excel 文件路径和本次归档目录,以及是否有任何需要人工复核的证据或假设;如果存在行程单和电子发票对应同一笔费用,必须明确提醒用户:行程单未重复计入金额,只作为附件和行程明细使用。
> 简单说:看到本 README 后,大模型应自动完成“提取 PDF 原文 → 分析生成 JSON → 填写 Excel”,只有在信息不确定时才暂停询问用户。
---
## 核心思想
这个流程分成三段:
1. **Python 提取原文**:只把 PDF 内容提取成 TXT,不理解、不计算、不猜测。
2. **大模型分析**:读取 TXT,判断出差人、日期、事由、费用分类和金额,输出标准 JSON。
3. **Python 填表**:只读取 JSON,把数据填入 Excel 模板,生成最终 XLSX。
也就是说:**Python 只做机械衔接,大模型负责理解发票内容。**
---
## 基础三件套
每次复用这个流程,只需要保留下面三个基础文件:
| 文件 | 作用 |
|------|------|
| `差旅费明细表.xlsx` | 原始 Excel 模板,脚本会复制它,不直接修改 |
| `差旅报销.py` | 唯一 Python 脚本,提供 `extract` / `schema` / `fill` 三个命令 |
| `README.md` | 本说明,给人和大模型看 |
PDF 发票、`pdf_raw_text.txt`、`analyzed_result.json`、最终 XLSX 都是每次报销产生的过程文件或结果文件。
---
## 为什么使用 XLSX
为了跨平台,模板统一使用 `.xlsx`。
这样脚本只依赖 Python 包:
```text
pdfplumber # 提取 PDF 文本
openpyxl # 复制和填写 xlsx 模板
```
不再依赖 LibreOffice,也不需要系统命令做 `.xls` / `.xlsx` 转换。
如果你手里只有 `.xls` 模板,请先用 Excel、WPS 或 LibreOffice 手动另存为:
```text
差旅费明细表.xlsx
```
---
## 依赖安装
### 第一步:检测 Python 环境
在运行脚本前,应先确认 Python 和 pip 可用。以下是各平台的检测方法:
#### Windows
```powershell
# 检测 Python 是否可用
py --version
py -m pip --version
# 如果提示找不到 python,或版本异常,尝试:
where python
where pip
```
**注意**:Windows 应用商店的 Python(从 Microsoft Store 安装)可能有执行限制。如果 `py` 命令不可用或行为异常,请从 [python.org](https://www.python.org/downloads/) 或使用 conda 安装官方版本。
#### Linux / macOS
```bash
python3 --version
python3 -m pip --version
# 如果提示找不到 python3,尝试:
which python3
which pip3
```
---
### 第二步:安装依赖
确认 Python 可用后,安装所需的两个库:
#### Windows
```powershell
py -m pip install pdfplumber openpyxl
```
#### Linux / macOS
```bash
python3 -m pip install pdfplumber openpyxl
```
---
### 第三步:验证安装
```bash
python3 -c "import pdfplumber, openpyxl; print('依赖安装成功')"
```
如果提示 `ModuleNotFoundError`,说明安装失败,需要重新执行上一步。
---
### 常见问题
| 问题 | 解决方法 |
|------|----------|
| `py` 命令找不到 | 从 python.org 下载安装包,勾选 "Add Python to PATH" |
| `python3` 命令找不到 | `sudo apt install python3`(Linux)或从 python.org 安装(macOS)|
| 应用商店 Python 执行受限 | 卸载应用商店版本,安装官方 Python |
| pip 安装失败 | 尝试 `python3 -m pip install --upgrade pip` 先升级 pip |
---
## 标准目录结构
每次开始处理前,根目录只需要保留基础文件,并把本次报销附件直接放到根目录:
```text
报销目录/
├── 差旅费明细表.xlsx # 必需:模板
├── 差旅报销.py # 必需:脚本
├── README.md # 必需:说明
├── 住宿.pdf # 本次报销 PDF,运行 extract 后会被移动
├── 机票*.pdf # 本次报销 PDF,运行 extract 后会被移动
├── 滴滴出行行程报销单*.pdf # 本次报销 PDF,运行 extract 后会被移动
├── 滴滴电子发票*.pdf # 本次报销 PDF,运行 extract 后会被移动
└── 打车截图.jpg # 本次报销图片附件,运行 extract 后会被移动
```
运行 `extract` 后会先形成处理当天目录,例如:
```text
报销目录/
└── 20260529/
├── 住宿.pdf
├── 机票*.pdf
├── 滴滴出行行程报销单*.pdf
├── 滴滴电子发票*.pdf
├── 打车截图.jpg
└── 报销输出/
└── pdf_raw_text.txt
```
运行 `fill` 成功后,会把当天目录重命名为本次归档目录,并保留 `报销输出/` 结构:
```text
报销目录/
└── 20260520-20260522_北京-广州_售后维修/
├── 住宿.pdf
├── 机票*.pdf
├── 滴滴出行行程报销单*.pdf
├── 滴滴电子发票*.pdf
├── 打车截图.jpg
└── 报销输出/
├── pdf_raw_text.txt
├── analyzed_result.json
└── 差旅费明细表_xxxx_20260520_广州_售后维修.xlsx
```
---
## 使用流程
### Step 1:提取 PDF 原文
```bash
python3 差旅报销.py extract
```
Windows 可用:
```powershell
py 差旅报销.py extract
```
运行时会把根目录的 PDF/JPG/JPEG/PNG 移动到处理当天目录,例如 `20260529/`。如果当天目录已存在且已有附件,脚本会询问是否合并;不合并会创建 `20260529-1/`、`20260529-2/` 这样的新目录。
输出位于本次处理目录中,例如:
```text
20260529/报销输出/pdf_raw_text.txt
```
这个文件按 PDF 文件分块保存原始文本,例如:
```text
============================================================
文件: 20260529/住宿.pdf
============================================================
电子发票(增值税专用发票)
...
============================================================
文件: 20260529/滴滴出行行程报销单A.pdf
============================================================
滴滴出行-行程单
...
```
注意:这一步**不会**提取金额、不会判断类型、不会计算总额。
---
### Step 2:让大模型分析 TXT 并输出 JSON
把本次处理目录中的 `报销输出/pdf_raw_text.txt` 交给大模型,让大模型生成同目录下的 JSON,例如:
```text
20260529/报销输出/analyzed_result.json
```
可以先查看 JSON 模板:
```bash
python3 差旅报销.py schema
```
也可以直接输出模板文件:
```bash
python3 差旅报销.py schema --output
```
输出:
```text
报销输出/schema_example.json
```
### Step 3(复核):核对 JSON 与 PDF 原文
**重要**:生成 JSON 后,必须重新读取 `pdf_raw_text.txt`,逐项核对:
1. **金额核对**:每个 evidence.金额 是否与 PDF 原文中该文件的实际金额一致(检查"价税合计"或"合计"字段)。
2. **人员核对**:出差人姓名是否与 PDF 原文中一致。
3. **日期核对**:出发/结束日期是否与行程单或机票日期一致。
4. **费用分类核对**:每笔费用是否归类正确(住宿/交通/饮食/其他)。
5. **行程单处理**:同一笔滴滴费用同时有行程单和电子发票时,确认金额只计入了电子发票,行程单不重复计入。
6. **单据张数核对**:附件总张数是否与 evidence 条目数一致。
如果发现错误,修正 JSON 后重新核对,直到完全一致后再进入 Step 4。
---
### Step 4:读取 JSON 并填表
脚本会读取 JSON 并填入 Excel:
```bash
python3 差旅报销.py fill 20260529/报销输出/analyzed_result.json
```
Windows 可用:
```powershell
py 差旅报销.py fill 20260529/报销输出/analyzed_result.json
```
脚本会:
1. 校验 JSON 字段是否完整
2. 校验金额合计是否一致
3. 校验附件张数是否一致
4. 复制 `差旅费明细表.xlsx`
5. 用 openpyxl 填入固定单元格
6. 输出最终 XLSX 到本次处理目录的 `报销输出/`
7. 将本次处理目录重命名为 `出发日期-结束日期_出发地-目的地_事由简写` 格式
输出文件命名规则:
```text
差旅费明细表_[出差人]_[出发日期YYYYMMDD]_[目的地]_[事由简写].xlsx
```
示例:
```text
20260520-20260522_北京-广州_售后维修/报销输出/差旅费明细表_xxxx_20260520_广州_售后维修.xlsx
```
#### 给大模型的推荐提示词
```text
请根据本 README 自动处理当前目录的差旅报销。
你的任务:
1. 先运行 `python3 差旅报销.py extract`,脚本会把根目录 PDF/JPG/JPEG/PNG 移动到本次处理目录,并提取 PDF 原文。
2. 读取 extract 输出提示中的 `报销输出/pdf_raw_text.txt`,从 PDF 原文中识别出差人、出差事由、出发日期、结束日期、出发地、目的地、出行方式、出差天数。
3. 将费用分为:住宿、交通、饮食、其他。
4. 每个费用类别都要给出金额、单据张数、说明。
5. 计算附件总张数、报销总额、预借差旅费、补领金额、退还金额。
6. 每个金额必须写入 evidence,说明来自哪个文件、依据是哪段文本。
7. 如果同一笔滴滴/打车费用同时有行程单和电子发票,金额只按电子发票计入一次;行程单只作为附件和行程明细使用,不要与发票重复计入金额。
8. 如果某个字段无法可靠判断,必须先问用户,不要编造;在未确认前不要填表。
9. **复核**:重新读取 `pdf_raw_text.txt`,逐项核对生成的 JSON 是否与 PDF 原文中金额、日期、人员等信息一致;如果发现错误,修正后重新生成 JSON。
10. 将最终 JSON 保存到本次处理目录中的 `报销输出/analyzed_result.json`,JSON 必须符合 README 中的 schema。
11. 确认 `questions` 为空、金额合计一致、附件张数一致后,运行 `python3 差旅报销.py fill 本次处理目录/报销输出/analyzed_result.json` 生成最终 Excel,并把本次目录重命名为 `出发日期-结束日期_出发地-目的地_事由简写` 格式。
12. 最后告诉用户生成的 Excel 文件路径和本次归档目录,并列出需要人工复核的证据或假设;如果存在行程单和电子发票对应同一笔费用,必须明确提醒用户:行程单未重复计入金额,只作为附件和行程明细使用。
```
---
## JSON Schema
大模型必须输出如下结构:
```json
{
"meta": {
"出差人": "xxxx",
"出差事由": "前往广州进行售后维修工作",
"出发日期": "2026/5/20",
"结束日期": "2026/5/22",
"出发地": "北京",
"目的地": "广州",
"出行方式": "飞机",
"出差天数": 3,
"事由简写": "售后维修"
},
"expenses": {
"住宿": {
"金额": 510.0,
"单据张数": 1,
"说明": "广州市番禺明乐宾馆住宿费"
},
"交通": {
"金额": 2013.77,
"单据张数": 5,
"说明": "机票1690.00 + 滴滴323.77"
},
"饮食": {
"金额": 0.0,
"单据张数": 0,
"说明": "未发现餐饮发票"
},
"其他": {
"金额": 0.0,
"单据张数": 0,
"说明": "无其他费用"
}
},
"summary": {
"附件总张数": 6,
"报销总额": 2523.77,
"预借差旅费": 0.0,
"补领金额": 2523.77,
"退还金额": 0.0
},
"evidence": [
{
"文件": "住宿.pdf",
"类别": "住宿",
"金额": 510.0,
"依据": "价税合计(小写)¥510.00"
},
{
"文件": "机票预订出行订单尾号87377629-电子普通发票.pdf",
"类别": "交通",
"金额": 1690.0,
"依据": "价税合计(小写)¥1690.00"
}
],
"questions": []
}
```
### 字段说明
| 字段 | 说明 |
|------|------|
| `meta.出差人` | 出差人姓名,不确定就问用户 |
| `meta.出差事由` | 出差目的,不确定就问用户 |
| `meta.出发日期` / `meta.结束日期` | 格式建议为 `YYYY/M/D` |
| `meta.出差天数` | 含首尾日期,例如 5/20 至 5/22 为 3 天 |
| `expenses.住宿` | 酒店住宿类费用 |
| `expenses.交通` | 机票、火车、打车等交通类费用 |
| `expenses.饮食` | 餐饮费用,没有就 0 |
| `expenses.其他` | 不属于前三类的费用,没有就 0 |
| `summary.附件总张数` | 四类费用单据张数之和 |
| `summary.报销总额` | 四类费用金额之和 |
| `evidence` | 每笔金额的来源证据,便于人工核对 |
| `questions` | 如果大模型无法判断,可以列出问题;最终填表前应为空 |
---
## Excel 填写位置
脚本只填写这些单元格:
| 单元格 | 内容 |
|--------|------|
| `E3` | 出差事由 |
| `A6` | 出发日期-结束日期 |
| `B6` | 出发地-目的地 |
| `C6` | 出差天数 |
| `D6` | 出行方式 |
| `H6` / `I6` | 住宿单据张数 / 住宿金额 |
| `H7` / `I7` | 饮食单据张数 / 饮食金额 |
| `H8` / `I8` | 交通单据张数 / 交通金额 |
| `H9` / `I9` | 其他单据张数 / 其他金额 |
脚本**不会覆盖公式单元格**,例如:
| 单元格 | 公式 |
|--------|------|
| `H2` | 附件张数合计 |
| `F9` | 天数合计 |
| `C10` | 报销总额 |
| `H10` | 补领金额 |
| `H11` | 退还金额 |
---
## 校验规则
`fill` 命令会检查:
```text
summary.报销总额 = 住宿金额 + 交通金额 + 饮食金额 + 其他金额
summary.附件总张数 = 住宿单据张数 + 交通单据张数 + 饮食单据张数 + 其他单据张数
```
如果不一致,脚本会停止,不会写 Excel。
---
## 本次数据示例
```json
{
"meta": {
"出差人": "xxxx",
"出差事由": "前往广州进行售后维修工作",
"出发日期": "2026/5/20",
"结束日期": "2026/5/22",
"出发地": "北京",
"目的地": "广州",
"出行方式": "飞机",
"出差天数": 3,
"事由简写": "售后维修"
},
"expenses": {
"住宿": {"金额": 510.0, "单据张数": 1, "说明": "住宿费"},
"交通": {"金额": 2013.77, "单据张数": 5, "说明": "机票1690.00 + 滴滴323.77"},
"饮食": {"金额": 0.0, "单据张数": 0, "说明": "无餐饮发票"},
"其他": {"金额": 0.0, "单据张数": 0, "说明": "无其他费用"}
},
"summary": {
"附件总张数": 6,
"报销总额": 2523.77,
"预借差旅费": 0.0,
"补领金额": 2523.77,
"退还金额": 0.0
},
"evidence": [],
"questions": []
}
```
---
## 常见错误
### 1. `找不到模板文件:差旅费明细表.xlsx`
确认当前目录存在:
```text
差旅费明细表.xlsx
```
如果只有 `.xls`,请先用 Excel / WPS / LibreOffice 另存为 `.xlsx`。
### 2. `报销总额不一致`
说明 JSON 里的总额和分类费用合计不一致,检查:
```text
住宿 + 交通 + 饮食 + 其他
```
### 3. `附件总张数不一致`
说明 JSON 里的附件总数和分类单据张数合计不一致。
### 4. 缺少 Python 库
Windows:
```powershell
py -m pip install pdfplumber openpyxl
```
Linux / macOS:
```bash
python3 -m pip install pdfplumber openpyxl
```