mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
1 parent
3709a62c24
commit
1a9059429e
10 files changed
+928
-64
No files matched your search
+55
-15
@@ -29,6 +29,7 @@ const settingsElements = Object.fromEntries(["Button", "Panel", "Form", "Title",
|
|||||||
const settings = { sequence: 0, busy: false, page: 0, scope: "document", object: "", path: [], selected: -1, data: null, actions: [], drafts: new Map(), view: "", baseline: {}, suspended: false, invoice: false };
|
const settings = { sequence: 0, busy: false, page: 0, scope: "document", object: "", path: [], selected: -1, data: null, actions: [], drafts: new Map(), view: "", baseline: {}, suspended: false, invoice: false };
|
||||||
const invoicePanel = { tab: "basic", entries: [], formats: [], drafts: new Map(), key: "", result: null };
|
const invoicePanel = { tab: "basic", entries: [], formats: [], drafts: new Map(), key: "", result: null };
|
||||||
const invoiceDocuments = new WeakMap();
|
const invoiceDocuments = new WeakMap();
|
||||||
|
const invoiceReferenceStandard = "http://www.ofdspec.org/2016";
|
||||||
|
|
||||||
let wasmPromise = null;
|
let wasmPromise = null;
|
||||||
let remoteDownload = null;
|
let remoteDownload = null;
|
||||||
@@ -5827,7 +5828,7 @@ function renderInvoiceSettings() {
|
|||||||
async function newInvoiceDraft() {
|
async function newInvoiceDraft() {
|
||||||
if (!state.editing) return;
|
if (!state.editing) return;
|
||||||
if (invoicePanel.drafts.get("new")?.dirty.size && !window.confirm("放弃尚未应用的新建发票?")) return;
|
if (invoicePanel.drafts.get("new")?.dirty.size && !window.confirm("放弃尚未应用的新建发票?")) return;
|
||||||
const format = invoicePanel.formats.find(value => value.create);
|
const format = invoicePanel.formats.find(value => value.create && value.standard !== invoiceReferenceStandard) || invoicePanel.formats.find(value => value.create);
|
||||||
if (!format) throw new Error("暂无可新建票种");
|
if (!format) throw new Error("暂无可新建票种");
|
||||||
const invoice = { kind: format.kind, standard: format.standard, version: format.version, buyer: {}, seller: {}, items: [], facts: [] };
|
const invoice = { kind: format.kind, standard: format.standard, version: format.version, buyer: {}, seller: {}, items: [], facts: [] };
|
||||||
const scope = invoiceScope();
|
const scope = invoiceScope();
|
||||||
@@ -5860,12 +5861,16 @@ function renderInvoiceBasic(container, draft) {
|
|||||||
invoiceInput(grid, labels[field.field.split(".").at(-1)] || field.label, `Field_${field.field.replaceAll(".", "_")}`, display, value => {
|
invoiceInput(grid, labels[field.field.split(".").at(-1)] || field.label, `Field_${field.field.replaceAll(".", "_")}`, display, value => {
|
||||||
invoiceSetValue(draft.invoice, field.field, -1, value);
|
invoiceSetValue(draft.invoice, field.field, -1, value);
|
||||||
invoiceDirty(draft, "model");
|
invoiceDirty(draft, "model");
|
||||||
}, !state.editing || !(field.editable || !draft.entry && field.field === "currency" && draft.invoice.standard !== "EInvoice") || !!draft.entry && !draft.detail.capabilities?.edit).title = display !== value ? value : field.label;
|
}, !state.editing || !(field.editable || !draft.entry && field.field === "currency" && invoiceIsXBRL(draft.invoice)) || !!draft.entry && !draft.detail.capabilities?.edit).title = display !== value ? value : field.label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!draft.entry) renderInvoiceAdvanced(container, draft);
|
if (!draft.entry) renderInvoiceAdvanced(container, draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invoiceIsXBRL(invoice) {
|
||||||
|
return invoice.standard?.startsWith("http://xbrl.mof.gov.cn/taxonomy/") || false;
|
||||||
|
}
|
||||||
|
|
||||||
function renderInvoiceAdvanced(container, draft) {
|
function renderInvoiceAdvanced(container, draft) {
|
||||||
const details = document.createElement("details"), summary = document.createElement("summary");
|
const details = document.createElement("details"), summary = document.createElement("summary");
|
||||||
summary.textContent = "高级";
|
summary.textContent = "高级";
|
||||||
@@ -5876,9 +5881,10 @@ function renderInvoiceAdvanced(container, draft) {
|
|||||||
container.append(details);
|
container.append(details);
|
||||||
const formats = invoicePanel.formats.filter(value => value.create);
|
const formats = invoicePanel.formats.filter(value => value.create);
|
||||||
const selected = formats.findIndex(value => value.kind === draft.invoice.kind && value.standard === draft.invoice.standard && value.version === draft.invoice.version);
|
const selected = formats.findIndex(value => value.kind === draft.invoice.kind && value.standard === draft.invoice.standard && value.version === draft.invoice.version);
|
||||||
settingsSelect(details, "票种", "InvoiceFormat", formats.map((value, index) => [index, `${{ inv: "增值税", einv: "电子票", atr: "航空票", rai: "铁路票" }[value.kind] || value.kind} · ${value.version} · ${value.standard === "EInvoice" ? "XML" : "XBRL"}`]), selected, async value => {
|
settingsSelect(details, "票种", "InvoiceFormat", formats.map((value, index) => [index, `${{ inv: "增值税", einv: "电子票", atr: "航空票", rai: "铁路票" }[value.kind] || value.kind} · ${value.version} · ${value.standard === invoiceReferenceStandard ? "标引" : invoiceIsXBRL(value) ? "XBRL" : "XML"}`]), selected, async value => {
|
||||||
try {
|
try {
|
||||||
const format = formats[Number(value)];
|
const format = formats[Number(value)];
|
||||||
|
if (draft.invoice.standard !== format.standard) draft.options.encoding.schemaRef = "";
|
||||||
Object.assign(draft.invoice, { kind: format.kind, standard: format.standard, version: format.version });
|
Object.assign(draft.invoice, { kind: format.kind, standard: format.standard, version: format.version });
|
||||||
invoiceDirty(draft, "model");
|
invoiceDirty(draft, "model");
|
||||||
await invoiceRefreshFields(draft);
|
await invoiceRefreshFields(draft);
|
||||||
@@ -5889,7 +5895,7 @@ function renderInvoiceAdvanced(container, draft) {
|
|||||||
});
|
});
|
||||||
invoiceInput(details, "名称", "Name", draft.options.name, value => { draft.options.name = value; invoiceDirty(draft, "model"); });
|
invoiceInput(details, "名称", "Name", draft.options.name, value => { draft.options.name = value; invoiceDirty(draft, "model"); });
|
||||||
const encoding = draft.options.encoding;
|
const encoding = draft.options.encoding;
|
||||||
if (draft.invoice.standard !== "EInvoice") {
|
if (invoiceIsXBRL(draft.invoice)) {
|
||||||
invoiceInput(details, "模式", "SchemaRef", encoding.schemaRef, value => { encoding.schemaRef = value; invoiceDirty(draft, "model"); }).title = "schemaRef";
|
invoiceInput(details, "模式", "SchemaRef", encoding.schemaRef, value => { encoding.schemaRef = value; invoiceDirty(draft, "model"); }).title = "schemaRef";
|
||||||
for (const [key, label] of [["id", "标识"], ["identifier", "主体"], ["scheme", "方案"], ["instant", "时点"], ["startDate", "起日"], ["endDate", "止日"]]) {
|
for (const [key, label] of [["id", "标识"], ["identifier", "主体"], ["scheme", "方案"], ["instant", "时点"], ["startDate", "起日"], ["endDate", "止日"]]) {
|
||||||
invoiceInput(details, label, `Context_${key}`, encoding.context[key], value => { encoding.context[key] = value; invoiceDirty(draft, "model"); }).title = `context.${key}`;
|
invoiceInput(details, label, `Context_${key}`, encoding.context[key], value => { encoding.context[key] = value; invoiceDirty(draft, "model"); }).title = `context.${key}`;
|
||||||
@@ -5913,7 +5919,7 @@ function renderInvoiceAdvanced(container, draft) {
|
|||||||
renderDocumentSettings();
|
renderDocumentSettings();
|
||||||
}, false, "InvoiceUnitAdd", details);
|
}, false, "InvoiceUnitAdd", details);
|
||||||
}
|
}
|
||||||
for (const [key, label] of [["headerXML", "票头"], ["sellerAuthenticationXML", "认证"], ["taxSupervisionXML", "监制"], ["taxBureauSignatureXML", "签名"]]) {
|
for (const [key, label] of draft.invoice.standard === "EInvoice" ? [["headerXML", "票头"], ["sellerAuthenticationXML", "认证"], ["taxSupervisionXML", "监制"], ["taxBureauSignatureXML", "签名"]] : []) {
|
||||||
invoiceInput(details, label, `Fragment_${key}`, draft.fragments[key], value => {
|
invoiceInput(details, label, `Fragment_${key}`, draft.fragments[key], value => {
|
||||||
draft.fragments[key] = value;
|
draft.fragments[key] = value;
|
||||||
invoiceDirty(draft, "model");
|
invoiceDirty(draft, "model");
|
||||||
@@ -6023,7 +6029,7 @@ function renderInvoiceFields(container, draft) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const options = facts.map(({ fact, item }, index) => [index, `${item >= 0 ? `明细${item + 1} · ` : ""}${fact.name || "未命名"}${fact.context ? ` · ${fact.context}` : ""} · ${index + 1}`]);
|
const options = facts.map(({ fact, item }, index) => [index, `${item >= 0 ? `明细${item + 1} · ` : ""}${fact.name || "未命名"}${fact.context ? ` · ${fact.context}` : ""} · ${index + 1}`]);
|
||||||
const owners = [...(!draft.entry ? [[-1, "基本"]] : []), ...(draft.invoice.items || []).flatMap((item, index) => !draft.entry || draft.addedItems.has(item) ? [[index, `明细${index + 1}`]] : [])];
|
const owners = [...(!draft.entry && draft.invoice.standard !== invoiceReferenceStandard ? [[-1, "基本"]] : []), ...(draft.invoice.items || []).flatMap((item, index) => !draft.entry || draft.addedItems.has(item) ? [[index, `明细${index + 1}`]] : [])];
|
||||||
if (state.editing && owners.length) {
|
if (state.editing && owners.length) {
|
||||||
if (!owners.some(([index]) => index === draft.factOwner)) draft.factOwner = owners[0][0];
|
if (!owners.some(([index]) => index === draft.factOwner)) draft.factOwner = owners[0][0];
|
||||||
settingsSelect(container, "归属", "InvoiceFactOwner", owners, draft.factOwner, value => { draft.factOwner = Number(value); });
|
settingsSelect(container, "归属", "InvoiceFactOwner", owners, draft.factOwner, value => { draft.factOwner = Number(value); });
|
||||||
@@ -6103,8 +6109,8 @@ function renderInvoiceFields(container, draft) {
|
|||||||
invoiceDirty(draft, "fields");
|
invoiceDirty(draft, "fields");
|
||||||
};
|
};
|
||||||
invoiceInput(container, "数值", "FactValue", change?.value ?? fact.value, value => update("value", value), !writable, "textarea");
|
invoiceInput(container, "数值", "FactValue", change?.value ?? fact.value, value => update("value", value), !writable, "textarea");
|
||||||
invoiceInput(container, "空值", "FactNil", change?.nil ?? fact.nil, value => update("nil", value), !writable, "checkbox");
|
invoiceInput(container, "空值", "FactNil", change?.nil ?? fact.nil, value => update("nil", value), !writable || draft.invoice.standard === invoiceReferenceStandard, "checkbox");
|
||||||
if (!fresh) invoiceInput(container, "删除", "FactDelete", change?.delete || false, value => update("delete", value), !writable, "checkbox");
|
if (!fresh) invoiceInput(container, "删除", "FactDelete", change?.delete || false, value => update("delete", value), !writable || draft.invoice.standard === invoiceReferenceStandard, "checkbox");
|
||||||
else if (state.editing) settingsButton("删除", () => {
|
else if (state.editing) settingsButton("删除", () => {
|
||||||
const index = Number(field.split(".").at(-1));
|
const index = Number(field.split(".").at(-1));
|
||||||
(item < 0 ? draft.invoice.facts : draft.invoice.items[item].fields).splice(index, 1);
|
(item < 0 ? draft.invoice.facts : draft.invoice.items[item].fields).splice(index, 1);
|
||||||
@@ -6115,6 +6121,7 @@ function renderInvoiceFields(container, draft) {
|
|||||||
|
|
||||||
function renderInvoiceAppearance(container, draft) {
|
function renderInvoiceAppearance(container, draft) {
|
||||||
const capabilities = draft.detail.capabilities || {};
|
const capabilities = draft.detail.capabilities || {};
|
||||||
|
const creatingReference = !draft.entry && draft.invoice.standard === invoiceReferenceStandard;
|
||||||
const writable = state.editing && !!draft.entry;
|
const writable = state.editing && !!draft.entry;
|
||||||
const fields = (draft.detail.fields || []).filter(field => field.bindable);
|
const fields = (draft.detail.fields || []).filter(field => field.bindable);
|
||||||
const choices = fields.map(field => [JSON.stringify([field.field, field.item]), `${field.item >= 0 ? `明细${field.item + 1} · ` : ""}${field.label}`]);
|
const choices = fields.map(field => [JSON.stringify([field.field, field.item]), `${field.item >= 0 ? `明细${field.item + 1} · ` : ""}${field.label}`]);
|
||||||
@@ -6179,14 +6186,14 @@ function renderInvoiceAppearance(container, draft) {
|
|||||||
draft.bindings = draft.bindings.filter(binding => !(JSON.stringify([binding.field, binding.item]) === draft.field && binding.page === draft.page && binding.objectId === draft.object));
|
draft.bindings = draft.bindings.filter(binding => !(JSON.stringify([binding.field, binding.item]) === draft.field && binding.page === draft.page && binding.objectId === draft.object));
|
||||||
invoiceDirty(draft, "bindings");
|
invoiceDirty(draft, "bindings");
|
||||||
renderDocumentSettings();
|
renderDocumentSettings();
|
||||||
}, !writable || !bound.length, "InvoiceUnbind", actions);
|
}, !writable || !capabilities.bind || !bound.length, "InvoiceUnbind", actions);
|
||||||
settingsButton("清空", () => {
|
settingsButton("清空", () => {
|
||||||
draft.bindings = [];
|
draft.bindings = [];
|
||||||
invoiceDirty(draft, "bindings");
|
invoiceDirty(draft, "bindings");
|
||||||
renderDocumentSettings();
|
renderDocumentSettings();
|
||||||
}, !writable || !draft.bindings.length, "InvoiceUnbindAll", actions);
|
}, !writable || !capabilities.bind || !draft.bindings.length, "InvoiceUnbindAll", actions);
|
||||||
}
|
}
|
||||||
if (writable) {
|
if (writable || state.editing && creatingReference) {
|
||||||
const details = document.createElement("details"), summary = document.createElement("summary");
|
const details = document.createElement("details"), summary = document.createElement("summary");
|
||||||
summary.textContent = "生成";
|
summary.textContent = "生成";
|
||||||
details.className = "invoice-advanced";
|
details.className = "invoice-advanced";
|
||||||
@@ -6199,6 +6206,7 @@ function renderInvoiceAppearance(container, draft) {
|
|||||||
settingsSelect(details, "字体", "InvoiceFont", [["", "选择"], ...fonts.map(font => [font.id, font.fontName || font.familyName || font.id]), ...libraryFonts.map(font => [`file:${font.id}`, font.fullName || font.name])], draft.fontChoice || draft.layout.font, value => {
|
settingsSelect(details, "字体", "InvoiceFont", [["", "选择"], ...fonts.map(font => [font.id, font.fontName || font.familyName || font.id]), ...libraryFonts.map(font => [`file:${font.id}`, font.fullName || font.name])], draft.fontChoice || draft.layout.font, value => {
|
||||||
draft.fontChoice = value;
|
draft.fontChoice = value;
|
||||||
draft.layout.font = value.startsWith("file:") ? "" : value;
|
draft.layout.font = value.startsWith("file:") ? "" : value;
|
||||||
|
if (creatingReference) invoiceDirty(draft, "model");
|
||||||
renderDocumentSettings();
|
renderDocumentSettings();
|
||||||
});
|
});
|
||||||
settingsButton("添加", () => openFontFile(el.fontInput), false, "InvoiceAddFont", details);
|
settingsButton("添加", () => openFontFile(el.fontInput), false, "InvoiceAddFont", details);
|
||||||
@@ -6221,7 +6229,7 @@ function renderInvoiceAppearance(container, draft) {
|
|||||||
else refresh();
|
else refresh();
|
||||||
}, !draft.layout.font || !!fonts.find(font => font.id === draft.layout.font)?.embedded, "InvoiceEmbedFont", details);
|
}, !draft.layout.font || !!fonts.find(font => font.id === draft.layout.font)?.embedded, "InvoiceEmbedFont", details);
|
||||||
for (const [key, label] of [["fontSize", "字号"], ["pageWidth", "页宽"], ["pageHeight", "页高"], ["margin", "边距"]]) {
|
for (const [key, label] of [["fontSize", "字号"], ["pageWidth", "页宽"], ["pageHeight", "页高"], ["margin", "边距"]]) {
|
||||||
const input = invoiceInput(details, label, `Layout_${key}`, draft.layout[key], value => { draft.layout[key] = Number(value); }, false, "number", "mm");
|
const input = invoiceInput(details, label, `Layout_${key}`, draft.layout[key], value => { draft.layout[key] = Number(value); if (creatingReference) invoiceDirty(draft, "model"); }, false, "number", "mm");
|
||||||
input.min = key === "margin" ? "0" : "0.01";
|
input.min = key === "margin" ? "0" : "0.01";
|
||||||
}
|
}
|
||||||
settingsButton("生成", async () => {
|
settingsButton("生成", async () => {
|
||||||
@@ -6321,6 +6329,7 @@ function readInvoiceFile(accept) {
|
|||||||
|
|
||||||
async function validateInvoiceDraft(draft) {
|
async function validateInvoiceDraft(draft) {
|
||||||
const request = draft.dirty.size ? buildInvoiceRequest(draft) : { invoice: draft.invoice };
|
const request = draft.dirty.size ? buildInvoiceRequest(draft) : { invoice: draft.invoice };
|
||||||
|
if (!await prepareInvoiceRequest(draft, request)) return;
|
||||||
const result = await callWASM("ofdgoValidateInvoice", JSON.stringify({ ...draft.scope, source: draft.entry?.source, index: draft.entry?.index, digest: draft.detail.digest, ...request }));
|
const result = await callWASM("ofdgoValidateInvoice", JSON.stringify({ ...draft.scope, source: draft.entry?.source, index: draft.entry?.index, digest: draft.detail.digest, ...request }));
|
||||||
if (invoiceCurrent() !== draft) return;
|
if (invoiceCurrent() !== draft) return;
|
||||||
const appearance = result.appearance || [];
|
const appearance = result.appearance || [];
|
||||||
@@ -6334,21 +6343,52 @@ async function validateInvoiceDraft(draft) {
|
|||||||
async function applyInvoiceDraft(draft, synchronize = draft.synchronize) {
|
async function applyInvoiceDraft(draft, synchronize = draft.synchronize) {
|
||||||
if (!state.editing || !draft.dirty.size) return;
|
if (!state.editing || !draft.dirty.size) return;
|
||||||
const request = buildInvoiceRequest(draft);
|
const request = buildInvoiceRequest(draft);
|
||||||
request.synchronize = synchronize;
|
request.synchronize = draft.invoice.standard === invoiceReferenceStandard || synchronize;
|
||||||
|
if (!await prepareInvoiceRequest(draft, request)) return;
|
||||||
if (request.operation === "replace" && !window.confirm("替换此来源中的全部发票原文?" + invoiceOtherDraftWarning(draft))) return;
|
if (request.operation === "replace" && !window.confirm("替换此来源中的全部发票原文?" + invoiceOtherDraftWarning(draft))) return;
|
||||||
await commitInvoice(draft, request);
|
await commitInvoice(draft, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function prepareInvoiceRequest(draft, request) {
|
||||||
|
if (request.operation !== "create" || draft.invoice.standard !== invoiceReferenceStandard) return true;
|
||||||
|
if (!draft.layout.font && !draft.fontChoice?.startsWith("file:")) {
|
||||||
|
invoicePanel.tab = "appearance";
|
||||||
|
draft.layoutOpen = true;
|
||||||
|
renderDocumentSettings();
|
||||||
|
throw new Error("请选择标引票面字体");
|
||||||
|
}
|
||||||
|
const sequence = settings.sequence, openSeq = state.openSeq;
|
||||||
|
const snapshot = JSON.stringify(request), fontChoice = draft.fontChoice;
|
||||||
|
settingsElements.Form.inert = true;
|
||||||
|
try {
|
||||||
|
if (fontChoice?.startsWith("file:")) {
|
||||||
|
const font = fontManager.editorFonts().find(value => `file:${value.id}` === fontChoice && !value.disabled);
|
||||||
|
if (!font) throw new Error("所选字体不可用");
|
||||||
|
request.fontData = invoiceBase64(await readTextFont(font));
|
||||||
|
}
|
||||||
|
if (sequence !== settings.sequence || openSeq !== state.openSeq || invoiceCurrent() !== draft || !settingsElements.Panel.open || !settings.invoice) return false;
|
||||||
|
if (draft.fontChoice !== fontChoice || JSON.stringify(buildInvoiceRequest(draft)) !== snapshot) throw new Error("内容已变更,请重新应用");
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
if (sequence === settings.sequence) settingsElements.Form.inert = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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("基本、明细与原字段不能同时应用,请先还原一项");
|
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.invoice.standard === invoiceReferenceStandard || draft.synchronize };
|
||||||
if (section === "xml") {
|
if (section === "xml") {
|
||||||
Object.assign(request, { operation: draft.entry ? "replace" : "import", xml: draft.xml, options: { name: draft.options.name } });
|
Object.assign(request, { operation: draft.entry ? "replace" : "import", xml: draft.xml, options: { name: draft.options.name } });
|
||||||
} else {
|
} else {
|
||||||
const options = structuredClone(draft.options);
|
const options = structuredClone(draft.options);
|
||||||
for (const [key, text] of Object.entries(draft.fragments)) {
|
if (!invoiceIsXBRL(draft.invoice)) options.encoding = {};
|
||||||
|
if (!draft.entry && draft.invoice.standard === invoiceReferenceStandard) {
|
||||||
|
delete options.encoding;
|
||||||
|
options.layout = structuredClone(draft.layout);
|
||||||
|
}
|
||||||
|
for (const [key, text] of draft.invoice.standard === "EInvoice" ? Object.entries(draft.fragments) : []) {
|
||||||
options.encoding[key] = invoiceBase64(new TextEncoder().encode(text));
|
options.encoding[key] = invoiceBase64(new TextEncoder().encode(text));
|
||||||
}
|
}
|
||||||
if (!draft.entry) Object.assign(request, { operation: "create", invoice: draft.invoice, options });
|
if (!draft.entry) Object.assign(request, { operation: "create", invoice: draft.invoice, options });
|
||||||
|
|||||||
@@ -1186,6 +1186,27 @@ func changeDocumentInvoice(args []js.Value) (any, error) {
|
|||||||
return invoiceEditorInfo{editorInfo: info.(editorInfo), InvoiceResult: result}, nil
|
return invoiceEditorInfo{editorInfo: info.(editorInfo), InvoiceResult: result}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepareInvoiceLayout 在发票创建事务内导入票面字体,不修改调用方布局
|
||||||
|
// 入参: ctx 取消上下文, editor 事务编辑器, request 发票请求
|
||||||
|
// 返回: error 字体处理错误
|
||||||
|
func prepareInvoiceLayout(ctx context.Context, editor *ofdgo.Editor, request *invoiceRequest) error {
|
||||||
|
if request.Options.Layout == nil || len(request.FontData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
layout := *request.Options.Layout
|
||||||
|
request.Options.Layout = &layout
|
||||||
|
file := ofdgo.FontFile{Data: request.FontData}
|
||||||
|
if layout.Font != "" {
|
||||||
|
return editor.EmbedFont(ctx, layout.Font, file, 0)
|
||||||
|
}
|
||||||
|
id, err := editor.AddFont(file, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
layout.Font = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// applyInvoiceRequest 校验来源摘要并执行发票操作
|
// applyInvoiceRequest 校验来源摘要并执行发票操作
|
||||||
// 入参: ctx 取消上下文, editor 事务编辑器, request 操作参数
|
// 入参: ctx 取消上下文, editor 事务编辑器, request 操作参数
|
||||||
// 返回: ofdgo.InvoiceUpdateResult 提交结果, error 校验或写入错误
|
// 返回: ofdgo.InvoiceUpdateResult 提交结果, error 校验或写入错误
|
||||||
@@ -1211,6 +1232,9 @@ func applyInvoiceRequest(ctx context.Context, editor *ofdgo.Editor, request invo
|
|||||||
case "create", "import":
|
case "create", "import":
|
||||||
var source ofdgo.InvoiceSource
|
var source ofdgo.InvoiceSource
|
||||||
var err error
|
var err error
|
||||||
|
if err = prepareInvoiceLayout(ctx, editor, &request); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if request.Operation == "create" {
|
if request.Operation == "create" {
|
||||||
source, err = editor.CreateInvoice(ctx, request.Invoice, request.Options)
|
source, err = editor.CreateInvoice(ctx, request.Invoice, request.Options)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+83
-5
@@ -103,8 +103,21 @@ func (r *Reader) InvoiceCapabilities(ctx context.Context, source InvoiceSource,
|
|||||||
return result, fmt.Errorf("invoice index out of range")
|
return result, fmt.Errorf("invoice index out of range")
|
||||||
}
|
}
|
||||||
if report.Invoices[index].Standard == ofdNamespace2016 {
|
if report.Invoices[index].Standard == ofdNamespace2016 {
|
||||||
for _, operation := range []string{"create", "edit", "items", "bind", "generate", "sync", "delete"} {
|
result.Create = true
|
||||||
result.Reasons[operation] = "对象引用标引仅支持读取和导出"
|
result.Delete = true
|
||||||
|
for _, operation := range []string{"items", "bind", "generate"} {
|
||||||
|
result.Reasons[operation] = "对象引用依赖已有图元,不定义从零业务创建、明细分组或独立票面绑定"
|
||||||
|
}
|
||||||
|
for _, editable := range invoiceReferenceEditability(report.Invoices[index]) {
|
||||||
|
result.Edit = result.Edit || editable
|
||||||
|
}
|
||||||
|
result.Edit = result.Edit && len(report.Issues) == 0
|
||||||
|
result.Sync = result.Edit
|
||||||
|
result.Reasons["edit"] = "需核对原文字,多对象仅支持按原引用顺序及原码点边界等长修改,字体和布局须通过校验"
|
||||||
|
result.Reasons["sync"] = result.Reasons["edit"]
|
||||||
|
if !result.Edit {
|
||||||
|
result.Reasons["edit"] = "没有有效且无共享的文字引用"
|
||||||
|
result.Reasons["sync"] = result.Reasons["edit"]
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -254,6 +267,9 @@ func (e *Editor) CreateInvoice(ctx context.Context, invoice Invoice, options Inv
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
if invoice.Standard == ofdNamespace2016 {
|
||||||
|
return e.createInvoiceReferences(ctx, invoice, options)
|
||||||
|
}
|
||||||
encoding := InvoiceEncodingOptions{}
|
encoding := InvoiceEncodingOptions{}
|
||||||
if options.Encoding != nil {
|
if options.Encoding != nil {
|
||||||
encoding = *options.Encoding
|
encoding = *options.Encoding
|
||||||
@@ -272,6 +288,13 @@ func (e *Editor) ImportInvoice(ctx context.Context, data []byte, options Invoice
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
root, err := invoiceXMLTree(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceSource{}, err
|
||||||
|
}
|
||||||
|
if invoiceReferenceRoot(root) {
|
||||||
|
return e.importInvoiceReferences(ctx, data, options)
|
||||||
|
}
|
||||||
report, err := invoiceWritableReport(ctx, data)
|
report, err := invoiceWritableReport(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvoiceSource{}, err
|
return InvoiceSource{}, err
|
||||||
@@ -335,6 +358,41 @@ func (e *Editor) UpdateInvoice(ctx context.Context, source InvoiceSource, digest
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
root, err := invoiceXMLTree(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
if source.Kind == "tag" && invoiceReferenceRoot(root) {
|
||||||
|
var result InvoiceUpdateResult
|
||||||
|
err := e.Transaction(func(next *Editor) error {
|
||||||
|
updated, err := next.ReplaceInvoiceData(ctx, source, digest, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
actual, err := next.ExportInvoice(ctx, updated)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result = InvoiceUpdateResult{Source: updated, Digest: InvoiceDigest(actual), Synchronized: true}
|
||||||
|
r, err := next.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
signed, err := r.hasOutputSignatures(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if signed {
|
||||||
|
result.Issues = append(result.Issues, InvoiceIssue{Code: "signature-recheck-required", Message: "modified signed content requires signature verification", Source: updated})
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
report, err := invoiceWritableReport(ctx, data)
|
report, err := invoiceWritableReport(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvoiceUpdateResult{}, err
|
return InvoiceUpdateResult{}, err
|
||||||
@@ -367,12 +425,12 @@ func (e *Editor) UpdateInvoice(ctx context.Context, source InvoiceSource, digest
|
|||||||
}
|
}
|
||||||
pages := make(map[string]string)
|
pages := make(map[string]string)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
for _, binding := range entry.bindings {
|
for i, binding := range entry.bindings {
|
||||||
page, err := next.Page(binding.Page)
|
page, err := next.Page(binding.Page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pages[fmt.Sprintf("%d/%s", entry.index, binding.ObjectID)] = page.ID
|
pages[fmt.Sprintf("%d/%d", entry.index, i)] = page.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updated, err := next.ReplaceInvoiceData(ctx, source, digest, data)
|
updated, err := next.ReplaceInvoiceData(ctx, source, digest, data)
|
||||||
@@ -405,7 +463,7 @@ func (e *Editor) UpdateInvoice(ctx context.Context, source InvoiceSource, digest
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := range entry.bindings {
|
for i := range entry.bindings {
|
||||||
id := pages[fmt.Sprintf("%d/%s", entry.index, entry.bindings[i].ObjectID)]
|
id := pages[fmt.Sprintf("%d/%d", entry.index, i)]
|
||||||
page := slices.IndexFunc(next.pages, func(page PageContent) bool { return sameResourceID(page.ID, id) })
|
page := slices.IndexFunc(next.pages, func(page PageContent) bool { return sameResourceID(page.ID, id) })
|
||||||
if page < 0 {
|
if page < 0 {
|
||||||
return fmt.Errorf("invoice appearance page no longer exists")
|
return fmt.Errorf("invoice appearance page no longer exists")
|
||||||
@@ -486,6 +544,13 @@ func (e *Editor) EditInvoice(ctx context.Context, source InvoiceSource, digest s
|
|||||||
if InvoiceDigest(data) != digest {
|
if InvoiceDigest(data) != digest {
|
||||||
return InvoiceUpdateResult{}, fmt.Errorf("invoice source changed")
|
return InvoiceUpdateResult{}, fmt.Errorf("invoice source changed")
|
||||||
}
|
}
|
||||||
|
root, err := invoiceXMLTree(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
if source.Kind == "tag" && invoiceReferenceRoot(root) {
|
||||||
|
return e.editInvoiceReferences(ctx, source, digest, changes, synchronize)
|
||||||
|
}
|
||||||
changes = slices.Clone(changes)
|
changes = slices.Clone(changes)
|
||||||
for i := range changes {
|
for i := range changes {
|
||||||
change := &changes[i]
|
change := &changes[i]
|
||||||
@@ -524,6 +589,13 @@ func (e *Editor) SetInvoice(ctx context.Context, source InvoiceSource, digest st
|
|||||||
if InvoiceDigest(data) != digest {
|
if InvoiceDigest(data) != digest {
|
||||||
return InvoiceUpdateResult{}, fmt.Errorf("invoice source changed")
|
return InvoiceUpdateResult{}, fmt.Errorf("invoice source changed")
|
||||||
}
|
}
|
||||||
|
root, err := invoiceXMLTree(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
if source.Kind == "tag" && invoiceReferenceRoot(root) {
|
||||||
|
return e.setInvoiceReferences(ctx, source, digest, index, invoice, synchronize)
|
||||||
|
}
|
||||||
data, err = UpdateInvoiceXML(ctx, data, index, invoice)
|
data, err = UpdateInvoiceXML(ctx, data, index, invoice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvoiceUpdateResult{}, err
|
return InvoiceUpdateResult{}, err
|
||||||
@@ -682,6 +754,9 @@ func (r *Reader) InvoiceBindings(ctx context.Context, source InvoiceSource, inde
|
|||||||
if index < 0 || index >= len(report.Invoices) {
|
if index < 0 || index >= len(report.Invoices) {
|
||||||
return nil, fmt.Errorf("invoice index out of range")
|
return nil, fmt.Errorf("invoice index out of range")
|
||||||
}
|
}
|
||||||
|
if report.Invoices[index].Standard == ofdNamespace2016 {
|
||||||
|
return r.invoiceReferenceBindings(ctx, report.Invoices[index])
|
||||||
|
}
|
||||||
entries, err := r.invoiceBindings(ctx, source, InvoiceDigest(data), true)
|
entries, err := r.invoiceBindings(ctx, source, InvoiceDigest(data), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -712,6 +787,9 @@ func (r *Reader) CheckInvoiceAppearance(ctx context.Context, source InvoiceSourc
|
|||||||
if index < 0 || index >= len(report.Invoices) {
|
if index < 0 || index >= len(report.Invoices) {
|
||||||
return nil, fmt.Errorf("invoice index out of range")
|
return nil, fmt.Errorf("invoice index out of range")
|
||||||
}
|
}
|
||||||
|
if report.Invoices[index].Standard == ofdNamespace2016 {
|
||||||
|
return slices.Clone(report.Issues), ctx.Err()
|
||||||
|
}
|
||||||
bindings, err := r.InvoiceBindings(ctx, source, index)
|
bindings, err := r.InvoiceBindings(ctx, source, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ func (e *Editor) pruneStructureReferences(ctx context.Context, parts map[string]
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !slices.ContainsFunc(tags, func(tag CustomTag) bool {
|
knownTags, err := e.source.reader.structureReferenceTags(ctx, tags)
|
||||||
return structureOfficial(tag, tag.NameSpace)
|
if err != nil {
|
||||||
}) {
|
return err
|
||||||
|
}
|
||||||
|
if !slices.Contains(knownTags, true) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
pages := make(map[string]bool, len(e.pages))
|
pages := make(map[string]bool, len(e.pages))
|
||||||
@@ -185,8 +187,8 @@ func (e *Editor) pruneStructureReferences(ctx context.Context, parts map[string]
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
protected := make(map[string]bool)
|
protected := make(map[string]bool)
|
||||||
for _, tag := range tags {
|
for index, tag := range tags {
|
||||||
if !structureOfficial(tag, tag.NameSpace) {
|
if !knownTags[index] {
|
||||||
protected[e.source.reader.ResPath(tag.FileLoc)] = true
|
protected[e.source.reader.ResPath(tag.FileLoc)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +196,7 @@ func (e *Editor) pruneStructureReferences(ctx context.Context, parts map[string]
|
|||||||
originals := make(map[string][]byte)
|
originals := make(map[string][]byte)
|
||||||
redirects := make(map[int]string)
|
redirects := make(map[int]string)
|
||||||
for index, tag := range tags {
|
for index, tag := range tags {
|
||||||
if !structureOfficial(tag, tag.NameSpace) {
|
if !knownTags[index] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := e.source.reader.ResPath(tag.FileLoc)
|
name := e.source.reader.ResPath(tag.FileLoc)
|
||||||
@@ -421,7 +423,11 @@ func (e *Editor) annotationStructureSource(page int, wanted map[string]bool) (*e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !slices.ContainsFunc(tags, func(tag CustomTag) bool { return structureOfficial(tag, tag.NameSpace) }) {
|
knownTags, err := base.reader.structureReferenceTags(context.Background(), tags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !slices.Contains(knownTags, true) {
|
||||||
return base, nil
|
return base, nil
|
||||||
}
|
}
|
||||||
if _, err := base.reader.Doc(); err != nil {
|
if _, err := base.reader.Doc(); err != nil {
|
||||||
@@ -461,7 +467,13 @@ func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !structureOfficial(tag, root.name.Space) {
|
if !structureOfficial(tag, root.name.Space) {
|
||||||
return data, nil
|
structure, err := parseStructure(ctx, data, tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !structureInvoiceReferences(structure) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
count := 0
|
count := 0
|
||||||
@@ -528,9 +540,13 @@ func (m *editorPageImport) structures() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
knownTags, err := m.reader.structureReferenceTags(m.ctx, tags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var entries []byte
|
var entries []byte
|
||||||
for i, tag := range tags {
|
for i, tag := range tags {
|
||||||
if !structureOfficial(tag, tag.NameSpace) {
|
if !knownTags[i] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data, err := m.readFile(m.reader.ResPath(tag.FileLoc))
|
data, err := m.readFile(m.reader.ResPath(tag.FileLoc))
|
||||||
|
|||||||
@@ -153,7 +153,11 @@ func (e *Editor) AddStructureNode(index int, parent []int, node StructureNode) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if structureOfficial(tag, root.name.Space) && target.name.Space == root.name.Space && target.name.Local == "ObjectRef" {
|
structure, err := parseStructure(context.Background(), data, tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if (structureOfficial(tag, root.name.Space) || structureInvoiceReferences(structure)) && target.name.Space == root.name.Space && target.name.Local == "ObjectRef" {
|
||||||
return nil, fmt.Errorf("ObjectRef cannot contain child nodes")
|
return nil, fmt.Errorf("ObjectRef cannot contain child nodes")
|
||||||
}
|
}
|
||||||
content := bytes.Clone(encoded)
|
content := bytes.Clone(encoded)
|
||||||
@@ -213,7 +217,11 @@ func (e *Editor) SetStructureReferences(index int, path []int, references []Stru
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !structureOfficial(tag, root.name.Space) || node.name.Local == "ObjectRef" {
|
structure, err := parseStructure(context.Background(), data, tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !(structureOfficial(tag, root.name.Space) || structureInvoiceReferences(structure)) || node.name.Local == "ObjectRef" {
|
||||||
return nil, fmt.Errorf("custom tag does not support ObjectRef binding")
|
return nil, fmt.Errorf("custom tag does not support ObjectRef binding")
|
||||||
}
|
}
|
||||||
for parent := node; parent != nil; parent = parent.parent {
|
for parent := node; parent != nil; parent = parent.parent {
|
||||||
@@ -426,7 +434,7 @@ func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag,
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := collect(root, structureOfficial(tag, root.Namespace)); err != nil {
|
if err := collect(root, structureOfficial(tag, root.Namespace) || structureInvoiceReferences(root)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(refs) == 0 {
|
if len(refs) == 0 {
|
||||||
|
|||||||
+10
-2
@@ -180,11 +180,16 @@ func invoiceLayoutFactVisible(invoice Invoice, fact InvoiceFact) bool {
|
|||||||
// 入参: value 发票,fact 原始字段
|
// 入参: value 发票,fact 原始字段
|
||||||
// 返回: 公共字段关联及是否明确映射
|
// 返回: 公共字段关联及是否明确映射
|
||||||
func invoiceEDRMFactBinding(value Invoice, fact InvoiceFact) (InvoiceBinding, bool) {
|
func invoiceEDRMFactBinding(value Invoice, fact InvoiceFact) (InvoiceBinding, bool) {
|
||||||
if value.Standard != invoiceEDRM || fact.Namespace != invoiceEDRM {
|
if value.Standard != invoiceEDRM && value.Standard != ofdNamespace2016 || fact.Namespace != value.Standard {
|
||||||
return InvoiceBinding{}, false
|
return InvoiceBinding{}, false
|
||||||
}
|
}
|
||||||
var path strings.Builder
|
var path strings.Builder
|
||||||
for _, segment := range strings.Split(strings.TrimPrefix(strings.ReplaceAll(fact.Source.NodePath, "{"+invoiceEDRM+"}", "{}"), "/"), "/") {
|
nodePath := strings.ReplaceAll(fact.Source.NodePath, "{"+invoiceEDRM+"}", "{}")
|
||||||
|
if value.Standard == ofdNamespace2016 {
|
||||||
|
nodePath = strings.Replace(nodePath, "/root[1]", "/eInvoice[1]", 1)
|
||||||
|
nodePath = strings.ReplaceAll(nodePath, "/", "/{}")
|
||||||
|
}
|
||||||
|
for _, segment := range strings.Split(strings.TrimPrefix(nodePath, "/"), "/") {
|
||||||
name, index, ok := strings.Cut(segment, "[")
|
name, index, ok := strings.Cut(segment, "[")
|
||||||
if !ok || !strings.HasPrefix(name, "{}") || !strings.HasSuffix(index, "]") {
|
if !ok || !strings.HasPrefix(name, "{}") || !strings.HasSuffix(index, "]") {
|
||||||
return InvoiceBinding{}, false
|
return InvoiceBinding{}, false
|
||||||
@@ -499,6 +504,9 @@ func invoiceLayoutText(text *TextObject, value string, metrics FontMetrics) (flo
|
|||||||
bottom = math.Max(bottom, y+float64(descender)*unit)
|
bottom = math.Max(bottom, y+float64(descender)*unit)
|
||||||
for _, char := range textCodeRunes(code.Value) {
|
for _, char := range textCodeRunes(code.Value) {
|
||||||
glyph := metrics.GlyphIndex(char)
|
glyph := metrics.GlyphIndex(char)
|
||||||
|
if glyph == 0 {
|
||||||
|
return 0, fmt.Errorf("invoice font has no glyph for %U", char)
|
||||||
|
}
|
||||||
var bounds Box
|
var bounds Box
|
||||||
if provider, ok := metrics.(FontGlyphBounds); ok {
|
if provider, ok := metrics.(FontGlyphBounds); ok {
|
||||||
bounds, err = provider.GlyphBounds(glyph, text.Size)
|
bounds, err = provider.GlyphBounds(glyph, text.Size)
|
||||||
|
|||||||
+505
-9
@@ -21,9 +21,12 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InvoiceDigest 计算原始导出数据的SHA256摘要
|
// InvoiceDigest 计算原始导出数据的SHA256摘要
|
||||||
@@ -236,7 +239,15 @@ func (e *Editor) ExportInvoice(ctx context.Context, source InvoiceSource) ([]byt
|
|||||||
if e == nil || e.source == nil {
|
if e == nil || e.source == nil {
|
||||||
return nil, fmt.Errorf("invoice source not found")
|
return nil, fmt.Errorf("invoice source not found")
|
||||||
}
|
}
|
||||||
return e.source.reader.ExportInvoice(ctx, source)
|
if len(e.pages) == 0 {
|
||||||
|
return e.source.reader.ExportInvoice(invoiceWriteContext(ctx), source)
|
||||||
|
}
|
||||||
|
r, err := e.ReaderContext(invoiceWriteContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
return r.ExportInvoice(ctx, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceInvoiceData 仅替换选中声明,独立资源写时复制,旧数据允许诊断而新数据必须无诊断
|
// ReplaceInvoiceData 仅替换选中声明,独立资源写时复制,旧数据允许诊断而新数据必须无诊断
|
||||||
@@ -273,24 +284,35 @@ func (e *Editor) changeInvoiceSource(ctx context.Context, source InvoiceSource,
|
|||||||
if InvoiceDigest(old) != digest {
|
if InvoiceDigest(old) != digest {
|
||||||
return InvoiceSource{}, fmt.Errorf("invoice source has changed")
|
return InvoiceSource{}, fmt.Errorf("invoice source has changed")
|
||||||
}
|
}
|
||||||
|
objectReference := false
|
||||||
if source.Kind == "tag" {
|
if source.Kind == "tag" {
|
||||||
root, err := invoiceXMLTree(ctx, old)
|
root, err := invoiceXMLTree(ctx, old)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InvoiceSource{}, err
|
return InvoiceSource{}, err
|
||||||
}
|
}
|
||||||
if root.name.Space == ofdNamespace2016 && root.name.Local == "root" && invoiceObjectReferences(root) {
|
objectReference = invoiceReferenceRoot(root)
|
||||||
return InvoiceSource{}, fmt.Errorf("object reference invoice is read-only")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !remove {
|
if !remove {
|
||||||
if len(data) > invoiceXMLLimit {
|
if len(data) > invoiceXMLLimit {
|
||||||
return InvoiceSource{}, fmt.Errorf("invoice data limit exceeded")
|
return InvoiceSource{}, fmt.Errorf("invoice data limit exceeded")
|
||||||
}
|
}
|
||||||
if _, err := invoiceWritableReport(ctx, data); err != nil {
|
if objectReference {
|
||||||
return InvoiceSource{}, err
|
r, err := e.ReaderContext(ctx)
|
||||||
}
|
if err != nil {
|
||||||
if err := invoiceValidateXMLWrite(ctx, old, data); err != nil {
|
return InvoiceSource{}, err
|
||||||
return InvoiceSource{}, err
|
}
|
||||||
|
_, err = r.invoiceReferenceReport(ctx, data)
|
||||||
|
r.Close()
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceSource{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := invoiceWritableReport(ctx, data); err != nil {
|
||||||
|
return InvoiceSource{}, err
|
||||||
|
}
|
||||||
|
if err := invoiceValidateXMLWrite(ctx, old, data); err != nil {
|
||||||
|
return InvoiceSource{}, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if bytes.Equal(old, data) {
|
if bytes.Equal(old, data) {
|
||||||
return source, ctx.Err()
|
return source, ctx.Err()
|
||||||
@@ -473,3 +495,477 @@ func (r *Reader) invoiceSourceNode(ctx context.Context, source InvoiceSource) (s
|
|||||||
}
|
}
|
||||||
return name, data, selected, reference, ctx.Err()
|
return name, data, selected, reference, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invoiceReferenceReport 校验当前文档中可解引用的发票标引,不将对象标识作为业务值
|
||||||
|
// 入参: ctx 取消上下文, data 标引原文
|
||||||
|
// 返回: InvoiceReport 解引用结果, error 格式或引用错误
|
||||||
|
func (r *Reader) invoiceReferenceReport(ctx context.Context, data []byte) (InvoiceReport, error) {
|
||||||
|
root, err := invoiceXMLTree(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceReport{}, err
|
||||||
|
}
|
||||||
|
if !invoiceReferenceRoot(root) {
|
||||||
|
return InvoiceReport{}, fmt.Errorf("expected an OFD object reference invoice")
|
||||||
|
}
|
||||||
|
report, err := r.readInvoiceXML(ctx, data, InvoiceSource{Kind: "tag"})
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceReport{}, err
|
||||||
|
}
|
||||||
|
if len(report.Invoices) != 1 || len(report.Invoices[0].Facts) == 0 || len(report.Issues) != 0 {
|
||||||
|
return InvoiceReport{}, fmt.Errorf("invoice object references are missing, ambiguous or invalid")
|
||||||
|
}
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// importInvoiceReferences 将已有页面的有效引用创建为独立标引,保留原始XML扩展
|
||||||
|
// 入参: ctx 取消上下文, data 标引原文, options 创建选项
|
||||||
|
// 返回: InvoiceSource 标引来源, error 校验或提交错误
|
||||||
|
func (e *Editor) importInvoiceReferences(ctx context.Context, data []byte, options InvoiceCreateOptions) (InvoiceSource, error) {
|
||||||
|
if options.Layout != nil || options.Encoding != nil {
|
||||||
|
return InvoiceSource{}, fmt.Errorf("object reference import requires existing pages and no encoding options")
|
||||||
|
}
|
||||||
|
var source InvoiceSource
|
||||||
|
err := e.Transaction(func(next *Editor) error {
|
||||||
|
r, err := next.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = r.invoiceReferenceReport(ctx, data)
|
||||||
|
r.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseStructure(ctx, data, CustomTag{TypeID: structureTypeOfficial, NameSpace: ofdNamespace2016})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
index, err := next.AddStructure(CustomTag{TypeID: "root", NameSpace: ofdNamespace2016}, *root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := next.editStructure(index, func(_ []byte, _ *editorXML, _ CustomTag) ([]byte, error) { return bytes.Clone(data), nil }); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r, err = next.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
tags, err := r.CustomTagsContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
source = InvoiceSource{Kind: "tag", ID: strconv.Itoa(index), FileLoc: r.ResPath(tags[index].FileLoc)}
|
||||||
|
_, err = r.ExportInvoice(ctx, source)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceSource{}, err
|
||||||
|
}
|
||||||
|
return source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// invoiceReferenceBindings 解析唯一单对象事实的固有绑定,不猜测多对象文字分配
|
||||||
|
// 入参: ctx 取消上下文, invoice 解引用发票
|
||||||
|
// 返回: []InvoiceBinding 事实关联, error 文档读取错误
|
||||||
|
func (r *Reader) invoiceReferenceBindings(ctx context.Context, invoice Invoice) ([]InvoiceBinding, error) {
|
||||||
|
doc, err := r.DocContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pages := signaturePageNumbers(doc)
|
||||||
|
counts := make(map[string]int)
|
||||||
|
for _, fact := range invoice.Facts {
|
||||||
|
for _, ref := range fact.References {
|
||||||
|
for _, id := range ref.ObjectIDs {
|
||||||
|
counts[editorResourceID(ref.PageID)+"/"+editorResourceID(id)]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var bindings []InvoiceBinding
|
||||||
|
for i, fact := range invoice.Facts {
|
||||||
|
if len(fact.References) != 1 || len(fact.References[0].ObjectIDs) != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ref := fact.References[0]
|
||||||
|
page, _ := resourceValue(pages, ref.PageID)
|
||||||
|
if page == 0 || counts[editorResourceID(ref.PageID)+"/"+editorResourceID(ref.ObjectIDs[0])] != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bindings = append(bindings, InvoiceBinding{Field: fmt.Sprintf("facts.%d", i), Item: -1, Page: page - 1, ObjectID: ref.ObjectIDs[0]})
|
||||||
|
}
|
||||||
|
return bindings, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// invoiceReferenceEditability 一次计数并生成无共享引用的事实能力,字体和布局仍由事务预检
|
||||||
|
// 入参: invoice 发票
|
||||||
|
// 返回: []bool 与事实索引对应的编辑条件
|
||||||
|
func invoiceReferenceEditability(invoice Invoice) []bool {
|
||||||
|
counts := make(map[string]int)
|
||||||
|
for _, fact := range invoice.Facts {
|
||||||
|
for _, ref := range fact.References {
|
||||||
|
for _, id := range ref.ObjectIDs {
|
||||||
|
counts[editorResourceID(ref.PageID)+"/"+editorResourceID(id)]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := make([]bool, len(invoice.Facts))
|
||||||
|
for i, fact := range invoice.Facts {
|
||||||
|
result[i] = len(fact.References) != 0
|
||||||
|
for _, ref := range fact.References {
|
||||||
|
result[i] = result[i] && editorResourceID(ref.PageID) != "" && len(ref.ObjectIDs) != 0
|
||||||
|
for _, id := range ref.ObjectIDs {
|
||||||
|
result[i] = result[i] && editorResourceID(id) != "" && counts[editorResourceID(ref.PageID)+"/"+editorResourceID(id)] == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// editInvoiceReferences 原子修改引用目标并重新解引用校验,XML摘要外必须核对原文字
|
||||||
|
// 入参: ctx 取消上下文, source 来源, digest 标引摘要, changes 事实修改, synchronize 必须为真
|
||||||
|
// 返回: InvoiceUpdateResult 同步结果, error 冲突、布局或提交错误
|
||||||
|
func (e *Editor) editInvoiceReferences(ctx context.Context, source InvoiceSource, digest string, changes []InvoiceFieldChange, synchronize bool) (InvoiceUpdateResult, error) {
|
||||||
|
if !synchronize {
|
||||||
|
return InvoiceUpdateResult{}, fmt.Errorf("object reference values are page text and require synchronization")
|
||||||
|
}
|
||||||
|
var result InvoiceUpdateResult
|
||||||
|
err := e.Transaction(func(next *Editor) error {
|
||||||
|
r, err := next.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
data, err := r.ExportInvoice(ctx, source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if digest == "" || InvoiceDigest(data) != digest {
|
||||||
|
return fmt.Errorf("invoice source changed")
|
||||||
|
}
|
||||||
|
report, err := r.invoiceReferenceReport(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
invoice := report.Invoices[0]
|
||||||
|
invoice.Facts = slices.Clone(invoice.Facts)
|
||||||
|
bindings, err := r.invoiceReferenceBindings(ctx, invoice)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var selected []InvoiceBinding
|
||||||
|
var singles []InvoiceBinding
|
||||||
|
joint := make(map[int][]InvoiceBinding)
|
||||||
|
editable := invoiceReferenceEditability(invoice)
|
||||||
|
seen := make(map[int]bool)
|
||||||
|
for _, change := range changes {
|
||||||
|
if change.Source.Kind != "" && change.Source.Kind != source.Kind || change.Source.ID != "" && change.Source.ID != source.ID || change.Source.FileLoc != "" && change.Source.FileLoc != source.FileLoc || change.Revision != "" && change.Revision != digest {
|
||||||
|
return fmt.Errorf("invoice field belongs to another source or revision")
|
||||||
|
}
|
||||||
|
index := slices.IndexFunc(invoice.Facts, func(fact InvoiceFact) bool { return fact.Source.NodePath == change.Source.NodePath })
|
||||||
|
if index < 0 || seen[index] || change.Delete || change.Nil || change.ExpectedNil != nil && *change.ExpectedNil || change.ExpectedValue == nil || *change.ExpectedValue != invoice.Facts[index].Value {
|
||||||
|
return fmt.Errorf("object reference edit requires a unique fact and matching expected text")
|
||||||
|
}
|
||||||
|
seen[index] = true
|
||||||
|
if change.Value == invoice.Facts[index].Value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !editable[index] {
|
||||||
|
return fmt.Errorf("invoice text object is shared by multiple facts")
|
||||||
|
}
|
||||||
|
binding := slices.IndexFunc(bindings, func(binding InvoiceBinding) bool { return binding.Field == fmt.Sprintf("facts.%d", index) })
|
||||||
|
value, err := invoiceReferenceDisplay(invoice, invoice.Facts[index], change.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if binding >= 0 {
|
||||||
|
selected = append(selected, bindings[binding])
|
||||||
|
singles = append(singles, bindings[binding])
|
||||||
|
} else {
|
||||||
|
fact := invoice.Facts[index]
|
||||||
|
if utf8.RuneCountInString(value) != utf8.RuneCountInString(fact.Value) || value == "" {
|
||||||
|
return fmt.Errorf("multiple invoice references require equal-length positioned replacement")
|
||||||
|
}
|
||||||
|
for _, ref := range fact.References {
|
||||||
|
page := slices.IndexFunc(next.pages, func(page PageContent) bool { return sameResourceID(page.ID, ref.PageID) })
|
||||||
|
if page < 0 {
|
||||||
|
return fmt.Errorf("invoice reference page not found")
|
||||||
|
}
|
||||||
|
for _, id := range ref.ObjectIDs {
|
||||||
|
item := InvoiceBinding{Field: fmt.Sprintf("facts.%d", index), Item: -1, Page: page, ObjectID: id}
|
||||||
|
joint[index] = append(joint[index], item)
|
||||||
|
selected = append(selected, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invoice.Facts[index].Value = value
|
||||||
|
}
|
||||||
|
structures, err := r.StructureContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, structure := range structures {
|
||||||
|
if strconv.Itoa(i) == source.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if structure.Error != "" {
|
||||||
|
return fmt.Errorf("cannot verify shared invoice references")
|
||||||
|
}
|
||||||
|
var check func(*StructureNode) error
|
||||||
|
check = func(node *StructureNode) error {
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, ref := range node.References {
|
||||||
|
for _, binding := range selected {
|
||||||
|
if sameResourceID(next.pages[binding.Page].ID, ref.PageID) {
|
||||||
|
for _, id := range ref.ObjectIDs {
|
||||||
|
if sameResourceID(id, binding.ObjectID) {
|
||||||
|
return fmt.Errorf("invoice text object is referenced by another structure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j := range node.Children {
|
||||||
|
if err := check(&node.Children[j]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if err := check(structure.Root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, fact := range report.Invoices[0].Facts {
|
||||||
|
if len(joint[i]) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := []rune(invoice.Facts[i].Value)
|
||||||
|
offset := 0
|
||||||
|
var original strings.Builder
|
||||||
|
for _, binding := range joint[i] {
|
||||||
|
object, err := next.Object(binding.Page, binding.ObjectID)
|
||||||
|
if err != nil || object.Type != "TextObject" {
|
||||||
|
return fmt.Errorf("joint invoice reference requires text objects")
|
||||||
|
}
|
||||||
|
text := object.TextObject.Text()
|
||||||
|
original.WriteString(text)
|
||||||
|
end := offset + utf8.RuneCountInString(text)
|
||||||
|
if end > len(value) {
|
||||||
|
return fmt.Errorf("invoice reference text length changed")
|
||||||
|
}
|
||||||
|
replacement := string(value[offset:end])
|
||||||
|
offset = end
|
||||||
|
if text == replacement {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
analysis, err := next.AnalyzeTextSelection(binding.Page, TextSelection{IDs: []string{binding.ObjectID}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if analysis.Text != text {
|
||||||
|
return fmt.Errorf("invoice reference requires explicit line break mapping")
|
||||||
|
}
|
||||||
|
if err := next.UpdateTextSelection(binding.Page, analysis.Selection, TextSelectionEdit{End: utf8.RuneCountInString(text), Text: replacement}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if original.String() != fact.Value || offset != len(value) {
|
||||||
|
return fmt.Errorf("invoice reference text changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := next.SyncInvoicePages(ctx, invoice, singles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
view, err := next.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer view.Close()
|
||||||
|
verified, err := view.invoiceReferenceReport(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range invoice.Facts {
|
||||||
|
if verified.Invoices[0].Facts[i].Value != invoice.Facts[i].Value {
|
||||||
|
return fmt.Errorf("invoice text failed to round-trip after synchronization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = InvoiceUpdateResult{Source: source, Digest: digest, Synchronized: true}
|
||||||
|
signed, err := view.hasOutputSignatures(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if signed {
|
||||||
|
result.Issues = append(result.Issues, InvoiceIssue{Code: "signature-recheck-required", Message: "modified signed content requires signature verification", Source: source})
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// invoiceReferenceDisplay 保留金额符号及已识别日期的显示格式,不改写引用结构
|
||||||
|
// 入参: invoice 原发票, fact 原事实, value 新业务值
|
||||||
|
// 返回: string 显示文字, error 值格式错误
|
||||||
|
func invoiceReferenceDisplay(invoice Invoice, fact InvoiceFact, value string) (string, error) {
|
||||||
|
binding, ok := invoiceEDRMFactBinding(invoice, fact)
|
||||||
|
if !ok {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
switch binding.Field {
|
||||||
|
case "amount", "taxAmount", "totalAmount":
|
||||||
|
old := strings.TrimSpace(fact.Value)
|
||||||
|
prefix := ""
|
||||||
|
for _, symbol := range []string{"¥", "¥"} {
|
||||||
|
if strings.HasPrefix(old, symbol) {
|
||||||
|
prefix = symbol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(value), "¥¥"))
|
||||||
|
if invoiceDecimal(value) == nil {
|
||||||
|
return "", fmt.Errorf("invalid invoice amount")
|
||||||
|
}
|
||||||
|
return prefix + value, nil
|
||||||
|
case "date":
|
||||||
|
layouts := []string{"2006年01月02日", "2006-01-02", "20060102", "2006/01/02"}
|
||||||
|
for _, layout := range layouts {
|
||||||
|
if _, err := time.Parse(layout, fact.Value); err == nil {
|
||||||
|
for _, input := range layouts {
|
||||||
|
if date, err := time.Parse(input, value); err == nil {
|
||||||
|
return date.Format(layout), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("invalid invoice date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("invoice date display format is unknown")
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setInvoiceReferences 将已有摘要字段变化映射回唯一事实,原Facts作为图元旧值基线
|
||||||
|
// 入参: ctx 取消上下文, source 来源, digest 标引摘要, index 发票索引, invoice 新业务值, synchronize 是否同步
|
||||||
|
// 返回: InvoiceUpdateResult 同步结果, error 不可表达字段或提交错误
|
||||||
|
func (e *Editor) setInvoiceReferences(ctx context.Context, source InvoiceSource, digest string, index int, invoice Invoice, synchronize bool) (InvoiceUpdateResult, error) {
|
||||||
|
if index != 0 {
|
||||||
|
return InvoiceUpdateResult{}, fmt.Errorf("invoice index out of range")
|
||||||
|
}
|
||||||
|
r, err := e.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
report, err := r.ReadInvoice(ctx, source)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
if len(report.Invoices) != 1 {
|
||||||
|
return InvoiceUpdateResult{}, fmt.Errorf("object reference invoice not found")
|
||||||
|
}
|
||||||
|
before := report.Invoices[0]
|
||||||
|
if !reflect.DeepEqual(invoice.Facts, before.Facts) || !reflect.DeepEqual(invoice.Items, before.Items) {
|
||||||
|
return InvoiceUpdateResult{}, fmt.Errorf("object reference SetInvoice requires unchanged original facts and items; use EditInvoice for fact edits")
|
||||||
|
}
|
||||||
|
var changes []InvoiceFieldChange
|
||||||
|
for _, field := range invoiceLayoutFields(invoice) {
|
||||||
|
value, err := invoiceBindingValue(before, InvoiceBinding{Field: field.field, Item: field.item})
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceUpdateResult{}, err
|
||||||
|
}
|
||||||
|
if value == field.value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match := -1
|
||||||
|
for i, fact := range before.Facts {
|
||||||
|
binding, ok := invoiceEDRMFactBinding(before, fact)
|
||||||
|
if ok && binding.Field == field.field && binding.Item == field.item {
|
||||||
|
if match >= 0 {
|
||||||
|
return InvoiceUpdateResult{}, fmt.Errorf("invoice summary field is ambiguous")
|
||||||
|
}
|
||||||
|
match = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match < 0 {
|
||||||
|
return InvoiceUpdateResult{}, fmt.Errorf("invoice field %s has no object reference", field.field)
|
||||||
|
}
|
||||||
|
fact := before.Facts[match]
|
||||||
|
changes = append(changes, InvoiceFieldChange{Source: fact.Source, Value: field.value, ExpectedValue: &fact.Value})
|
||||||
|
}
|
||||||
|
return e.editInvoiceReferences(ctx, source, digest, changes, synchronize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createInvoiceReferences 按已知样例摘要字段生成信息页和真实标引,不推定税务业务创建规范
|
||||||
|
// 入参: ctx 取消上下文, invoice 摘要数据, options 必填票面字体和排版
|
||||||
|
// 返回: InvoiceSource 标引来源, error 不可表达字段、排版或提交错误
|
||||||
|
func (e *Editor) createInvoiceReferences(ctx context.Context, invoice Invoice, options InvoiceCreateOptions) (InvoiceSource, error) {
|
||||||
|
if options.Layout == nil || options.Layout.Font == "" || options.Encoding != nil {
|
||||||
|
return InvoiceSource{}, fmt.Errorf("object reference creation requires Editor.CreateInvoice with Layout.Font and no standalone encoding")
|
||||||
|
}
|
||||||
|
if invoice.Kind != "inv" || invoice.Version != "1.0" || invoice.Number == "" || len(invoice.Facts) != 0 || len(invoice.Items) != 0 || len(invoice.Sources) != 0 {
|
||||||
|
return InvoiceSource{}, fmt.Errorf("object reference creation supports version 1.0 invoice summaries without existing facts, sources or inferred item groups")
|
||||||
|
}
|
||||||
|
paths := make(map[string]string)
|
||||||
|
probe := Invoice{Standard: ofdNamespace2016}
|
||||||
|
for _, path := range invoiceEDRMPaths() {
|
||||||
|
field, _ := invoiceEDRMField(&probe, nil, path)
|
||||||
|
if field == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*field = "mapped"
|
||||||
|
for _, candidate := range invoiceLayoutFields(probe) {
|
||||||
|
if candidate.value == "mapped" {
|
||||||
|
paths[candidate.field] = strings.TrimPrefix(path, "/eInvoice/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*field = ""
|
||||||
|
}
|
||||||
|
for _, field := range invoiceLayoutFields(invoice) {
|
||||||
|
if field.value != "" && field.field != "kind" && field.field != "standard" && field.field != "version" && paths[field.field] == "" {
|
||||||
|
return InvoiceSource{}, fmt.Errorf("object reference sample cannot represent field %s", field.field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
issues, err := ValidateInvoice(ctx, invoice)
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceSource{}, err
|
||||||
|
}
|
||||||
|
if len(issues) != 0 {
|
||||||
|
return InvoiceSource{}, fmt.Errorf("invalid object reference invoice: %s", issues[0].Message)
|
||||||
|
}
|
||||||
|
var source InvoiceSource
|
||||||
|
err = e.Transaction(func(next *Editor) error {
|
||||||
|
display := invoice
|
||||||
|
display.Kind, display.Standard, display.Version = "", "", ""
|
||||||
|
bindings, err := next.AppendInvoicePages(ctx, display, *options.Layout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root := StructureNode{Name: "root", Namespace: ofdNamespace2016, Attributes: []StructureAttribute{{Name: "version", Value: "1.0"}}}
|
||||||
|
for _, binding := range bindings {
|
||||||
|
node := &root
|
||||||
|
for _, name := range strings.Split(paths[binding.Field], "/") {
|
||||||
|
i := slices.IndexFunc(node.Children, func(child StructureNode) bool { return child.Name == name })
|
||||||
|
if i < 0 {
|
||||||
|
node.Children = append(node.Children, StructureNode{Name: name, Namespace: ofdNamespace2016})
|
||||||
|
i = len(node.Children) - 1
|
||||||
|
}
|
||||||
|
node = &node.Children[i]
|
||||||
|
}
|
||||||
|
node.Children = append(node.Children, StructureNode{Name: "ObjectRef", Namespace: ofdNamespace2016, Attributes: []StructureAttribute{{Name: "PageRef", Value: next.pages[binding.Page].ID}}, Text: binding.ObjectID})
|
||||||
|
}
|
||||||
|
data, err := encodeStructureNode(root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
source, err = next.importInvoiceReferences(ctx, data, InvoiceCreateOptions{})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return InvoiceSource{}, err
|
||||||
|
}
|
||||||
|
return source, nil
|
||||||
|
}
|
||||||
+120
-18
@@ -118,9 +118,13 @@ type invoiceXMLPatch struct {
|
|||||||
func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
||||||
format, supported := invoiceWriteFormat(value)
|
format, supported := invoiceWriteFormat(value)
|
||||||
probe := Invoice{Items: []InvoiceItem{{}}}
|
probe := Invoice{Items: []InvoiceItem{{}}}
|
||||||
if value.Standard == invoiceEDRM {
|
if value.Standard == invoiceEDRM || value.Standard == ofdNamespace2016 {
|
||||||
for _, path := range invoiceEDRMPaths() {
|
for _, path := range invoiceEDRMPaths() {
|
||||||
if p, _ := invoiceEDRMField(&probe, &probe.Items[0], path); p != nil {
|
item := &probe.Items[0]
|
||||||
|
if value.Standard == ofdNamespace2016 {
|
||||||
|
item = nil
|
||||||
|
}
|
||||||
|
if p, _ := invoiceEDRMField(&probe, item, path); p != nil {
|
||||||
*p = "editable"
|
*p = "editable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,6 +154,18 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
referenceFields := map[string]int{}
|
||||||
|
referenceEditable := map[string]bool{}
|
||||||
|
var referenceFacts []bool
|
||||||
|
if value.Standard == ofdNamespace2016 {
|
||||||
|
referenceFacts = invoiceReferenceEditability(value)
|
||||||
|
for i, fact := range value.Facts {
|
||||||
|
if binding, ok := invoiceEDRMFactBinding(value, fact); ok && binding.Item < 0 {
|
||||||
|
referenceFields[binding.Field]++
|
||||||
|
referenceEditable[binding.Field] = fact.Source.NodePath != "" && referenceFacts[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
var result []InvoiceFieldDescriptor
|
var result []InvoiceFieldDescriptor
|
||||||
for _, field := range invoiceLayoutFields(value) {
|
for _, field := range invoiceLayoutFields(value) {
|
||||||
if strings.HasPrefix(field.field, "facts.") || strings.HasPrefix(field.field, "items.fields.") {
|
if strings.HasPrefix(field.field, "facts.") || strings.HasPrefix(field.field, "items.fields.") {
|
||||||
@@ -169,6 +185,9 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
|||||||
}
|
}
|
||||||
mapped, _ := invoiceBindingValue(probe, binding)
|
mapped, _ := invoiceBindingValue(probe, binding)
|
||||||
d.Editable = supported && format.Edit && mapped == "editable"
|
d.Editable = supported && format.Edit && mapped == "editable"
|
||||||
|
if value.Standard == ofdNamespace2016 && len(value.Facts) > 0 {
|
||||||
|
d.Editable = d.Editable && referenceFields[field.field] == 1 && referenceEditable[field.field]
|
||||||
|
}
|
||||||
switch field.field {
|
switch field.field {
|
||||||
case "date":
|
case "date":
|
||||||
d.Type = "date"
|
d.Type = "date"
|
||||||
@@ -184,12 +203,15 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
|||||||
}
|
}
|
||||||
result = append(result, d)
|
result = append(result, d)
|
||||||
}
|
}
|
||||||
appendFact := func(fact InvoiceFact, field string, item int) {
|
appendFact := func(fact InvoiceFact, field string, item, index int) {
|
||||||
d := InvoiceFieldDescriptor{Field: field, Label: fact.Name, Group: "facts", Type: "text", Item: item, Value: fact.Value, Bindable: value.Standard != ofdNamespace2016}
|
d := InvoiceFieldDescriptor{Field: field, Label: fact.Name, Group: "facts", Type: "text", Item: item, Value: fact.Value, Bindable: value.Standard != ofdNamespace2016}
|
||||||
if item >= 0 {
|
if item >= 0 {
|
||||||
d.Group = "items"
|
d.Group = "items"
|
||||||
}
|
}
|
||||||
d.Editable = supported && format.Edit && fact.Source.NodePath != "" && fact.Namespace != invoiceXBRL
|
d.Editable = supported && format.Edit && fact.Source.NodePath != "" && fact.Namespace != invoiceXBRL
|
||||||
|
if value.Standard == ofdNamespace2016 {
|
||||||
|
d.Editable = d.Editable && item < 0 && referenceFacts[index]
|
||||||
|
}
|
||||||
if invoiceNumeric(value.Kind, fact.Name) {
|
if invoiceNumeric(value.Kind, fact.Name) {
|
||||||
d.Type = "decimal"
|
d.Type = "decimal"
|
||||||
}
|
}
|
||||||
@@ -202,27 +224,28 @@ func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
|||||||
result = append(result, d)
|
result = append(result, d)
|
||||||
}
|
}
|
||||||
for i, fact := range value.Facts {
|
for i, fact := range value.Facts {
|
||||||
appendFact(fact, fmt.Sprintf("facts.%d", i), -1)
|
appendFact(fact, fmt.Sprintf("facts.%d", i), -1, i)
|
||||||
}
|
}
|
||||||
for i, item := range value.Items {
|
for i, item := range value.Items {
|
||||||
for j, fact := range item.Fields {
|
for j, fact := range item.Fields {
|
||||||
appendFact(fact, fmt.Sprintf("items.fields.%d", j), i)
|
appendFact(fact, fmt.Sprintf("items.fields.%d", j), i, j)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvoiceFormats 返回独立格式能力副本,创建及编辑能力按业务版本分别声明
|
// InvoiceFormats 返回独立格式能力副本,创建及编辑能力按业务版本分别声明
|
||||||
|
// 对象引用的创建及编辑限于Editor文档接口,创建必须提供Layout及字体
|
||||||
// 返回: 已适配格式列表
|
// 返回: 已适配格式列表
|
||||||
func InvoiceFormats() []InvoiceFormat {
|
func InvoiceFormats() []InvoiceFormat {
|
||||||
result := []InvoiceFormat{{Kind: "einv", Standard: InvoiceStandardEInvoice, Version: "0.3", Read: true, Create: true, Edit: true, Items: true}}
|
result := []InvoiceFormat{{Kind: "einv", Standard: InvoiceStandardEInvoice, Version: "0.3", Read: true, Create: true, Edit: true, Items: true}}
|
||||||
result = append(result, InvoiceFormat{Kind: "inv", Standard: invoiceEDRM, Version: "1.0", Read: true, Edit: true, Items: true})
|
result = append(result, InvoiceFormat{Kind: "inv", Standard: invoiceEDRM, Version: "1.0", Read: true, Create: true, Edit: true, Items: true})
|
||||||
result = append(result, InvoiceFormat{Kind: "inv", Standard: ofdNamespace2016, Version: "1.0", Read: true})
|
result = append(result, InvoiceFormat{Kind: "inv", Standard: ofdNamespace2016, Version: "1.0", Read: true, Create: true, Edit: true})
|
||||||
for _, standard := range []string{InvoiceStandardVAT2023, InvoiceStandardDigital2023, InvoiceStandardAir2023, InvoiceStandardRail2023} {
|
for _, standard := range []string{InvoiceStandardVAT2023, InvoiceStandardDigital2023, InvoiceStandardAir2023, InvoiceStandardRail2023} {
|
||||||
kind, version := invoiceTaxonomy(standard)
|
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"})
|
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})
|
return append(result, InvoiceFormat{Kind: "atr", Standard: InvoiceStandardAir2021, Version: "2021-11-30", Read: true, Create: true, Edit: true, Items: true}, InvoiceFormat{Kind: "inv", Standard: InvoiceStandardVAT2021, Version: "2021-09-30", Read: true, Create: true, Edit: true, Items: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeInvoices 提取独立XML或XBRL中的发票,不执行联网或完整XSD验证
|
// DecodeInvoices 提取独立XML或XBRL中的发票,不执行联网或完整XSD验证
|
||||||
@@ -452,6 +475,9 @@ func EncodeInvoiceWithOptions(ctx context.Context, value Invoice, options Invoic
|
|||||||
if err := invoiceWriteLimits(ctx, value, options); err != nil {
|
if err := invoiceWriteLimits(ctx, value, options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if value.Standard == ofdNamespace2016 {
|
||||||
|
return nil, fmt.Errorf("object reference creation requires Editor.CreateInvoice with Layout.Font; standalone XML cannot create page objects")
|
||||||
|
}
|
||||||
issues, err := ValidateInvoice(ctx, value)
|
issues, err := ValidateInvoice(ctx, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -467,7 +493,12 @@ func EncodeInvoiceWithOptions(ctx context.Context, value Invoice, options Invoic
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
if value.Standard == InvoiceStandardEInvoice {
|
if value.Standard == invoiceEDRM {
|
||||||
|
if len(options.HeaderXML)+len(options.SellerAuthenticationXML)+len(options.TaxSupervisionXML)+len(options.TaxBureauSignatureXML) != 0 {
|
||||||
|
return nil, fmt.Errorf("EDRM creation does not support EInvoice sections")
|
||||||
|
}
|
||||||
|
data, err = invoiceEncodeEDRM(value)
|
||||||
|
} else if value.Standard == InvoiceStandardEInvoice {
|
||||||
data, err = invoiceEncodePlain(value)
|
data, err = invoiceEncodePlain(value)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
data, err = invoiceWritePlainSections(ctx, data, options)
|
data, err = invoiceWritePlainSections(ctx, data, options)
|
||||||
@@ -648,22 +679,36 @@ func invoiceEncodeXBRL(value Invoice, options InvoiceEncodingOptions) ([]byte, e
|
|||||||
ids[unit.ID] = true
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
factsByName := map[string]InvoiceFact{}
|
||||||
|
for _, fact := range value.Facts {
|
||||||
|
if fact.Source.NodePath != "" || fact.Namespace == invoiceXBRL {
|
||||||
|
return nil, fmt.Errorf("source facts require source-preserving invoice update")
|
||||||
|
}
|
||||||
|
if _, exists := factsByName[fact.Name]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate invoice fact %s", fact.Name)
|
||||||
|
}
|
||||||
|
factsByName[fact.Name] = fact
|
||||||
|
}
|
||||||
header := map[string]bool{}
|
header := map[string]bool{}
|
||||||
for _, name := range invoiceWriteHeader(value) {
|
for _, name := range invoiceWriteHeader(value) {
|
||||||
p := invoiceXMLField(&value, value.Kind, name)
|
p := invoiceXMLField(&value, value.Kind, name)
|
||||||
if p != nil && *p != "" {
|
fact, has := factsByName[name]
|
||||||
if err := invoiceWriteFact(&b, value, InvoiceFact{Name: name, Value: *p}, options); err != nil {
|
if has && p != nil && (fact.Nil && *p != "" || !fact.Nil && fact.Value != *p) {
|
||||||
|
return nil, fmt.Errorf("conflicting mapped invoice fact %s", name)
|
||||||
|
}
|
||||||
|
if !has && p != nil && *p != "" {
|
||||||
|
fact, has = InvoiceFact{Name: name, Value: *p}, true
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
if err := invoiceWriteFact(&b, value, fact, options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
header[name] = true
|
header[name] = true
|
||||||
}
|
}
|
||||||
for _, fact := range value.Facts {
|
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] {
|
if header[fact.Name] {
|
||||||
return nil, fmt.Errorf("duplicate mapped invoice fact %s", fact.Name)
|
continue
|
||||||
}
|
}
|
||||||
if err := invoiceWriteFact(&b, value, fact, options); err != nil {
|
if err := invoiceWriteFact(&b, value, fact, options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1488,6 +1533,63 @@ 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")
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invoiceEncodeEDRM 按2019版路径创建业务XML,不伪造认证字段或丢弃扩展事实
|
||||||
|
// 入参: value 明确提供的发票业务字段
|
||||||
|
// 返回: 独立XML及不可表达错误
|
||||||
|
func invoiceEncodeEDRM(value Invoice) ([]byte, error) {
|
||||||
|
if len(value.Facts) != 0 {
|
||||||
|
return nil, fmt.Errorf("EDRM facts require source-preserving invoice update")
|
||||||
|
}
|
||||||
|
if _, err := time.Parse("2006-01-02", value.Date); err != nil {
|
||||||
|
return nil, fmt.Errorf("EDRM creation requires a calendar date")
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(xml.Header)
|
||||||
|
fmt.Fprintf(&b, `<eInvoice xmlns="%s" Version="1.0">`, invoiceEDRM)
|
||||||
|
write := func(name, text string) {
|
||||||
|
if text != "" {
|
||||||
|
fmt.Fprintf(&b, "<%s>%s</%s>", name, invoiceWriteEscape(text), name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, name := range strings.Fields("InvoiceCode InvoiceNo IssueDate Buyer Seller TaxInclusiveTotalAmount Note TaxTotalAmount TaxExclusiveTotalAmount") {
|
||||||
|
if name == "Buyer" || name == "Seller" {
|
||||||
|
fmt.Fprintf(&b, "<%s>", name)
|
||||||
|
for _, path := range invoiceEDRMPaths() {
|
||||||
|
if strings.HasPrefix(path, "/eInvoice/"+name+"/") {
|
||||||
|
p, _ := invoiceEDRMField(&value, nil, path)
|
||||||
|
write(path[strings.LastIndexByte(path, '/')+1:], *p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "</%s>", name)
|
||||||
|
} else {
|
||||||
|
p, _ := invoiceEDRMField(&value, nil, "/eInvoice/"+name)
|
||||||
|
write(name, *p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(value.Items) > 0 {
|
||||||
|
b.WriteString("<GoodsInfos>")
|
||||||
|
for _, item := range value.Items {
|
||||||
|
if len(item.Fields) != 0 {
|
||||||
|
return nil, fmt.Errorf("EDRM item facts require source-preserving invoice update")
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(item, InvoiceItem{}) {
|
||||||
|
return nil, fmt.Errorf("EDRM creation requires a nonempty item")
|
||||||
|
}
|
||||||
|
b.WriteString("<GoodsInfo>")
|
||||||
|
for _, path := range invoiceEDRMPaths() {
|
||||||
|
if strings.HasPrefix(path, "/eInvoice/GoodsInfos/GoodsInfo/") {
|
||||||
|
p, _ := invoiceEDRMField(&value, &item, path)
|
||||||
|
write(path[strings.LastIndexByte(path, '/')+1:], *p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("</GoodsInfo>")
|
||||||
|
}
|
||||||
|
b.WriteString("</GoodsInfos>")
|
||||||
|
}
|
||||||
|
b.WriteString("</eInvoice>")
|
||||||
|
return []byte(b.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
// invoiceUpdateEDRM 按2019版原节点写回摘要及明细,保留扩展与未修改字节
|
// invoiceUpdateEDRM 按2019版原节点写回摘要及明细,保留扩展与未修改字节
|
||||||
// 入参: ctx 上下文,data 原文,old 原发票,value 目标发票
|
// 入参: ctx 上下文,data 原文,old 原发票,value 目标发票
|
||||||
// 返回: 修改后XML及错误
|
// 返回: 修改后XML及错误
|
||||||
@@ -2068,7 +2170,7 @@ func invoiceWriteOrderedInsert(data []byte, spans map[string]invoiceXMLSpan, par
|
|||||||
// 入参: value 发票
|
// 入参: value 发票
|
||||||
// 返回: 缺少元组字段错误
|
// 返回: 缺少元组字段错误
|
||||||
func invoiceWriteCreation(value Invoice) error {
|
func invoiceWriteCreation(value Invoice) error {
|
||||||
if value.Standard == InvoiceStandardEInvoice {
|
if value.Standard == InvoiceStandardEInvoice || value.Standard == invoiceEDRM {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for i, item := range value.Items {
|
for i, item := range value.Items {
|
||||||
@@ -2080,7 +2182,7 @@ func invoiceWriteCreation(value Invoice) error {
|
|||||||
p := invoiceItemField(&item, value.Kind, name)
|
p := invoiceItemField(&item, value.Kind, name)
|
||||||
present := p != nil && *p != ""
|
present := p != nil && *p != ""
|
||||||
for _, f := range item.Fields {
|
for _, f := range item.Fields {
|
||||||
if f.Name == name && (f.Nil || f.Value != "") {
|
if f.Name == name {
|
||||||
present = true
|
present = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2105,7 +2207,7 @@ func invoiceWriteKnownFact(kind, name string) bool {
|
|||||||
var extras string
|
var extras string
|
||||||
switch kind {
|
switch kind {
|
||||||
case "inv":
|
case "inv":
|
||||||
extras = "TypeOfInvoice UniqueCodeOfInvoice LocationOfInvoice IdentifyingCode NumberOfInvoiceMachine SecurityCode TaxIncludedAmountInWords Payee Reviewer Issuer AddressPhoneNumberOfPurchaser DepositBankAndAccountNumberOfPurchaser AddressPhoneNumberOfSeller DepositBankAndAccountNumberOfSeller"
|
extras = "TypeOfInvoice UniqueCodeOfInvoice LocationOfInvoice IdentifyingCode NumberOfInvoiceMachine SecurityCode SpecialInvoiceType InvoiceIssuingSign TaxIncludedAmountInWords Payee Reviewer Issuer AddressPhoneNumberOfPurchaser DepositBankAndAccountNumberOfPurchaser AddressPhoneNumberOfSeller DepositBankAndAccountNumberOfSeller"
|
||||||
case "atr":
|
case "atr":
|
||||||
extras = "PassengerName ValidIdNumber Endorsement NumberOfGpOrder Fare FuelSurcharge VatRate CivilAviationDevelopmentFund OtherTaxes ETicketNumber VerificationCode PromptInformation Insurance AgentCode IssueParty MarkingOfDomesticOrInternational"
|
extras = "PassengerName ValidIdNumber Endorsement NumberOfGpOrder Fare FuelSurcharge VatRate CivilAviationDevelopmentFund OtherTaxes ETicketNumber VerificationCode PromptInformation Insurance AgentCode IssueParty MarkingOfDomesticOrInternational"
|
||||||
case "rai":
|
case "rai":
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const InvoiceStandardAir2023 = "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/atr"
|
|||||||
// InvoiceStandardRail2023 表示2023版铁路电子客票分类标准
|
// InvoiceStandardRail2023 表示2023版铁路电子客票分类标准
|
||||||
const InvoiceStandardRail2023 = "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/rai"
|
const InvoiceStandardRail2023 = "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/rai"
|
||||||
|
|
||||||
// InvoiceStandardVAT2021 表示2021版增值税发票分类标准,仅支持读取
|
// InvoiceStandardVAT2021 表示2021版增值税发票分类标准
|
||||||
const InvoiceStandardVAT2021 = "http://xbrl.mof.gov.cn/taxonomy/2021-09-30/inv"
|
const InvoiceStandardVAT2021 = "http://xbrl.mof.gov.cn/taxonomy/2021-09-30/inv"
|
||||||
|
|
||||||
// InvoiceStandardAir2021 表示2021版航空运输电子客票行程单分类标准
|
// InvoiceStandardAir2021 表示2021版航空运输电子客票行程单分类标准
|
||||||
@@ -73,7 +73,7 @@ const InvoiceStandardAir2021 = "http://xbrl.mof.gov.cn/taxonomy/2021-11-30/atr"
|
|||||||
// InvoiceStandardEInvoice 表示无命名空间的EInvoice业务标准,版本由Invoice.Version指定
|
// InvoiceStandardEInvoice 表示无命名空间的EInvoice业务标准,版本由Invoice.Version指定
|
||||||
const InvoiceStandardEInvoice = "EInvoice"
|
const InvoiceStandardEInvoice = "EInvoice"
|
||||||
|
|
||||||
// InvoiceStandardObjectReference 表示基于OFD对象引用的只读发票标引
|
// InvoiceStandardObjectReference 表示基于OFD对象引用的发票标引,业务值来自当前文档的图元
|
||||||
const InvoiceStandardObjectReference = ofdNamespace2016
|
const InvoiceStandardObjectReference = ofdNamespace2016
|
||||||
|
|
||||||
// InvoiceStandardEDRM2019 表示2019版电子发票业务标准
|
// InvoiceStandardEDRM2019 表示2019版电子发票业务标准
|
||||||
@@ -1037,6 +1037,13 @@ func invoiceObjectReferences(root *invoiceXMLNode) bool {
|
|||||||
return walk(root) && count > 0
|
return walk(root) && count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invoiceReferenceRoot 识别样例使用的OFD发票标引根,不推断其他业务命名空间
|
||||||
|
// 入参: root XML根节点
|
||||||
|
// 返回: bool 是否为对象引用发票标引
|
||||||
|
func invoiceReferenceRoot(root *invoiceXMLNode) bool {
|
||||||
|
return root != nil && root.name.Space == ofdNamespace2016 && root.name.Local == "root" && invoiceObjectReferences(root)
|
||||||
|
}
|
||||||
|
|
||||||
// invoicePlainXML 解析数电发票0.3及2019版电子发票XML
|
// invoicePlainXML 解析数电发票0.3及2019版电子发票XML
|
||||||
// 入参: ctx 上下文,root 根节点,source 来源
|
// 入参: ctx 上下文,root 根节点,source 来源
|
||||||
// 返回: 发票及诊断,取消错误
|
// 返回: 发票及诊断,取消错误
|
||||||
|
|||||||
+86
-1
@@ -122,7 +122,7 @@ func (r *Reader) readStructureContext(ctx context.Context, tag CustomTag) (Struc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return item, fmt.Errorf("read custom tag %s: %w", tag.FileLoc, err)
|
return item, fmt.Errorf("read custom tag %s: %w", tag.FileLoc, err)
|
||||||
}
|
}
|
||||||
item.CanBind = structureOfficial(tag, item.Root.Namespace)
|
item.CanBind = structureOfficial(tag, item.Root.Namespace) || structureInvoiceReferences(item.Root)
|
||||||
item.Internal = tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && item.Root.Namespace == richTextNamespace && item.Root.Name == "RichText"
|
item.Internal = tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && item.Root.Namespace == richTextNamespace && item.Root.Name == "RichText"
|
||||||
item.Internal = item.Internal || tag.TypeID == invoiceTagType && tag.NameSpace == invoiceNamespace && item.Root.Namespace == invoiceNamespace && item.Root.Name == "Invoice"
|
item.Internal = item.Internal || tag.TypeID == invoiceTagType && tag.NameSpace == invoiceNamespace && item.Root.Namespace == invoiceNamespace && item.Root.Name == "Invoice"
|
||||||
return item, nil
|
return item, nil
|
||||||
@@ -213,9 +213,94 @@ func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*Structure
|
|||||||
if root == nil {
|
if root == nil {
|
||||||
return nil, fmt.Errorf("empty custom tag document")
|
return nil, fmt.Errorf("empty custom tag document")
|
||||||
}
|
}
|
||||||
|
if structureInvoiceReferences(root) {
|
||||||
|
var bind func(*StructureNode)
|
||||||
|
bind = func(node *StructureNode) {
|
||||||
|
if node.Name == "ObjectRef" {
|
||||||
|
for _, attr := range node.Attributes {
|
||||||
|
if attr.Name == "PageRef" && attr.Namespace == "" {
|
||||||
|
node.References = []StructureReference{{PageID: attr.Value, ObjectIDs: structureObjectIDs(node.Text)}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range node.Children {
|
||||||
|
bind(&node.Children[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bind(root)
|
||||||
|
}
|
||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// structureInvoiceReferences 仅识别已核实的root和InvoiceNo发票引用树,不扩展公文类型规则
|
||||||
|
// 入参: root 标引根节点
|
||||||
|
// 返回: bool 是否为纯对象引用发票
|
||||||
|
func structureInvoiceReferences(root *StructureNode) bool {
|
||||||
|
if root == nil || root.Name != "root" || root.Namespace != ofdNamespace2016 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
known, refs := false, 0
|
||||||
|
for _, child := range root.Children {
|
||||||
|
known = known || child.Name == "InvoiceNo" && child.Namespace == ofdNamespace2016
|
||||||
|
}
|
||||||
|
var check func(*StructureNode) bool
|
||||||
|
check = func(node *StructureNode) bool {
|
||||||
|
if node.Namespace != ofdNamespace2016 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if node.Name == "ObjectRef" {
|
||||||
|
page := ""
|
||||||
|
for _, attr := range node.Attributes {
|
||||||
|
if attr.Name == "PageRef" && attr.Namespace == "" {
|
||||||
|
page = attr.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids := structureObjectIDs(node.Text)
|
||||||
|
if len(node.Children) != 0 || len(ids) == 0 || editorResourceID(page) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, id := range ids {
|
||||||
|
if editorResourceID(id) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refs++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if node.Name == "ObjectData" {
|
||||||
|
return len(node.Children) == 0
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(node.Text) != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range node.Children {
|
||||||
|
if !check(&node.Children[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return known && check(root) && refs > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// structureReferenceTags 按声明及实际内容识别可维护引用的标引,不将未知解析失败当作公文
|
||||||
|
// 入参: ctx 取消上下文, tags 当前文档标引声明
|
||||||
|
// 返回: []bool 与声明顺序对应的引用能力, error 取消错误
|
||||||
|
func (r *Reader) structureReferenceTags(ctx context.Context, tags []CustomTag) ([]bool, error) {
|
||||||
|
known := make([]bool, len(tags))
|
||||||
|
for i, tag := range tags {
|
||||||
|
known[i] = structureOfficial(tag, tag.NameSpace)
|
||||||
|
if !known[i] {
|
||||||
|
item, err := r.readStructureContext(ctx, tag)
|
||||||
|
known[i] = err == nil && item.CanBind
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return known, nil
|
||||||
|
}
|
||||||
|
|
||||||
// structureOfficial 识别公文、富文本及发票标引的明确引用规则,不推断未知行业语义
|
// structureOfficial 识别公文、富文本及发票标引的明确引用规则,不推断未知行业语义
|
||||||
// 入参: tag 标引入口, namespace 内容根节点命名空间
|
// 入参: tag 标引入口, namespace 内容根节点命名空间
|
||||||
// 返回: bool 是否使用公文引用规则
|
// 返回: bool 是否使用公文引用规则
|
||||||
|
|||||||
Reference in new issue
Block a user