Invoice Organizer - 发票整理技能
概述
中国电子发票与行程单批量整理工具。自动识别文档类型、提取关键信息、生成统计表格。
核心能力:
- 🔍 自动识别文档类型:发票 vs 行程单 vs 结账单
- 📦 ZIP压缩包支持:自动解压并检索压缩包内的PDF/OFD发票
- 🔄 重复发票检测:基于发票号码自动识别重复,标红高亮
- 📊 批量提取发票字段:号码、日期、买卖方、项目、金额、税率、税额、价税合计
- ✈️ 支持机票报销凭证:提取航班号、起降地等附加信息
- 🚗 识别网约车行程单:提取平台、行程日期、金额(标注为非发票)
- 📈 多维度统计汇总:按月/季度/年度/费用类别
- 📂 智能输出路径:根据整理范围自动保存到对应文件夹
- 🏷️ 报销类别自动分类(按月整理时):按销售方和项目内容自动分类到 综合费用(招待费/办公费/行政后勤/采购垫付/物流垫付/其它)和 差旅费用(住宿/交通/油补/餐补/通讯/其它),再按公司主体分子文件夹,并移动到对应目录(重名时覆盖)
- 🤝 两阶段交互式分类确认:无法自动识别的发票,分两阶段让用户确认——第一阶段选择大类别,第二阶段选择子类
目录结构:按类别整理/大类/子类/公司主体/发票.pdf
⚠️ 重要区分:行程单不是发票!
- 网约车行程单(享道/滴滴/高德/曹操等)= 运输记录凭证,非发票
- 机票报销凭证 = 是发票(项目名称为"经纪代理服务机票款")
- 结账单/账单 = 非发票
核心规则(必须遵守)
⚠️ 按月整理的三条核心规则:
- 寻找月份文件夹:按月份整理 = 找到命名为月份的文件夹(如
20260331),只整理该文件夹下的发票- 先去重再确认:先去重(同一发票号码只保留一张),去重后的待确认发票才需要交互确认
- 两阶段确认:无法自动识别的发票,先确认大类别(综合费用/差旅费用),再确认子类
使用场景
| 场景 | 用户指令示例 | CLI参数 | 输出位置 |
|------|-------------|---------|----------|
| 指定文件夹 | "整理这个文件夹下的发票" | input_dir=子文件夹路径 | 子文件夹下 |
| 按年整理 | "帮我整理2025年的发票" | --year 2025 | 2025年/发票统计汇总表_2025.xlsx |
| 按月整理 | "帮我整理2026年3月发票" | --month 2026-03 | 2026年/20260331/发票统计汇总表_2026-03.xlsx |
| 交互选择 | "帮我整理发票" | 无参数 | 提示选择 → 根据选择 |
| 全量整理 | "整理所有发票" | 仅 input_dir | 发票统计汇总表.xlsx |
工作流程
第一步:确认范围
询问用户或从上下文确定:
- 目标目录:包含发票文件的文件夹路径
- 处理范围:
- 指定文件夹 → 全量扫描该文件夹
- 指定年份 →
--year 2025 - 指定月份 →
--month 2026-03(推荐日常报销) - 不确定 → 运行
--list让用户选择
- 输出位置:根据范围自动确定(见上表)
第二步:扫描文件
使用 scripts/invoice_extractor.py 扫描目标目录:
按月整理(推荐日常报销):
# 支持 2026-03, 202603, 2026年3月, 2026年03月 等格式
# ⚠️ 直接定位到月份文件夹(如20260331),只扫描该文件夹下的发票
# ⚠️ 分类结果也放在月份文件夹下的"按类别整理/"目录
python ~/.workbuddy/skills/invoice-organizer/scripts/invoice_extractor.py "<目标目录>" --month 2026-03
# 输出: <目标目录>/2026年/20260331/发票统计汇总表_2026-03.xlsx
# 分类: <目标目录>/2026年/20260331/按类别整理/
按年整理:
python ~/.workbuddy/skills/invoice-organizer/scripts/invoice_extractor.py "<目标目录>" --year 2025
# 输出: <目标目录>/2025年/发票统计汇总表_2025.xlsx
指定文件夹(全量):
# 直接指定子文件夹
python ~/.workbuddy/skills/invoice-organizer/scripts/invoice_extractor.py "<目标目录>/2024年/发票-20241031"
# 输出: <目标目录>/2024年/发票-20241031/发票统计汇总表.xlsx
全量整理:
python ~/.workbuddy/skills/invoice-organizer/scripts/invoice_extractor.py "<目标目录>"
# 输出: <目标目录>/发票统计汇总表.xlsx
查看可用年份/月份:
python ~/.workbuddy/skills/invoice-organizer/scripts/invoice_extractor.py "<目标目录>" --list
其他参数:
# 自定义输出路径
python invoice_extractor.py "<目标目录>" -o "<输出路径>.xlsx"
# 同时输出JSON(便于程序处理)
python invoice_extractor.py "<目标目录>" --json
# 禁用缓存,强制全量提取
python invoice_extractor.py "<目标目录>" --no-cache
月份文件夹名识别
脚本支持多种月份文件夹命名格式,自动匹配对应月份:
| 文件夹名 | 匹配月份 | 说明 |
|----------|----------|------|
| 01月 / 1月 / 01 | 当年该月 | 需在年份文件夹下 |
| 2026年4月 / 2026年04月 | 2026年4月 | 文件夹名含年份 |
| 202604 | 2026年4月 | 6位纯数字=年月 |
| 2026-04 / 2026_04 | 2026年4月 | 分隔符格式 |
| 20260331 | 2026年3月 | 8位日期格式,自动提取年月 |
| 发票-20240301 | 2024年3月 | 带前缀8位日期 |
| 发票-20240430 | 2024年4月 | 同上 |
输出路径规则
| 整理范围 | 输出文件路径 | 示例 |
|----------|-------------|------|
| 全量 | <根目录>/发票统计汇总表.xlsx | 报销发票/发票统计汇总表.xlsx |
| 按年 | <根目录>/<年>年/发票统计汇总表_<年>.xlsx | 报销发票/2025年/发票统计汇总表_2025.xlsx |
| 按月 | <根目录>/<年>年/<月文件夹>/发票统计汇总表_<年>-<月>.xlsx | 报销发票/2026年/20260331/发票统计汇总表_2026-03.xlsx |
| 指定文件夹 | <子文件夹>/发票统计汇总表.xlsx | 发票-20241031/发票统计汇总表.xlsx |
设计原则:生成的表格和发票文件放在一起,便于对照查看和提交报销。
也可以直接在Python中调用:
from invoice_extractor import scan_directory, process_single_file, create_output_workbook, _detect_duplicates
# 按月扫描(含ZIP内文件)
file_entries = scan_directory(base_dir, month_filter=(2026, 3))
# file_entries 是 dict list: [{'filepath': ..., 'source_zip': None/..., 'zip_inner_path': None/...}]
results = [process_single_file(e['filepath']) for e in file_entries]
duplicate_groups = _detect_duplicates(results)
wb = create_output_workbook(results, base_dir, duplicate_groups=duplicate_groups)
wb.save(output_path)
第三步:审查结果
检查提取结果的准确性:
- 确认文档分类是否正确(发票/行程单/结账单)
- 检查关键字段是否完整(发票号码、日期、金额)
- 核对价税合计(必须从发票原文提取,不要自行计算)
- 标注提取不完整的记录
第四步:补充修正
对提取不完整的记录进行手动修正:
- 旧版增值税电子发票(印章覆盖格式):购买方/销售方可能缺失
- OFD文件:已支持解析,自动识别三种内部XML结构
- 扫描件PDF:文本无法提取
第五步:交付成果
生成的Excel包含以下Sheet:
| Sheet | 内容 | 说明 | |-------|------|------| | 发票明细 | 所有发票的详细记录 | 含"是否重复"和"来源"列,重复项标红 | | 行程单明细 | 网约车等行程单记录 | 标注为"非发票",含重复检测 | | 非发票文件 | 结账单、未识别文件 | 备注说明原因 | | 重复发票 | 重复发票/行程单汇总 | 仅在有重复时出现,首次出现标黄、重复标红 | | 按月汇总 | 月度统计 | 数量+金额+税额 | | 按季度汇总 | 季度统计 | 数量+金额+税额 | | 按年度汇总 | 年度统计 | 数量+金额+税额 | | 按费用类别汇总 | 按项目类别统计 | 含占比 |
支持的发票类型
详见 references/invoice_types.md
| 类型 | 识别方式 | 提取难度 | |------|---------|---------| | 全电普通发票 | PDF文本 "电子发票(普通发票)" | ★☆☆ | | 全电增值税专用发票 | PDF文本 "电子发票(增值税专用发票)" | ★☆☆ | | 增值税电子普通发票 | PDF文本 "增值税电子普通发票" | ★★☆ | | 机票报销凭证 | 文件名含"机票"+"报销凭证" | ★★★ | | 网约车行程单 | 文件名含"行程单"/"出行" | ★★☆ (非发票) | | OFD发票 | 文件扩展名 .ofd,解析内部XML | ★★☆ |
注意事项
- 数据以发票原文为准:价税合计等字段必须从发票PDF/OFD原文直接提取,禁止通过金额+税额自行计算
- 区分发票和行程单:行程单不是发票,在统计表格中要单独一个Sheet
- 提取失败处理:对无法提取的文件(扫描件),在表格中标注原因
- 金额精度:所有金额保留2位小数,使用Excel数字格式
- 表格位置:生成的Excel放在发票所在的对应文件夹下,便于对照查看和报销提交
- 旧版发票:印章覆盖格式的发票,部分字段(购买方、销售方)提取可能不完整,需要人工核对
- 批次对应:保持文件路径中的年份和报销批次信息,便于追溯
- OFD格式:已支持OFD解析,自动识别三种内部结构(XBRL机票/CustomDatas普通发票/Content.xml文本提取)
- 增量缓存:提取结果缓存在
<根目录>/.workbuddy/invoice_cache.json,重复运行只处理新增/修改的文件,大幅提升速度 - 按月报销:推荐使用
--month参数按月整理,输出文件名带月份后缀,避免覆盖 - 按年整理:使用
--year参数,输出文件放在年份文件夹下 - 智能引导:不指定 --year 或 --month 时,自动扫描目录结构并提示可用选项
- ZIP压缩包:自动解压ZIP文件中的PDF/OFD发票进行提取,来源列标注"ZIP:文件名"
- 重复检测:以发票号码为主键自动检测重复发票(行程单用号码+日期组合),重复项标红、首次出现标黄
- 报销类别分类(仅按月整理):按销售方名称和项目内容自动分类到 综合费用/差旅费用 的各子类别,移动到
按类别整理/大类/子类别/目录(重名时覆盖),只处理原件,重复发票不参与分类 - 分类汇总表(仅按月整理):在
按类别整理/目录下生成发票分类汇总表_YYYY-MM.xlsx,包含带报销类别列的发票明细和分类汇总Sheet - 两阶段交互式分类确认(仅按月整理):无法自动识别的发票,使用
--interactive参数启动分组渐进式两阶段询问流程
两阶段交互式分类确认流程(重要)
⚠️ 脚本本身不会等待用户交互! 需要Agent介入分阶段处理:
- 运行
--interactive→ 脚本扫描并保存待确认发票到JSON → Agent展示分组并询问用户选择- Agent更新JSON的
main_category字段 → 运行--continue-interactive→ Agent询问用户选择子类- Agent更新JSON的
sub_category字段 → 运行--continue-interactive→ 脚本处理发票
完整操作流程
第一步:启动交互模式
python invoice_extractor.py "<目标目录>" --month 2026-03 --interactive
- 脚本扫描发票,发现待确认发票后按销售方分组展示
- 将待确认发票信息保存到
按类别整理/_pending_invoices.json - 重复发票仅在汇总表中标注,不参与分类整理
- 脚本运行后会输出分组信息和JSON保存路径,然后退出
第二步:Agent询问用户选择大类别
脚本会显示类似以下的分组信息:
【第1组】湃智造机器人科技(东莞)有限责任公司:共 8 张发票
- 26319166100004309333.pdf | 项目:未知 | 金额:未知
- 26449123866000006845.pdf | 项目:未知 | 金额:未知
... 共8张
【第2组】成都携程旅行社有限公司:共 1 张发票
- 订单1128146963327850-电子普通发票.pdf | 项目:退票费 | 金额:367.92
请输入: '组序号 大类别' 或 'all 大类别'
示例: '1 综合费用' 或 'all 差旅费用'
Agent需要使用 AskUserQuestion 询问用户:
请为这些待确认发票选择报销大类别:
- 第1组(8张):选择"综合费用"还是"差旅费用"?
- 第2组(1张):选择"综合费用"还是"差旅费用"?
报销大类别选项:
- 综合费用(招待费/办公费/行政后勤/采购垫付/物流垫付/其它)
- 差旅费用(住宿/交通/油补/餐补/通讯/其它)
第三步:Agent更新JSON的main_category字段
用户选择后,Agent必须手动更新 _pending_invoices.json:
# 读取JSON
with open('_pending_invoices.json', 'r', encoding='utf-8') as f:
data = json.load(f)
# 为所有待确认发票设置 main_category(根据用户选择)
for inv in data['pending_invoices']:
if 'main_category' not in inv or not inv.get('main_category'):
inv['main_category'] = '差旅费用' # 用户选择的大类别
# 保存JSON
with open('_pending_invoices.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
第四步:继续交互,Agent询问用户选择子类
python invoice_extractor.py "<目标目录>" --month 2026-03 --continue-interactive
- 脚本检测到
main_category已设置,切换到子类选择阶段 - 显示按大类分组的子类选项
脚本会显示类似以下的子类选项:
【综合费用】共 8 张,请选择子类:
1. 招待费 2. 办公费 3. 行政后勤
4. 采购垫付 5. 物流垫付 6. 其它
【差旅费用】共 1 张,请选择子类:
1. 住宿 2. 交通 3. 油补 4. 餐补 5. 通讯 6. 其它
Agent需要使用 AskUserQuestion 询问用户选择子类。
第五步:Agent更新JSON的sub_category字段
用户选择后,Agent必须手动更新JSON:
# 读取JSON
with open('_pending_invoices.json', 'r', encoding='utf-8') as f:
data = json.load(f)
# 为所有待确认发票设置 sub_category(根据用户选择)
for inv in data['pending_invoices']:
if inv.get('main_category') == '综合费用':
inv['sub_category'] = '招待费' # 用户选择的子类
elif inv.get('main_category') == '差旅费用':
inv['sub_category'] = '交通' # 用户选择的子类
# 保存JSON
with open('_pending_invoices.json', 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
第六步:完成处理
python invoice_extractor.py "<目标目录>" --month 2026-03 --continue-interactive
- 脚本检测到
main_category和sub_category都已设置 - 将发票原件移动到对应分类目录:
按类别整理/大类/子类/公司主体/发票.pdf - 重复发票仅在汇总表中标注,不移动
- 清理临时JSON文件
JSON结构说明
_pending_invoices.json 的结构:
{
"pending_invoices": [
{
"filename": "xxx.pdf",
"filepath": "原始文件路径",
"seller": "销售方名称",
"buyer": "购买方名称",
"item": "项目名称",
"amount": "金额",
"invoice_no": "发票号码",
"main_category": "综合费用", // 第一阶段设置
"sub_category": "招待费", // 第二阶段设置
"is_original": true,
"duplicate_group": null
}
],
"groups": [
{
"group_idx": 1,
"seller": "湃智造机器人科技(东莞)有限责任公司",
"count": 8
}
],
"phase": "group_sub_category_selection",
"originals_pending_count": 9
}
报销类别选项汇总
大类别: | 大类 | 子类 | 说明 | |------|------|------| | 综合费用 | 招待费 | 餐饮、酒水、娱乐招待 | | | 办公费 | 办公用品、设备、软件 | | | 行政后勤 | 物业、保洁、维修、水电 | | | 采购垫付 | 材料、五金、建材 | | | 物流垫付 | 快递、运输、仓储 | | | 其它 | 无法归类 | | 差旅费用 | 住宿 | 酒店、宾馆住宿 | | | 交通 | 打车、机票、高铁 | | | 油补 | 加油 | | | 餐补 | 出差餐费 | | | 通讯 | 话费、宽带 | | | 其它 | 无法归类 |
常见问题
Q: 脚本运行后没有停在交互界面?
A: 脚本设计为非阻塞式。运行 --interactive 后会扫描并保存到JSON,然后退出。Agent需要:
- 读取JSON中的分组信息
- 使用
AskUserQuestion询问用户 - 手动更新JSON
- 再次运行
--continue-interactive
Q: 如何批量处理?
A: 用户可以用 'all 综合费用' 一次性为所有待确认发票选择同一大类别,脚本支持批量格式:'组序号,组序号 大类别'(逗号分隔多个组)。
Q: 重复发票如何处理? A: 重复发票(同一发票号码的多次出现)只在汇总表中标注为"首次出现"或"重复",不参与分类整理和移动。
ZIP内PDF+OFD去重
每个ZIP包内可能同时包含同一发票的PDF和OFD两个版本:
- 系统优先使用PDF,跳过OFD(字段信息通常更完整)
- 日志中显示:
[去重] ZIP xxx.zip: 跳过 1 个OFD文件(PDF+OFD同号)
依赖
- Python 3.8+
- pdfplumber (PDF文本和表格提取)
- openpyxl (Excel生成)
- 可选: ofd (OFD格式解析)
更新历史
2026-05-14 修复
问题1:分类汇总表不反映用户确认结果
- 原因:
create_categorized_workbook函数使用自动分类结果,没有读取用户已确认的分类 - 修复:在
--continue-interactive模式下,处理完成后重新生成分类汇总表,优先使用用户确认的分类结果 - 实现:增加
confirmed_categories参数,读取_pending_invoices.json中的用户确认结果
问题2:待确认文件夹残留已处理发票
- 原因:发票被复制到正确分类目录后,原文件没有从待确认目录删除
- 修复:处理完成后自动清理待确认目录中的已处理发票
问题3:火车票识别失败
- 原因:火车票的项目名称为空,销售方和购买方相同,无法通过关键词匹配识别
- 修复:
- 增加
invoice_type_keywords支持:通过发票类型关键词识别 - 增加启发式规则:销售方和购买方相同 + 金额在合理范围(10-1000元)+ 销售方包含"科技/机器人"等关键词 → 自动识别为"差旅费用/交通"
- 交通类关键词增加:"铁路"、"中国铁路"、"客票"、"动车"、"普速"
- 增加
历史版本
- 2026-05-14:初始版本,支持发票提取、ZIP解压、重复检测、两阶段交互式分类确认
微信扫一扫