feat(电子发票): 完善发票识别并统一命名空间
build.yaml / build (push) Successful in 53s

This commit is contained in:
xiaoqidun committed 2026-10-16 15:12:42 +08:00
1 parent 68d6a19656
commit 6639653843
6 files changed
+106 -44

No files matched your search

+1 -2
View File
@@ -5857,7 +5857,6 @@ async function newInvoiceDraft() {
function renderInvoiceBasic(container, draft) { function renderInvoiceBasic(container, draft) {
const fields = (draft.detail.fields || []).filter(field => field.item < 0 && !field.field.startsWith("facts.") && !field.field.startsWith("items.")); const fields = (draft.detail.fields || []).filter(field => field.item < 0 && !field.field.startsWith("facts.") && !field.field.startsWith("items."));
const standards = {"http://www.edrm.org.cn/schema/e-invoice/2019": "电子发票/2019", "http://www.ofdspec.org/2016": "发票标引"};
const labels = { const labels = {
kind: "票种", standard: "规范", version: "版本", number: "号码", code: "代码", date: "日期", kind: "票种", standard: "规范", version: "版本", number: "号码", code: "代码", date: "日期",
currency: "币种", status: "状态", amount: "金额", taxAmount: "税额", totalAmount: "合计", remarks: "备注", currency: "币种", status: "状态", amount: "金额", taxAmount: "税额", totalAmount: "合计", remarks: "备注",
@@ -5871,7 +5870,7 @@ function renderInvoiceBasic(container, draft) {
container.append(title, grid); container.append(title, grid);
for (const field of fields.filter(field => field.group === group)) { for (const field of fields.filter(field => field.group === group)) {
const value = invoiceValue(draft.invoice, field.field); const value = invoiceValue(draft.invoice, field.field);
const display = field.field === "standard" ? standards[value] || value : value; const display = field.field === "standard" ? draft.detail.standardLabel || value : value;
invoiceInput(grid, labels[field.field.split(".").at(-1)] || field.label, `Field_${field.field.replaceAll(".", "_")}`, display, value => { invoiceInput(grid, labels[field.field.split(".").at(-1)] || field.label, `Field_${field.field.replaceAll(".", "_")}`, display, value => {
invoiceSetValue(draft.invoice, field.field, -1, value); invoiceSetValue(draft.invoice, field.field, -1, value);
invoiceDirty(draft, "model"); invoiceDirty(draft, "model");
+8 -1
View File
@@ -1004,6 +1004,13 @@ func documentInvoice(args []js.Value) (any, error) {
return nil, fmt.Errorf("invoice index out of range") return nil, fmt.Errorf("invoice index out of range")
} }
value := report.Invoices[request.Index] value := report.Invoices[request.Index]
standardLabel := value.Standard
switch value.Standard {
case ofdgo.InvoiceStandardEDRM2019:
standardLabel = "电子发票/2019"
case ofdgo.InvoiceStandardObjectReference:
standardLabel = "发票标引"
}
capabilities, err := r.InvoiceCapabilities(ctx, request.Source, request.Index) capabilities, err := r.InvoiceCapabilities(ctx, request.Source, request.Index)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1012,7 +1019,7 @@ func documentInvoice(args []js.Value) (any, error) {
if bindingErr != nil { if bindingErr != nil {
report.Issues = append(report.Issues, ofdgo.InvoiceIssue{Code: "binding-error", Message: bindingErr.Error(), Source: request.Source}) report.Issues = append(report.Issues, ofdgo.InvoiceIssue{Code: "binding-error", Message: bindingErr.Error(), Source: request.Source})
} }
return map[string]any{"invoice": value, "fields": ofdgo.InvoiceFields(value), "capabilities": capabilities, "bindings": bindings, "digest": ofdgo.InvoiceDigest(data), "xml": string(data), "issues": report.Issues}, nil return map[string]any{"invoice": value, "standardLabel": standardLabel, "fields": ofdgo.InvoiceFields(value), "capabilities": capabilities, "bindings": bindings, "digest": ofdgo.InvoiceDigest(data), "xml": string(data), "issues": report.Issues}, nil
} }
// exportDocumentInvoice 导出发票原文 // exportDocumentInvoice 导出发票原文
+1 -1
View File
@@ -992,7 +992,7 @@ func invoicePayloadSignature(ctx context.Context, data []byte) (bool, error) {
} }
node := nodes[len(nodes)-1] node := nodes[len(nodes)-1]
nodes = nodes[:len(nodes)-1] nodes = nodes[:len(nodes)-1]
if node.name.Space == "http://www.w3.org/2000/09/xmldsig#" && node.name.Local == "Signature" || root.name.Space == "" && root.name.Local == "EInvoice" && node.parent == root && node.name.Space == "" && node.name.Local == "TaxBureauSignature" { if node.name.Space == invoiceXMLSignature && node.name.Local == "Signature" || root.name.Space == "" && root.name.Local == "EInvoice" && node.parent == root && node.name.Space == "" && node.name.Local == "TaxBureauSignature" {
return true, nil return true, nil
} }
nodes = append(nodes, node.children...) nodes = append(nodes, node.children...)
+10 -2
View File
@@ -34,7 +34,7 @@ func InvoiceDigest(data []byte) string {
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])
} }
// ReadInvoice 读取当前文档及版本声明的发票,标引按对象引用提取文本 // ReadInvoice 读取当前文档及版本声明的发票,字段路径相对导出XML,标引按对象引用提取文本
// 入参: ctx 取消上下文, source 精确来源 // 入参: ctx 取消上下文, source 精确来源
// 返回: InvoiceReport 发票及诊断, error 来源或读取错误 // 返回: InvoiceReport 发票及诊断, error 来源或读取错误
func (r *Reader) ReadInvoice(ctx context.Context, source InvoiceSource) (InvoiceReport, error) { func (r *Reader) ReadInvoice(ctx context.Context, source InvoiceSource) (InvoiceReport, error) {
@@ -43,7 +43,12 @@ func (r *Reader) ReadInvoice(ctx context.Context, source InvoiceSource) (Invoice
if err != nil { if err != nil {
return InvoiceReport{}, err return InvoiceReport{}, err
} }
report, err := r.readInvoiceXML(ctx, data, source) local := source
local.NodePath = ""
report, err := r.readInvoiceXML(ctx, data, local)
for i := range report.Invoices {
report.Invoices[i].Sources = []InvoiceSource{source}
}
report.DocumentIndex = r.DocumentIndex() report.DocumentIndex = r.DocumentIndex()
if r.versionInfo != nil { if r.versionInfo != nil {
report.Version = r.versionInfo.ID report.Version = r.versionInfo.ID
@@ -64,6 +69,9 @@ func (r *Reader) readInvoiceXML(ctx context.Context, data []byte, source Invoice
return report, err return report, err
} }
if root.name.Space != ofdNamespace2016 || root.name.Local != "root" || !invoiceObjectReferences(root) { if root.name.Space != ofdNamespace2016 || root.name.Local != "root" || !invoiceObjectReferences(root) {
if root.name.Local != "eInvoice" && root.name.Local != "EInvoice" && root.name.Local != "xbrl" {
return InvoiceReport{}, nil
}
return report, nil return report, nil
} }
structure, err := parseStructure(ctx, data, CustomTag{TypeID: structureTypeOfficial, NameSpace: ofdNamespace2016}) structure, err := parseStructure(ctx, data, CustomTag{TypeID: structureTypeOfficial, NameSpace: ofdNamespace2016})
+25 -24
View File
@@ -117,7 +117,7 @@ type invoiceXMLPatch struct {
func InvoiceFields(value Invoice) []InvoiceFieldDescriptor { func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
format, supported := invoiceWriteFormat(value) format, supported := invoiceWriteFormat(value)
probe := Invoice{Items: []InvoiceItem{{}}} probe := Invoice{Items: []InvoiceItem{{}}}
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
probe.Number, probe.Date = "editable", "editable" probe.Number, probe.Date = "editable", "editable"
for _, group := range invoicePlainGroups() { for _, group := range invoicePlainGroups() {
for _, name := range strings.Fields(group.value) { for _, name := range strings.Fields(group.value) {
@@ -171,7 +171,7 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
case "amount", "taxAmount", "totalAmount", "items.quantity", "items.unitPrice", "items.amount", "items.taxAmount": case "amount", "taxAmount", "totalAmount", "items.quantity", "items.unitPrice", "items.amount", "items.taxAmount":
d.Type = "decimal" d.Type = "decimal"
case "items.taxRate": case "items.taxRate":
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
d.Type = "decimal" d.Type = "decimal"
} }
} }
@@ -208,13 +208,14 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
// InvoiceFormats 返回独立格式能力副本,创建及编辑能力按业务版本分别声明 // InvoiceFormats 返回独立格式能力副本,创建及编辑能力按业务版本分别声明
// 返回: 已适配格式列表 // 返回: 已适配格式列表
func InvoiceFormats() []InvoiceFormat { func InvoiceFormats() []InvoiceFormat {
result := []InvoiceFormat{{Kind: "einv", Standard: "EInvoice", Version: "0.3", Read: true, Create: true, Edit: true, Items: true}} result := []InvoiceFormat{{Kind: "einv", Standard: InvoiceStandardEInvoice, Version: "0.3", Read: true, Create: true, Edit: true, Items: true}}
result = append(result, InvoiceFormat{Kind: "inv", Standard: invoiceEDRM, Version: "1.0", Read: true, Items: true}) result = append(result, InvoiceFormat{Kind: "inv", Standard: invoiceEDRM, Version: "1.0", Read: true, Items: true})
result = append(result, InvoiceFormat{Kind: "inv", Standard: ofdNamespace2016, Version: "1.0", Read: true}) result = append(result, InvoiceFormat{Kind: "inv", Standard: ofdNamespace2016, Version: "1.0", Read: true})
for _, kind := range []string{"inv", "einv", "atr", "rai"} { for _, standard := range []string{InvoiceStandardVAT2023, InvoiceStandardDigital2023, InvoiceStandardAir2023, InvoiceStandardRail2023} {
result = append(result, InvoiceFormat{Kind: kind, Standard: "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/" + kind, Version: "2023-12-31", Read: true, Create: true, Edit: true, Items: kind == "inv" || kind == "atr"}) kind, version := invoiceTaxonomy(standard)
result = append(result, InvoiceFormat{Kind: kind, Standard: standard, Version: version, Read: true, Create: true, Edit: true, Items: kind == "inv" || kind == "atr"})
} }
return append(result, InvoiceFormat{Kind: "atr", Standard: "http://xbrl.mof.gov.cn/taxonomy/2021-11-30/atr", Version: "2021-11-30", Read: true, Edit: true, Items: true}) return append(result, InvoiceFormat{Kind: "atr", Standard: InvoiceStandardAir2021, Version: "2021-11-30", Read: true, Edit: true, Items: true}, InvoiceFormat{Kind: "inv", Standard: InvoiceStandardVAT2021, Version: "2021-09-30", Read: true, Items: true})
} }
// DecodeInvoices 提取独立XML或XBRL中的发票,不执行联网或完整XSD验证 // DecodeInvoices 提取独立XML或XBRL中的发票,不执行联网或完整XSD验证
@@ -280,7 +281,7 @@ func ValidateInvoice(ctx context.Context, value Invoice) ([]InvoiceIssue, error)
add("invalid-date", "Date", "invalid invoice date") add("invalid-date", "Date", "invalid invoice date")
} }
} }
if value.Currency != "" && !invoiceCurrencyUnit("{http://www.xbrl.org/2003/iso4217}"+value.Currency) { if value.Currency != "" && !invoiceCurrencyUnit("{"+InvoiceCurrencyNamespace+"}"+value.Currency) {
add("invalid-value", "Currency", "invalid ISO currency code") add("invalid-value", "Currency", "invalid ISO currency code")
} }
for _, field := range []invoiceWriteField{{"Amount", value.Amount}, {"TaxAmount", value.TaxAmount}, {"TotalAmount", value.TotalAmount}} { for _, field := range []invoiceWriteField{{"Amount", value.Amount}, {"TaxAmount", value.TaxAmount}, {"TotalAmount", value.TotalAmount}} {
@@ -303,11 +304,11 @@ func ValidateInvoice(ctx context.Context, value Invoice) ([]InvoiceIssue, error)
add("invalid-decimal", fmt.Sprintf("Items[%d].%s", i, field.name), "invalid decimal lexical value") add("invalid-decimal", fmt.Sprintf("Items[%d].%s", i, field.name), "invalid decimal lexical value")
} }
} }
if value.Standard == "EInvoice" && item.TaxRate != "" && invoiceDecimal(item.TaxRate) == nil { if value.Standard == InvoiceStandardEInvoice && item.TaxRate != "" && invoiceDecimal(item.TaxRate) == nil {
add("invalid-decimal", fmt.Sprintf("Items[%d].TaxRate", i), "invalid decimal lexical value") add("invalid-decimal", fmt.Sprintf("Items[%d].TaxRate", i), "invalid decimal lexical value")
} }
} }
if value.Kind == "inv" || value.Standard == "EInvoice" { if value.Kind == "inv" || value.Standard == InvoiceStandardEInvoice {
for _, total := range []invoiceWriteField{{"Amount", value.Amount}, {"TaxAmount", value.TaxAmount}} { for _, total := range []invoiceWriteField{{"Amount", value.Amount}, {"TaxAmount", value.TaxAmount}} {
sum := new(big.Rat) sum := new(big.Rat)
complete := len(value.Items) > 0 complete := len(value.Items) > 0
@@ -459,7 +460,7 @@ func EncodeInvoiceWithOptions(ctx context.Context, value Invoice, options Invoic
return nil, err return nil, err
} }
var data []byte var data []byte
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
data, err = invoiceEncodePlain(value) data, err = invoiceEncodePlain(value)
if err == nil { if err == nil {
data, err = invoiceWritePlainSections(ctx, data, options) data, err = invoiceWritePlainSections(ctx, data, options)
@@ -621,7 +622,7 @@ func invoiceEncodeXBRL(value Invoice, options InvoiceEncodingOptions) ([]byte, e
} }
var b strings.Builder var b strings.Builder
b.WriteString(xml.Header) b.WriteString(xml.Header)
fmt.Fprintf(&b, `<xbrli:xbrl xmlns:xbrli="%s" xmlns:xsi="%s" xmlns:iso4217="http://www.xbrl.org/2003/iso4217" xmlns:f="%s" xmlns:link="http://www.xbrl.org/2003/linkbase" xmlns:xlink="http://www.w3.org/1999/xlink">`, invoiceXBRL, invoiceXSI, value.Standard) fmt.Fprintf(&b, `<xbrli:xbrl xmlns:xbrli="%s" xmlns:xsi="%s" xmlns:iso4217="%s" xmlns:f="%s" xmlns:link="%s" xmlns:xlink="%s">`, invoiceXBRL, invoiceXSI, InvoiceCurrencyNamespace, value.Standard, invoiceLinkbase, invoiceXLink)
fmt.Fprintf(&b, `<link:schemaRef xlink:type="simple" xlink:href="%s"/><xbrli:context id="%s"><xbrli:entity><xbrli:identifier scheme="%s">%s</xbrli:identifier></xbrli:entity><xbrli:period>`, invoiceWriteEscape(options.SchemaRef), options.Context.ID, invoiceWriteEscape(options.Context.Scheme), invoiceWriteEscape(options.Context.Identifier)) fmt.Fprintf(&b, `<link:schemaRef xlink:type="simple" xlink:href="%s"/><xbrli:context id="%s"><xbrli:entity><xbrli:identifier scheme="%s">%s</xbrli:identifier></xbrli:entity><xbrli:period>`, invoiceWriteEscape(options.SchemaRef), options.Context.ID, invoiceWriteEscape(options.Context.Scheme), invoiceWriteEscape(options.Context.Identifier))
if options.Context.Instant != "" { if options.Context.Instant != "" {
if options.Context.StartDate != "" || options.Context.EndDate != "" { if options.Context.StartDate != "" || options.Context.EndDate != "" {
@@ -722,7 +723,7 @@ func invoiceWriteFact(b *strings.Builder, value Invoice, fact InvoiceFact, optio
if numeric { if numeric {
unit := "" unit := ""
for _, u := range options.Units { for _, u := range options.Units {
match := u.Namespace == "http://www.xbrl.org/2003/iso4217" && u.Name == value.Currency match := u.Namespace == InvoiceCurrencyNamespace && u.Name == value.Currency
if !invoiceMonetary(value.Kind, fact.Name) { if !invoiceMonetary(value.Kind, fact.Name) {
match = u.Namespace == invoiceXBRL && u.Name == "pure" match = u.Namespace == invoiceXBRL && u.Name == "pure"
if fact.Unit != "" { if fact.Unit != "" {
@@ -1215,7 +1216,7 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
if err != nil { if err != nil {
return nil, err return nil, err
} }
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
var expanded []invoiceXMLPatch var expanded []invoiceXMLPatch
for _, s := range spans { for _, s := range spans {
n := s.node n := s.node
@@ -1251,7 +1252,7 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
clean.Facts = nil clean.Facts = nil
clean.Sources = nil clean.Sources = nil
clean.Items = append([]InvoiceItem(nil), value.Items...) clean.Items = append([]InvoiceItem(nil), value.Items...)
if clean.Standard != "EInvoice" { if clean.Standard != InvoiceStandardEInvoice {
for _, name := range invoiceWriteHeader(clean) { for _, name := range invoiceWriteHeader(clean) {
previous := invoiceXMLField(&old, old.Kind, name) previous := invoiceXMLField(&old, old.Kind, name)
target := invoiceXMLField(&clean, clean.Kind, name) target := invoiceXMLField(&clean, clean.Kind, name)
@@ -1262,7 +1263,7 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
} }
for i := range clean.Items { for i := range clean.Items {
clean.Items[i].Fields = nil clean.Items[i].Fields = nil
if value.Standard != "EInvoice" && (value.Kind == "atr" || value.Kind == "inv") { if value.Standard != InvoiceStandardEInvoice && (value.Kind == "atr" || value.Kind == "inv") {
for _, f := range value.Items[i].Fields { for _, f := range value.Items[i].Fields {
if !invoiceWriteContains(invoiceWriteItemNames(value.Kind), f.Name) { if !invoiceWriteContains(invoiceWriteItemNames(value.Kind), f.Name) {
continue continue
@@ -1291,7 +1292,7 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
} }
} }
var generated []byte var generated []byte
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
generated, err = invoiceEncodePlain(clean) generated, err = invoiceEncodePlain(clean)
} else { } else {
options, e := invoiceWriteOriginalOptions(root, old) options, e := invoiceWriteOriginalOptions(root, old)
@@ -1310,7 +1311,7 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
var patches []invoiceXMLPatch var patches []invoiceXMLPatch
var trailingGroups []invoiceXMLPatch var trailingGroups []invoiceXMLPatch
var itemParent, generatedParent *invoiceXMLNode var itemParent, generatedParent *invoiceXMLNode
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
if len(invoiceChildren(root, "", "EInvoiceData")) != 1 || len(invoiceChildren(root, "", "TaxSupervisionInfo")) != 1 { if len(invoiceChildren(root, "", "EInvoiceData")) != 1 || len(invoiceChildren(root, "", "TaxSupervisionInfo")) != 1 {
return nil, fmt.Errorf("invoice requires unique data and supervision groups") return nil, fmt.Errorf("invoice requires unique data and supervision groups")
} }
@@ -1425,7 +1426,7 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
var matches []Invoice var matches []Invoice
for _, inv := range after.Invoices { for _, inv := range after.Invoices {
if inv.Kind == value.Kind && inv.Standard == value.Standard { if inv.Kind == value.Kind && inv.Standard == value.Standard {
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
matches = append(matches, inv) matches = append(matches, inv)
continue continue
} }
@@ -1503,8 +1504,8 @@ func invoiceWriteOriginalOptions(root *invoiceXMLNode, value Invoice) (InvoiceEn
} }
} }
for _, n := range root.children { for _, n := range root.children {
if n.name == (xml.Name{Space: "http://www.xbrl.org/2003/linkbase", Local: "schemaRef"}) && options.SchemaRef == "" { if n.name == (xml.Name{Space: invoiceLinkbase, Local: "schemaRef"}) && options.SchemaRef == "" {
options.SchemaRef = invoiceXMLAttr(n, "http://www.w3.org/1999/xlink", "href") options.SchemaRef = invoiceXMLAttr(n, invoiceXLink, "href")
} }
if n.name == (xml.Name{Space: invoiceXBRL, Local: "context"}) && invoiceXMLAttr(n, "", "id") == ref { if n.name == (xml.Name{Space: invoiceXBRL, Local: "context"}) && invoiceXMLAttr(n, "", "id") == ref {
dims, ok := invoiceContext(n) dims, ok := invoiceContext(n)
@@ -1684,7 +1685,7 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
} }
space, name := value.Standard, invoiceWriteTuple(value.Kind) space, name := value.Standard, invoiceWriteTuple(value.Kind)
names := invoiceWriteItemNames(value.Kind) names := invoiceWriteItemNames(value.Kind)
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
space = "" space = ""
name = "IssuItemInformation" name = "IssuItemInformation"
names = strings.Fields("ItemName SpecMod MeaUnits Quantity UnPrice Amount TaxRate ComTaxAm") names = strings.Fields("ItemName SpecMod MeaUnits Quantity UnPrice Amount TaxRate ComTaxAm")
@@ -1731,7 +1732,7 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
if path == "" { if path == "" {
for _, f := range item.Fields { for _, f := range item.Fields {
mapped := invoiceItemField(&InvoiceItem{}, value.Kind, f.Name) mapped := invoiceItemField(&InvoiceItem{}, value.Kind, f.Name)
if value.Standard == "EInvoice" || !invoiceWriteContains(names, f.Name) { if value.Standard == InvoiceStandardEInvoice || !invoiceWriteContains(names, f.Name) {
return nil, fmt.Errorf("unsupported new invoice item fact %s", f.Name) return nil, fmt.Errorf("unsupported new invoice item fact %s", f.Name)
} }
if mapped != nil { if mapped != nil {
@@ -1793,7 +1794,7 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
if len(ordered) == 0 && len(combined) > 0 { if len(ordered) == 0 && len(combined) > 0 {
var p invoiceXMLPatch var p invoiceXMLPatch
var err error var err error
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
p, err = invoiceWriteOrderedInsert(data, spans, parent, "", name, strings.Fields("SellerInformation BuyerInformation BasicInformation IssuItemInformation SpecificInformation AdditionalInformation"), combined) p, err = invoiceWriteOrderedInsert(data, spans, parent, "", name, strings.Fields("SellerInformation BuyerInformation BasicInformation IssuItemInformation SpecificInformation AdditionalInformation"), combined)
} else { } else {
p, err = invoiceWriteInsert(spans[parent.path], combined, data) p, err = invoiceWriteInsert(spans[parent.path], combined, data)
@@ -1837,7 +1838,7 @@ func invoiceWriteOrderedInsert(data []byte, spans map[string]invoiceXMLSpan, par
// 入参: value 发票 // 入参: value 发票
// 返回: 缺少元组字段错误 // 返回: 缺少元组字段错误
func invoiceWriteCreation(value Invoice) error { func invoiceWriteCreation(value Invoice) error {
if value.Standard == "EInvoice" { if value.Standard == InvoiceStandardEInvoice {
return nil return nil
} }
for i, item := range value.Items { for i, item := range value.Items {
+61 -14
View File
@@ -28,14 +28,59 @@ import (
"time" "time"
) )
// invoiceXBRL 表示XBRL实例命名空间 // invoiceXMLNamespace 表示XML预定义命名空间
const invoiceXBRL = "http://www.xbrl.org/2003/instance" const invoiceXMLNamespace = "http://www.w3.org/XML/1998/namespace"
// invoiceXSI 表示XMLSchema实例命名空间 // invoiceXSI 表示XMLSchema实例命名空间
const invoiceXSI = "http://www.w3.org/2001/XMLSchema-instance" const invoiceXSI = "http://www.w3.org/2001/XMLSchema-instance"
// invoiceXLink 表示XML链接命名空间
const invoiceXLink = "http://www.w3.org/1999/xlink"
// invoiceXMLSignature 表示XML数字签名命名空间
const invoiceXMLSignature = "http://www.w3.org/2000/09/xmldsig#"
// invoiceXBRL 表示XBRL实例命名空间
const invoiceXBRL = "http://www.xbrl.org/2003/instance"
// invoiceLinkbase 表示XBRL链接库命名空间
const invoiceLinkbase = "http://www.xbrl.org/2003/linkbase"
// invoiceDimension 表示XBRL实例维度命名空间
const invoiceDimension = "http://xbrl.org/2006/xbrldi"
// InvoiceCurrencyNamespace 表示InvoiceUnit中ISO货币度量使用的命名空间
const InvoiceCurrencyNamespace = "http://www.xbrl.org/2003/iso4217"
// InvoiceStandardVAT2023 表示2023版增值税发票分类标准
const InvoiceStandardVAT2023 = "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/inv"
// InvoiceStandardDigital2023 表示2023版数字化电子发票分类标准
const InvoiceStandardDigital2023 = "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/einv"
// InvoiceStandardAir2023 表示2023版航空运输电子客票行程单分类标准
const InvoiceStandardAir2023 = "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/atr"
// InvoiceStandardRail2023 表示2023版铁路电子客票分类标准
const InvoiceStandardRail2023 = "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/rai"
// InvoiceStandardVAT2021 表示2021版增值税发票分类标准,仅支持读取
const InvoiceStandardVAT2021 = "http://xbrl.mof.gov.cn/taxonomy/2021-09-30/inv"
// InvoiceStandardAir2021 表示2021版航空运输电子客票行程单分类标准
const InvoiceStandardAir2021 = "http://xbrl.mof.gov.cn/taxonomy/2021-11-30/atr"
// InvoiceStandardEInvoice 表示无命名空间的EInvoice业务标准,版本由Invoice.Version指定
const InvoiceStandardEInvoice = "EInvoice"
// InvoiceStandardObjectReference 表示基于OFD对象引用的只读发票标引
const InvoiceStandardObjectReference = ofdNamespace2016
// InvoiceStandardEDRM2019 表示2019版电子发票业务标准
const InvoiceStandardEDRM2019 = "http://www.edrm.org.cn/schema/e-invoice/2019"
// invoiceEDRM 表示2019版电子发票业务命名空间 // invoiceEDRM 表示2019版电子发票业务命名空间
const invoiceEDRM = "http://www.edrm.org.cn/schema/e-invoice/2019" const invoiceEDRM = InvoiceStandardEDRM2019
// invoiceDecimalPattern 仅接受XML十进制词法 // invoiceDecimalPattern 仅接受XML十进制词法
var invoiceDecimalPattern = regexp.MustCompile(`^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$`) var invoiceDecimalPattern = regexp.MustCompile(`^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$`)
@@ -150,7 +195,7 @@ func invoiceXMLTree(ctx context.Context, data []byte) (*invoiceXMLNode, error) {
return nil, fmt.Errorf("invoice XML exceeds namespace URI limit") return nil, fmt.Errorf("invoice XML exceeds namespace URI limit")
} }
} }
n := &invoiceXMLNode{name: v.Name, attrs: v.Attr, parent: current, ns: map[string]string{"xml": "http://www.w3.org/XML/1998/namespace"}, counts: map[xml.Name]int{}, start: d.InputOffset()} n := &invoiceXMLNode{name: v.Name, attrs: v.Attr, parent: current, ns: map[string]string{"xml": invoiceXMLNamespace}, counts: map[xml.Name]int{}, start: d.InputOffset()}
index := 1 index := 1
if current != nil { if current != nil {
for k, v := range current.ns { for k, v := range current.ns {
@@ -223,16 +268,18 @@ func invoiceXMLTree(ctx context.Context, data []byte) (*invoiceXMLNode, error) {
// 返回: 票种及版本,未知时为空 // 返回: 票种及版本,未知时为空
func invoiceTaxonomy(ns string) (string, string) { func invoiceTaxonomy(ns string) (string, string) {
switch ns { switch ns {
case "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/inv": case InvoiceStandardVAT2023:
return "inv", "2023-12-31" return "inv", "2023-12-31"
case "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/einv": case InvoiceStandardDigital2023:
return "einv", "2023-12-31" return "einv", "2023-12-31"
case "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/atr": case InvoiceStandardAir2023:
return "atr", "2023-12-31" return "atr", "2023-12-31"
case "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/rai": case InvoiceStandardRail2023:
return "rai", "2023-12-31" return "rai", "2023-12-31"
case "http://xbrl.mof.gov.cn/taxonomy/2021-11-30/atr": case InvoiceStandardAir2021:
return "atr", "2021-11-30" return "atr", "2021-11-30"
case InvoiceStandardVAT2021:
return "inv", "2021-09-30"
} }
return "", "" return "", ""
} }
@@ -351,7 +398,7 @@ func parseInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (In
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
if n.name.Space == invoiceXBRL || n.name.Space == "http://www.xbrl.org/2003/linkbase" { if n.name.Space == invoiceXBRL || n.name.Space == invoiceLinkbase {
return nil return nil
} }
if len(n.children) > 0 && invoiceXMLAttr(n, "", "contextRef") == "" { if len(n.children) > 0 && invoiceXMLAttr(n, "", "contextRef") == "" {
@@ -421,7 +468,7 @@ func parseInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (In
return nil return nil
} }
if numeric && invoiceMonetary(kind, fact.Name) && invoiceCurrencyUnit(unitValues[fact.Unit]) { if numeric && invoiceMonetary(kind, fact.Name) && invoiceCurrencyUnit(unitValues[fact.Unit]) {
currency := strings.TrimPrefix(unitValues[fact.Unit], "{http://www.xbrl.org/2003/iso4217}") currency := strings.TrimPrefix(unitValues[fact.Unit], "{"+InvoiceCurrencyNamespace+"}")
cf := fact cf := fact
cf.Value = currency cf.Value = currency
cf.Unit = "" cf.Unit = ""
@@ -762,7 +809,7 @@ func invoiceContext(n *invoiceXMLNode) ([]InvoiceDimension, bool) {
seen := map[xml.Name]bool{} seen := map[xml.Name]bool{}
var walk func(*invoiceXMLNode) var walk func(*invoiceXMLNode)
walk = func(c *invoiceXMLNode) { walk = func(c *invoiceXMLNode) {
if c.name.Space == "http://xbrl.org/2006/xbrldi" && (c.name.Local == "explicitMember" || c.name.Local == "typedMember") { if c.name.Space == invoiceDimension && (c.name.Local == "explicitMember" || c.name.Local == "typedMember") {
name, ok := invoiceQName(c, invoiceXMLAttr(c, "", "dimension")) name, ok := invoiceQName(c, invoiceXMLAttr(c, "", "dimension"))
if !ok || name.Space == "" || seen[name] { if !ok || name.Space == "" || seen[name] {
valid = false valid = false
@@ -905,7 +952,7 @@ func invoiceMonetary(kind, name string) bool {
// 入参: unit 展开的单位表示 // 入参: unit 展开的单位表示
// 返回: 是否为三位大写币种度量 // 返回: 是否为三位大写币种度量
func invoiceCurrencyUnit(unit string) bool { func invoiceCurrencyUnit(unit string) bool {
const prefix = "{http://www.xbrl.org/2003/iso4217}" const prefix = "{" + InvoiceCurrencyNamespace + "}"
if !strings.HasPrefix(unit, prefix) { if !strings.HasPrefix(unit, prefix) {
return false return false
} }
@@ -995,7 +1042,7 @@ func invoiceObjectReferences(root *invoiceXMLNode) bool {
// 返回: 发票及诊断,取消错误 // 返回: 发票及诊断,取消错误
func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSource) (InvoiceReport, error) { func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSource) (InvoiceReport, error) {
report := InvoiceReport{} report := InvoiceReport{}
inv := Invoice{Kind: "einv", Standard: "EInvoice", Version: "0.3", Sources: []InvoiceSource{source}} inv := Invoice{Kind: "einv", Standard: InvoiceStandardEInvoice, Version: "0.3", Sources: []InvoiceSource{source}}
namespace, itemPath := "", "/EInvoice/EInvoiceData/IssuItemInformation" namespace, itemPath := "", "/EInvoice/EInvoiceData/IssuItemInformation"
if root.name.Local == "eInvoice" { if root.name.Local == "eInvoice" {
if invoiceXMLAttr(root, "", "Version") != "1.0" || len(invoiceChildren(root, invoiceEDRM, "InvoiceNo")) == 0 { if invoiceXMLAttr(root, "", "Version") != "1.0" || len(invoiceChildren(root, invoiceEDRM, "InvoiceNo")) == 0 {