# 财务报销自动化

自动从 PDF 发票或图片中提取信息，生成发票汇总表与易耗品出库单，并可选在财务系统中自动填报报销单。

**支持发票类型区分**：系统自动识别高铁票、酒店住宿等差旅发票与普通发票。差旅发票不生成易耗品出库单，走差旅报销流程；普通发票生成出库单，走普通报销流程。

## 项目结构

```
├── pyproject.toml                  # 项目配置（依赖、工具链）
├── uv.lock                         # 依赖锁定文件
├── Makefile                        # 任务脚本（跨平台）
├── tasks.py                        # 任务脚本（Windows 兼容）
├── .pre-commit-config.yaml         # pre-commit 钩子配置
├── .env.example                    # 环境变量示例（SSO 地址、LLM 配置等）
├── config.example.json             # 用户配置示例
├── 易耗品、出库单.doc              # 易耗品出库单 Word 模板
├── src/
│   ├── __init__.py                 # 包初始化 / 日志器
│   ├── config.py                   # 配置加载
│   ├── bot.py                      # 浏览器自动填报
│   ├── pipeline.py                 # CLI 流程编排
│   ├── main.py                     # CLI 入口
│   ├── doc/                        # 文档处理模块
│   │   ├── extractor.py            # 编排入口：串联 PDF 读取 → LLM 提取 → 分类
│   │   ├── pdf.py                  # PDF 图片渲染（PyMuPDF，供多模态 LLM 使用）
│   │   ├── llm_extractor.py        # LLM 信息提取
│   │   ├── matcher.py              # 数据匹配与校验
│   │   ├── invoice.py              # 发票类型常量、分类逻辑、CSV 读写工具
│   │   ├── fill_consumable_doc.py  # 将 CSV 填入易耗品出库单（Word COM）
│   │   ├── prompt.py               # LLM 提示词模板
│   │   └── prompts/                # 提示词模板文件
│   └── web/
│       ├── app.py                  # Web 服务入口
│       ├── templates/
│       │   ├── index.html          # PC 端主页
│       │   └── mobile_upload.html  # 移动端扫码上传
│       ├── static/
│       │   ├── css/                # 样式文件
│       │   └── js/                 # 前端脚本
│       └── uploads/                # 按会话隔离的上传与产物目录
├── scripts/                        # CLI 数据目录
│   ├── data/                       # 发票源文件、config.json 与 .invoice_cache 缓存
│   └── test_*.py                   # 测试脚本
├── tests/                          # 测试目录
├── docs/                           # 用户文档（API 说明、操作指南等）
├── images/                         # 浏览器调试截图
└── *.pdf / *.jpg / *.png           # 发票 PDF 或图片（CLI 模式，放在 scripts/data/）
```

## 数据流

```mermaid
flowchart TB
    PDF[PDF 发票 / 图片] --> Extract[extractor 多模态提取]
    Extract --> Cache[(.invoice_cache/*.json)]

    Cache --> Classify{发票类型分类}
    Classify -->|差旅发票| Travel[高铁票 / 酒店住宿]
    Classify -->|普通发票| General[普通发票]
    Classify -->|支付记录| Payment[支付截图]
    Classify -->|申请单| Application[出差事前申请单]

    Travel --> Matcher[matcher 金额匹配]
    Payment --> Matcher
    Matcher --> MatchResult[(match_result.json)]

    Cache --> TravelLLM[LLM 差旅信息提取]
    MatchResult --> TravelLLM
    TravelLLM --> TravelInfo[(travel_info.json)]

    Cache --> NormalLLM[LLM 普通发票信息提取]
    MatchResult --> NormalLLM
    NormalLLM --> NormalInfo[(normal_info.json)]

    TravelInfo -->|差旅基本信息| Bot_T[bot/travel.py<br/>差旅填报流程]
    TravelInfo -->|报销明细| Bot_T
    TravelInfo -->|支付方式| Bot_T
    TravelInfo -->|补助清单| Bot_T
    TravelInfo -->|附件清单| Bot_T
    Bot_T --> Submit_T[差旅报销提交]

    NormalInfo -->|报销说明| Bot_G[bot/normal.py<br/>普通填报流程]
    NormalInfo -->|发票总数/金额| Bot_G
    NormalInfo -->|支付方式| Bot_G
    NormalInfo -->|附件清单| Bot_G
    Bot_G --> Submit_G[普通报销提交]

    General --> CSV[(invoice_summary.csv)]
    CSV --> Fill[fill_consumable_doc]
    Fill --> Doc[易耗品、出库单.doc]
```

