# 报销助手（提交方）· 框架说明

> 定位：**帮你自己少跑腿** —— 发票拖进来 → 自动解析 + 校验 → 页面复核 → 导出交财务。
> 不是"把不能报的变成能报"。校验只提示，不做规避。

---

## 一、目录结构

```
报销助手/
├── 数据/
│   ├── 报销台账.json     ← 唯一主数据（发票 + 报销单 + 汇总）
│   ├── 规则库.json       ← 校验规则（★ 未配置的项不做判定）
│   ├── 收件箱/           ← 把发票文件拖进这里
│   │   └── 已处理/       ← 解析完自动归档（保留原始文件，可追溯）
│   ├── 日志/             ← 每次解析/校验的日志
│   └── 备份/             ← reset 时的台账备份
├── 脚本/
│   ├── invoice_core.py        共用：路径 / 台账读写 / ID / 哈希
│   ├── invoice_parse.py       解析入库（XML / PDF / 图片）
│   ├── invoice_check.py       校验引擎（可单独重跑）
│   ├── build_page.py          生成自包含可编辑页面
│   ├── reset_ledger.py        清空台账（带备份）
│   └── make_test_fixtures.py  造测试样例（合成数据）
├── 工具/
│   └── 报销助手.html     ← 自包含页面（数据内联，可离线打开）
└── 测试样例/             ← 合成样例，验证链路用，可随时删
```

## 二、三步用法

```bash
# ① 把发票文件拖进 报销助手/数据/收件箱/
# ② 解析 + 校验 + 入台账
/usr/bin/python3 报销助手/脚本/invoice_parse.py

# ③ 打开页面复核（改分类/挂客户/填陪同人数 → 点「💾 保存」）
open 报销助手/工具/报销助手.html
```

只解析不入库（看识别效果）：`/usr/bin/python3 脚本/invoice_parse.py --dry`
单独重跑校验：`/usr/bin/python3 脚本/invoice_check.py`
清空台账：`/usr/bin/python3 脚本/reset_ledger.py`

## 三、支持的票据格式

| 格式 | 解析方式 | 可靠性 |
|---|---|---|
| **数电票 PDF**（主流） | 文本层 + **macOS Vision OCR 双通道** | ⭐⭐⭐ 推荐；文本层若是「标签在前、值在后」的乱序，OCR 取视觉顺序补齐 |
| **ZIP 包**（打车/高速通行费） | 自动解包，**只取包内 PDF**（发票 + 行程单），文件名保留为 `车牌_原名.pdf` | ⭐⭐⭐ 车牌命名即归入「私车公用」 |
| 电子发票 XML | 直接读字段（不靠识别） | ⭐⭐⭐ 最稳，有 XML 优先给 XML |
| OFD 版式文件 | 不抓取（跳过后在抓取摘要里列出） | 需在税务数字账户另存为 PDF |
| 拍照/截图图片 | macOS Vision OCR | ⭐ 必须人工复核金额与号码 |

**识别不到就留空 + 记入「缺失字段」，绝不猜数字**；金额只能取到票面最大值时标注「**推定值，请核对**」。

## 四、校验状态说明

优先级：**异常** > **重复** > **待补** > **待核** > **通过**

| 状态 | 含义 | 例子 |
|---|---|---|
| 异常 | 明确不合规，需处理 | 金额不自洽、抬头/税号不符、发票类型不可报、招待超标 |
| 重复 | 同一张票重复出现 | 同文件哈希 / 同发票号码+同开票日期 |
| 待补 | 缺信息，补齐即可 | 缺开票日期、招待缺关联客户/陪同人数 |
| 待核 | 需人工确认 | 疑似连号发票、类型不在允许清单 |
| 通过 | 无问题 | — |

## 五、规则库：已按公司制度落地（v2）

规则来源：**CA-01-03-A3《付款、费用报销管理制度》**（财务部）§5.3.4 费用标准 + 制度 PPT「报销费用标准总览」+《付款&费用报销单据粘贴要求》+《公司账户信息（公章）-2026》。

### 已落地的标准（引擎会据此判超标）

| 费用类别 | 超一类城市 | 一类城市 | 二类城市 | 备注 |
|---|---|---|---|---|
| 住宿费 | **350** 元/人/晚 | **300** 元/人/晚 | **220** 元/人/晚 | 按日限额内据实报销 |
| 目的地交通费 | **80** 元/天 | | | 按日限额内据实报销 |
| 旅途交通费 | 实报实销 | | | 火车硬席座/高铁二等座/轮船普通舱/飞机经济舱 |
| 出差补助 | **80** 元/天 | | | 不满一天减半 **40** 元；只能本人申报 |
| 私车公用 | 油车 **1.5** 元/公里；电车 **0.8** 元/公里 | | | 停车费/路桥费凭票实报实销 |
| 业务招待 | 餐厅 **120** 元/人/餐；外卖 **40** 元/人/餐 | | | 需事前《招待申请》 |

**城市分级**（制度 §5.3.3）：
- 超一类：北京、上海、广州、深圳
- 一类：南京、天津、长沙、宁波、无锡、厦门、成都、重庆、杭州、苏州、武汉、郑州、昆明、温州、合肥、西安、青岛
- 二类：上述以外的其他城市

**公司开票信息**（校验抬头/税号用）：珠海宝锐生物科技有限公司 ｜ 税号 **91440400592109797M**

**其他已落地规则**：同性同住两人间 1.2 倍、三人间 1.5 倍 ｜ 加班市内交通 1 次/月/人（需加班申请）｜ 出租车 ≤50 公里（21 点后到达机场或早 8 点前起飞）｜ 跨年度票据失效（年末关账前必须报销）｜ 电子发票只能使用一次（A5 打印）｜ 各类申请需事前申请并关联（招待/外出/出差/加班）｜ 必备附件（住宿清单、行程单、差旅费报销汇总表、行驶记录明细表）｜ 粘贴规范（胶水不钉书针、面单+底单、鱼鳞状粘贴）。

> ⚠️ 制度修订后需同步更新 `数据/规则库.json`；其中标注 `[待确认]` 的（如招待人均是否含本人）为制度未明确项，需财务确认后再启用判定。

## 六、页面能力

- **可编辑**：费用类别（下拉）、关联客户/项目、陪同人数、备注 —— 改动即时存本机浏览器
- **筛选**：类别 / 状态 / 月份 / 关键字，实时显示"筛选后合计"
- **汇总**：按费用类别、按月
- **导出**：💾 保存 ｜ 📄 导出 HTML（含当前数据，可离线发人）｜ 📊 导出 CSV（给财务）｜ 🗂 导出 JSON ｜ 🖨 打印 ｜ ↺ 重置
- **自包含**：数据内联，零外链，`file://` 直开

## 七、已知边界（写清楚，避免误解）

1. **不做发票真伪自动查验**：全国增值税发票查验平台有验证码，且每份发票每天最多查 5 次 —— 全自动批量刷既有技术限制也有合规风险。需要查验时人工在平台查，或用税务数字账户。
2. **图片票据必须人工复核**：OCR 对金额、发票号码的识别错误率不可忽略。
3. **重复检测口径**：同文件（内容哈希）+ 同发票号码&开票日期。同一张票的 PDF 与 XML 两个版本会被号码级查重拦住。
4. **本页改动只存本机浏览器**，导出 HTML 才把数据带走；换电脑需重新导出/导入。

## 八、邮箱自动抓票（报销专用邮箱）

报销发票统一发到 **407584398@qq.com**，系统每 30 分钟自动抓取一次。

**一次性配置（3 步）**：
1. QQ 邮箱网页版 → 设置 → 账户 → **POP3/IMAP/SMTP 服务** → 开启 **IMAP/SMTP 服务** → 生成**授权码**
2. 把授权码填进 `报销助手/数据/邮箱配置.json` 的 `"授权码"` 字段（**本机文件里填，不要发到聊天里**）
3. 验证：`/usr/bin/python3 报销助手/脚本/fetch_invoices_mail.py --test`

**日常**：不用管。定时任务每 30 分钟跑一次 → 抓到新票据才推送提醒，并把附件落到 `数据/收件箱/` → 自动解析 + 校验 + 刷新页面。

**手动跑**：
```bash
/usr/bin/python3 报销助手/脚本/fetch_invoices_mail.py            # 抓取+处理+生成页面
/usr/bin/python3 报销助手/脚本/fetch_invoices_mail.py --days 30  # 只抓最近 30 天
/usr/bin/python3 报销助手/脚本/fetch_invoices_mail.py --only-fetch
```
**只收什么**：`附件后缀` 默认 `[".pdf", ".zip"]` —— PDF 直接入库；ZIP（打车/高速通行费常以车牌命名打包）自动解包取其中 PDF；OFD/PNG 等**记为「跳过」并列在抓取摘要里**，不会静默丢弃。

**安全**：只读不删；按 Message-ID + UID + 附件哈希三重去重；同封邮件里同一张票的多格式（ofd/pdf/png）只保留最佳格式（PDF 优先）；授权码只存本机、文件权限 600。

## 九、补录字段（填了才能判超标）

页面「补录」列会**按费用类别**显示需要补的信息：

| 费用类别 | 需要补录 | 用于判定 |
|---|---|---|
| 差旅-住宿 | 城市档位、间夜数、住宿清单（已附/未附） | 住宿日均 vs 上限 |
| 业务招待 | 就餐人数 | 人均 vs 120/40 元 |
| 私车公用 | 公里数、油车/电车、行驶记录明细表 | 燃油费上限 |
| 差旅（交通/补助） | 出差天数、是否关联申请单 | 目的地交通费日均、事前申请 |
| 机票/网约车 | 行程单（已附/未附） | 必备附件 |

## 十、打印包（发票 + 配套材料一起打）

```bash
/usr/bin/python3 脚本/build_print_pack.py                  # 全部发票
/usr/bin/python3 脚本/build_print_pack.py --period 2026-09 # 指定报销期间
/usr/bin/python3 脚本/build_print_pack.py --only-ok        # 只打「通过」的
/usr/bin/python3 脚本/build_print_pack.py --ids INV-xxx,INV-yyy
```

