# 财务报销自动化 自动从 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 # 配置加载 │ ├── exceptions.py # 异常定义 │ ├── pipeline.py # CLI 流程编排 │ ├── pipeline_core.py # CLI/Web 公共管道逻辑 │ ├── main.py # CLI 入口 │ ├── agent/ # Agent 调度模块 │ │ ├── orchestrator.py # 总调度入口 │ │ ├── coordinator.py # 校验-修正循环 │ │ ├── session.py # 状态机与会话数据 │ │ └── events.py # SSE 事件发射 │ ├── core/ # 核心业务逻辑 │ │ ├── extraction/ # 信息提取 │ │ │ ├── extractor.py # 编排入口:串联文件扫描 → 提取 → 分类 │ │ │ └── llm_extractor.py # LLM 多模态信息提取 │ │ ├── matching/ # 金额匹配 │ │ │ └── matcher.py # 支付记录与发票关联 │ │ └── validation/ # 校验模块 │ │ └── validator.py # 声明式校验器 │ ├── infra/ # 基础设施层 │ │ ├── browser/ # 浏览器自动化 │ │ │ ├── base.py # BaseBot 基类 │ │ │ ├── travel.py # 差旅报销填报流程 │ │ │ └── normal.py # 普通报销填报流程 │ │ ├── documents/ # 文档处理 │ │ │ ├── invoice.py # 发票数据模型 + CSV 工具 │ │ │ ├── pdf.py # PDF 图片渲染 │ │ │ └── consumable.py # 易耗品出库单填写(Word COM) │ │ └── llm/ # LLM 接口 │ │ ├── prompt.py # 提示词加载 │ │ └── prompts/ # 提示词模板文件 │ └── web/ # Web 界面模块 │ ├── app.py # Flask 应用入口 │ ├── routes.py # 路由定义 │ ├── pipeline_web.py # Web 管道逻辑 │ ├── sse_handler.py # SSE 日志流处理 │ ├── 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/) ``` ## 声明式校验器 `src/core/validation/validator.py` 采用**规则配置与校验引擎分离**的设计模式,支持声明式定义校验规则: ### 设计特点 | 特性 | 说明 | |------|------| | **声明式配置** | 校验规则以数据结构形式定义,无需编写代码 | | **统一路径定位** | 使用 `path` 统一定位字段,如 `["basic_info", "travel_purpose"]` | | **自定义校验函数** | 支持为字段定义自定义校验逻辑(日期格式、正数检查等) | | **数组元素校验** | 支持校验数组字段的最小元素数量及每个元素的必填字段 | | **向后兼容** | 支持简单格式 `["field1", "field2"]` 和详细格式 `{"path": [...], "custom_check": ...}` | ### 规则配置示例 ```python # 差旅报销校验规则 TRAVEL_VALIDATION_RULES = { "fields": [ {"path": ["basic_info", "travel_purpose"], "description": "出差事由"}, {"path": ["basic_info", "start_date"], "custom_check": _is_valid_date}, ], "arrays": [ { "path": ["payment_methods"], "min_items": 1, # 至少1条支付记录 "element_fields": [ {"path": ["card_date"], "description": "刷卡日期"}, {"path": ["card_amount"], "custom_check": _is_positive_number}, ], }, ], } ``` ### 校验规则类型 | 规则类型 | 用途 | 关键字段 | |----------|------|----------| | `fields` | 顶层单值字段校验 | `path`, `custom_check`, `check_empty` | | `arrays` | 数组字段校验 | `path`, `min_items`, `element_fields` | ### 内置校验函数 - `_is_valid_date(value)` — 检查日期格式是否为 `YYYY-MM-DD` - `_is_positive_number(value)` — 检查值是否为正数 ### 扩展自定义校验 ```python # 定义自定义校验函数 def check_vehicle_type(value): valid_types = ["飞机", "火车", "汽车", "打车"] return isinstance(value, str) and value.strip() in valid_types # 在规则中使用 {"path": ["vehicle_type"], "custom_check": check_vehicle_type} ``` ## 数据流 ```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[infra/browser/travel.py
差旅填报流程] TravelInfo -->|报销明细| Bot_T TravelInfo -->|支付方式| Bot_T TravelInfo -->|补助清单| Bot_T TravelInfo -->|附件清单| Bot_T Bot_T --> Submit_T[差旅报销提交] NormalInfo -->|报销说明| Bot_G[infra/browser/normal.py
普通填报流程] NormalInfo -->|发票总数/金额| Bot_G NormalInfo -->|支付方式| Bot_G NormalInfo -->|附件清单| Bot_G Bot_G --> Submit_G[普通报销提交] General --> CSV[(invoice_summary.csv)] CSV --> Fill[consumable.py] Fill --> Doc[易耗品、出库单.doc] ``` ### 关键中间产物 | 文件 | 生成阶段 | 作用 | |------|---------|------| | `.invoice_cache/*.json` | extractor 提取 | 单张发票/支付记录/申请单的结构化数据 | | `match_result.json` | matcher 匹配 | 支付截图与发票的关联关系(按金额匹配) | | `travel_info.json` | LLM 差旅信息提取 | 综合发票缓存 + 匹配结果,生成差旅报销所需的全部结构化数据 | | `normal_info.json` | LLM 普通发票信息提取 | 综合普通发票 + 匹配结果,生成普通报销所需的全部结构化数据 | | `invoice_summary.csv` | extractor 提取 | 普通发票汇总(用于生成易耗品出库单) | ### bot 模块架构 `infra/browser/` 包负责浏览器自动化填报,仅接收已提取的信息并执行填报操作,不承担信息提取职责: | 模块 | 职责 | |------|------| | `infra/browser/base.py` | `BaseBot` 基类:浏览器生命周期、登录、导航、截图 | | `infra/browser/travel.py` | 差旅填报流程:基本信息 → 差旅明细 → 支付方式 → 补助清单 → 附件上传 | | `infra/browser/normal.py` | 普通填报流程:基本信息 → 总明细 → 支付方式 → 附件上传 | | `infra/browser/__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.infra.documents.consumable uv run python -m src.infra.documents.consumable --csv invoice_summary.csv --doc "易耗品、出库单.doc" uv run python -m src.infra.documents.consumable --config scripts/config.json # 指定配置文件 uv run python -m src.infra.documents.consumable --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//.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//`: | 产物 | 说明 | |------|------| | `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` 含敏感信息,请勿提交到公开仓库 - **发票类型区分**:差旅发票(高铁票/酒店住宿)不会生成易耗品出库单,差旅报销填报流程已完整实现(含差旅信息提取、明细录入、支付方式、补助清单、附件上传)