### 关键中间产物

| 文件 | 生成阶段 | 作用 |
|------|---------|------|
| `.invoice_cache/*.json` | extractor 提取 | 单张发票/支付记录/申请单的结构化数据 |
| `match_result.json` | matcher 匹配 | 支付截图与发票的关联关系（按金额匹配） |
| `travel_info.json` | LLM 差旅信息提取 | 综合发票缓存 + 匹配结果，生成差旅报销所需的全部结构化数据 |
| `normal_info.json` | LLM 普通发票信息提取 | 综合普通发票 + 匹配结果，生成普通报销所需的全部结构化数据 |
| `invoice_summary.csv` | extractor 提取 | 普通发票汇总（用于生成易耗品出库单） |

### bot 模块架构

`bot/` 包负责浏览器自动化填报，仅接收已提取的信息并执行填报操作，不承担信息提取职责：

| 模块 | 职责 |
|------|------|
| `bot/base.py` | `BaseBot` 基类：浏览器生命周期、登录、导航、截图 |
| `bot/travel.py` | 差旅填报流程：基本信息 → 差旅明细 → 支付方式 → 补助清单 → 附件上传 |
| `bot/normal.py` | 普通填报流程：基本信息 → 总明细 → 支付方式 → 附件上传 |
| `bot/__init__.py` | 入口函数：`run_bot()` / `run_bot_web()`，负责类型判断和流程路由 |

## 环境要求