- **按制度粘贴顺序合并**：首页批次清单（可当面单要素参考）→ 逐张发票 → 其配套材料（酒店=入住结账单、机票/打车=行程单）
- 输出到 `打印包/报销打印包_<期间>_<时间戳>.pdf`，并写 `数据/打印包.json`（页面读取最新包信息）
- 配套材料的关联方式：① 同一封邮件 ② 配套材料文件名里含发票号码（如航旅行程单 `电子行程单_26318018111072370897.pdf` ↔ 吉祥航空发票同号）
- 无法纳入的文件（OFD、缺失原始文件）会在输出里明确列出，不静默跳过

## 十一、路线图

- **P0 ✅**：解析入库 + 制度规则校验 + 可编辑台账 + 汇总 + 导出
- **P1 ✅**：制度落地（费用标准/城市分级/事前申请/必备附件）→ 校验引擎能判超标与缺件
- **P2 ✅**：报销邮箱自动抓票（每 30 分钟，抓到才推送；PDF + ZIP 解包）
- **P3 ✅**：打印包（发票 + 结账单/行程单按制度顺序合并 PDF）
- **P4 待做**：自动生成《差旅费报销汇总表》（模板已拿到：附件2）
- **P4 待做**：飞书审批对接（钉钉当前流程）／费用-客户关联分析

## 十二、测试样例（合成数据）

`测试样例/` 下 6 个文件覆盖：正常住宿票、铁路电子客票、餐饮招待（触发待补）、金额不自洽（触发异常）、同号换版（触发重复）、PDF 版式票。

```bash
/usr/bin/python3 脚本/make_test_fixtures.py      # 重新生成
/usr/bin/python3 脚本/invoice_parse.py 测试样例/样例1_住宿_正常.xml
/usr/bin/python3 脚本/reset_ledger.py            # 清空演示数据
```

## 判定原则（重要，避免误读状态）

**提取不到 ≠ 票面有问题。** 状态含义：

| 状态 | 含义 | 要不要慌 |
|---|---|---|
| 🔴 异常 | 票面事实与制度冲突（抬头/税号不符、不可报类型、超标） | 要，须重开或走超标申请 |
| 🟣 重复 | 同一发票号码在台账内已存在 | 确认是否重复报销 |
| 🟡 待补 | **工具没提取到**或**只有你知道**的信息（事前申请/公里数/间夜数） | 页面补录后重判即可 |
| 🔵 待核 | 提取结果存疑（如金额+税额≠合计，疑为解析误差） | 照票核对一眼 |
| ✅ 通过 | 票面 + 制度自动校验项全过 | 可直接进报销单 |

**抬头/税号的双重判定**：先看结构化字段；字段抠不出来时，把票面全文压平空白后查公司名称/税号关键词 → 命中即「文本核验通过」（提示里会写明）。**票面文本里完全没有公司名称** 才会判"很可能抬头开错"。

**航空运输电子客票行程单（数电票版）本身就是发票**，无需另附行程单。ETC/网约车的 `清单`/`行程报销单`/`结算单` 属配套材料，不是发票。

## 已知残留（需人工/后续迭代）

- 网约车、ETC 版式的销方名称多靠「文内公司名推定」，页面会显示 ⚠ 提示，请核对。
- 住宿的入住结账单与住宿发票按金额无法一一对应（结账单含多间多晚），需按日期人工关联；打印包已把全部配套材料追加在包内。

## 十二、差旅费报销汇总表自动归集（`脚本/travel_summary.py`）

**用法**：你只要填好汇总表的「出差日期 + 出差地」，其余交给流程。

```bash
python3 脚本/travel_summary.py --book 规则文件/附件2-差旅费报销汇总表-8月W4.xlsx --batch 8月W4
# 可选：--mode report 只出报告不写文件 ｜ --no-pack 不出打印包
```

**流程五步**
1. **按票面内容归类**（不是靠文件名）：住宿/铁路/航空/租车/加油/市内交通/招待/其他。
2. **按票种取归行日期**：铁路看**乘车日**（铁路常滞后开票，用开票日会串到别趟）、租车看**还车日**、住宿看**酒店城市**匹配出差地、其余看开票日期。
3. **分类汇总进表格**：`D飞机/E火车/F巴士/G轮船/H其他/I市内/J住宿/L招待/M补助/N其他`。
4. **只补空格，绝不覆盖手填值**；差异单独列出（"表内 X ｜ 发票归集 Y"）。
5. **出打印包**：汇总表（A4 横版，置于第 1 页）→ 旅途交通 → 市内交通 → 住宿 → 招待 → 其他费用。

**出差补助**：按制度 80 元/天自动填（不满一天减半）。

**依赖**：`脚本/extract_clues.py` 先把「行程日期 / 还车时间 / 销方名称」抽成 `数据/_票面线索.json`（合并写入，单张运行不会覆盖全量）。

**已知口径**：手填值与归集值不一致时**不自动改表**，只在报告里列差异，由报销人拍板。
