mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
2904a0d914
commit
3709a62c24
3 files changed
+294
-6
No files matched your search
@@ -1088,6 +1088,11 @@ func invoiceBindingCoverage(before, after Invoice, bindings []InvoiceBinding) er
|
||||
if a.Name != b.Name || a.Namespace != b.Namespace || a.Context != b.Context || a.Unit != b.Unit {
|
||||
return fmt.Errorf("bound invoice fact identity changed")
|
||||
}
|
||||
if mapped, ok := invoiceEDRMFactBinding(before, a); ok {
|
||||
if next, valid := invoiceEDRMFactBinding(after, b); valid && mapped.Field == next.Field && mapped.Item == next.Item {
|
||||
bound[fieldKey{mapped.Field, mapped.Item}] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for key, value := range old {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InvoiceLayout 指定发票信息页的字体资源及毫米尺寸,不代表税务机关的固定票样
|
||||
@@ -123,6 +124,18 @@ func invoiceLayoutFactVisible(invoice Invoice, fact InvoiceFact) bool {
|
||||
if fact.Nil || fact.Value == "" || fact.Namespace == invoiceXBRL && (fact.Name == "context" || fact.Name == "unit") {
|
||||
return false
|
||||
}
|
||||
if binding, ok := invoiceEDRMFactBinding(invoice, fact); ok {
|
||||
value, err := invoiceBindingValue(invoice, binding)
|
||||
raw := fact.Value
|
||||
if binding.Field == "date" {
|
||||
if date, err := time.Parse("2006年01月02日", raw); err == nil {
|
||||
raw = date.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
if err == nil && value == raw {
|
||||
return false
|
||||
}
|
||||
}
|
||||
kind, _ := invoiceTaxonomy(fact.Namespace)
|
||||
if kind != "" && kind == invoice.Kind {
|
||||
if field := invoiceXMLField(&invoice, kind, fact.Name); field != nil && *field == fact.Value {
|
||||
@@ -163,6 +176,46 @@ func invoiceLayoutFactVisible(invoice Invoice, fact InvoiceFact) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// invoiceEDRMFactBinding 按2019版完整路径识别摘要与原始字段的同一语义
|
||||
// 入参: value 发票,fact 原始字段
|
||||
// 返回: 公共字段关联及是否明确映射
|
||||
func invoiceEDRMFactBinding(value Invoice, fact InvoiceFact) (InvoiceBinding, bool) {
|
||||
if value.Standard != invoiceEDRM || fact.Namespace != invoiceEDRM {
|
||||
return InvoiceBinding{}, false
|
||||
}
|
||||
var path strings.Builder
|
||||
for _, segment := range strings.Split(strings.TrimPrefix(strings.ReplaceAll(fact.Source.NodePath, "{"+invoiceEDRM+"}", "{}"), "/"), "/") {
|
||||
name, index, ok := strings.Cut(segment, "[")
|
||||
if !ok || !strings.HasPrefix(name, "{}") || !strings.HasSuffix(index, "]") {
|
||||
return InvoiceBinding{}, false
|
||||
}
|
||||
path.WriteByte('/')
|
||||
path.WriteString(strings.TrimPrefix(name, "{}"))
|
||||
}
|
||||
probe := Invoice{Items: []InvoiceItem{{}}}
|
||||
field, _ := invoiceEDRMField(&probe, &probe.Items[0], path.String())
|
||||
if field == nil {
|
||||
return InvoiceBinding{}, false
|
||||
}
|
||||
*field = "mapped"
|
||||
for _, candidate := range invoiceLayoutFields(probe) {
|
||||
if candidate.value != "mapped" {
|
||||
continue
|
||||
}
|
||||
if candidate.item < 0 {
|
||||
return InvoiceBinding{Field: candidate.field, Item: -1}, true
|
||||
}
|
||||
for i, item := range value.Items {
|
||||
for _, existing := range item.Fields {
|
||||
if existing.Source.NodePath == fact.Source.NodePath {
|
||||
return InvoiceBinding{Field: candidate.field, Item: i}, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return InvoiceBinding{}, false
|
||||
}
|
||||
|
||||
// invoiceBindingValue 读取明确字段与明细索引,不接受未知字段或隐式索引
|
||||
// 入参: invoice 发票数据, binding 字段绑定
|
||||
// 返回: string 字段原文, error 字段或索引错误
|
||||
|
||||
+236
-6
@@ -27,6 +27,7 @@ import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
@@ -117,7 +118,13 @@ type invoiceXMLPatch struct {
|
||||
func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
||||
format, supported := invoiceWriteFormat(value)
|
||||
probe := Invoice{Items: []InvoiceItem{{}}}
|
||||
if value.Standard == InvoiceStandardEInvoice {
|
||||
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) {
|
||||
@@ -209,7 +216,7 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
||||
// 返回: 已适配格式列表
|
||||
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, 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)
|
||||
@@ -1058,14 +1065,20 @@ func invoiceWriteXMLProblems(ctx context.Context, data []byte, tuples bool) (map
|
||||
empty := nilValue == "true" || nilValue == "1"
|
||||
date := kind != "" && invoiceWriteDateFact(kind, n.name.Local)
|
||||
plainDate := root.name == (xml.Name{Local: "EInvoice"}) && path == "/EInvoice/TaxSupervisionInfo/IssueTime"
|
||||
if len(n.children) == 0 && !empty && (date || plainDate) {
|
||||
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 ") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1092,6 +1105,18 @@ func invoiceWriteXMLProblems(ctx context.Context, data []byte, tuples bool) (map
|
||||
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 {
|
||||
@@ -1212,6 +1237,9 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
|
||||
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
|
||||
@@ -1453,6 +1481,180 @@ func UpdateInvoiceXML(ctx context.Context, data []byte, invoiceIndex int, value
|
||||
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 字段
|
||||
// 返回: 是否存在
|
||||
@@ -1685,7 +1887,10 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
|
||||
}
|
||||
space, name := value.Standard, invoiceWriteTuple(value.Kind)
|
||||
names := invoiceWriteItemNames(value.Kind)
|
||||
if value.Standard == InvoiceStandardEInvoice {
|
||||
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")
|
||||
@@ -1695,6 +1900,7 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
|
||||
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]
|
||||
@@ -1706,6 +1912,7 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
|
||||
}
|
||||
}
|
||||
original[s.node.parent.path] = s.node.parent
|
||||
originalItems[s.node.parent.path] = item
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1732,7 +1939,7 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
|
||||
if path == "" {
|
||||
for _, f := range item.Fields {
|
||||
mapped := invoiceItemField(&InvoiceItem{}, value.Kind, f.Name)
|
||||
if value.Standard == InvoiceStandardEInvoice || !invoiceWriteContains(names, 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 {
|
||||
@@ -1763,6 +1970,29 @@ func invoiceWriteMergeItems(ctx context.Context, data []byte, spans map[string]i
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user