Documentation
¶
Overview ¶
Package invoice 从 OFD 电子发票中抽取结构化发票信息。
抽取以 OFD 包内嵌的发票结构化附件(通常为 Doc_0/Attachs/original_invoice.xml) 为主,附件缺失不可用时返回错误;页面文字层只用于补齐票面视觉字段(发票标题、 价税合计大写金额),并据此推断发票类型。不涉及 PDF 等非 OFD 输入。
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
View Source
var ErrNoAttachment = errors.New("未找到 OFD 发票结构化附件(Attachs 目录下的 XML)")
ErrNoAttachment 表示 OFD 包内未找到可用的发票结构化附件。
Functions ¶
This section is empty.
Types ¶
type Decimal ¶
type Decimal struct {
// contains filtered or unexported fields
}
Decimal 表示精确的十进制数值(金额、数量、税率),基于任意精度的有理数。 JSON 序列化为十进制字符串(如 "123.45"),避免浮点误差。
func (*Decimal) MarshalJSON ¶
MarshalJSON 把 Decimal 输出为十进制字符串。
type Detail ¶
type Detail struct {
// Name 是项目名称。
Name string `json:"name,omitempty"`
// Model 是规格型号。
Model string `json:"model,omitempty"`
// Unit 是单位。
Unit string `json:"unit,omitempty"`
// Count 是数量。
Count *Decimal `json:"count,omitempty"`
// Price 是单价。
Price *Decimal `json:"price,omitempty"`
// Amount 是金额。
Amount *Decimal `json:"amount,omitempty"`
// TaxRate 是税率(小数形式,如 0.13)。
TaxRate *Decimal `json:"tax_rate,omitempty"`
// TaxAmount 是税额。
TaxAmount *Decimal `json:"tax_amount,omitempty"`
}
Detail 是一行价税明细。
type Invoice ¶
type Invoice struct {
// Title 是票面标题,例如「增值税电子普通发票」。
Title string `json:"title,omitempty"`
// Type 是由标题推断的发票类型:普通发票、专用发票或通行费。
Type string `json:"type,omitempty"`
// MachineNumber 是机器编号。
MachineNumber string `json:"machine_number,omitempty"`
// Code 是发票代码。
Code string `json:"code,omitempty"`
// Number 是发票号码。
Number string `json:"number,omitempty"`
// Date 是开票日期(通常为 YYYY-MM-DD)。
Date string `json:"date,omitempty"`
// Checksum 是发票校验码。
Checksum string `json:"checksum,omitempty"`
// TotalAmountString 是价税合计的大写金额文本,来自页面文字层。
TotalAmountString string `json:"total_amount_string,omitempty"`
// Amount 是不含税金额。
Amount *Decimal `json:"amount,omitempty"`
// TaxAmount 是税额。
TaxAmount *Decimal `json:"tax_amount,omitempty"`
// TotalAmount 是价税合计(小写)。
TotalAmount *Decimal `json:"total_amount,omitempty"`
// TaxControlCode 是税控码。
TaxControlCode string `json:"tax_control_code,omitempty"`
// Payee 是收款人。
Payee string `json:"payee,omitempty"`
// Reviewer 是复核人。
Reviewer string `json:"reviewer,omitempty"`
// Drawer 是开票人。
Drawer string `json:"drawer,omitempty"`
// Buyer 是购买方信息。
Buyer Party `json:"buyer"`
// Seller 是销售方信息。
Seller Party `json:"seller"`
// Details 是商品或劳务明细行。
Details []Detail `json:"details"`
// Attachment 是实际使用到的附件包内路径。
Attachment string `json:"attachment"`
// Warnings 是抽取过程中的非致命提示。
Warnings []string `json:"warnings,omitempty"`
}
Invoice 是一张电子发票的抽取结果。
func Extract ¶
Extract 从 OFD 电子发票中抽取发票信息。input 支持文件路径(string)、 文件数据([]byte)或 io.Reader。
抽取以上一节内嵌结构化附件为主:先尝试 Doc_0/Attachs/original_invoice.xml, 不存在时扫描所有包含 attach 的 XML 条目,取第一个能解析出有效发票数据的文件。 之后读取页面文字层补齐票面标题与价税合计大写金额,并按标题推断发票类型。
Example ¶
ExampleExtract 演示从包含发票结构化附件的 OFD 字节中抽取发票信息。
package main
import (
"bytes"
"fmt"
"github.com/klauspost/compress/zip"
"github.com/zc310/ofd/pkg/invoice"
)
// ExampleExtract 演示从包含发票结构化附件的 OFD 字节中抽取发票信息。
func main() {
data := sampleInvoiceOFD()
got, err := invoice.Extract(data)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(got.Type)
fmt.Println(got.Code, got.Number)
fmt.Println(got.TotalAmountString)
fmt.Println(got.Buyer.Name)
fmt.Println(got.Details[0].Name, got.Details[0].TaxRate)
}
// sampleInvoiceOFD 构造一个最小 OFD,包含发票结构化附件与票面文字层。
func sampleInvoiceOFD() []byte {
files := map[string]string{
"OFD.xml": `<?xml version="1.0" encoding="UTF-8"?>
<OFD xmlns="http://www.ofdspec.org/2016" Version="1.0" DocType="OFD">
<DocBody>
<DocInfo><DocID>invoice</DocID><Title>发票</Title></DocInfo>
<DocRoot>Doc_0/Document.xml</DocRoot>
</DocBody>
</OFD>`,
"Doc_0/Document.xml": `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="http://www.ofdspec.org/2016">
<CommonData>
<MaxUnitID>10</MaxUnitID>
<PageArea><PhysicalBox>0 0 210 297</PhysicalBox><ApplicationBox>0 0 210 297</ApplicationBox></PageArea>
</CommonData>
<Pages><Page ID="1" BaseLoc="Pages/Page_0/Content.xml"/></Pages>
</Document>`,
"Doc_0/Pages/Page_0/Content.xml": `<?xml version="1.0" encoding="UTF-8"?>
<Page xmlns="http://www.ofdspec.org/2016">
<Area><PhysicalBox>0 0 210 297</PhysicalBox><ApplicationBox>0 0 210 297</ApplicationBox></Area>
<Content>
<Layer ID="1" Type="Body">
<TextObject ID="1" Boundary="70 10 70 8" Font="1" Size="4"><TextCode X="30" Y="20">增值税电子普通发票</TextCode></TextObject>
<TextObject ID="2" Boundary="110 260 60 6" Font="1" Size="3"><TextCode X="100" Y="262">价税合计(大写)壹佰壹拾叁元整</TextCode></TextObject>
</Layer>
</Content>
</Page>`,
"Doc_0/Attachs/original_invoice.xml": `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:cn:gov:tax:invoice">
<MachineNo>444000224046</MachineNo>
<InvoiceCode>033001900111</InvoiceCode>
<InvoiceNo>12345678</InvoiceNo>
<IssueDate>2026-09-18</IssueDate>
<InvoiceCheckCode>6A5B2C3D1E4F</InvoiceCheckCode>
<TaxExclusiveTotalAmount>100.00</TaxExclusiveTotalAmount>
<TaxTotalAmount>13.00</TaxTotalAmount>
<TaxInclusiveTotalAmount>113.00</TaxInclusiveTotalAmount>
<Payee>王五</Payee>
<Checker>赵六</Checker>
<InvoiceClerk>张三</InvoiceClerk>
<Buyer><BuyerName>买方公司</BuyerName><BuyerTaxID>911100000000000000</BuyerTaxID></Buyer>
<Seller><SellerName>卖方公司</SellerName><SellerTaxID>911100001111000000</SellerTaxID></Seller>
<GoodsInfos>
<G1>
<Item>办公用品*中性笔</Item>
<Specification>0.5mm</Specification>
<MeasurementDimension>支</MeasurementDimension>
<Quantity>10.00</Quantity>
<Price>2.00</Price>
<Amount>20.00</Amount>
<TaxScheme>13%</TaxScheme>
<TaxAmount>2.60</TaxAmount>
</G1>
</GoodsInfos>
</Invoice>`,
}
var data bytes.Buffer
archive := zip.NewWriter(&data)
for name, value := range files {
writer, err := archive.Create(name)
if err != nil {
panic(err)
}
if _, err := writer.Write([]byte(value)); err != nil {
panic(err)
}
}
if err := archive.Close(); err != nil {
panic(err)
}
return data.Bytes()
}
Output: 普通发票 033001900111 12345678 壹佰壹拾叁元整 买方公司 办公用品*中性笔 0.13
Click to show internal directories.
Click to hide internal directories.