mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
b8d90174a0
commit
68d6a19656
7 files changed
+219
-17
No files matched your search
@@ -5857,6 +5857,7 @@ async function newInvoiceDraft() {
|
||||
|
||||
function renderInvoiceBasic(container, draft) {
|
||||
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 = {
|
||||
kind: "票种", standard: "规范", version: "版本", number: "号码", code: "代码", date: "日期",
|
||||
currency: "币种", status: "状态", amount: "金额", taxAmount: "税额", totalAmount: "合计", remarks: "备注",
|
||||
@@ -5870,7 +5871,7 @@ function renderInvoiceBasic(container, draft) {
|
||||
container.append(title, grid);
|
||||
for (const field of fields.filter(field => field.group === group)) {
|
||||
const value = invoiceValue(draft.invoice, field.field);
|
||||
const display = field.field === "standard" && value === "http://www.edrm.org.cn/schema/e-invoice/2019" ? "电子发票/2019" : value;
|
||||
const display = field.field === "standard" ? standards[value] || value : value;
|
||||
invoiceInput(grid, labels[field.field.split(".").at(-1)] || field.label, `Field_${field.field.replaceAll(".", "_")}`, display, value => {
|
||||
invoiceSetValue(draft.invoice, field.field, -1, value);
|
||||
invoiceDirty(draft, "model");
|
||||
|
||||
+23
-9
@@ -994,6 +994,9 @@ func documentInvoice(args []js.Value) (any, error) {
|
||||
return nil, err
|
||||
}
|
||||
report, err := ofdgo.DecodeInvoices(ctx, data)
|
||||
if request.Source.Kind == "tag" && len(report.Invoices) == 0 {
|
||||
report, err = r.ReadInvoice(ctx, request.Source)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1114,20 +1117,23 @@ func validateInvoiceDraft(ctx context.Context, request invoiceRequest) (any, err
|
||||
if source.Kind == "" {
|
||||
source = request.Source
|
||||
}
|
||||
data, err := editor.ExportInvoice(ctx, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report, err := ofdgo.DecodeInvoices(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issues = append(issues, report.Issues...)
|
||||
view, err := editor.ReaderContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer view.Close()
|
||||
data, err := view.ExportInvoice(ctx, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report, err := ofdgo.DecodeInvoices(ctx, data)
|
||||
if source.Kind == "tag" && len(report.Invoices) == 0 {
|
||||
report, err = view.ReadInvoice(ctx, source)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issues = append(issues, report.Issues...)
|
||||
for index, invoice := range report.Invoices {
|
||||
validation, err := ofdgo.ValidateInvoice(ctx, invoice)
|
||||
if err != nil {
|
||||
@@ -1273,6 +1279,14 @@ func applyInvoiceBatch(ctx context.Context, editor *ofdgo.Editor, request invoic
|
||||
return result, fmt.Errorf("invalid invoice batch size")
|
||||
}
|
||||
report, err := ofdgo.DecodeInvoices(ctx, data)
|
||||
if request.Source.Kind == "tag" && len(report.Invoices) == 0 {
|
||||
view, viewErr := editor.ReaderContext(ctx)
|
||||
if viewErr != nil {
|
||||
return result, viewErr
|
||||
}
|
||||
defer view.Close()
|
||||
report, err = view.ReadInvoice(ctx, request.Source)
|
||||
}
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -95,13 +95,19 @@ func (r *Reader) InvoiceCapabilities(ctx context.Context, source InvoiceSource,
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
report, err := parseInvoiceXML(ctx, data, source)
|
||||
report, err := r.readInvoiceXML(ctx, data, source)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if index < 0 || index >= len(report.Invoices) {
|
||||
return result, fmt.Errorf("invoice index out of range")
|
||||
}
|
||||
if report.Invoices[index].Standard == ofdNamespace2016 {
|
||||
for _, operation := range []string{"create", "edit", "items", "bind", "generate", "sync", "delete"} {
|
||||
result.Reasons[operation] = "对象引用标引仅支持读取和导出"
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
format, known := invoiceWriteFormat(report.Invoices[index])
|
||||
result.Create = known && format.Create
|
||||
result.Edit = known && format.Edit
|
||||
@@ -669,7 +675,7 @@ func (r *Reader) InvoiceBindings(ctx context.Context, source InvoiceSource, inde
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report, err := parseInvoiceXML(ctx, data, source)
|
||||
report, err := r.readInvoiceXML(ctx, data, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -699,7 +705,7 @@ func (r *Reader) CheckInvoiceAppearance(ctx context.Context, source InvoiceSourc
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report, err := parseInvoiceXML(ctx, data, source)
|
||||
report, err := r.readInvoiceXML(ctx, data, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+16
-1
@@ -103,6 +103,7 @@ type InvoiceIssue struct {
|
||||
}
|
||||
|
||||
// InvoiceFact 保存命名空间限定的业务事实、原始数值及精度属性,Nil与空文本分别表达
|
||||
// References保留对象引用标引的原始页标识和有序图元标识,不推测明细归属
|
||||
type InvoiceFact struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
@@ -114,6 +115,7 @@ type InvoiceFact struct {
|
||||
Nil bool `json:"nil,omitempty"`
|
||||
Source InvoiceSource `json:"source"`
|
||||
Dimensions []InvoiceDimension `json:"dimensions,omitempty"`
|
||||
References []StructureReference `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
// InvoiceDimension 保存上下文维度,名称和成员的命名空间不依赖XML前缀
|
||||
@@ -165,7 +167,7 @@ func (r *Reader) InvoicesContext(ctx context.Context) (InvoiceReport, error) {
|
||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "read-error", Message: readErr.Error(), Source: source})
|
||||
return nil
|
||||
}
|
||||
parsed, err := parseInvoiceXML(ctx, data, source)
|
||||
parsed, err := r.readInvoiceXML(ctx, data, source)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -266,6 +268,19 @@ func (r *Reader) InvoicesContext(ctx context.Context) (InvoiceReport, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return InvoiceReport{}, err
|
||||
}
|
||||
payloads := make(map[[3]string]bool)
|
||||
for _, value := range report.Invoices {
|
||||
if value.Standard != ofdNamespace2016 && value.Number != "" {
|
||||
payloads[[3]string{value.Kind, value.Code, value.Number}] = true
|
||||
}
|
||||
}
|
||||
filtered := report.Invoices[:0]
|
||||
for _, value := range report.Invoices {
|
||||
if value.Standard != ofdNamespace2016 || !payloads[[3]string{value.Kind, value.Code, value.Number}] {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
report.Invoices = filtered
|
||||
invoiceSourceConflicts(&report)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return InvoiceReport{}, err
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InvoiceDigest 计算原始导出数据的SHA256摘要
|
||||
@@ -33,6 +34,158 @@ func InvoiceDigest(data []byte) string {
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ReadInvoice 读取当前文档及版本声明的发票,标引按对象引用提取文本
|
||||
// 入参: ctx 取消上下文, source 精确来源
|
||||
// 返回: InvoiceReport 发票及诊断, error 来源或读取错误
|
||||
func (r *Reader) ReadInvoice(ctx context.Context, source InvoiceSource) (InvoiceReport, error) {
|
||||
ctx = invoiceWriteContext(ctx)
|
||||
data, err := r.ExportInvoice(ctx, source)
|
||||
if err != nil {
|
||||
return InvoiceReport{}, err
|
||||
}
|
||||
report, err := r.readInvoiceXML(ctx, data, source)
|
||||
report.DocumentIndex = r.DocumentIndex()
|
||||
if r.versionInfo != nil {
|
||||
report.Version = r.versionInfo.ID
|
||||
}
|
||||
return report, err
|
||||
}
|
||||
|
||||
// readInvoiceXML 在当前文档作用域解析业务载荷或对象引用标引
|
||||
// 入参: ctx 取消上下文, data 原始XML, source 来源
|
||||
// 返回: InvoiceReport 发票及诊断, error 读取错误
|
||||
func (r *Reader) readInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (InvoiceReport, error) {
|
||||
report, err := parseInvoiceXML(ctx, data, source)
|
||||
if err != nil || source.Kind != "tag" || len(report.Invoices) != 0 {
|
||||
return report, err
|
||||
}
|
||||
root, err := invoiceXMLTree(ctx, data)
|
||||
if err != nil {
|
||||
return report, err
|
||||
}
|
||||
if root.name.Space != ofdNamespace2016 || root.name.Local != "root" || !invoiceObjectReferences(root) {
|
||||
return report, nil
|
||||
}
|
||||
structure, err := parseStructure(ctx, data, CustomTag{TypeID: structureTypeOfficial, NameSpace: ofdNamespace2016})
|
||||
if err != nil {
|
||||
return report, err
|
||||
}
|
||||
known := false
|
||||
for _, node := range structure.Children {
|
||||
known = known || node.Namespace == ofdNamespace2016 && node.Name == "InvoiceNo"
|
||||
}
|
||||
if !known {
|
||||
return report, nil
|
||||
}
|
||||
doc, err := r.DocContext(ctx)
|
||||
if err != nil {
|
||||
return report, err
|
||||
}
|
||||
pages := signaturePageNumbers(doc)
|
||||
cache := make(map[int]map[string][]string)
|
||||
inv := Invoice{Kind: "inv", Standard: ofdNamespace2016, Version: "1.0", Sources: []InvoiceSource{source}}
|
||||
assigned := make(map[string]bool)
|
||||
total := 0
|
||||
var visit func(*StructureNode, string, string) error
|
||||
visit = func(node *StructureNode, nodePath, fieldPath string) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if node.Namespace != ofdNamespace2016 {
|
||||
return nil
|
||||
}
|
||||
var refs []StructureReference
|
||||
for _, child := range node.Children {
|
||||
refs = append(refs, child.References...)
|
||||
}
|
||||
if len(refs) != 0 {
|
||||
factSource := source
|
||||
factSource.NodePath = nodePath
|
||||
fact := InvoiceFact{Name: node.Name, Namespace: node.Namespace, Source: factSource, References: refs}
|
||||
valid := true
|
||||
for _, ref := range refs {
|
||||
page, _ := resourceValue(pages, ref.PageID)
|
||||
if page != 0 && cache[page] == nil {
|
||||
content, err := r.PageContentByIndexContext(ctx, page-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
texts := make(map[string][]string)
|
||||
var collect func(GraphicObject)
|
||||
collect = func(object GraphicObject) {
|
||||
if object.Type == "TextObject" {
|
||||
id := editorResourceID(object.TextObject.ID)
|
||||
texts[id] = append(texts[id], object.TextObject.Text())
|
||||
}
|
||||
if object.Type == "CompositeGraphicUnit" {
|
||||
for child := range object.CompositeGraphicUnit.objects() {
|
||||
collect(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, layer := range content.Content.Layer {
|
||||
for _, object := range layer.Objects {
|
||||
collect(object)
|
||||
}
|
||||
}
|
||||
cache[page] = texts
|
||||
}
|
||||
for _, id := range ref.ObjectIDs {
|
||||
values := cache[page][editorResourceID(id)]
|
||||
if len(values) != 1 {
|
||||
valid = false
|
||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "invalid-reference", Message: "invoice text reference is missing or ambiguous", Source: factSource, Field: nodePath})
|
||||
continue
|
||||
}
|
||||
if len(values[0]) > invoiceXMLLimit-total {
|
||||
return fmt.Errorf("invoice extraction limit exceeded")
|
||||
}
|
||||
total += len(values[0])
|
||||
fact.Value += values[0]
|
||||
}
|
||||
}
|
||||
inv.Facts = append(inv.Facts, fact)
|
||||
dst, numeric := invoiceEDRMField(&inv, nil, fieldPath)
|
||||
if dst != nil && valid {
|
||||
value := strings.TrimSpace(fact.Value)
|
||||
if numeric {
|
||||
value = strings.TrimSpace(strings.TrimLeft(value, "¥¥"))
|
||||
if invoiceDecimal(value) == nil {
|
||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "invalid-decimal", Message: "invalid invoice amount", Source: factSource, Field: nodePath})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if assigned[fieldPath] {
|
||||
*dst = ""
|
||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "ambiguous-field", Message: "multiple invoice field references", Source: factSource, Field: nodePath})
|
||||
} else {
|
||||
*dst = value
|
||||
assigned[fieldPath] = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
counts := make(map[string]int)
|
||||
for i := range node.Children {
|
||||
child := &node.Children[i]
|
||||
counts[child.Name]++
|
||||
if err := visit(child, fmt.Sprintf("%s/%s[%d]", nodePath, child.Name, counts[child.Name]), fieldPath+"/"+child.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := visit(structure, "/root[1]", "/eInvoice"); err != nil {
|
||||
return InvoiceReport{}, err
|
||||
}
|
||||
if date, err := time.Parse("2006年01月02日", inv.Date); err == nil {
|
||||
inv.Date = date.Format("2006-01-02")
|
||||
}
|
||||
invoiceAmountCheck(&inv, &report, source)
|
||||
report.Invoices = append(report.Invoices, inv)
|
||||
return report, ctx.Err()
|
||||
}
|
||||
|
||||
// ExportInvoice 导出当前文档及版本声明的发票,内联内容补齐继承的命名空间
|
||||
// 相同选择器对应多个声明时拒绝操作,允许已识别的旧数据带解析诊断
|
||||
// 入参: ctx 取消上下文, source 提取结果中的精确来源
|
||||
@@ -112,6 +265,15 @@ func (e *Editor) changeInvoiceSource(ctx context.Context, source InvoiceSource,
|
||||
if InvoiceDigest(old) != digest {
|
||||
return InvoiceSource{}, fmt.Errorf("invoice source has changed")
|
||||
}
|
||||
if source.Kind == "tag" {
|
||||
root, err := invoiceXMLTree(ctx, old)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
if root.name.Space == ofdNamespace2016 && root.name.Local == "root" && invoiceObjectReferences(root) {
|
||||
return InvoiceSource{}, fmt.Errorf("object reference invoice is read-only")
|
||||
}
|
||||
}
|
||||
if !remove {
|
||||
if len(data) > invoiceXMLLimit {
|
||||
return InvoiceSource{}, fmt.Errorf("invoice data limit exceeded")
|
||||
|
||||
@@ -148,7 +148,7 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
||||
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: true}
|
||||
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
|
||||
@@ -178,7 +178,7 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
||||
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: true}
|
||||
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"
|
||||
}
|
||||
@@ -210,6 +210,7 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
||||
func InvoiceFormats() []InvoiceFormat {
|
||||
result := []InvoiceFormat{{Kind: "einv", Standard: "EInvoice", 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: ofdNamespace2016, Version: "1.0", Read: true})
|
||||
for _, kind := range []string{"inv", "einv", "atr", "rai"} {
|
||||
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"})
|
||||
}
|
||||
@@ -269,7 +270,7 @@ func ValidateInvoice(ctx context.Context, value Invoice) ([]InvoiceIssue, error)
|
||||
return issues, nil
|
||||
}
|
||||
for _, field := range []invoiceWriteField{{"Number", value.Number}, {"Date", value.Date}, {"TotalAmount", value.TotalAmount}} {
|
||||
if strings.TrimSpace(field.value) == "" {
|
||||
if value.Standard != ofdNamespace2016 && strings.TrimSpace(field.value) == "" {
|
||||
add("required", field.name, "missing invoice field")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1062,6 +1062,9 @@ func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSo
|
||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "invalid-value", Field: f.Name, Message: "nil fact contains a value", Source: s})
|
||||
return nil
|
||||
}
|
||||
if numeric && inv.Standard == invoiceEDRM && strings.TrimSpace(f.Value) == "" && !f.Nil {
|
||||
return nil
|
||||
}
|
||||
if numeric && !f.Nil && invoiceDecimal(f.Value) == nil {
|
||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "invalid-decimal", Field: f.Name, Message: "invalid decimal lexical value", Source: s})
|
||||
return nil
|
||||
|
||||
Reference in new issue
Block a user