invoice

package
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

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

func (d *Decimal) MarshalJSON() ([]byte, error)

MarshalJSON 把 Decimal 输出为十进制字符串。

func (*Decimal) String

func (d *Decimal) String() string

String 返回 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

func Extract(input any) (*Invoice, error)

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

type Party

type Party struct {
	// Name 是名称。
	Name string `json:"name,omitempty"`
	// Code 是纳税人识别号。
	Code string `json:"code,omitempty"`
	// Address 是地址电话。
	Address string `json:"address,omitempty"`
	// Account 是开户行及账号。
	Account string `json:"account,omitempty"`
}

Party 是发票一方的资料(购买方或销售方)。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL