README
¶
ofd-invoice
ofd-invoice 是 OFD 电子发票信息抽取与汇总工具,从增值税电子发票、铁路电子客票 OFD 文件中
读取结构化附件(通常为 Doc_0/Attachs/original_invoice.xml)或自定义标引,输出 JSON、CSV/XLSX 对账报表,并能把多张
发票合并为一个便于打印的 PDF。适合财务入账、对账和自动化处理。
使用本工具处理文档前,请阅读项目根目录的 免责声明。抽取结果来自 文件内容本身,不代表文件真实、完整、可信或具有法律效力。
功能
- 优先读取 OFD 包内嵌的发票结构化附件(税控开具的电子普票/专票通常自带)。
- 规范路径
Doc_0/Attachs/original_invoice.xml不存在时,自动扫描所有名称含attach的 XML 条目,取第一个能解析出有效发票数据的文件;命名空间差异不影响字段匹配。 - 没有有效附件时,沿
Document.xml的CustomTags清单读取标引数据,按字段中的ObjectRef和PageRef定位页面文字,支持号码、日期、购销方、金额及明细。多段引用 按引用顺序拼接,缺少页面或文字对象时明确报错。attachment字段记录实际使用的 标引数据文件路径,例如Doc_0/Tags/CustomTag.xml。 - 抽取字段包括:发票标题、类型、机器编号、代码、号码、开票日期、校验码、不含税金额、 税额、价税合计(小写与大写)、税控码、收款人/复核人/开票人、购销方资料(名称、税号、 地址电话、开户账号)以及逐行价税明细(名称、规格、单位、数量、单价、金额、税率、税额)。
- 金额、数量、税率按十进制精确表示(基于任意精度有理数),税率兼容
13%、13和0.13三种写法并归一化为小数。 - 除自定义标引明确引用的字段外,页面文字层用于补齐票面标题与价税合计大写金额, 并按标题推断类型(普通/专用/通行费/铁路电子客票); 页面读取失败不中断附件抽取,只记录警告。
- 结构化数据有效性按发票标志字段判定:必须带发票代码、铁路电子客票号或价税合计(取或,定额
发票可能没代码、代码齐全的票也可能缺金额)。只有购方名称之类的零散字段不算——Attachs
里可能放着版式说明、签章属性之类的 XML,兜一个几乎全空的 Invoice 会让调用方看不出
出错,汇总还会安静地算出 0 元。不满足时报"不是发票"(
ErrNotInvoice)而不是空壳; 没有 attach XML 和自定义标引数据时返回ErrNoAttachment,两者在库里是不同的错误。不处理 PDF 等 非 OFD 输入。 - 自校验:比对同一张票内部的金额关系(不含税金额 + 税额是否等于价税合计、逐行明细 汇总是否等于票面金额),不自洽的发票在结果与报表的异常列中标出。
- 多票汇总:金额按最小货币单位(分)四舍五入后求和;红字(红冲)发票的负金额照常 参与合计;合计按销售方(按纳税人识别号)、发票类型和开票月份分组。
- 重复识别:发票代码与号码相同视为同一张。默认只标记不丢弃,重复项仍参与合计;
--dedup时重复项移入skipped且不计入合计。 - 对账报表:CSV(UTF-8 BOM + CRLF,Excel 直接打开)与 XLSX。明细、分组汇总、 异常与跳过三个工作表,统一版式:标题 + 副标题(带张数与三项合计)+ 第 4 行表头 + 冻结窗格 + 隔行底色 + 自动筛选;金额是带两位小数的数值单元格,负数标红,合计行 用粗顶线与明细分开。
- 合并打印:把多张发票合并为一个 PDF,保留矢量文字与按票划分的大纲,可在首页插入 一张汇总对账表。
构建
在项目根目录执行:
go build -o ofd-invoice ./cmd/ofd-invoice
快速开始
命令格式:
ofd-invoice [选项] input.ofd
# 输出紧凑 JSON 到标准输出(单张发票保持单个 Invoice 对象)
ofd-invoice invoice.ofd
# 输出缩进后的 JSON
ofd-invoice --pretty invoice.ofd
# 写入文件
ofd-invoice -o invoice.json invoice.ofd
ofd-invoice --pretty -o invoice.json invoice.ofd
# 多张发票汇总(输出含 items 与 summary 的批量 JSON)
ofd-invoice --pretty a.ofd b.ofd c.ofd
# 批目录,按开票日期排序;跳过抽不出附件的文件继续处理
ofd-invoice --input-dir ./发票 --sort date --skip-broken
# 导出对账报表(格式按扩展名推断,也可用 --format 指定)
ofd-invoice -o 九月对账.csv --input-dir ./九月
ofd-invoice -o 九月对账.xlsx --input-dir ./九月
ofd-invoice --format xlsx -o 对账.xlsx --input-dir ./九月 --sort date
# 合并成一个 PDF,首页放汇总对账表;按发票号码排序
ofd-invoice --merge-pdf 九月.pdf --cover --sort code --input-dir ./九月
未指定 -o / --output 时输出到标准输出;输出路径为 - 同样表示标准输出。
工具不允许输出文件覆盖任何一张输入的 OFD 文件,也不允许与 --merge-pdf 指向同一个文件。
输出形态:单个输入、只要 JSON、且没有改变处理口径(未用 --skip-broken / --dedup)时,
输出仍是单个 Invoice 对象,已有脚本无需改动;多张发票、批目录或 CSV/XLSX 报表时输出批量
形态 {items, skipped, summary},字段与 github.com/zc310/ofd/pkg/invoice 的模型一致,
字段缺失时为空或不出现。金额在 JSON 里始终是十进制字符串(如 "113.00"),不要当数字
解析后再转回文本,否则会丢小数位。
命令选项
| 选项 | 默认值 | 说明 |
|---|---|---|
-o, --output PATH |
标准输出 | 输出路径,使用 - 输出到标准输出 |
--format FORMAT |
按扩展名 | 输出格式 json、csv 或 xlsx |
--pretty |
关闭 | 缩进 JSON 输出(只适用于 JSON) |
--input-dir DIR |
空 | 批量处理目录下全部 .ofd / .zip 文件,按名排序 |
--skip-broken |
关闭 | 跳过抽取失败的输入继续处理(默认整批报错) |
--dedup |
关闭 | 按发票代码与号码去重,重复票不计入合计 |
--sort KEY |
input |
页序与报表排序:input、date、code、name |
--merge-pdf PATH |
空 | 把全部发票合并为一个 PDF 并写入该路径 |
--cover |
关闭 | 合并 PDF 时在首页插入汇总对账页(需 --merge-pdf) |
--cover-title TITLE |
默认标题 | 汇总对账页标题 |
-h, --help |
关闭 | 显示命令帮助 |
--version |
关闭 | 输出工具版本 |
布尔选项不带值
--cover、--pretty、--dedup、--skip-broken、--cell-border 都是布尔开关,写
--cover 或 --cover=true,不能写 --cover true:后面的 true 会被当成输入文件
路径(报"输入文件 true 不存在")。
退出码
0:抽取成功,结果已输出;--version输出版本号也算成功。1:抽取或输出失败(例如未找到发票结构化附件、附件不是发票数据、解析失败、写出文件失败、合并 PDF 失败)。2:命令参数或输入输出配置无效(缺少输入、格式不支持、--cover未配合--merge-pdf、 输出覆盖输入等)。
合并 PDF 的行为
- 页序由
--sort决定。--cover的汇总页排在最前,并且独占一整张纸——它不会跟着--nup挤进九宫格的某一格。指定了--paper时汇总页与票页用同一张纸,但汇总页恒为 纵向:--orientation auto可能把票页选成横向(例如--nup 4时 210×140 的票在 2×2 横向更省),汇总页仍取该纸张的纵向,版心与列宽按实际尺寸重算。 - 合并走「逐张解析成渲染文档,自己按版面输出 PDF」,保留矢量文字和按票划分的大纲, 不生成中间文件;每张票在大纲里有一条,拼版时指向它所在的那张纸与格子。
- 被去重跳过和抽取失败的发票不会进入 PDF,但会出现在
-o输出的汇总结果里。 - 单张发票默认 64MB、全部输入合计 512MB、最多 1000 张,超限直接报错。
纸张与拼版
# 非 A4 的票(卷式发票 210×140、火车票等)居中补白到 A4 打印
ofd-invoice --merge-pdf 补白.pdf --paper A4 --input-dir ./九月
# 一张 A4 纵向放两张票(1:1 原始尺寸),并画出裁切线
ofd-invoice --merge-pdf 两格.pdf --nup 2 --cell-border --input-dir ./九月
# 四格 2×2 + 5mm 间距
ofd-invoice --merge-pdf 四格.pdf --nup 4 --nup-gap 5 --input-dir ./九月
# 票源是竖高版式时改用横向
ofd-invoice --merge-pdf 横向.pdf --nup 2 --orientation landscape --input-dir ./九月
-
不给
--paper且--nup 1(默认):每页按源页自身尺寸 1:1 输出,不缩放不补白。 -
--fit只有contain:等比缩放并居中补白。不提供「填满裁切」——裁切会切掉票面 边缘的印章与价税合计,对发票等于毁票。 -
票面永不被放大。源页比格子小时按 1:1 印、多出来的部分留白:票面的物理尺寸本身 就是信息(定额发票的票面大小、火车票票幅),放大后打印件与票面对不上。也不提供填满 裁切——裁切会切掉票面边缘的印章与价税合计。
-
拼版份数只给约定俗成的档位,不给的份数直接报错,不做任意排布。网格固定为纵向: 两格、三格是单列纵向拼接,四格起才分列(4 是 2×2、6 是 2×3、8 是 2×4、9 是 3×3)。
-
方向默认
auto:比较纵向与横向的占用率(源页面等比缩放后占格子面积的比例), 取高者。规则由票源长宽比与格子长宽比是否匹配决定,不由格子行列数决定。以 210×140 的 卷式票为例:--nup纵向占用率 横向占用率 auto 选 2(格子 210×148.5 / 297×105) 94% 53% 纵向,票 1:1 4(格子 105×148.5 / 148.5×105) 47% 94% 横向,2×2 同一个票源,2 格与 4 格的最优方向相反——按"列数是否多于行数"判方向会把这两种都选错。 需要固定方向时用
--orientation portrait或landscape覆盖。 -
拼版页不写文字标注(页码、发票号)。矢量绘制接口目前没有文字绘制入口,只有字形 轮廓;
--cell-border的裁切线不受影响。 -
--cell-border用灰色虚线(0xBF、0.2mm、3mm 实 4mm 空,空白比实线段长,密集短线在 票据打印分辨率下会糊成灰实线),画在实际放了票的格子 周围;实线容易被当成票面的一部分,虚线一眼能认出是后加的辅助线。一页一票 (未指定--paper且--nup 1)时纸张就是页面本身,没有可裁的东西,因此不画框。
按票取页
# 每张票只打第 1 页
ofd-invoice --merge-pdf 首页.pdf --pages first --input-dir ./九月
# 每张票只打票面页(页面上有发票代码、发票号码、价税合计或开票日期字样的那页)
ofd-invoice --merge-pdf 票面.pdf --pages face --input-dir ./九月
# 按票内页码取
ofd-invoice --merge-pdf 清单.pdf --pages 1,3-5 --input-dir ./九月
电子普票常见「第 1 页发票清单 + 第 2 页票面」,这种票用 --pages first 会拿到清单页,
应该用 --pages face。face 靠文字层识别,认不出时退回第 1 页而不是不输出——发票票面
信息在附件 XML 里,金额汇总不受取页影响。
批量口径
- 默认严格:任一输入抽不出发票结构化数据,整批报错并返回
1。--skip-broken打开后 失败项记入结果并继续,避免整批金额在无人察觉时少算。 - 金额:按精确十进制求和后四舍五入到分,输出
100.00这类定长形态;红字发票的负金额 参与合计,--dedup与排序都不改变金额口径。 - 分组:销售方按纳税人识别号分组(税号相同视为同一销售方),发票类型与开票月份按票面 值分组,缺失值归入「未知」而不是被丢弃。
CI 示例
ofd-invoice --pretty -o invoice.json invoice.ofd
status=$?
test "$status" -eq 0