- **Python 3.12+**
- **uv** 包管理器（[安装指南](https://docs.astral.sh/uv/getting-started/installation/)）
- Windows（易耗品出库单填写依赖 Microsoft Word + COM，仅 Windows 可用）

## 快速开始

### 1. 安装依赖

```bash
# 同步所有依赖（运行时 + 开发工具）
make install
# Windows 上等效命令：
python tasks.py install
```

项目使用 `uv` 管理依赖，所有包版本锁定在 `uv.lock` 中，确保可复现。

| 依赖 | 用途 |
|------|------|
| PyMuPDF | PDF 图片渲染（供多模态 LLM 使用） |
| llama-index | LLM 信息提取（发票识别、差旅信息提取） |
| playwright | 财务系统浏览器自动化 |
| flask | Web 服务 |
| pywin32 | 填写 Word 出库单（`fill_consumable_doc`） |

### 2. 准备数据（CLI 模式）

将发票 PDF 或图片（`.jpg`、`.png`、`.webp`、`.bmp`）放在 `scripts/data/` 目录下。

### 3. 配置

编辑 `scripts/config.json`（参考 `config.example.json`）：

```json
{
    "username": "你的工号",
    "password": "你的密码",
    "default_name": "默认报销人姓名",
    "default_card_no": "默认公务卡号",
    "default_person_id": "默认人员编号",
    "consumable_storage": "物料存储地"
}
```

| 字段 | 说明 |
|------|------|
| `username` / `password` | 信息门户登录凭据 |
| `default_name` | 默认报销人姓名 |
| `default_card_no` | 默认公务卡号 |
| `default_person_id` | 默认人员编号（工号） |
| `consumable_storage` | 出库单「存放地点」列默认值（默认: `躬行楼 C205`） |

**服务端配置**（SSO 地址、报销系统 URL、LLM 参数）通过环境变量提供，有默认值，一般无需修改：

| 环境变量 | 默认值 | 说明 |
|----------|--------|------|
| `SSO_LOGIN_URL` | `https://tyrz.fynu.edu.cn/sso/login` | SSO 登录地址 |
| `PORTAL_URL` | `https://tyrz.fynu.edu.cn/oshall` | 统一信息平台地址 |
| `REIMBURSE_URL` | `http://210.45.32.214:8081` | 报销系统地址 |
| `REIMBURSE_PAGE` | `/expen/common/common?v=4.0` | 普通报销页面路径 |
| `TRAVEL_PAGE` | `/expen/travel/travel?v=4.0` | 差旅报销页面路径 |
| `LLM_MODEL` | `qwen-vl-max` | LLM 模型名称 |
| `LLM_API_BASE` | `http://localhost:8080/v1` | LLM API 地址 |
| `LLM_API_KEY` | `lm-studio` | LLM API 密钥 |

### 4. 运行（CLI）

```bash
# 全流程（发票提取 → 浏览器填报）
make run
# Windows 等效：
python tasks.py run

# 仅执行某一步
uv run python src/main.py --step invoice    # 仅发票提取
uv run python src/main.py --step submit     # 仅浏览器填报

# 覆盖配置中的登录凭据
uv run python src/main.py -u 工号 -p 密码
```

### 5. 填写易耗品出库单（CLI）

需已生成 `invoice_summary.csv`，且本机已安装 **Microsoft Word**：

```bash
uv run python -m src.doc.fill_consumable_doc
uv run python -m src.doc.fill_consumable_doc --csv invoice_summary.csv --doc "易耗品、出库单.doc"
uv run python -m src.doc.fill_consumable_doc --config scripts/config.json  # 指定配置文件
uv run python -m src.doc.fill_consumable_doc --no-backup   # 不生成 .doc.bak 备份
```

填写规则概要：

- 表头「日期」使用**填写当天**的日期（非发票开票日期）
- 从 `spec_model` 解析品名、规格、单位、数量、单价；`card_amount` 写入金额列
- 单价/金额保留两位小数；表格内统一为 **宋体五号（10.5 磅）**
- 存放地点取自 `consumable_storage`（默认: `躬行楼 C205`）；购货人/领用人签字、备注保持空白

## 执行步骤说明

| 步骤 | 命令 | 说明 |
|------|------|------|
| 发票提取 | `--step invoice` | 扫描 `scripts/data/` 目录的 PDF 和图片，生成 `invoice_summary.csv` |
| 浏览器填报 | `--step submit` | 登录信息门户 → 报销系统 → 自动填单、上传附件 |

> 全流程执行时数据在内存中流转，CSV 为参考产物。分步执行时，缓存数据会自动成为下一步的输入。

### 缓存机制

系统使用 JSON 缓存作为数据中转站，串联整个处理流程：

```
源文件 (PDF/图片) → LLM 多模态提取 → JSON 缓存 → 匹配/分类/填报
```

| 缓存文件 | 说明 |
|----------|------|
| `<文件名>.json` | 每个源文件的 LLM 提取结果（发票信息、支付记录等） |
| `match_result.json` | 发票与支付记录的匹配结果 |
| `travel_info.json` | 差旅信息（事由、地点、时间等）提取结果 |

缓存位置：CLI 模式为 `scripts/data/.invoice_cache/`，Web 模式为 `src/web/uploads/<session_id>/.invoice_cache/`。缓存文件与源文件同名（如 `发票1.pdf` 对应 `.invoice_cache/发票1.json`），后续步骤（金额匹配、发票分类、浏览器填报）均从缓存读取结构化数据。删除缓存后下次处理会重新提取。

### CSV 字段说明

发票级别 CSV（`invoice_summary.csv`）使用英文列名：

| 列名 | 说明 |
|------|------|
| `index` | 行号 |
| `invoice_type` | 发票类型：`train` / `hotel` / `general` |
| `invoice_number` | 电子发票号码 |
| `invoice_date` | 开票日期 |
| `item_name` | 货物或应税劳务名称 |
| `spec_model` | 规格型号（差旅发票为出发站→到达站） |
| `total_amount` | 发票含税金额 |
| `seller_name` | 销方名称 |
| `departure` / `arrival` | 出发站 / 到达站（高铁票专用） |
| `train_no` / `ride_date` / `seat_class` | 车次 / 乘车日期 / 座位等级（高铁票专用） |
| `person_name` | 人员姓名 |
| `card_date` / `card_no` / `card_amount` | 刷卡日期 / 公务卡号 / 刷卡金额 |
| `remark` | 备注 |
| `person_id` | 工号 |

## Web 服务

提供浏览器界面：上传文件 → 自动处理 → 在线编辑 → 下载产物 → 可选提交财务系统。

```bash
uv run python src/web/app.py
```

访问 `http://localhost:5000`。

### 推荐使用流程

1. 上传 PDF 或图片（或上传已有 CSV）
2. 填写配置（账号、密码、姓名、公务卡号、存放地点等），可上传 `config.json` 一键填充
3. 点击 **开始处理** — 完成发票提取、生成 CSV，系统自动识别发票类型并分类统计
4. **普通发票**：自动生成 **易耗品、出库单.doc**，可下载
5. **差旅发票**（高铁票/酒店住宿）：跳过出库单生成，直接进入差旅报销流程
6. 在表格中核对、修改发票数据（提交财务系统前会自动保存）
7. 确认无误后点击 **提交到财务系统**

### 处理流程

上传 PDF 或图片 → LLM 识别文档类型 → 结构化提取 → 分类处理

系统通过 LLM 多模态识别自动判断每张文档的类型，无需手动指定：

| 文档类型 | 处理方式 | 状态 |
|----------|----------|------|
| 发票（高铁票/酒店住宿/普通发票） | 提取发票信息 → 金额匹配 → 分类 | 已实现 |
| 支付记录（刷卡截图） | 提取刷卡信息 → 与发票匹配 | 已实现 |
| 出差事前申请单 | 提取出差事由、地点、时间 | 已实现 |
| 飞机票 | 同高铁票处理流程 | 计划中 |

### 功能一览

| 功能 | 说明 |
|------|------|
| 发票提取 | 上传 PDF 或图片后自动完成 |
| 发票类型自动分类 | 高铁票/酒店住宿/普通发票，自动分流处理 |
| 易耗品出库单 | 仅普通发票自动生成 Word，差旅发票跳过 |
| 表格在线编辑 | 处理完成后可修改 CSV 各字段；保存后重新生成出库单 |
| 财务系统填报 | 单独按钮触发，处理阶段不会自动提交 |
| 实时日志 | SSE 推送处理进度 |
| 配置上传 | 支持上传 `config.json` 填充表单 |
| 手机扫码上传 | 二维码打开移动端页面，拍照上传，PC 端轮询同步 |

> Web 端浏览器填报以无头模式运行。未上传 PDF 或图片时，填报阶段会跳过附件上传。
> 出库单生成需要 **Windows + Word + pywin32**；若失败，页面会显示具体原因，CSV 等其它产物仍可正常使用。

### 会话产物

每次上传生成独立会话，产物存放在 `src/web/uploads/<session_id>/`：

| 产物 | 说明 |
|------|------|
| `invoice_summary.csv` | 发票汇总数据（含 LLM 识别结果） |
| `payment_records.csv` | 支付记录级别数据（含匹配结果） |
| `travel_applications.json` | 出差事前申请单数据（JSON 格式） |
| `易耗品、出库单.doc` | 自动填写的出库单（仅普通发票） |
| `config.json` | 当次会话配置 |
| `session.log` | 处理日志 |
| `result.json` | 处理结果 |
| `.invoice_cache/` | LLM 提取结果缓存（JSON 格式，避免重复处理） |

会话缓存机制与 CLI 模式相同，缓存目录中的 JSON 数据是后续匹配、分类和填报的唯一数据来源。

接口说明见 [API.md](./API.md)。

## 开发任务

项目提供统一的任务脚本，支持跨平台使用：

| 任务 | Makefile | tasks.py | 说明 |
|------|----------|----------|------|
| 安装依赖 | `make install` | `python tasks.py install` | 同步依赖 + 安装 pre-commit |
| 代码检查 | `make check` | `python tasks.py check` | Ruff lint + 格式化 + MyPy 类型检查 + deptry 依赖检查 |
| 运行测试 | `make test` | `python tasks.py test` | pytest + 覆盖率报告 |
| 运行 CLI | `make run` | `python tasks.py run` | 执行全流程 |
| 清理缓存 | `make clean` | `python tasks.py clean` | 删除虚拟环境和缓存 |

## 代码质量

项目配置了完整的代码质量工具链：

- **Ruff** — 快速 lint 检查和代码格式化（替代 flake8 + isort + black）
- **MyPy** — 严格模式类型检查（`strict = true`）
- **deptry** — 检测未声明、未使用、过时依赖
- **pre-commit** — 提交前自动运行 Ruff 检查和格式化

所有检查通过后方可提交代码。

## 注意事项

- 浏览器填报时会打开或使用 Chromium，请勿手动干扰自动化流程
- 调试截图保存在 `images/` 目录
- 项目根目录需保留 `易耗品、出库单.doc` 模板；Web 每次从模板复制到会话目录再填写，不修改原模板
- `scripts/config.json` 含敏感信息，请勿提交到公开仓库
- **发票类型区分**：差旅发票（高铁票/酒店住宿）不会生成易耗品出库单，差旅报销填报流程已完整实现（含差旅信息提取、明细录入、支付方式、补助清单、附件上传）