mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
30f4f76260
commit
2daae08a0f
2 files changed
+133
-15
No files matched your search
+24
-8
@@ -5672,6 +5672,7 @@ async function loadInvoiceSettings(sequence, openSeq) {
|
|||||||
function invoiceDraft(detail, entry, scope) {
|
function invoiceDraft(detail, entry, scope) {
|
||||||
return {
|
return {
|
||||||
detail, invoice: structuredClone(detail.invoice), entry, scope,
|
detail, invoice: structuredClone(detail.invoice), entry, scope,
|
||||||
|
baselineFields: structuredClone(detail.fields || []),
|
||||||
dirty: new Set(), changes: new Map(),
|
dirty: new Set(), changes: new Map(),
|
||||||
addedItems: new Set(),
|
addedItems: new Set(),
|
||||||
bindings: structuredClone(detail.bindings || []), xml: detail.xml || "", page: state.pageIndex, object: "", field: "",
|
bindings: structuredClone(detail.bindings || []), xml: detail.xml || "", page: state.pageIndex, object: "", field: "",
|
||||||
@@ -5972,7 +5973,7 @@ function renderInvoiceItems(container, draft) {
|
|||||||
draft.item = next;
|
draft.item = next;
|
||||||
invoiceDirty(draft, "model");
|
invoiceDirty(draft, "model");
|
||||||
await invoiceRefreshFields(draft);
|
await invoiceRefreshFields(draft);
|
||||||
renderDocumentSettings();
|
if (invoiceCurrent() === draft) renderDocumentSettings();
|
||||||
};
|
};
|
||||||
settingsButton("添加", async () => {
|
settingsButton("添加", async () => {
|
||||||
const item = {};
|
const item = {};
|
||||||
@@ -5981,14 +5982,14 @@ function renderInvoiceItems(container, draft) {
|
|||||||
draft.item = items.length - 1;
|
draft.item = items.length - 1;
|
||||||
invoiceDirty(draft, "model");
|
invoiceDirty(draft, "model");
|
||||||
await invoiceRefreshFields(draft);
|
await invoiceRefreshFields(draft);
|
||||||
renderDocumentSettings();
|
if (invoiceCurrent() === draft) renderDocumentSettings();
|
||||||
}, !editable, "InvoiceItemAdd", actions);
|
}, !editable, "InvoiceItemAdd", actions);
|
||||||
settingsButton("删除", async () => {
|
settingsButton("删除", async () => {
|
||||||
if (draft.changes.size) throw new Error("请先应用原字段修改,再删除明细");
|
if (draft.changes.size) throw new Error("请先应用原字段修改,再删除明细");
|
||||||
items.splice(draft.item, 1);
|
items.splice(draft.item, 1);
|
||||||
invoiceDirty(draft, "model");
|
invoiceDirty(draft, "model");
|
||||||
await invoiceRefreshFields(draft);
|
await invoiceRefreshFields(draft);
|
||||||
renderDocumentSettings();
|
if (invoiceCurrent() === draft) renderDocumentSettings();
|
||||||
}, !editable || !items.length, "InvoiceItemDelete", actions);
|
}, !editable || !items.length, "InvoiceItemDelete", actions);
|
||||||
settingsButton("上移", () => reorder(-1), !editable || draft.item === 0 || !items.length, "InvoiceItemUp", actions);
|
settingsButton("上移", () => reorder(-1), !editable || draft.item === 0 || !items.length, "InvoiceItemUp", actions);
|
||||||
settingsButton("下移", () => reorder(1), !editable || draft.item >= items.length - 1, "InvoiceItemDown", actions);
|
settingsButton("下移", () => reorder(1), !editable || draft.item >= items.length - 1, "InvoiceItemDown", actions);
|
||||||
@@ -5998,7 +5999,7 @@ function renderInvoiceItems(container, draft) {
|
|||||||
async function invoiceRefreshFields(draft) {
|
async function invoiceRefreshFields(draft) {
|
||||||
const invoice = structuredClone(draft.invoice);
|
const invoice = structuredClone(draft.invoice);
|
||||||
const detail = await callWASM("ofdgoInvoice", JSON.stringify({ invoice, ...draft.scope }));
|
const detail = await callWASM("ofdgoInvoice", JSON.stringify({ invoice, ...draft.scope }));
|
||||||
if (invoiceCurrent() === draft && JSON.stringify(draft.invoice) === JSON.stringify(invoice)) draft.detail.fields = detail.fields;
|
if ([...invoicePanel.drafts.values()].includes(draft) && JSON.stringify(draft.invoice) === JSON.stringify(invoice)) draft.detail.fields = detail.fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInvoiceFields(container, draft) {
|
function renderInvoiceFields(container, draft) {
|
||||||
@@ -6206,13 +6207,17 @@ function renderInvoiceAppearance(container, draft) {
|
|||||||
settingsButton("生成", async () => {
|
settingsButton("生成", async () => {
|
||||||
if (draft.dirty.size) throw new Error("请先应用草稿");
|
if (draft.dirty.size) throw new Error("请先应用草稿");
|
||||||
if (!draft.layout.font && !draft.fontChoice?.startsWith("file:")) throw new Error("请选择字体");
|
if (!draft.layout.font && !draft.fontChoice?.startsWith("file:")) throw new Error("请选择字体");
|
||||||
|
const openSeq = state.openSeq, sequence = settings.sequence;
|
||||||
|
const fontChoice = draft.fontChoice, layout = structuredClone(draft.layout);
|
||||||
let fontData;
|
let fontData;
|
||||||
if (draft.fontChoice?.startsWith("file:")) {
|
if (fontChoice?.startsWith("file:")) {
|
||||||
const font = libraryFonts.find(value => `file:${value.id}` === draft.fontChoice);
|
const font = libraryFonts.find(value => `file:${value.id}` === fontChoice);
|
||||||
if (!font) throw new Error("所选字体不可用");
|
if (!font) throw new Error("所选字体不可用");
|
||||||
fontData = invoiceBase64(await readTextFont(font));
|
fontData = invoiceBase64(await readTextFont(font));
|
||||||
}
|
}
|
||||||
await commitInvoice(draft, { operation: "generate", layout: draft.layout, fontData });
|
if (openSeq !== state.openSeq || sequence !== settings.sequence || invoiceCurrent() !== draft || !settingsElements.Panel.open || settingsElements.Kind.value !== "invoices") return;
|
||||||
|
if (draft.dirty.size || draft.fontChoice !== fontChoice || JSON.stringify(draft.layout) !== JSON.stringify(layout)) throw new Error("内容已变更,请重新生成");
|
||||||
|
await commitInvoice(draft, { operation: "generate", layout, fontData });
|
||||||
}, !capabilities.generate, "InvoiceGenerate", details, capabilities.reasons?.generate || (draft.bindings.length ? "请先解除绑定" : ""));
|
}, !capabilities.generate, "InvoiceGenerate", details, capabilities.reasons?.generate || (draft.bindings.length ? "请先解除绑定" : ""));
|
||||||
settingsButton("同步", async () => {
|
settingsButton("同步", async () => {
|
||||||
if (draft.dirty.size) throw new Error("请先应用草稿");
|
if (draft.dirty.size) throw new Error("请先应用草稿");
|
||||||
@@ -6245,6 +6250,11 @@ function resetInvoiceDraft(draft) {
|
|||||||
else if (section === "bindings") draft.bindings = structuredClone(draft.detail.bindings || []);
|
else if (section === "bindings") draft.bindings = structuredClone(draft.detail.bindings || []);
|
||||||
else {
|
else {
|
||||||
draft.invoice = structuredClone(draft.detail.invoice);
|
draft.invoice = structuredClone(draft.detail.invoice);
|
||||||
|
draft.detail.fields = structuredClone(draft.baselineFields);
|
||||||
|
draft.addedItems.clear();
|
||||||
|
draft.item = 0;
|
||||||
|
draft.fact = 0;
|
||||||
|
draft.factOwner = -1;
|
||||||
draft.options = { name: "", encoding: { schemaRef: "", context: {}, units: [] } };
|
draft.options = { name: "", encoding: { schemaRef: "", context: {}, units: [] } };
|
||||||
draft.fragments = {};
|
draft.fragments = {};
|
||||||
}
|
}
|
||||||
@@ -6309,6 +6319,7 @@ async function applyInvoiceDraft(draft) {
|
|||||||
|
|
||||||
function buildInvoiceRequest(draft) {
|
function buildInvoiceRequest(draft) {
|
||||||
if (draft.dirty.has("xml") && draft.dirty.size > 1) throw new Error("原文替换与其他修改冲突,草稿已保留,请先还原其中一项");
|
if (draft.dirty.has("xml") && draft.dirty.size > 1) throw new Error("原文替换与其他修改冲突,草稿已保留,请先还原其中一项");
|
||||||
|
if (draft.dirty.has("model") && draft.dirty.has("fields")) throw new Error("基本、明细与原字段不能同时应用,请先还原一项");
|
||||||
const section = draft.dirty.has("xml") ? "xml" : "model";
|
const section = draft.dirty.has("xml") ? "xml" : "model";
|
||||||
const request = { synchronize: draft.synchronize };
|
const request = { synchronize: draft.synchronize };
|
||||||
if (section === "xml") {
|
if (section === "xml") {
|
||||||
@@ -6338,6 +6349,10 @@ function invoiceBase64(bytes) {
|
|||||||
|
|
||||||
async function commitInvoice(draft, request) {
|
async function commitInvoice(draft, request) {
|
||||||
if (!state.editing || settings.busy || document.body.hasAttribute("aria-busy")) return;
|
if (!state.editing || settings.busy || document.body.hasAttribute("aria-busy")) return;
|
||||||
|
if (!settingsElements.Panel.open || settingsElements.Kind.value !== "invoices" || invoiceCurrent() !== draft) return;
|
||||||
|
const scope = invoiceScope();
|
||||||
|
if (draft.scope.documentIndex !== scope.documentIndex || draft.scope.version !== scope.version || draft.scope.revision !== scope.revision) throw new Error("文档已变更,请重新打开配置");
|
||||||
|
const key = invoicePanel.key;
|
||||||
settings.busy = true;
|
settings.busy = true;
|
||||||
settingsElements.Form.inert = true;
|
settingsElements.Form.inert = true;
|
||||||
settingsElements.Status.textContent = "正在保存修改";
|
settingsElements.Status.textContent = "正在保存修改";
|
||||||
@@ -6348,7 +6363,7 @@ async function commitInvoice(draft, request) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = invoicePanel.result;
|
const result = invoicePanel.result;
|
||||||
invoicePanel.drafts.delete(invoicePanel.key);
|
if (invoicePanel.drafts.get(key) === draft) invoicePanel.drafts.delete(key);
|
||||||
const currentScope = invoiceScope();
|
const currentScope = invoiceScope();
|
||||||
for (const [key, value] of invoicePanel.drafts) {
|
for (const [key, value] of invoicePanel.drafts) {
|
||||||
if (value.scope.documentIndex !== currentScope.documentIndex || value.scope.version !== currentScope.version) continue;
|
if (value.scope.documentIndex !== currentScope.documentIndex || value.scope.version !== currentScope.version) continue;
|
||||||
@@ -6365,6 +6380,7 @@ async function commitInvoice(draft, request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!settingsElements.Panel.open || settingsElements.Kind.value !== "invoices" || invoicePanel.key !== key) return;
|
||||||
if (result?.source?.kind) invoicePanel.key = JSON.stringify([invoiceSourceKey(result.source), draft.entry?.index || 0]);
|
if (result?.source?.kind) invoicePanel.key = JSON.stringify([invoiceSourceKey(result.source), draft.entry?.index || 0]);
|
||||||
await loadDocumentSettings();
|
await loadDocumentSettings();
|
||||||
const warning = invoiceIssues(result?.issues);
|
const warning = invoiceIssues(result?.issues);
|
||||||
|
|||||||
+109
-7
@@ -15,10 +15,13 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -127,7 +130,10 @@ func (r *Reader) InvoiceCapabilities(ctx context.Context, source InvoiceSource,
|
|||||||
result.Sync = false
|
result.Sync = false
|
||||||
result.Generate = false
|
result.Generate = false
|
||||||
result.Bind = false
|
result.Bind = false
|
||||||
for _, operation := range []string{"sync", "generate", "bind"} {
|
result.Edit = false
|
||||||
|
result.Delete = false
|
||||||
|
result.Items = false
|
||||||
|
for _, operation := range []string{"edit", "delete", "items", "sync", "generate", "bind"} {
|
||||||
result.Reasons[operation] = "票面关联损坏,请先修复关联"
|
result.Reasons[operation] = "票面关联损坏,请先修复关联"
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -333,7 +339,7 @@ func (e *Editor) UpdateInvoice(ctx context.Context, source InvoiceSource, digest
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
previous, err := parseInvoiceXML(ctx, original, source)
|
previous, err := parseInvoiceXML(ctx, original, InvoiceSource{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -353,6 +359,16 @@ func (e *Editor) UpdateInvoice(ctx context.Context, source InvoiceSource, digest
|
|||||||
if synchronize && len(entries) == 0 {
|
if synchronize && len(entries) == 0 {
|
||||||
return fmt.Errorf("invoice has no appearance bindings")
|
return fmt.Errorf("invoice has no appearance bindings")
|
||||||
}
|
}
|
||||||
|
pages := make(map[string]string)
|
||||||
|
for _, entry := range entries {
|
||||||
|
for _, binding := range entry.bindings {
|
||||||
|
page, err := next.Page(binding.Page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pages[fmt.Sprintf("%d/%s", entry.index, binding.ObjectID)] = page.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
updated, err := next.ReplaceInvoiceData(ctx, source, digest, data)
|
updated, err := next.ReplaceInvoiceData(ctx, source, digest, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -374,11 +390,24 @@ func (e *Editor) UpdateInvoice(ctx context.Context, source InvoiceSource, digest
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if synchronize {
|
if entry.record.Invoice >= len(previous.Invoices) {
|
||||||
if entry.record.Invoice >= len(previous.Invoices) {
|
return fmt.Errorf("bound invoice no longer exists")
|
||||||
return fmt.Errorf("bound invoice no longer exists")
|
}
|
||||||
|
originalBindings := slices.Clone(entry.bindings)
|
||||||
|
entry.bindings, err = invoiceRemapBindings(previous.Invoices[entry.record.Invoice], report.Invoices[entry.record.Invoice], entry.bindings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range entry.bindings {
|
||||||
|
id := pages[fmt.Sprintf("%d/%s", entry.index, entry.bindings[i].ObjectID)]
|
||||||
|
page := slices.IndexFunc(next.pages, func(page PageContent) bool { return sameResourceID(page.ID, id) })
|
||||||
|
if page < 0 {
|
||||||
|
return fmt.Errorf("invoice appearance page no longer exists")
|
||||||
}
|
}
|
||||||
if err := invoiceBindingCoverage(previous.Invoices[entry.record.Invoice], report.Invoices[entry.record.Invoice], entry.bindings); err != nil {
|
entry.bindings[i].Page = page
|
||||||
|
}
|
||||||
|
if synchronize {
|
||||||
|
if err := invoiceBindingCoverage(previous.Invoices[entry.record.Invoice], report.Invoices[entry.record.Invoice], originalBindings); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := next.SyncInvoicePages(ctx, report.Invoices[entry.record.Invoice], entry.bindings); err != nil {
|
if err := next.SyncInvoicePages(ctx, report.Invoices[entry.record.Invoice], entry.bindings); err != nil {
|
||||||
@@ -389,7 +418,29 @@ func (e *Editor) UpdateInvoice(ctx context.Context, source InvoiceSource, digest
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := next.UpdateStructureNode(entry.index, nil, StructureNode{Name: "Invoice", Namespace: invoiceNamespace, Text: string(encoded)}); err != nil {
|
if err := next.editStructure(entry.index, func(data []byte, root *editorXML, _ CustomTag) ([]byte, error) {
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
for i, binding := range entry.bindings {
|
||||||
|
if binding.Field == originalBindings[i].Field {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
patch, err := editorXMLAttributePatch(data, root.children[i], "Name", binding.Field)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
patches = append(patches, patch)
|
||||||
|
}
|
||||||
|
data = editorPatchXML(data, patches)
|
||||||
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var text bytes.Buffer
|
||||||
|
if err := xml.EscapeText(&text, encoded); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorXMLDirectText(data, root, text.Bytes())
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -943,6 +994,57 @@ func invoicePayloadSignature(ctx context.Context, data []byte) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invoiceRemapBindings 按唯一事实身份维护索引关联,不限制未关联业务字段的修改
|
||||||
|
// 入参: before 原发票, after 新发票, bindings 原关联
|
||||||
|
// 返回: []InvoiceBinding 更新后的关联, error 身份缺失或歧义错误
|
||||||
|
func invoiceRemapBindings(before, after Invoice, bindings []InvoiceBinding) ([]InvoiceBinding, error) {
|
||||||
|
result := slices.Clone(bindings)
|
||||||
|
for i := range result {
|
||||||
|
binding := &result[i]
|
||||||
|
oldFacts, newFacts, prefix := before.Facts, after.Facts, "facts."
|
||||||
|
if strings.HasPrefix(binding.Field, "items.") {
|
||||||
|
if binding.Item < 0 || binding.Item >= len(before.Items) || binding.Item >= len(after.Items) {
|
||||||
|
return nil, fmt.Errorf("bound invoice item no longer exists")
|
||||||
|
}
|
||||||
|
if len(before.Items) != len(after.Items) && !reflect.DeepEqual(before.Items[binding.Item], after.Items[binding.Item]) {
|
||||||
|
return nil, fmt.Errorf("bound invoice item identity changed")
|
||||||
|
}
|
||||||
|
oldFacts, newFacts, prefix = before.Items[binding.Item].Fields, after.Items[binding.Item].Fields, "items.fields."
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(binding.Field, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index, err := strconv.Atoi(strings.TrimPrefix(binding.Field, prefix))
|
||||||
|
if err != nil || index < 0 || index >= len(oldFacts) {
|
||||||
|
return nil, fmt.Errorf("bound invoice fact no longer exists")
|
||||||
|
}
|
||||||
|
fact := oldFacts[index]
|
||||||
|
identity := func(candidate InvoiceFact) bool {
|
||||||
|
return candidate.Name == fact.Name && candidate.Namespace == fact.Namespace && candidate.Context == fact.Context && candidate.Unit == fact.Unit && slices.Equal(candidate.Dimensions, fact.Dimensions)
|
||||||
|
}
|
||||||
|
oldCount, match, count := 0, -1, 0
|
||||||
|
for _, candidate := range oldFacts {
|
||||||
|
if identity(candidate) {
|
||||||
|
oldCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j, candidate := range newFacts {
|
||||||
|
if identity(candidate) {
|
||||||
|
count++
|
||||||
|
match = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if oldCount != 1 || count != 1 || fact.Source.NodePath == "" || newFacts[match].Source.NodePath != fact.Source.NodePath {
|
||||||
|
if reflect.DeepEqual(oldFacts, newFacts) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("bound invoice fact identity is missing or ambiguous")
|
||||||
|
}
|
||||||
|
binding.Field = fmt.Sprintf("%s%d", prefix, match)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// invoiceBindingCoverage 拒绝将未关联的字段修改误报为已同步,不猜测缺失图元
|
// invoiceBindingCoverage 拒绝将未关联的字段修改误报为已同步,不猜测缺失图元
|
||||||
// 入参: before 原发票, after 新发票, bindings 显式关联
|
// 入参: before 原发票, after 新发票, bindings 显式关联
|
||||||
// 返回: error 未绑定字段错误
|
// 返回: error 未绑定字段错误
|
||||||
|
|||||||
Reference in new issue
Block a user