Files
ofdgo/ofdgo_invoice_write.go
T
2026-10-16 22:03:16 +08:00

2167 lines
78 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"fmt"
"io"
"math/big"
"reflect"
"slices"
"sort"
"strings"
"time"
"unicode/utf8"
)
// InvoiceFormat 描述已适配格式,校验不代表完整XSD验证或发票验真
type InvoiceFormat struct {
Kind string `json:"kind"`
Standard string `json:"standard"`
Version string `json:"version"`
Read bool `json:"read"`
Create bool `json:"create"`
Edit bool `json:"edit"`
Items bool `json:"items"`
}
// InvoiceFieldDescriptor 发票字段描述
// Item为从0开始的明细索引,非明细字段为-1
// Type为text、decimal、date或datetime,Editable表示字段支持编辑,提交时仍需校验
type InvoiceFieldDescriptor struct {
Field string `json:"field"`
Label string `json:"label"`
Group string `json:"group"`
Type string `json:"type"`
Item int `json:"item"`
Value string `json:"value"`
Editable bool `json:"editable"`
Bindable bool `json:"bindable"`
}
// InvoiceFieldChange 修改已有叶节点,Revision为源XML的SHA256十六进制摘要
// 未提供Revision时必须提供ExpectedValue,ExpectedNil缺省表示原节点非空值
type InvoiceFieldChange struct {
Source InvoiceSource `json:"source"`
Value string `json:"value"`
Nil bool `json:"nil,omitempty"`
Delete bool `json:"delete,omitempty"`
Revision string `json:"revision,omitempty"`
ExpectedValue *string `json:"expectedValue,omitempty"`
ExpectedNil *bool `json:"expectedNil,omitempty"`
}
// InvoiceContext 保存调用方指定的单主体、无维度XBRL上下文
type InvoiceContext struct {
ID string `json:"id"`
Identifier string `json:"identifier"`
Scheme string `json:"scheme"`
Instant string `json:"instant,omitempty"`
StartDate string `json:"startDate,omitempty"`
EndDate string `json:"endDate,omitempty"`
}
// InvoiceUnit 保存单一度量的XBRL单位,不推测币种或数量单位
type InvoiceUnit struct {
ID string `json:"id"`
Namespace string `json:"namespace"`
Name string `json:"name"`
}
// InvoiceEncodingOptions 指定XBRL定义或官方XML片段,不加载外部资源或生成税务签名
type InvoiceEncodingOptions struct {
SchemaRef string `json:"schemaRef"`
Context InvoiceContext `json:"context"`
Units []InvoiceUnit `json:"units"`
HeaderXML []byte `json:"headerXML,omitempty"`
SellerAuthenticationXML []byte `json:"sellerAuthenticationXML,omitempty"`
TaxSupervisionXML []byte `json:"taxSupervisionXML,omitempty"`
TaxBureauSignatureXML []byte `json:"taxBureauSignatureXML,omitempty"`
}
// invoiceWriteField 保存按规范排列的字段与值
type invoiceWriteField struct{ name, value string }
// invoiceXMLSpan 保存节点原始字节边界
type invoiceXMLSpan struct {
node *invoiceXMLNode
begin, inner, close, end int
empty bool
}
// invoiceXMLPatch 保存不重序列化其他节点的字节修改
type invoiceXMLPatch struct {
begin, end int
data []byte
}
// InvoiceFields 获取发票字段描述,原始事实通过来源补丁修改
// 入参: value 发票数据
// 返回: []InvoiceFieldDescriptor 字段描述列表
func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
format, supported := invoiceWriteFormat(value)
probe := Invoice{Items: []InvoiceItem{{}}}
if value.Standard == invoiceEDRM {
for _, path := range invoiceEDRMPaths() {
if p, _ := invoiceEDRMField(&probe, &probe.Items[0], path); p != nil {
*p = "editable"
}
}
} else if value.Standard == InvoiceStandardEInvoice {
probe.Number, probe.Date = "editable", "editable"
for _, group := range invoicePlainGroups() {
for _, name := range strings.Fields(group.value) {
if p, _ := invoicePlainField(&probe, nil, "/EInvoice/EInvoiceData/"+group.name+"/"+name); p != nil {
*p = "editable"
}
}
}
for _, name := range strings.Fields("ItemName SpecMod MeaUnits Quantity UnPrice Amount TaxRate ComTaxAm") {
if p, _ := invoicePlainField(&probe, &probe.Items[0], "/EInvoice/EInvoiceData/IssuItemInformation/"+name); p != nil {
*p = "editable"
}
}
} else {
for _, name := range invoiceWriteHeader(value) {
if p := invoiceXMLField(&probe, value.Kind, name); p != nil {
*p = "editable"
}
}
for _, name := range invoiceWriteItemNames(value.Kind) {
if p := invoiceItemField(&probe.Items[0], value.Kind, name); p != nil {
*p = "editable"
}
}
}
var result []InvoiceFieldDescriptor
for _, field := range invoiceLayoutFields(value) {
if strings.HasPrefix(field.field, "facts.") || strings.HasPrefix(field.field, "items.fields.") {
continue
}
d := InvoiceFieldDescriptor{Field: field.field, Label: field.label, Group: "basic", Type: "text", Item: field.item, Value: field.value, Bindable: value.Standard != ofdNamespace2016}
binding := InvoiceBinding{Field: field.field, Item: field.item}
if field.item >= 0 {
binding.Item = 0
d.Group = "items"
}
if strings.HasPrefix(field.field, "buyer.") {
d.Group = "buyer"
}
if strings.HasPrefix(field.field, "seller.") {
d.Group = "seller"
}
mapped, _ := invoiceBindingValue(probe, binding)
d.Editable = supported && format.Edit && mapped == "editable"
switch field.field {
case "date":
d.Type = "date"
if value.Kind == "einv" {
d.Type = "datetime"
}
case "amount", "taxAmount", "totalAmount", "items.quantity", "items.unitPrice", "items.amount", "items.taxAmount":
d.Type = "decimal"
case "items.taxRate":
if value.Standard == InvoiceStandardEInvoice {
d.Type = "decimal"
}
}
result = append(result, d)
}
appendFact := func(fact InvoiceFact, field string, item int) {
d := InvoiceFieldDescriptor{Field: field, Label: fact.Name, Group: "facts", Type: "text", Item: item, Value: fact.Value, Bindable: value.Standard != ofdNamespace2016}
if item >= 0 {
d.Group = "items"
}
d.Editable = supported && format.Edit && fact.Source.NodePath != "" && fact.Namespace != invoiceXBRL
if invoiceNumeric(value.Kind, fact.Name) {
d.Type = "decimal"
}
if invoiceWriteDateFact(value.Kind, fact.Name) {
d.Type = "date"
}
if value.Kind == "einv" && (fact.Name == "RequestTime" || fact.Name == "IssueTime") {
d.Type = "datetime"
}
result = append(result, d)
}
for i, fact := range value.Facts {
appendFact(fact, fmt.Sprintf("facts.%d", i), -1)
}
for i, item := range value.Items {
for j, fact := range item.Fields {
appendFact(fact, fmt.Sprintf("items.fields.%d", j), i)
}
}
return result
}
// InvoiceFormats 返回独立格式能力副本,创建及编辑能力按业务版本分别声明
// 返回: 已适配格式列表
func InvoiceFormats() []InvoiceFormat {
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, Edit: true, Items: true})
result = append(result, InvoiceFormat{Kind: "inv", Standard: ofdNamespace2016, Version: "1.0", Read: true})
for _, standard := range []string{InvoiceStandardVAT2023, InvoiceStandardDigital2023, InvoiceStandardAir2023, InvoiceStandardRail2023} {
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: 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验证
// 入参: ctx 取消上下文,data 原始XML
// 返回: 提取结果及解析错误
func DecodeInvoices(ctx context.Context, data []byte) (InvoiceReport, error) {
return parseInvoiceXML(ctx, data, InvoiceSource{})
}
// invoiceWriteContext 统一空上下文
// 入参: ctx 取消上下文
// 返回: 可用上下文
func invoiceWriteContext(ctx context.Context) context.Context {
if ctx == nil {
return context.Background()
}
return ctx
}
// invoiceWriteFormat 查找完整格式标识,不推测缺失版本
// 入参: value 发票
// 返回: 格式与匹配状态
func invoiceWriteFormat(value Invoice) (InvoiceFormat, bool) {
for _, f := range InvoiceFormats() {
if value.Kind == f.Kind && value.Standard == f.Standard && value.Version == f.Version {
return f, true
}
}
return InvoiceFormat{}, false
}
// ValidateInvoice 检查已映射字段的词法、金额及可表达性,不替代税务业务或XSD验证
// 入参: ctx 取消上下文,value 发票
// 返回: 字段诊断及取消错误
func ValidateInvoice(ctx context.Context, value Invoice) ([]InvoiceIssue, error) {
ctx = invoiceWriteContext(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
if err := invoiceWriteLimits(ctx, value, InvoiceEncodingOptions{}); err != nil {
return nil, err
}
if len(value.Items) > 100000 || len(value.Facts) > 100000 {
return nil, fmt.Errorf("invoice exceeds structural limit")
}
var issues []InvoiceIssue
add := func(code, field, message string) {
issues = append(issues, InvoiceIssue{Code: code, Field: field, Message: message})
}
f, ok := invoiceWriteFormat(value)
if !ok {
add("unsupported", "Standard", "unsupported invoice format or version")
return issues, nil
}
for _, field := range []invoiceWriteField{{"Number", value.Number}, {"Date", value.Date}, {"TotalAmount", value.TotalAmount}} {
if value.Standard != ofdNamespace2016 && strings.TrimSpace(field.value) == "" {
add("required", field.name, "missing invoice field")
}
}
if value.Date != "" {
date := strings.Replace(value.Date, " ", "T", 1)
if _, ok := invoicePeriodTime(date); !ok {
add("invalid-date", "Date", "invalid invoice date")
}
}
if value.Currency != "" && !invoiceCurrencyUnit("{"+InvoiceCurrencyNamespace+"}"+value.Currency) {
add("invalid-value", "Currency", "invalid ISO currency code")
}
for _, field := range []invoiceWriteField{{"Amount", value.Amount}, {"TaxAmount", value.TaxAmount}, {"TotalAmount", value.TotalAmount}} {
if field.value != "" && invoiceDecimal(field.value) == nil {
add("invalid-decimal", field.name, "invalid decimal lexical value")
}
}
report := InvoiceReport{}
invoiceAmountCheck(&value, &report, InvoiceSource{})
issues = append(issues, report.Issues...)
if !f.Items && len(value.Items) > 0 {
add("unsupported", "Items", "format has no supported invoice item tuple")
}
for i, item := range value.Items {
if err := ctx.Err(); err != nil {
return nil, err
}
for _, field := range []invoiceWriteField{{"Quantity", item.Quantity}, {"UnitPrice", item.UnitPrice}, {"Amount", item.Amount}, {"TaxAmount", item.TaxAmount}} {
if field.value != "" && invoiceDecimal(field.value) == nil {
add("invalid-decimal", fmt.Sprintf("Items[%d].%s", i, field.name), "invalid decimal lexical value")
}
}
if value.Standard == InvoiceStandardEInvoice && item.TaxRate != "" && invoiceDecimal(item.TaxRate) == nil {
add("invalid-decimal", fmt.Sprintf("Items[%d].TaxRate", i), "invalid decimal lexical value")
}
}
if value.Kind == "inv" || value.Standard == InvoiceStandardEInvoice {
for _, total := range []invoiceWriteField{{"Amount", value.Amount}, {"TaxAmount", value.TaxAmount}} {
sum := new(big.Rat)
complete := len(value.Items) > 0
for _, item := range value.Items {
s := item.Amount
if total.name == "TaxAmount" {
s = item.TaxAmount
}
n := invoiceDecimal(s)
if n == nil {
complete = false
break
}
sum.Add(sum, n)
}
if n := invoiceDecimal(total.value); complete && n != nil && sum.Cmp(n) != 0 {
add("amount-mismatch", total.name, "item sum differs from invoice total")
}
}
}
total := 0
for _, field := range invoiceWriteAllValues(value) {
if err := ctx.Err(); err != nil {
return nil, err
}
total += len(field.value)
if total > invoiceXMLLimit {
return nil, fmt.Errorf("invoice exceeds size limit")
}
if !invoiceWriteText(field.value) {
add("invalid-value", field.name, "invalid XML character or oversized field")
}
}
return issues, nil
}
// invoiceWriteAllValues 收集公共字段及扩展事实文本
// 入参: value 发票
// 返回: 待校验字段
func invoiceWriteAllValues(value Invoice) []invoiceWriteField {
r := []invoiceWriteField{{"Number", value.Number}, {"Code", value.Code}, {"Date", value.Date}, {"Status", value.Status}, {"Remarks", value.Remarks}}
for _, p := range []InvoiceParty{value.Buyer, value.Seller} {
for _, s := range []string{p.Name, p.TaxID, p.Address, p.Phone, p.Bank, p.Account} {
r = append(r, invoiceWriteField{"Party", s})
}
}
for _, item := range value.Items {
for _, s := range []string{item.Name, item.Specification, item.Unit, item.Quantity, item.UnitPrice, item.Amount, item.TaxRate, item.TaxAmount} {
r = append(r, invoiceWriteField{"Items", s})
}
for _, f := range item.Fields {
r = append(r, invoiceWriteField{f.Name, f.Value})
}
}
for _, f := range value.Facts {
r = append(r, invoiceWriteField{f.Name, f.Value})
}
return r
}
// invoiceWriteText 检查XML文本且不替换非法字符
// 入参: value 原始文本
// 返回: 文本是否合法
func invoiceWriteText(value string) bool {
if len(value) > invoiceXMLLimit || !utf8.ValidString(value) {
return false
}
for _, r := range value {
if r != 9 && r != 10 && r != 13 && (r < 32 || r == 0xfffe || r == 0xffff) {
return false
}
}
return true
}
// invoiceWriteHeader 返回明确映射的规范字段顺序
// 入参: value 发票
// 返回: 字段名列表
func invoiceWriteHeader(value Invoice) []string {
switch value.Kind {
case "inv":
return strings.Fields("CodeOfInvoice NumberOfInvoice DateOfIssue NameOfPurchaser TaxpayerIdentificationNumberUnifiedSocialCreditCodeOfPurchaser TotalAmountExcludingTax TotalTaxAmount TaxIncludedAmountInFigures NameOfSeller TaxpayerIdentificationNumberUnifiedSocialCreditCodeOfSeller Remarks")
case "einv":
return strings.Fields("InvoiceNumber SellerName SellerIdNum TotalAmWithoutTax RequestTime TotalTaxAm TotalTax-includedAmount")
case "atr":
return strings.Fields("ElectronicInvoiceAirTransportReceiptNumber IssuingStatus VatTaxAmount TotalAmount IssueDate NameOfPurchaser UnifiedSocialCreditCodeOfPurchaser NameOfSeller")
case "rai":
return strings.Fields("ElectronicInvoiceRailwayETicketNumber DateOfIssue NameOfPurchaser UnifiedSocialCreditCodeOfPurchaser TotalAmountExcludingTax TaxAmount Fare NameOfSeller UnifiedSocialCreditCodeOfSeller Remarks")
}
return nil
}
// invoiceWriteItemNames 返回元组内部字段顺序
// 入参: kind 票种
// 返回: 规范字段列表
func invoiceWriteItemNames(kind string) []string {
if kind == "atr" {
return strings.Fields("DepartureStation DestinationStation FlightSegment Carrier Flight Class CarrierDate DepartureTime FareBasis EffectiveDate ExpirationDate FreeBaggageAllowance")
}
return strings.Fields("NatureOfInvoiceLine NameOfTaxableGoodsOrServices Specification UnitOfGoodsOrServices QuantityOfGoodsOrServices UnitPriceOfGoodsOrServices AmountExcludingTax TaxRate TaxAmount")
}
// invoiceWriteTuple 返回已支持的明细元组名称
// 入参: kind 票种
// 返回: 元组名
func invoiceWriteTuple(kind string) string {
if kind == "atr" {
return "DetailInformationOfAirTicketTuple"
}
return "InformationOfTaxableGoodsOrServicesDetailItemsTuple"
}
// invoiceWriteEscape 转义已校验的XML值
// 入参: value 文本
// 返回: XML安全文本
func invoiceWriteEscape(value string) string {
var b bytes.Buffer
_ = xml.EscapeText(&b, []byte(value))
return b.String()
}
// EncodeInvoice 从明确业务字段生成独立XML,不将提取摘要覆盖原始载荷
// 入参: ctx 取消上下文,value 发票
// 返回: 结构化XML及创建错误
func EncodeInvoice(ctx context.Context, value Invoice) ([]byte, error) {
return EncodeInvoiceWithOptions(ctx, value, InvoiceEncodingOptions{})
}
// EncodeInvoiceWithOptions 使用调用方指定的XBRL定义创建独立结构化文件
// 入参: ctx 上下文,value 发票,options 分类标准入口、上下文及单位
// 返回: XML及创建错误
func EncodeInvoiceWithOptions(ctx context.Context, value Invoice, options InvoiceEncodingOptions) ([]byte, error) {
ctx = invoiceWriteContext(ctx)
if err := invoiceWriteLimits(ctx, value, options); err != nil {
return nil, err
}
issues, err := ValidateInvoice(ctx, value)
if err != nil {
return nil, err
}
if len(issues) > 0 {
return nil, fmt.Errorf("invoice %s: %s", issues[0].Field, issues[0].Message)
}
f, _ := invoiceWriteFormat(value)
if !f.Create {
return nil, fmt.Errorf("invoice format does not support standalone creation")
}
if err := invoiceWriteCreation(value); err != nil {
return nil, err
}
var data []byte
if value.Standard == InvoiceStandardEInvoice {
data, err = invoiceEncodePlain(value)
if err == nil {
data, err = invoiceWritePlainSections(ctx, data, options)
}
} else {
data, err = invoiceEncodeXBRL(value, options)
}
if err != nil {
return nil, err
}
report, err := DecodeInvoices(ctx, data)
if err != nil {
return nil, err
}
if len(report.Issues) > 0 {
return nil, fmt.Errorf("encoded invoice: %s", report.Issues[0].Message)
}
if len(report.Invoices) != 1 {
return nil, fmt.Errorf("encoded invoice has ambiguous contexts")
}
if err = invoiceWriteRepresented(value, report.Invoices[0]); err != nil {
return nil, err
}
return data, nil
}
// invoiceWriteLimits 在构造字段列表和XML前限制节点数及转义后的保守空间上界
// 入参: ctx 上下文,value 发票,options 显式定义和XML片段
// 返回: 取消或限额错误
func invoiceWriteLimits(ctx context.Context, value Invoice, options InvoiceEncodingOptions) error {
if err := ctx.Err(); err != nil {
return err
}
if len(value.Items) > 100000 || len(value.Facts) > 100000 || len(options.Units) > 256 || len(options.SchemaRef) > 4096 || len(options.Context.Scheme) > 4096 || len(options.Context.Identifier) > 64<<10 {
return fmt.Errorf("invoice exceeds structural limit")
}
total := 0
charge := func(values ...string) error {
if err := ctx.Err(); err != nil {
return err
}
if total > invoiceXMLLimit-512 {
return fmt.Errorf("invoice exceeds encoded size limit")
}
total += 512
for _, s := range values {
if len(s) > (invoiceXMLLimit-total)/6 {
return fmt.Errorf("invoice exceeds encoded size limit")
}
total += 6 * len(s)
}
return nil
}
for _, s := range []string{value.Kind, value.Standard, value.Version, value.Number, value.Code, value.Date, value.Currency, value.Status, value.Amount, value.TaxAmount, value.TotalAmount, value.Remarks} {
if err := charge(s); err != nil {
return err
}
}
for _, party := range []InvoiceParty{value.Buyer, value.Seller} {
if err := charge(party.Name, party.TaxID, party.Address, party.Phone, party.Bank, party.Account); err != nil {
return err
}
}
facts := func(fields []InvoiceFact) error {
if len(fields) > 100000 {
return fmt.Errorf("invoice exceeds structural limit")
}
for _, f := range fields {
if err := charge(f.Name, f.Namespace, f.Value, f.Context, f.Unit, f.Decimals, f.Precision); err != nil {
return err
}
if len(f.Dimensions) > 128 {
return fmt.Errorf("invoice exceeds structural limit")
}
for _, d := range f.Dimensions {
if err := charge(d.Name, d.Namespace, d.Value); err != nil {
return err
}
}
}
return nil
}
if err := facts(value.Facts); err != nil {
return err
}
for _, item := range value.Items {
for _, text := range []string{item.Name, item.Specification, item.Unit, item.Quantity, item.UnitPrice, item.Amount, item.TaxRate, item.TaxAmount} {
if err := charge(text); err != nil {
return err
}
}
if err := facts(item.Fields); err != nil {
return err
}
}
if err := charge(options.SchemaRef, options.Context.ID, options.Context.Identifier, options.Context.Scheme, options.Context.Instant, options.Context.StartDate, options.Context.EndDate); err != nil {
return err
}
for _, unit := range options.Units {
if len(unit.Namespace) > 4096 {
return fmt.Errorf("invoice unit namespace exceeds limit")
}
if err := charge(unit.ID, unit.Namespace, unit.Name); err != nil {
return err
}
}
for _, raw := range [][]byte{options.HeaderXML, options.SellerAuthenticationXML, options.TaxSupervisionXML, options.TaxBureauSignatureXML} {
if len(raw) > invoiceXMLLimit-total {
return fmt.Errorf("invoice sections exceed size limit")
}
total += len(raw)
}
return nil
}
// invoiceWriteRepresented 拒绝格式无法承载的非空公共字段
// 入参: want 输入业务数据,got 编码后数据
// 返回: 不可表达字段错误
func invoiceWriteRepresented(want, got Invoice) error {
a := []string{want.Number, want.Code, want.Date, want.Status, want.Amount, want.TaxAmount, want.TotalAmount, want.Remarks, want.Buyer.Name, want.Buyer.TaxID, want.Buyer.Address, want.Buyer.Phone, want.Buyer.Bank, want.Buyer.Account, want.Seller.Name, want.Seller.TaxID, want.Seller.Address, want.Seller.Phone, want.Seller.Bank, want.Seller.Account}
b := []string{got.Number, got.Code, got.Date, got.Status, got.Amount, got.TaxAmount, got.TotalAmount, got.Remarks, got.Buyer.Name, got.Buyer.TaxID, got.Buyer.Address, got.Buyer.Phone, got.Buyer.Bank, got.Buyer.Account, got.Seller.Name, got.Seller.TaxID, got.Seller.Address, got.Seller.Phone, got.Seller.Bank, got.Seller.Account}
for i, v := range a {
if v != "" && v != b[i] {
return fmt.Errorf("invoice format cannot represent field value at index %d", i)
}
}
if want.Currency != "" && want.Currency != got.Currency {
return fmt.Errorf("invoice format cannot represent currency")
}
if len(want.Items) != len(got.Items) {
return fmt.Errorf("invoice format cannot represent items")
}
for i, v := range want.Items {
w := got.Items[i]
for j, a := range []string{v.Name, v.Specification, v.Unit, v.Quantity, v.UnitPrice, v.Amount, v.TaxRate, v.TaxAmount} {
b := []string{w.Name, w.Specification, w.Unit, w.Quantity, w.UnitPrice, w.Amount, w.TaxRate, w.TaxAmount}[j]
if a != "" && a != b {
return fmt.Errorf("invoice format cannot represent item %d field %d", i, j)
}
}
}
return nil
}
// invoiceEncodeXBRL 构造单主体实例,扩展事实不得携带无法重建的上下文或元组
// 入参: value 发票,options 显式分类标准、上下文和单位
// 返回: XBRL数据及表达错误
func invoiceEncodeXBRL(value Invoice, options InvoiceEncodingOptions) ([]byte, error) {
if options.SchemaRef == "" || !invoiceWriteText(options.SchemaRef) || !invoiceNCName(options.Context.ID) || options.Context.Identifier == "" || options.Context.Scheme == "" {
return nil, fmt.Errorf("XBRL creation requires schema reference and explicit context")
}
for _, s := range []string{options.Context.Identifier, options.Context.Scheme, options.Context.Instant, options.Context.StartDate, options.Context.EndDate} {
if !invoiceWriteText(s) {
return nil, fmt.Errorf("invalid XBRL context text")
}
}
if value.Currency == "" {
return nil, fmt.Errorf("XBRL creation requires an explicit currency")
}
var b strings.Builder
b.WriteString(xml.Header)
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))
if options.Context.Instant != "" {
if options.Context.StartDate != "" || options.Context.EndDate != "" {
return nil, fmt.Errorf("ambiguous XBRL period")
}
fmt.Fprintf(&b, "<xbrli:instant>%s</xbrli:instant>", invoiceWriteEscape(options.Context.Instant))
} else {
fmt.Fprintf(&b, "<xbrli:startDate>%s</xbrli:startDate><xbrli:endDate>%s</xbrli:endDate>", invoiceWriteEscape(options.Context.StartDate), invoiceWriteEscape(options.Context.EndDate))
}
b.WriteString("</xbrli:period></xbrli:context>")
ids := map[string]bool{options.Context.ID: true}
for _, unit := range options.Units {
if !invoiceNCName(unit.ID) || !invoiceNCName(unit.Name) || unit.Namespace == "" || !invoiceWriteText(unit.Namespace) || ids[unit.ID] {
return nil, fmt.Errorf("invalid or duplicate XBRL unit")
}
ids[unit.ID] = true
fmt.Fprintf(&b, `<xbrli:unit id="%s"><xbrli:measure xmlns:m="%s">m:%s</xbrli:measure></xbrli:unit>`, unit.ID, invoiceWriteEscape(unit.Namespace), unit.Name)
}
header := map[string]bool{}
for _, name := range invoiceWriteHeader(value) {
p := invoiceXMLField(&value, value.Kind, name)
if p != nil && *p != "" {
if err := invoiceWriteFact(&b, value, InvoiceFact{Name: name, Value: *p}, options); err != nil {
return nil, err
}
}
header[name] = true
}
for _, fact := range value.Facts {
if fact.Source.NodePath != "" || fact.Namespace == invoiceXBRL {
return nil, fmt.Errorf("source facts require source-preserving invoice update")
}
if header[fact.Name] {
return nil, fmt.Errorf("duplicate mapped invoice fact %s", fact.Name)
}
if err := invoiceWriteFact(&b, value, fact, options); err != nil {
return nil, err
}
header[fact.Name] = true
}
for _, item := range value.Items {
fmt.Fprintf(&b, "<f:%s>", invoiceWriteTuple(value.Kind))
facts := map[string]InvoiceFact{}
for _, fact := range item.Fields {
if _, ok := facts[fact.Name]; ok {
return nil, fmt.Errorf("duplicate invoice item fact")
}
facts[fact.Name] = fact
}
for _, name := range invoiceWriteItemNames(value.Kind) {
fact, has := facts[name]
p := invoiceItemField(&item, value.Kind, name)
if p != nil && *p != "" {
if has && (fact.Nil || fact.Value != *p) {
return nil, fmt.Errorf("conflicting invoice item fact %s", name)
}
if !has {
fact = InvoiceFact{Name: name, Value: *p}
}
has = true
}
if has {
if err := invoiceWriteFact(&b, value, fact, options); err != nil {
return nil, err
}
delete(facts, name)
}
}
if len(facts) > 0 {
return nil, fmt.Errorf("unsupported invoice item fields")
}
fmt.Fprintf(&b, "</f:%s>", invoiceWriteTuple(value.Kind))
}
b.WriteString("</xbrli:xbrl>")
return []byte(b.String()), nil
}
// invoiceWriteFact 写入单个无维度事实,数值不经过浮点转换
// 入参: b 输出,value 发票,fact 事实,options 上下文和单位
// 返回: 不支持的事实错误
func invoiceWriteFact(b *strings.Builder, value Invoice, fact InvoiceFact, options InvoiceEncodingOptions) error {
if !invoiceWriteKnownFact(value.Kind, fact.Name) {
return fmt.Errorf("unsupported invoice concept %s", fact.Name)
}
if !invoiceNCName(fact.Name) || fact.Namespace != "" && fact.Namespace != value.Standard || len(fact.Dimensions) > 0 || fact.Context != "" && fact.Context != options.Context.ID {
return fmt.Errorf("unsupported invoice fact context or namespace")
}
if fact.Nil && fact.Value != "" {
return fmt.Errorf("nil invoice fact contains value")
}
if !fact.Nil && invoiceWriteDateFact(value.Kind, fact.Name) {
if _, ok := invoicePeriodTime(fact.Value); !ok || strings.ContainsAny(fact.Value, "T ") {
return fmt.Errorf("invalid invoice date fact %s", fact.Name)
}
}
fmt.Fprintf(b, `<f:%s contextRef="%s"`, fact.Name, options.Context.ID)
numeric := invoiceNumeric(value.Kind, fact.Name)
if numeric {
unit := ""
for _, u := range options.Units {
match := u.Namespace == InvoiceCurrencyNamespace && u.Name == value.Currency
if !invoiceMonetary(value.Kind, fact.Name) {
match = u.Namespace == invoiceXBRL && u.Name == "pure"
if fact.Unit != "" {
match = u.ID == fact.Unit
}
}
if match && (fact.Unit == "" || fact.Unit == u.ID) {
unit = u.ID
break
}
}
if unit == "" {
return fmt.Errorf("missing matching XBRL unit for %s", fact.Name)
}
fmt.Fprintf(b, ` unitRef="%s"`, unit)
}
if fact.Nil {
b.WriteString(` xsi:nil="true"`)
} else if numeric {
if invoiceDecimal(fact.Value) == nil {
return fmt.Errorf("invalid decimal invoice fact")
}
if fact.Precision != "" {
if fact.Decimals != "" || !invoiceAccuracy(fact.Precision, true) {
return fmt.Errorf("invalid fact accuracy")
}
fmt.Fprintf(b, ` precision="%s"`, fact.Precision)
} else {
decimals := fact.Decimals
if decimals == "" {
decimals = "INF"
}
if !invoiceAccuracy(decimals, false) {
return fmt.Errorf("invalid fact accuracy")
}
fmt.Fprintf(b, ` decimals="%s"`, decimals)
}
} else if fact.Decimals != "" || fact.Precision != "" || fact.Unit != "" {
return fmt.Errorf("accuracy or unit on nonnumeric fact")
}
fmt.Fprintf(b, ">%s</f:%s>", invoiceWriteEscape(fact.Value), fact.Name)
return nil
}
// invoicePlainGroups 返回官方公共字段分组
// 返回: 组名与字段顺序
func invoicePlainGroups() []invoiceWriteField {
return []invoiceWriteField{{"SellerInformation", "SellerIdNum SellerName SellerAddr SellerTelNum SellerBankName SellerBankAccNum"}, {"BuyerInformation", "BuyerIdNum BuyerName"}, {"BasicInformation", "TotalAmWithoutTax TotalTaxAm TotalTax-includedAmount"}, {"AdditionalInformation", "Remark"}}
}
// invoiceEncodePlain 构造官方0.3字段路径,不捏造税务签名或编号
// 入参: value 发票
// 返回: XML及不可表达的扩展错误
func invoiceEncodePlain(value Invoice) ([]byte, error) {
if len(value.Facts) > 0 {
return nil, fmt.Errorf("plain XML facts require source-preserving invoice update")
}
var b strings.Builder
b.WriteString(xml.Header + `<EInvoice><Header><Version>0.3</Version></Header><EInvoiceData>`)
for _, group := range invoicePlainGroups() {
if group.name == "AdditionalInformation" {
continue
}
fmt.Fprintf(&b, "<%s>", group.name)
for _, name := range strings.Fields(group.value) {
p, _ := invoicePlainField(&value, nil, "/EInvoice/EInvoiceData/"+group.name+"/"+name)
if p != nil && *p != "" {
fmt.Fprintf(&b, "<%s>%s</%s>", name, invoiceWriteEscape(*p), name)
}
}
fmt.Fprintf(&b, "</%s>", group.name)
}
for _, item := range value.Items {
if len(item.Fields) > 0 {
return nil, fmt.Errorf("plain XML item facts require source-preserving invoice update")
}
b.WriteString("<IssuItemInformation>")
for _, name := range strings.Fields("ItemName SpecMod MeaUnits Quantity UnPrice Amount TaxRate ComTaxAm") {
p, _ := invoicePlainField(&value, &item, "/EInvoice/EInvoiceData/IssuItemInformation/"+name)
if p != nil && *p != "" {
fmt.Fprintf(&b, "<%s>%s</%s>", name, invoiceWriteEscape(*p), name)
}
}
b.WriteString("</IssuItemInformation>")
}
if value.Remarks != "" {
fmt.Fprintf(&b, "<AdditionalInformation><Remark>%s</Remark></AdditionalInformation>", invoiceWriteEscape(value.Remarks))
}
fmt.Fprintf(&b, "</EInvoiceData><TaxSupervisionInfo><InvoiceNumber>%s</InvoiceNumber><IssueTime>%s</IssueTime></TaxSupervisionInfo></EInvoice>", invoiceWriteEscape(value.Number), invoiceWriteEscape(value.Date))
return []byte(b.String()), nil
}
// invoiceWriteSpans 将有界解析树与原始字节边界关联,保留BOM和空元素
// 入参: ctx 上下文,data XML
// 返回: 根节点、路径索引及错误
func invoiceWriteSpans(ctx context.Context, data []byte) (*invoiceXMLNode, map[string]invoiceXMLSpan, error) {
root, err := invoiceXMLTree(ctx, data)
if err != nil {
return nil, nil, err
}
var nodes []*invoiceXMLNode
var walk func(*invoiceXMLNode)
walk = func(n *invoiceXMLNode) {
nodes = append(nodes, n)
for _, c := range n.children {
walk(c)
}
}
walk(root)
d := xml.NewDecoder(bytes.NewReader(data))
spans := map[string]invoiceXMLSpan{}
var stack []invoiceXMLSpan
index := 0
for {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
before := int(d.InputOffset())
token, err := d.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, nil, err
}
after := int(d.InputOffset())
switch token.(type) {
case xml.StartElement:
n := nodes[index]
index++
empty := after >= 2 && bytes.Equal(data[after-2:after], []byte("/>"))
stack = append(stack, invoiceXMLSpan{node: n, begin: before, inner: after, empty: empty})
case xml.EndElement:
s := stack[len(stack)-1]
stack = stack[:len(stack)-1]
s.close = before
s.end = after
if s.empty {
s.close = s.inner
s.end = s.inner
}
spans[s.node.path] = s
}
}
return root, spans, nil
}
// invoiceApplyXMLPatches 原子合并互不重叠的字节修改
// 入参: ctx 上下文,data 原文,patches 修改列表
// 返回: 新XML及错误
func invoiceApplyXMLPatches(ctx context.Context, data []byte, patches []invoiceXMLPatch) ([]byte, error) {
sort.SliceStable(patches, func(i, j int) bool { return patches[i].begin < patches[j].begin })
var b bytes.Buffer
offset := 0
for _, p := range patches {
if err := ctx.Err(); err != nil {
return nil, err
}
if p.begin < offset || p.end < p.begin || p.end > len(data) {
return nil, fmt.Errorf("overlapping invoice XML changes")
}
if b.Len()+p.begin-offset+len(p.data) > invoiceXMLLimit {
return nil, fmt.Errorf("invoice XML exceeds size limit")
}
b.Write(data[offset:p.begin])
b.Write(p.data)
offset = p.end
}
if b.Len()+len(data)-offset > invoiceXMLLimit {
return nil, fmt.Errorf("invoice XML exceeds size limit")
}
b.Write(data[offset:])
return b.Bytes(), nil
}
// PatchInvoice 精确修改已有事实叶节点,未知节点及其他字节保持不变
// 入参: ctx 上下文,data 原始XML,changes 节点修改与原值或摘要前置条件
// 返回: 修改后XML及路径、校验或取消错误
func PatchInvoice(ctx context.Context, data []byte, changes []InvoiceFieldChange) ([]byte, error) {
ctx = invoiceWriteContext(ctx)
report, err := DecodeInvoices(ctx, data)
if err != nil {
return nil, err
}
if len(report.Invoices) == 0 {
return nil, fmt.Errorf("unsupported invoice XML")
}
_, spans, err := invoiceWriteSpans(ctx, data)
if err != nil {
return nil, err
}
allowed := map[string]bool{}
for _, inv := range report.Invoices {
format, supported := invoiceWriteFormat(inv)
if !supported || !format.Edit {
continue
}
for _, f := range inv.Facts {
if f.Namespace != invoiceXBRL {
allowed[f.Source.NodePath] = true
}
}
}
sum := sha256.Sum256(data)
revision := hex.EncodeToString(sum[:])
seen := map[string]bool{}
var patches []invoiceXMLPatch
for _, change := range changes {
path := change.Source.NodePath
s, ok := spans[path]
if !ok || !allowed[path] || len(s.node.children) > 0 || seen[path] {
return nil, fmt.Errorf("missing, ambiguous or non-leaf invoice path")
}
seen[path] = true
if change.Revision == "" && change.ExpectedValue == nil {
return nil, fmt.Errorf("invoice change requires source revision or expected value")
}
if change.Revision != "" && change.Revision != revision {
return nil, fmt.Errorf("stale invoice source revision")
}
oldNil := invoiceXMLAttr(s.node, invoiceXSI, "nil")
isNil := oldNil == "true" || oldNil == "1"
expectedNil := false
if change.ExpectedNil != nil {
expectedNil = *change.ExpectedNil
}
if change.ExpectedValue != nil && (*change.ExpectedValue != s.node.text.String() || expectedNil != isNil) {
return nil, fmt.Errorf("stale invoice field value")
}
if !invoiceWriteText(change.Value) || change.Nil && change.Value != "" || change.Delete && (change.Nil || change.Value != "") {
return nil, fmt.Errorf("invalid invoice field change")
}
if !change.Delete && change.Nil == isNil && change.Value == s.node.text.String() {
continue
}
if change.Delete {
patches = append(patches, invoiceXMLPatch{s.begin, s.end, nil})
continue
}
replacement, err := invoiceWriteLeaf(s.node, change.Value, change.Nil)
if err != nil {
return nil, err
}
if change.Nil == isNil && !s.empty {
patches = append(patches, invoiceXMLPatch{s.inner, s.close, []byte(invoiceWriteEscape(change.Value))})
} else {
patches = append(patches, invoiceXMLPatch{s.begin, s.end, replacement})
}
}
result, err := invoiceApplyXMLPatches(ctx, data, patches)
if err != nil {
return nil, err
}
after, err := DecodeInvoices(ctx, result)
if err != nil {
return nil, err
}
if len(after.Invoices) == 0 {
return nil, fmt.Errorf("invoice patch removes all recognized facts")
}
if err := invoiceWriteNewIssues(report.Issues, after.Issues); err != nil {
return nil, err
}
if err := invoiceValidateXMLWrite(ctx, data, result); err != nil {
return nil, err
}
return result, nil
}
// invoiceValidateXMLWrite 检查已有字段词法及相对原文新增的损坏,不强制补齐历史创建字段
// 入参: ctx 上下文,before 原文,nil表示导入,after 待写入XML
// 返回: 新增或变更诊断错误
func invoiceValidateXMLWrite(ctx context.Context, before, after []byte) error {
ctx = invoiceWriteContext(ctx)
var original map[string]InvoiceIssue
if before != nil {
var err error
original, err = invoiceWriteXMLProblems(ctx, before, true)
if err != nil {
return err
}
}
problems, err := invoiceWriteXMLProblems(ctx, after, before != nil)
if err != nil {
return err
}
keys := make([]string, 0, len(problems))
for key := range problems {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := original[key]; !ok {
issue := problems[key]
return fmt.Errorf("invoice %s: %s", issue.Field, issue.Message)
}
}
return nil
}
// invoiceWriteXMLProblems 按节点路径及问题值区分诊断,避免不同候选的同类问题相互抵消
// 入参: ctx 上下文,data 原文,tuples 是否检查元组必有字段
// 返回: 定位诊断及解析错误
func invoiceWriteXMLProblems(ctx context.Context, data []byte, tuples bool) (map[string]InvoiceIssue, error) {
report, err := DecodeInvoices(ctx, data)
if err != nil {
return nil, err
}
root, err := invoiceXMLTree(ctx, data)
if err != nil {
return nil, err
}
problems := map[string]InvoiceIssue{}
nodes := map[string]*invoiceXMLNode{}
add := func(issue InvoiceIssue, value string) {
hash := sha256.Sum256([]byte(value))
key := issue.Code + "\x00" + issue.Field + "\x00" + issue.Source.NodePath + "\x00" + issue.Message + "\x00" + hex.EncodeToString(hash[:])
problems[key] = issue
}
var walk func(*invoiceXMLNode, string) error
walk = func(n *invoiceXMLNode, path string) error {
if err := ctx.Err(); err != nil {
return err
}
nodes[n.path] = n
if n.name.Space != "" {
path = "!" + path
}
path += "/" + n.name.Local
kind, _ := invoiceTaxonomy(n.name.Space)
nilValue := invoiceXMLAttr(n, invoiceXSI, "nil")
empty := nilValue == "true" || nilValue == "1"
date := kind != "" && invoiceWriteDateFact(kind, n.name.Local)
plainDate := root.name == (xml.Name{Local: "EInvoice"}) && path == "/EInvoice/TaxSupervisionInfo/IssueTime"
edrmDate := n.parent == root && n.name == (xml.Name{Space: invoiceEDRM, Local: "IssueDate"})
if len(n.children) == 0 && !empty && (date || plainDate || edrmDate) {
value := n.text.String()
parseValue := value
if plainDate {
parseValue = strings.Replace(value, " ", "T", 1)
}
if edrmDate {
if d, err := time.Parse("2006年01月02日", value); err == nil {
parseValue = d.Format("2006-01-02")
}
}
_, valid := invoicePeriodTime(parseValue)
if !valid || date && strings.ContainsAny(value, "T ") || edrmDate && len(parseValue) != 10 {
add(InvoiceIssue{Code: "invalid-date", Field: n.name.Local, Message: "invalid invoice date", Source: InvoiceSource{NodePath: n.path}}, value)
}
}
if tuples && kind != "" && invoiceItemTuple(kind, n.name.Local) {
required := strings.Fields("NatureOfInvoiceLine NameOfTaxableGoodsOrServices AmountExcludingTax TaxRate TaxAmount")
if kind == "atr" {
required = strings.Fields("DepartureStation DestinationStation FlightSegment Carrier Flight Class FareBasis FreeBaggageAllowance")
} else if n.name.Local == "InformationOfTollFeeDetailItemsTuple" {
required = strings.Fields("NatureOfInvoiceLine NameOfTollFeeItems PlateNumber TypesOfVehicle EntranceDate ExitDate AmountExcludingTax TaxRate TaxAmount")
}
for _, name := range required {
if len(invoiceChildren(n, n.name.Space, name)) == 0 {
add(InvoiceIssue{Code: "required-tuple-field", Field: name, Message: "missing invoice tuple field", Source: InvoiceSource{NodePath: n.path}}, "")
}
}
}
for _, child := range n.children {
if err := walk(child, path); err != nil {
return err
}
}
return nil
}
if err := walk(root, ""); err != nil {
return nil, err
}
for _, value := range report.Invoices {
if value.Standard != invoiceEDRM {
continue
}
issues, err := ValidateInvoice(ctx, value)
if err != nil {
return nil, err
}
for _, issue := range issues {
add(issue, "")
}
}
for _, issue := range report.Issues {
value := ""
if n := nodes[issue.Source.NodePath]; n != nil {
value = n.text.String() + "\x00" + invoiceXMLAttr(n, invoiceXSI, "nil")
}
add(issue, value)
}
return problems, nil
}
// invoiceWriteLeaf 使用展开名称写入单个节点,并保留原有属性
// 入参: n 原节点,value 新值,nilValue 空值标记
// 返回: 节点XML及编码错误
func invoiceWriteLeaf(n *invoiceXMLNode, value string, nilValue bool) ([]byte, error) {
var b bytes.Buffer
e := xml.NewEncoder(&b)
start := xml.StartElement{Name: n.name}
var prefixes []string
for prefix := range n.ns {
if prefix != "" && prefix != "xml" {
prefixes = append(prefixes, prefix)
}
}
sort.Strings(prefixes)
for _, prefix := range prefixes {
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns:" + prefix}, Value: n.ns[prefix]})
}
for _, a := range n.attrs {
if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" && a.Name.Space == "" || a.Name.Space == invoiceXSI && a.Name.Local == "nil" || nilValue && a.Name.Space == "" && (a.Name.Local == "decimals" || a.Name.Local == "precision") {
continue
}
start.Attr = append(start.Attr, a)
}
if nilValue {
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: invoiceXSI, Local: "nil"}, Value: "true"})
} else if invoiceXMLAttr(n, invoiceXSI, "nil") == "true" || invoiceXMLAttr(n, invoiceXSI, "nil") == "1" {
kind, _ := invoiceTaxonomy(n.name.Space)
if invoiceNumeric(kind, n.name.Local) {
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "decimals"}, Value: "INF"})
}
}
if err := e.EncodeToken(start); err != nil {
return nil, err
}
if err := e.EncodeToken(xml.CharData(value)); err != nil {
return nil, err
}
if err := e.EncodeToken(start.End()); err != nil {
return nil, err
}
if err := e.Flush(); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// invoiceWriteNewIssues 拒绝修改新增的解析诊断,不隐藏原始文件诊断
// 入参: before 修改前诊断,after 修改后诊断
// 返回: 新增诊断错误
func invoiceWriteNewIssues(before, after []InvoiceIssue) error {
counts := map[string]int{}
key := func(i InvoiceIssue) string { return i.Code + "\x00" + i.Field + "\x00" + i.Message }
for _, i := range before {
counts[key(i)]++
}
for _, i := range after {
k := key(i)
if counts[k] == 0 {
return fmt.Errorf("invoice update: %s", i.Message)
}
counts[k]--
}
return nil
}
// UpdateInvoiceXML 更新指定候选的公共字段和明细,保留未映射原文及其他候选
// 入参: ctx 上下文,data 原XML,invoiceIndex 候选序号,value 完整目标值
// 返回: 修改后XML及不可安全表达的修改错误
func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value Invoice) ([]byte, error) {
ctx = invoiceWriteContext(ctx)
before, err := DecodeInvoices(ctx, data)
if err != nil {
return nil, err
}
if invoiceIndex < 0 || invoiceIndex >= len(before.Invoices) {
return nil, fmt.Errorf("invoice index out of range")
}
old := before.Invoices[invoiceIndex]
if reflect.DeepEqual(old, value) {
return bytes.Clone(data), nil
}
if format, supported := invoiceWriteFormat(old); !supported || !format.Edit {
return nil, fmt.Errorf("invoice format does not support editing")
}
if value.Kind != old.Kind || value.Standard != old.Standard || value.Version != old.Version {
return nil, fmt.Errorf("invoice update cannot change format")
}
if value.Currency != old.Currency {
return nil, fmt.Errorf("invoice update cannot change shared currency units")
}
oldIssues, err := ValidateInvoice(ctx, old)
if err != nil {
return nil, err
}
newIssues, err := ValidateInvoice(ctx, value)
if err != nil {
return nil, err
}
filter := func(issues []InvoiceIssue) []InvoiceIssue {
var r []InvoiceIssue
for _, i := range issues {
if i.Code != "required" {
r = append(r, i)
}
}
return r
}
if err = invoiceWriteNewIssues(filter(oldIssues), filter(newIssues)); err != nil {
return nil, err
}
if value.Standard == invoiceEDRM {
return invoiceUpdateEDRM(ctx, data, old, value)
}
root, spans, err := invoiceWriteSpans(ctx, data)
if err != nil {
return nil, err
}
if value.Standard == InvoiceStandardEInvoice {
var expanded []invoiceXMLPatch
for _, s := range spans {
n := s.node
if !s.empty || n.name.Space != "" || n.parent == nil {
continue
}
container := n.parent == root && (n.name.Local == "EInvoiceData" || n.name.Local == "TaxSupervisionInfo")
if n.parent.name == (xml.Name{Local: "EInvoiceData"}) && n.parent.parent == root {
for _, g := range invoicePlainGroups() {
container = container || g.name == n.name.Local
}
}
if container {
p, e := invoiceWriteInsert(s, nil, data)
if e != nil {
return nil, e
}
expanded = append(expanded, p)
}
}
if len(expanded) > 0 {
normalized, e := invoiceApplyXMLPatches(ctx, data, expanded)
if e != nil {
return nil, e
}
return UpdateInvoiceXML(ctx, normalized, invoiceIndex, value)
}
}
if err = invoiceWriteUnmapped(old.Facts, value.Facts); err != nil {
return nil, err
}
clean := value
clean.Facts = nil
clean.Sources = nil
clean.Items = append([]InvoiceItem(nil), value.Items...)
if clean.Standard != InvoiceStandardEInvoice {
for _, name := range invoiceWriteHeader(clean) {
previous := invoiceXMLField(&old, old.Kind, name)
target := invoiceXMLField(&clean, clean.Kind, name)
if previous != nil && target != nil && *previous == *target {
*target = ""
}
}
}
for i := range clean.Items {
clean.Items[i].Fields = nil
if value.Standard != InvoiceStandardEInvoice && (value.Kind == "atr" || value.Kind == "inv") {
for _, f := range value.Items[i].Fields {
if !invoiceWriteContains(invoiceWriteItemNames(value.Kind), f.Name) {
continue
}
if p := invoiceItemField(&clean.Items[i], value.Kind, f.Name); p != nil {
if *p == "" && !f.Nil {
continue
}
f.Value = *p
if *p != "" {
f.Nil = false
}
}
f.Source = InvoiceSource{}
f.Context = ""
clean.Items[i].Fields = append(clean.Items[i].Fields, f)
}
}
}
if reflect.DeepEqual(old.Items, value.Items) {
clean.Items = nil
}
if invoiceWriteCreation(old) == nil {
if err = invoiceWriteCreation(value); err != nil {
return nil, err
}
}
var generated []byte
if value.Standard == InvoiceStandardEInvoice {
generated, err = invoiceEncodePlain(clean)
} else {
options, e := invoiceWriteOriginalOptions(root, old)
if e != nil {
return nil, e
}
generated, err = invoiceEncodeXBRL(clean, options)
}
if err != nil {
return nil, err
}
generatedRoot, err := invoiceXMLTree(ctx, generated)
if err != nil {
return nil, err
}
var patches []invoiceXMLPatch
var trailingGroups []invoiceXMLPatch
var itemParent, generatedParent *invoiceXMLNode
if value.Standard == InvoiceStandardEInvoice {
if len(invoiceChildren(root, "", "EInvoiceData")) != 1 || len(invoiceChildren(root, "", "TaxSupervisionInfo")) != 1 {
return nil, fmt.Errorf("invoice requires unique data and supervision groups")
}
itemParent = invoiceChildren(root, "", "EInvoiceData")[0]
generatedParent = invoiceChildren(generatedRoot, "", "EInvoiceData")[0]
for _, group := range append(invoicePlainGroups(), invoiceWriteField{"TaxSupervisionInfo", "InvoiceNumber IssueTime"}) {
parent, newParent := itemParent, generatedParent
if group.name == "TaxSupervisionInfo" {
parent, newParent = root, generatedRoot
}
existing := invoiceChildren(parent, "", group.name)
desired := invoiceChildren(newParent, "", group.name)
if len(existing) > 1 {
return nil, fmt.Errorf("ambiguous invoice group %s", group.name)
}
if len(existing) == 0 {
if len(desired) > 0 && len(desired[0].children) > 0 {
raw, e := invoiceWriteNode(desired[0])
if e != nil {
return nil, e
}
p, e := invoiceWriteOrderedInsert(data, spans, parent, "", group.name, strings.Fields("SellerInformation BuyerInformation BasicInformation IssuItemInformation SpecificInformation AdditionalInformation"), raw)
if e != nil {
return nil, e
}
if group.name == "AdditionalInformation" {
trailingGroups = append(trailingGroups, p)
} else {
patches = append(patches, p)
}
}
continue
}
var target *invoiceXMLNode
if len(desired) > 0 {
target = desired[0]
}
retained := invoiceXMLNode{}
if target != nil {
retained = *target
retained.children = append([]*invoiceXMLNode(nil), target.children...)
}
for _, name := range strings.Fields(group.value) {
path := "/EInvoice/EInvoiceData/" + group.name + "/" + name
if group.name == "TaxSupervisionInfo" {
path = "/EInvoice/TaxSupervisionInfo/" + name
}
previous, _ := invoicePlainField(&old, nil, path)
desired, _ := invoicePlainField(&value, nil, path)
if previous != nil && desired != nil && *previous == *desired && len(invoiceChildren(&retained, "", name)) == 0 {
retained.children = append(retained.children, invoiceChildren(existing[0], "", name)...)
}
}
ps, e := invoiceWriteMergeChildren(data, spans, existing[0], &retained, "", strings.Fields(group.value))
if e != nil {
return nil, e
}
patches = append(patches, ps...)
}
} else {
itemParent, generatedParent = root, generatedRoot
contextID := ""
for _, f := range old.Facts {
if f.Namespace == invoiceXBRL && f.Name == "context" {
contextID = f.Context
break
}
}
if contextID == "" {
return nil, fmt.Errorf("missing invoice context")
}
filtered := *root
filtered.children = nil
for _, n := range root.children {
if invoiceXMLAttr(n, "", "contextRef") == contextID {
filtered.children = append(filtered.children, n)
}
}
for _, name := range invoiceWriteHeader(value) {
previous := invoiceXMLField(&old, old.Kind, name)
desired := invoiceXMLField(&value, value.Kind, name)
if previous != nil && desired != nil && *previous == *desired {
generatedRoot.children = append(generatedRoot.children, invoiceChildren(&filtered, value.Standard, name)...)
}
}
ps, e := invoiceWriteMergeChildren(data, spans, &filtered, generatedRoot, value.Standard, invoiceWriteHeader(value))
if e != nil {
return nil, e
}
patches = append(patches, ps...)
}
ps, err := invoiceWriteMergeItems(ctx, data, spans, itemParent, generatedParent, old, value)
if err != nil {
return nil, err
}
patches = append(patches, ps...)
patches = append(patches, trailingGroups...)
result, err := invoiceApplyXMLPatches(ctx, data, patches)
if err != nil {
return nil, err
}
after, err := DecodeInvoices(ctx, result)
if err != nil {
return nil, err
}
if err = invoiceWriteNewIssues(before.Issues, after.Issues); err != nil {
return nil, err
}
if len(after.Invoices) != len(before.Invoices) {
return nil, fmt.Errorf("invoice update changed candidate contexts")
}
var matches []Invoice
for _, inv := range after.Invoices {
if inv.Kind == value.Kind && inv.Standard == value.Standard {
if value.Standard == InvoiceStandardEInvoice {
matches = append(matches, inv)
continue
}
for _, f := range inv.Facts {
if f.Name == "context" && f.Namespace == invoiceXBRL {
for _, of := range old.Facts {
if of.Name == "context" && of.Namespace == invoiceXBRL && of.Context == f.Context {
matches = append(matches, inv)
}
}
}
}
}
}
if len(matches) != 1 {
return nil, fmt.Errorf("invoice update target is ambiguous")
}
if err = invoiceWriteRepresented(value, matches[0]); err != nil {
return nil, err
}
if err = invoiceValidateXMLWrite(ctx, data, result); err != nil {
return nil, err
}
return result, nil
}
// invoiceEDRMPaths 列出2019版摘要与明细的映射路径
// 入参: 无
// 返回: 2019版可映射字段路径
func invoiceEDRMPaths() []string {
return strings.Fields("/eInvoice/InvoiceCode /eInvoice/InvoiceNo /eInvoice/IssueDate /eInvoice/Buyer/BuyerName /eInvoice/Buyer/BuyerTaxID /eInvoice/Seller/SellerName /eInvoice/Seller/SellerTaxID /eInvoice/TaxInclusiveTotalAmount /eInvoice/Note /eInvoice/TaxTotalAmount /eInvoice/TaxExclusiveTotalAmount /eInvoice/GoodsInfos/GoodsInfo/Item /eInvoice/GoodsInfos/GoodsInfo/Specification /eInvoice/GoodsInfos/GoodsInfo/MeasurementDimension /eInvoice/GoodsInfos/GoodsInfo/Quantity /eInvoice/GoodsInfos/GoodsInfo/Price /eInvoice/GoodsInfos/GoodsInfo/Amount /eInvoice/GoodsInfos/GoodsInfo/TaxScheme /eInvoice/GoodsInfos/GoodsInfo/TaxAmount")
}
// invoiceUpdateEDRM 按2019版原节点写回摘要及明细,保留扩展与未修改字节
// 入参: ctx 上下文,data 原文,old 原发票,value 目标发票
// 返回: 修改后XML及错误
func invoiceUpdateEDRM(ctx context.Context, data []byte, old, value Invoice) ([]byte, error) {
if err := invoiceWriteUnmapped(old.Facts, value.Facts); err != nil {
return nil, err
}
root, spans, err := invoiceWriteSpans(ctx, data)
if err != nil {
return nil, err
}
for _, name := range []string{"Buyer", "Seller", "GoodsInfos"} {
changed := name == "GoodsInfos" && !reflect.DeepEqual(old.Items, value.Items) || name == "Buyer" && old.Buyer != value.Buyer || name == "Seller" && old.Seller != value.Seller
groups := invoiceChildren(root, invoiceEDRM, name)
if len(groups) > 1 {
return nil, fmt.Errorf("ambiguous invoice group %s", name)
}
if changed && len(groups) == 1 && spans[groups[0].path].empty {
patch, err := invoiceWriteInsert(spans[groups[0].path], nil, data)
if err != nil {
return nil, err
}
normalized, err := invoiceApplyXMLPatches(ctx, data, []invoiceXMLPatch{patch})
if err != nil {
return nil, err
}
return invoiceUpdateEDRM(ctx, normalized, old, value)
}
}
var patches []invoiceXMLPatch
order := strings.Fields("DocID InvoiceCode InvoiceNo TypeCode IssueDate InvoiceCheckCode MachineNo TaxControlCode Buyer Tpvn Seller TaxInclusiveTotalAmount Note InvoiceClerk Payee Checker TaxTotalAmount TaxExclusiveTotalAmount GraphCode InvoiceSIA1 InvoiceSIA2 Signature GoodsInfos")
for _, group := range []string{"", "Buyer", "Seller"} {
parent := root
if group != "" {
parents := invoiceChildren(root, invoiceEDRM, group)
if len(parents) > 0 {
parent = parents[0]
} else {
parent = nil
}
}
desired := &invoiceXMLNode{name: xml.Name{Space: invoiceEDRM, Local: group}}
var names []string
for _, path := range invoiceEDRMPaths() {
parts := strings.Split(path, "/")
if group == "" && len(parts) != 3 || group != "" && (len(parts) != 4 || parts[2] != group) {
continue
}
previous, _ := invoiceEDRMField(&old, nil, path)
target, _ := invoiceEDRMField(&value, nil, path)
if *previous == *target {
continue
}
name := parts[len(parts)-1]
names = append(names, name)
node := &invoiceXMLNode{name: xml.Name{Space: invoiceEDRM, Local: name}}
text := *target
if name == "IssueDate" && parent != nil {
existing := invoiceChildren(parent, invoiceEDRM, name)
if len(existing) == 1 && strings.Contains(existing[0].text.String(), "年") && text != "" {
date, err := time.Parse("2006-01-02", text)
if err != nil {
return nil, err
}
text = date.Format("2006年01月02日")
}
}
node.text.WriteString(text)
node.parts = []invoiceXMLPart{{text: text}}
desired.children = append(desired.children, node)
desired.parts = append(desired.parts, invoiceXMLPart{child: node})
}
if len(names) == 0 {
continue
}
if parent == nil {
raw, err := invoiceWriteNode(desired)
if err != nil {
return nil, err
}
patch, err := invoiceWriteOrderedInsert(data, spans, root, invoiceEDRM, group, order, raw)
if err != nil {
return nil, err
}
patches = append(patches, patch)
} else {
for _, name := range names {
if len(invoiceChildren(parent, invoiceEDRM, name)) == 0 && group == "" {
raw, err := invoiceWriteNode(invoiceChildren(desired, invoiceEDRM, name)[0])
if err != nil {
return nil, err
}
patch, err := invoiceWriteOrderedInsert(data, spans, parent, invoiceEDRM, name, order, raw)
if err != nil {
return nil, err
}
patches = append(patches, patch)
continue
}
changes, err := invoiceWriteMergeChildren(data, spans, parent, desired, invoiceEDRM, []string{name})
if err != nil {
return nil, err
}
patches = append(patches, changes...)
}
}
}
if !reflect.DeepEqual(old.Items, value.Items) {
generated := &invoiceXMLNode{name: xml.Name{Space: invoiceEDRM, Local: "GoodsInfos"}}
for _, item := range value.Items {
node := &invoiceXMLNode{name: xml.Name{Space: invoiceEDRM, Local: "GoodsInfo"}}
for _, path := range invoiceEDRMPaths() {
if !strings.HasPrefix(path, "/eInvoice/GoodsInfos/GoodsInfo/") {
continue
}
p, _ := invoiceEDRMField(&value, &item, path)
leaf := &invoiceXMLNode{name: xml.Name{Space: invoiceEDRM, Local: path[strings.LastIndexByte(path, '/')+1:]}}
leaf.text.WriteString(*p)
leaf.parts = []invoiceXMLPart{{text: *p}}
node.children = append(node.children, leaf)
node.parts = append(node.parts, invoiceXMLPart{child: leaf})
}
generated.children = append(generated.children, node)
generated.parts = append(generated.parts, invoiceXMLPart{child: node})
}
parents := invoiceChildren(root, invoiceEDRM, "GoodsInfos")
if len(parents) == 0 {
raw, err := invoiceWriteNode(generated)
if err != nil {
return nil, err
}
patch, err := invoiceWriteInsert(spans[root.path], raw, data)
if err != nil {
return nil, err
}
patches = append(patches, patch)
} else {
changes, err := invoiceWriteMergeItems(ctx, data, spans, parents[0], generated, old, value)
if err != nil {
return nil, err
}
patches = append(patches, changes...)
}
}
result, err := invoiceApplyXMLPatches(ctx, data, patches)
if err != nil {
return nil, err
}
report, err := DecodeInvoices(ctx, result)
if err != nil {
return nil, err
}
if len(report.Invoices) != 1 {
return nil, fmt.Errorf("invoice update changed candidate count")
}
if err := invoiceWriteRepresented(value, report.Invoices[0]); err != nil {
return nil, err
}
if err := invoiceWriteRepresented(report.Invoices[0], value); err != nil {
return nil, err
}
if err := invoiceValidateXMLWrite(ctx, data, result); err != nil {
return nil, err
}
return result, nil
}
// invoiceWriteContains 判断字段是否位于已知顺序表
// 入参: names 字段列表,name 字段
// 返回: 是否存在
func invoiceWriteContains(names []string, name string) bool {
for _, n := range names {
if n == name {
return true
}
}
return false
}
// invoiceWriteUnmapped 拒绝通过摘要更改或删除未映射事实,原文修改使用PatchInvoice
// 入参: before 原事实,after 目标事实
// 返回: 不支持的事实修改错误
func invoiceWriteUnmapped(before, after []InvoiceFact) error {
if len(after) == 0 {
return nil
}
old := map[string]InvoiceFact{}
for _, f := range before {
old[f.Source.NodePath] = f
}
for _, f := range after {
prior, ok := old[f.Source.NodePath]
if !ok {
return fmt.Errorf("new unmapped facts require explicit source editing")
}
if f.Name != prior.Name || f.Namespace != prior.Namespace || f.Context != prior.Context || f.Unit != prior.Unit || f.Decimals != prior.Decimals || f.Precision != prior.Precision || !slices.Equal(f.Dimensions, prior.Dimensions) {
return fmt.Errorf("invoice update cannot alter fact metadata")
}
if f.Value != prior.Value || f.Nil != prior.Nil {
return fmt.Errorf("unmapped fact changes require PatchInvoice")
}
}
return nil
}
// invoiceWriteOriginalOptions 读取原上下文及单位,不猜测或改写定义
// 入参: root 原XML,value 原候选
// 返回: 原定义及不支持的复杂上下文错误
func invoiceWriteOriginalOptions(root *invoiceXMLNode, value Invoice) (InvoiceEncodingOptions, error) {
var options InvoiceEncodingOptions
ref := ""
for _, f := range value.Facts {
if f.Namespace == invoiceXBRL && f.Name == "context" {
ref = f.Context
break
}
}
for _, n := range root.children {
if n.name == (xml.Name{Space: invoiceLinkbase, Local: "schemaRef"}) && options.SchemaRef == "" {
options.SchemaRef = invoiceXMLAttr(n, invoiceXLink, "href")
}
if n.name == (xml.Name{Space: invoiceXBRL, Local: "context"}) && invoiceXMLAttr(n, "", "id") == ref {
dims, ok := invoiceContext(n)
if !ok || len(dims) > 0 {
return options, fmt.Errorf("invoice update requires an unambiguous nondimensional context")
}
entity := invoiceChildren(n, invoiceXBRL, "entity")[0]
id := invoiceChildren(entity, invoiceXBRL, "identifier")[0]
options.Context = InvoiceContext{ID: ref, Identifier: id.text.String(), Scheme: invoiceXMLAttr(id, "", "scheme")}
period := invoiceChildren(n, invoiceXBRL, "period")[0]
for _, p := range period.children {
switch p.name.Local {
case "instant":
options.Context.Instant = p.text.String()
case "startDate":
options.Context.StartDate = p.text.String()
case "endDate":
options.Context.EndDate = p.text.String()
}
}
}
if n.name == (xml.Name{Space: invoiceXBRL, Local: "unit"}) {
ms := invoiceChildren(n, invoiceXBRL, "measure")
if len(ms) == 1 {
q, ok := invoiceQName(ms[0], ms[0].text.String())
if ok {
options.Units = append(options.Units, InvoiceUnit{ID: invoiceXMLAttr(n, "", "id"), Namespace: q.Space, Name: q.Local})
}
}
}
}
if options.SchemaRef == "" {
return options, fmt.Errorf("invoice source has no schema reference")
}
return options, nil
}
// invoiceWriteNode 序列化生成的节点,使移动节点的命名空间自足
// 入参: n 节点
// 返回: XML及编码错误
func invoiceWriteNode(n *invoiceXMLNode) ([]byte, error) {
var b bytes.Buffer
e := xml.NewEncoder(&b)
var walk func(*invoiceXMLNode) error
walk = func(c *invoiceXMLNode) error {
start := xml.StartElement{Name: c.name}
for _, a := range c.attrs {
if a.Name.Space != "xmlns" && a.Name != (xml.Name{Local: "xmlns"}) {
start.Attr = append(start.Attr, a)
}
}
if err := e.EncodeToken(start); err != nil {
return err
}
for _, p := range c.parts {
if p.child != nil {
if err := walk(p.child); err != nil {
return err
}
} else {
if err := e.EncodeToken(xml.CharData(p.text)); err != nil {
return err
}
}
}
return e.EncodeToken(start.End())
}
if err := walk(n); err != nil {
return nil, err
}
if err := e.Flush(); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// invoiceWriteInsert 生成父节点内追加内容的修改,支持空元素
// 入参: span 父节点边界,raw 新内容,data 原文
// 返回: 插入修改及错误
func invoiceWriteInsert(span invoiceXMLSpan, raw, data []byte) (invoiceXMLPatch, error) {
if !span.empty {
return invoiceXMLPatch{span.close, span.close, raw}, nil
}
open := data[span.begin:span.inner]
nameEnd := bytes.IndexAny(open, " \t\r\n/>")
if nameEnd < 2 {
return invoiceXMLPatch{}, fmt.Errorf("invalid empty invoice element")
}
name := string(open[1:nameEnd])
replacement := append([]byte(nil), open[:len(open)-2]...)
replacement = append(replacement, '>')
replacement = append(replacement, raw...)
replacement = append(replacement, []byte("</"+name+">")...)
return invoiceXMLPatch{span.begin, span.end, replacement}, nil
}
// invoiceWriteMergeChildren 合并已知叶节点,其他属性、子节点及空白保持原样
// 入参: data 原文,spans 边界,old 原父节点,desired 目标父节点,space 命名空间,names 已知字段
// 返回: 修改列表及歧义错误
func invoiceWriteMergeChildren(data []byte, spans map[string]invoiceXMLSpan, old, desired *invoiceXMLNode, space string, names []string) ([]invoiceXMLPatch, error) {
var patches []invoiceXMLPatch
for rank, name := range names {
existing := invoiceChildren(old, space, name)
var targets []*invoiceXMLNode
if desired != nil {
targets = invoiceChildren(desired, space, name)
}
if len(existing) > 1 || len(targets) > 1 {
return nil, fmt.Errorf("ambiguous invoice field %s", name)
}
var raw []byte
if len(targets) > 0 {
var err error
raw, err = invoiceWriteNode(targets[0])
if err != nil {
return nil, err
}
}
if len(existing) > 0 {
s := spans[existing[0].path]
if len(existing[0].children) > 0 {
return nil, fmt.Errorf("non-leaf invoice field %s", name)
}
if len(targets) == 0 && (existing[0].text.String() == "" || invoiceXMLAttr(existing[0], invoiceXSI, "nil") == "true" || invoiceXMLAttr(existing[0], invoiceXSI, "nil") == "1") {
continue
}
if len(targets) > 0 && existing[0].text.String() == targets[0].text.String() && invoiceXMLAttr(existing[0], invoiceXSI, "nil") == invoiceXMLAttr(targets[0], invoiceXSI, "nil") {
continue
}
if len(targets) > 0 {
oldNil := invoiceXMLAttr(existing[0], invoiceXSI, "nil")
newNil := invoiceXMLAttr(targets[0], invoiceXSI, "nil")
wasNil := oldNil == "true" || oldNil == "1"
isNil := newNil == "true" || newNil == "1"
if !s.empty && wasNil == isNil {
patches = append(patches, invoiceXMLPatch{s.inner, s.close, []byte(invoiceWriteEscape(targets[0].text.String()))})
continue
}
var err error
raw, err = invoiceWriteLeaf(existing[0], targets[0].text.String(), isNil)
if err != nil {
return nil, err
}
}
patches = append(patches, invoiceXMLPatch{s.begin, s.end, raw})
} else if len(raw) > 0 {
position := -1
for _, next := range names[rank+1:] {
nodes := invoiceChildren(old, space, next)
if len(nodes) > 0 {
candidate := spans[nodes[0].path].begin
if position < 0 || candidate < position {
position = candidate
}
}
}
if position >= 0 {
patches = append(patches, invoiceXMLPatch{position, position, raw})
} else {
p, err := invoiceWriteInsert(spans[old.path], raw, data)
if err != nil {
return nil, err
}
patches = append(patches, p)
}
}
}
return patches, nil
}
// invoiceWriteMergeItems 按原明细来源重排或修改元组,删除仅作用于显式移除的明细
// 入参: ctx 上下文,data 原文,spans 边界,parent 原父节点,generated 新父节点,old 原发票,value 目标发票
// 返回: 明细修改及错误
func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]invoiceXMLSpan, parent, generated *invoiceXMLNode, old, value Invoice) ([]invoiceXMLPatch, error) {
if reflect.DeepEqual(old.Items, value.Items) {
return nil, nil
}
space, name := value.Standard, invoiceWriteTuple(value.Kind)
names := invoiceWriteItemNames(value.Kind)
if value.Standard == invoiceEDRM {
name = "GoodsInfo"
names = strings.Fields("Item Specification MeasurementDimension Quantity Price Amount TaxScheme TaxAmount")
} else if value.Standard == InvoiceStandardEInvoice {
space = ""
name = "IssuItemInformation"
names = strings.Fields("ItemName SpecMod MeaUnits Quantity UnPrice Amount TaxRate ComTaxAm")
}
targets := invoiceChildren(generated, space, name)
if len(targets) != len(value.Items) {
return nil, fmt.Errorf("unsupported invoice item shape")
}
original := map[string]*invoiceXMLNode{}
originalItems := map[string]InvoiceItem{}
for _, item := range old.Items {
for _, f := range item.Fields {
s := spans[f.Source.NodePath]
if s.node != nil && s.node.parent != nil && s.node.parent.parent == parent && s.node.parent.name.Space == space {
ref := invoiceXMLAttr(s.node, "", "contextRef")
for _, child := range s.node.parent.children {
if other := invoiceXMLAttr(child, "", "contextRef"); other != "" && other != ref {
return nil, fmt.Errorf("invoice item shares multiple contexts")
}
}
original[s.node.parent.path] = s.node.parent
originalItems[s.node.parent.path] = item
break
}
}
}
if len(original) != len(old.Items) {
return nil, fmt.Errorf("invoice items require explicit tuple sources")
}
seen := map[string]bool{}
var combined []byte
for i, item := range value.Items {
if err := ctx.Err(); err != nil {
return nil, err
}
path := ""
for _, f := range item.Fields {
s := spans[f.Source.NodePath]
if s.node != nil && s.node.parent != nil && original[s.node.parent.path] != nil {
if path != "" && path != s.node.parent.path {
return nil, fmt.Errorf("mixed invoice item sources")
}
path = s.node.parent.path
}
}
if path == "" {
for _, f := range item.Fields {
mapped := invoiceItemField(&InvoiceItem{}, value.Kind, f.Name)
if value.Standard == InvoiceStandardEInvoice || value.Standard == invoiceEDRM || !invoiceWriteContains(names, f.Name) {
return nil, fmt.Errorf("unsupported new invoice item fact %s", f.Name)
}
if mapped != nil {
p := invoiceItemField(&item, value.Kind, f.Name)
if f.Nil && *p != "" || !f.Nil && f.Value != *p {
return nil, fmt.Errorf("conflicting new invoice item fact %s", f.Name)
}
}
}
raw, err := invoiceWriteNode(targets[i])
if err != nil {
return nil, err
}
combined = append(combined, raw...)
continue
}
if seen[path] {
return nil, fmt.Errorf("duplicate invoice item source")
}
seen[path] = true
for _, f := range item.Fields {
if invoiceWriteContains(names, f.Name) {
continue
}
s, ok := spans[f.Source.NodePath]
if !ok || s.node.parent == nil || s.node.parent.path != path || f.Value != s.node.text.String() || f.Nil != (invoiceXMLAttr(s.node, invoiceXSI, "nil") == "true" || invoiceXMLAttr(s.node, invoiceXSI, "nil") == "1") {
return nil, fmt.Errorf("unmapped item fact changes require PatchInvoice")
}
}
n := original[path]
if value.Standard == invoiceEDRM {
previous := originalItems[path]
if err := invoiceWriteUnmapped(previous.Fields, item.Fields); err != nil {
return nil, err
}
var retained []*invoiceXMLNode
for _, child := range targets[i].children {
field := "/eInvoice/GoodsInfos/GoodsInfo/" + child.name.Local
before, _ := invoiceEDRMField(&old, &previous, field)
after, _ := invoiceEDRMField(&value, &item, field)
if before != nil && after != nil && *before == *after {
children := invoiceChildren(n, invoiceEDRM, child.name.Local)
if len(children) == 0 {
continue
}
if len(children) == 1 {
child = children[0]
}
}
retained = append(retained, child)
}
targets[i].children = retained
}
ps, err := invoiceWriteMergeChildren(data, spans, n, targets[i], space, names)
if err != nil {
return nil, err
}
s := spans[path]
for j := range ps {
ps[j].begin -= s.begin
ps[j].end -= s.begin
}
raw, err := invoiceApplyXMLPatches(ctx, data[s.begin:s.end], ps)
if err != nil {
return nil, err
}
combined = append(combined, raw...)
}
var ordered []invoiceXMLSpan
for path := range original {
ordered = append(ordered, spans[path])
}
sort.Slice(ordered, func(i, j int) bool { return ordered[i].begin < ordered[j].begin })
var patches []invoiceXMLPatch
for i, s := range ordered {
raw := []byte(nil)
if i == 0 {
raw = combined
}
patches = append(patches, invoiceXMLPatch{s.begin, s.end, raw})
}
if len(ordered) == 0 && len(combined) > 0 {
var p invoiceXMLPatch
var err error
if value.Standard == InvoiceStandardEInvoice {
p, err = invoiceWriteOrderedInsert(data, spans, parent, "", name, strings.Fields("SellerInformation BuyerInformation BasicInformation IssuItemInformation SpecificInformation AdditionalInformation"), combined)
} else {
p, err = invoiceWriteInsert(spans[parent.path], combined, data)
}
if err != nil {
return nil, err
}
patches = append(patches, p)
}
return patches, nil
}
// invoiceWriteOrderedInsert 在后继已知元素前插入,未知节点不移动
// 入参: data 原文,spans 边界,parent 父节点,space 命名空间,name 新节点名,names 规范顺序,raw 新内容
// 返回: 插入修改及错误
func invoiceWriteOrderedInsert(data []byte, spans map[string]invoiceXMLSpan, parent *invoiceXMLNode, space, name string, names []string, raw []byte) (invoiceXMLPatch, error) {
position := -1
following := false
for _, known := range names {
if known == name {
following = true
continue
}
if !following {
continue
}
for _, node := range invoiceChildren(parent, space, known) {
offset := spans[node.path].begin
if position < 0 || offset < position {
position = offset
}
}
}
if position >= 0 {
return invoiceXMLPatch{position, position, raw}, nil
}
return invoiceWriteInsert(spans[parent.path], raw, data)
}
// invoiceWriteCreation 检查官方元组序列中的必有字段,不虚构缺失业务值
// 入参: value 发票
// 返回: 缺少元组字段错误
func invoiceWriteCreation(value Invoice) error {
if value.Standard == InvoiceStandardEInvoice {
return nil
}
for i, item := range value.Items {
required := strings.Fields("NatureOfInvoiceLine NameOfTaxableGoodsOrServices AmountExcludingTax TaxRate TaxAmount")
if value.Kind == "atr" {
required = strings.Fields("DepartureStation DestinationStation FlightSegment Carrier Flight Class FareBasis FreeBaggageAllowance")
}
for _, name := range required {
p := invoiceItemField(&item, value.Kind, name)
present := p != nil && *p != ""
for _, f := range item.Fields {
if f.Name == name && (f.Nil || f.Value != "") {
present = true
}
}
if !present {
return fmt.Errorf("invoice item %d requires %s", i, name)
}
}
}
return nil
}
// invoiceWriteKnownFact 限制新建事实为已核实的分类标准概念
// 入参: kind 票种,name 概念名
// 返回: 是否支持新建
func invoiceWriteKnownFact(kind, name string) bool {
if invoiceWriteContains(invoiceWriteHeader(Invoice{Kind: kind}), name) {
return true
}
if (kind == "inv" || kind == "atr") && invoiceWriteContains(invoiceWriteItemNames(kind), name) {
return true
}
var extras string
switch kind {
case "inv":
extras = "TypeOfInvoice UniqueCodeOfInvoice LocationOfInvoice IdentifyingCode NumberOfInvoiceMachine SecurityCode TaxIncludedAmountInWords Payee Reviewer Issuer AddressPhoneNumberOfPurchaser DepositBankAndAccountNumberOfPurchaser AddressPhoneNumberOfSeller DepositBankAndAccountNumberOfSeller"
case "atr":
extras = "PassengerName ValidIdNumber Endorsement NumberOfGpOrder Fare FuelSurcharge VatRate CivilAviationDevelopmentFund OtherTaxes ETicketNumber VerificationCode PromptInformation Insurance AgentCode IssueParty MarkingOfDomesticOrInternational"
case "rai":
extras = "TypeOfVoucher ETicketNumber IssueParty IssuePartyCode TypeOfBusiness DepartureStation PhonicsOfDepartureStation DestinationStation PhonicsOfDestinationStation TrainNumber TravelDate DepartureTime AirConditioningCharacteristics SeatLevel Carriage Seat TypeOfRailwayTicket IdNumber Name TaxRate AddressPhoneNumberOfPurchaser DepositBankAndAccountNumberOfPurchaser AddressPhoneNumberOfSeller DepositBankAndAccountNumberOfSeller"
}
return invoiceWriteContains(strings.Fields(extras), name)
}
// invoiceWriteDateFact 识别规范中的日期类型,而非自由文本日期时间
// 入参: kind 票种,name 概念名
// 返回: 是否为日期事实
func invoiceWriteDateFact(kind, name string) bool {
return kind == "inv" && name == "DateOfIssue" || kind == "atr" && invoiceWriteContains(strings.Fields("IssueDate CarrierDate EffectiveDate ExpirationDate"), name) || kind == "rai" && (name == "DateOfIssue" || name == "TravelDate")
}
// invoiceWritePlainSections 接入调用方提供的官方头部、认证及签名片段,不生成税务签名
// 入参: ctx 上下文,data 生成XML,options 显式业务片段
// 返回: XML及片段错误
func invoiceWritePlainSections(ctx context.Context, data []byte, options InvoiceEncodingOptions) ([]byte, error) {
root, spans, err := invoiceWriteSpans(ctx, data)
if err != nil {
return nil, err
}
var patches []invoiceXMLPatch
for _, section := range []struct {
name string
data []byte
}{{"Header", options.HeaderXML}, {"SellerAuthentication", options.SellerAuthenticationXML}, {"TaxSupervisionInfo", options.TaxSupervisionXML}, {"TaxBureauSignature", options.TaxBureauSignatureXML}} {
if len(section.data) == 0 {
continue
}
n, err := invoiceXMLTree(ctx, section.data)
if err != nil {
return nil, err
}
if n.name != (xml.Name{Local: section.name}) {
return nil, fmt.Errorf("invalid invoice section %s", section.name)
}
if section.name == "Header" {
vs := invoiceChildren(n, "", "Version")
if len(vs) != 1 || vs[0].text.String() != "0.3" {
return nil, fmt.Errorf("invalid invoice header version")
}
}
old := invoiceChildren(root, "", section.name)
if len(old) > 0 {
s := spans[old[0].path]
patches = append(patches, invoiceXMLPatch{s.begin, s.end, section.data})
} else {
pos := spans[root.path].close
if section.name == "SellerAuthentication" {
pos = spans[invoiceChildren(root, "", "TaxSupervisionInfo")[0].path].begin
}
patches = append(patches, invoiceXMLPatch{pos, pos, section.data})
}
}
return invoiceApplyXMLPatches(ctx, data, patches)
}