mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
528162a276
commit
30f4f76260
6 files changed
+1697
-10
No files matched your search
@@ -411,7 +411,7 @@
|
||||
<h2 id="settingsTitle" class="form-title">文档配置</h2>
|
||||
<span id="settingsLocation" class="settings-location"></span>
|
||||
</header>
|
||||
<label class="form-row"><span>类型</span><select id="settingsKind"><option value="bookmarks">书签</option><option value="actions">动作</option><option value="templates">模板</option><option value="layers">图层</option><option value="extensions">扩展</option><option value="ColorSpace" hidden>色彩</option><option value="DrawParam" hidden>样式</option></select></label>
|
||||
<label class="form-row"><span>类型</span><select id="settingsKind"><option value="bookmarks">书签</option><option value="actions">动作</option><option value="templates">模板</option><option value="layers">图层</option><option value="extensions">扩展</option><option value="invoices">发票</option><option value="ColorSpace" hidden>色彩</option><option value="DrawParam" hidden>样式</option></select></label>
|
||||
<div id="settingsBody" class="settings-body">
|
||||
<div id="settingsContext"></div>
|
||||
<div id="settingsContent"></div>
|
||||
|
||||
@@ -1810,6 +1810,127 @@ body[data-hide-meta] .meta-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-dialog.invoice-dialog {
|
||||
width: 820px;
|
||||
max-width: calc(100vw - 26px);
|
||||
}
|
||||
|
||||
.invoice-tabs {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-block: 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.invoice-tabs > .button {
|
||||
flex: none;
|
||||
min-width: 2em;
|
||||
padding: 8px 0;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.invoice-tabs > .button[aria-selected="true"] {
|
||||
border-bottom-color: currentColor;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.invoice-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-block: 8px;
|
||||
}
|
||||
|
||||
.invoice-group-title {
|
||||
margin: 12px 0 8px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.invoice-advanced {
|
||||
margin-block: 12px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.invoice-advanced > summary {
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.invoice-advanced > .button {
|
||||
margin: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.invoice-unit {
|
||||
padding-block: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.invoice-table-scroll {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.invoice-table {
|
||||
width: 100%;
|
||||
min-width: 720px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.invoice-table th,
|
||||
.invoice-table td {
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.invoice-table th:first-child {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
.invoice-table th:nth-child(2) {
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
.invoice-table input:not([type="radio"]) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.invoice-dialog #settingsInvoiceXML {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.invoice-dialog .settings-footer {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.invoice-dialog #settingsStatus {
|
||||
min-width: 0;
|
||||
max-height: 6em;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.invoice-dialog #settingsActions {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.invoice-dialog #settingsActions {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-dialog :focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
+773
-3
@@ -27,6 +27,7 @@ const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "K
|
||||
const resourceManager = { items: [], selected: null, references: [], referenceOffset: 0, targets: [], pick: null, busy: false, sequence: 0, url: null, fileAction: null };
|
||||
const settingsElements = Object.fromEntries(["Button", "Panel", "Form", "Title", "Kind", "Context", "Content", "Actions", "Status"].map(name => [name, document.querySelector(`#settings${name}`)]));
|
||||
const settings = { sequence: 0, busy: false, page: 0, scope: "document", object: "", path: [], selected: -1, data: null, actions: [], drafts: new Map(), view: "", baseline: {}, suspended: false };
|
||||
const invoicePanel = { tab: "basic", entries: [], formats: [], drafts: new Map(), key: "", result: null };
|
||||
|
||||
let wasmPromise = null;
|
||||
let remoteDownload = null;
|
||||
@@ -1531,7 +1532,7 @@ editorClick(settingsElements.Button, openDocumentSettings);
|
||||
settingsElements.Kind.addEventListener("change", () => { rememberSettingsDraft(); settings.selected = -1; loadDocumentSettings(); });
|
||||
settingsElements.Form.addEventListener("submit", event => event.preventDefault());
|
||||
settingsElements.Panel.addEventListener("cancel", event => { event.preventDefault(); closeDocumentSettings(); });
|
||||
settingsElements.Panel.addEventListener("close", () => { if (!settings.suspended) { settings.sequence++; settings.drafts.clear(); settings.view = ""; } });
|
||||
settingsElements.Panel.addEventListener("close", () => { if (!settings.suspended) { settings.sequence++; settings.drafts.clear(); settings.view = ""; invoicePanel.drafts.clear(); invoicePanel.key = ""; } });
|
||||
document.querySelector("#objectActionsButton").addEventListener("click", async () => {
|
||||
const item = canvasEditor.selected;
|
||||
if (!item || item.scoped || item.items) return;
|
||||
@@ -3640,6 +3641,7 @@ async function changeDocument(name, item, ...args) {
|
||||
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope.key, Array.isArray(item.id) ? item.id : members.map(member => member.id), operation, ...args)
|
||||
: annotated && operation ? await callWASM("ofdgoChangeSelection", item.index, ids, operation, ...args)
|
||||
: await callWASM(name, ...(item ? [item.index, item.id] : []), ...args);
|
||||
if (name === "ofdgoChangeInvoice") invoicePanel.result = doc.invoiceResult;
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
@@ -4622,6 +4624,7 @@ async function openSelectedFonts(event) {
|
||||
if (changed) {
|
||||
await applyFontChange();
|
||||
if (resourceElements.Panel.open) await resourceTask(loadManagedResources);
|
||||
if (settingsElements.Panel.open && settingsElements.Kind.value === "invoices") { await loadEditorFonts(); renderDocumentSettings(); }
|
||||
}
|
||||
setStatus(saved ? "字体已保存" : "字体未保存,仅本次可用");
|
||||
} catch (err) {
|
||||
@@ -5427,6 +5430,7 @@ function settingsSnapshot() {
|
||||
}
|
||||
|
||||
function rememberSettingsDraft() {
|
||||
if (settings.renderedKind === "invoices") return;
|
||||
if (!state.editing || !settings.view || settings.busy) return;
|
||||
const values = settingsSnapshot();
|
||||
const changed = Object.keys(values).some(id => {
|
||||
@@ -5475,7 +5479,7 @@ function restoreSettingsDraft() {
|
||||
function closeDocumentSettings() {
|
||||
if (settings.busy) return;
|
||||
rememberSettingsDraft();
|
||||
if (settings.drafts.size && !window.confirm("配置尚未应用,放弃更改?")) return;
|
||||
if ((settings.drafts.size || [...invoicePanel.drafts.values()].some(draft => draft.dirty.size)) && !window.confirm("配置尚未应用,放弃更改?")) return;
|
||||
settingsElements.Panel.close();
|
||||
}
|
||||
|
||||
@@ -5499,6 +5503,8 @@ function settingsValue(id) {
|
||||
}
|
||||
|
||||
async function openDocumentSettings() {
|
||||
invoicePanel.drafts.clear();
|
||||
invoicePanel.key = "";
|
||||
if (!state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return;
|
||||
if (resourceManager.pick?.settings) { finishResourcePick(); return; }
|
||||
settings.page = state.pageIndex;
|
||||
@@ -5538,6 +5544,7 @@ async function loadDocumentSettings() {
|
||||
settingsElements.Form.inert = true;
|
||||
settingsElements.Status.textContent = "正在读取信息";
|
||||
try {
|
||||
if (kind === "invoices") { await loadInvoiceSettings(sequence, openSeq); return; }
|
||||
const data = ["bookmarks", "actions", "extensions"].includes(kind) ? await callWASM("ofdgoDocumentNavigation", kind) : {};
|
||||
if (sequence !== settings.sequence || openSeq !== state.openSeq || !settingsElements.Panel.open) return;
|
||||
data.resources = ["bookmarks", "extensions"].includes(kind) ? [] : await callWASM("ofdgoResources") || [];
|
||||
@@ -5588,6 +5595,7 @@ function renderDocumentSettings() {
|
||||
settingsElements.Content.replaceChildren();
|
||||
settingsElements.Actions.replaceChildren();
|
||||
const kind = settingsElements.Kind.value;
|
||||
settings.renderedKind = kind;
|
||||
const location = document.querySelector("#settingsLocation"), doc = state.doc;
|
||||
if (location) {
|
||||
const current = doc?.documents?.find(entry => entry.index === doc.documentIndex) || doc?.documents?.[doc?.documentIndex || 0];
|
||||
@@ -5595,6 +5603,12 @@ function renderDocumentSettings() {
|
||||
location.hidden = !(doc?.documents?.length > 1 || doc?.documentVersion);
|
||||
}
|
||||
settingsElements.Title.textContent = {ColorSpace: "色彩设置", DrawParam: "样式设置"}[kind] || "文档配置";
|
||||
settingsElements.Panel.classList.toggle("invoice-dialog", kind === "invoices");
|
||||
if (kind === "invoices") {
|
||||
renderInvoiceSettings();
|
||||
settingsButton("关闭", closeDocumentSettings, false, "Close").classList.add("settings-close");
|
||||
return;
|
||||
}
|
||||
if (kind === "actions") renderActionSettings();
|
||||
else if (kind === "bookmarks") renderBookmarkSettings();
|
||||
else if (kind === "extensions") renderExtensionSettings();
|
||||
@@ -5612,6 +5626,758 @@ function renderDocumentSettings() {
|
||||
settingsButton("关闭", closeDocumentSettings, false, "Close").classList.add("settings-close");
|
||||
}
|
||||
|
||||
function invoiceSourceKey(source = {}) {
|
||||
return JSON.stringify([source.kind || "", source.id || "", source.fileLoc || "", source.nodePath || ""]);
|
||||
}
|
||||
|
||||
function invoiceScope() {
|
||||
return {
|
||||
documentIndex: state.doc.documentIndex || 0,
|
||||
version: state.doc.documentVersion || "",
|
||||
...(state.editing ? { revision: state.editorInfo.revision } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function invoiceCandidates(invoices) {
|
||||
const counts = new Map(), result = [];
|
||||
for (const invoice of invoices || []) {
|
||||
for (const source of new Map((invoice.sources || []).map(value => [invoiceSourceKey(value), value])).values()) {
|
||||
const sourceKey = invoiceSourceKey(source);
|
||||
const index = counts.get(sourceKey) || 0;
|
||||
counts.set(sourceKey, index + 1);
|
||||
result.push({ invoice, source, index, key: JSON.stringify([sourceKey, index]) });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function loadInvoiceSettings(sequence, openSeq) {
|
||||
const data = await callWASM("ofdgoInvoices");
|
||||
if (sequence !== settings.sequence || openSeq !== state.openSeq || !settingsElements.Panel.open) return;
|
||||
invoicePanel.entries = invoiceCandidates(data.report?.invoices);
|
||||
invoicePanel.formats = data.formats || [];
|
||||
invoicePanel.issues = data.report?.issues || [];
|
||||
if (!invoicePanel.drafts.has(invoicePanel.key) && !invoicePanel.entries.some(entry => entry.key === invoicePanel.key)) invoicePanel.key = invoicePanel.entries[0]?.key || "";
|
||||
const entry = invoicePanel.entries.find(entry => entry.key === invoicePanel.key);
|
||||
if (entry && !invoicePanel.drafts.has(entry.key)) {
|
||||
const scope = invoiceScope();
|
||||
const detail = await callWASM("ofdgoInvoice", JSON.stringify({ source: entry.source, index: entry.index, ...scope }));
|
||||
if (sequence !== settings.sequence || openSeq !== state.openSeq || !settingsElements.Panel.open) return;
|
||||
invoicePanel.drafts.set(entry.key, invoiceDraft(detail, entry, scope));
|
||||
}
|
||||
settingsElements.Status.textContent = "";
|
||||
renderDocumentSettings();
|
||||
}
|
||||
|
||||
function invoiceDraft(detail, entry, scope) {
|
||||
return {
|
||||
detail, invoice: structuredClone(detail.invoice), entry, scope,
|
||||
dirty: new Set(), changes: new Map(),
|
||||
addedItems: new Set(),
|
||||
bindings: structuredClone(detail.bindings || []), xml: detail.xml || "", page: state.pageIndex, object: "", field: "",
|
||||
layout: { font: "", fontSize: 4, pageWidth: 210, pageHeight: 297, margin: 15 }, synchronize: !!detail.bindings?.length && !!detail.capabilities?.sync,
|
||||
options: { name: "", encoding: { schemaRef: "", context: {}, units: [] } },
|
||||
fragments: {}, item: 0
|
||||
};
|
||||
}
|
||||
|
||||
function invoiceCurrent() {
|
||||
return invoicePanel.drafts.get(invoicePanel.key);
|
||||
}
|
||||
|
||||
function invoiceDirty(draft, section) {
|
||||
draft.dirty.add(section);
|
||||
const button = document.querySelector("#settingsInvoiceApply");
|
||||
if (button) button.disabled = !state.editing;
|
||||
const reset = document.querySelector("#settingsInvoiceReset");
|
||||
if (reset) reset.disabled = !state.editing;
|
||||
}
|
||||
|
||||
function invoiceInput(container, label, id, value, change, disabled = false, type = "text", unit = "") {
|
||||
const field = settingsField(container, label, `Invoice${id}`, value, type, unit);
|
||||
field.disabled = disabled;
|
||||
field.addEventListener("input", () => change(type === "checkbox" ? field.checked : field.value));
|
||||
if (!state.editing && type !== "textarea") {
|
||||
const text = document.createElement("span");
|
||||
text.className = "settings-readonly";
|
||||
text.id = field.id;
|
||||
text.textContent = type === "checkbox" ? value ? "是" : "否" : value || "-";
|
||||
field.replaceWith(text);
|
||||
return text;
|
||||
}
|
||||
if (!state.editing && type === "textarea") {
|
||||
field.disabled = false;
|
||||
field.readOnly = true;
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
function invoiceValue(invoice, field, item = -1) {
|
||||
const keys = field.split(".");
|
||||
let value = keys[0] === "items" ? invoice.items?.[item] : invoice;
|
||||
if (keys[0] === "items") keys.shift();
|
||||
for (const key of keys) value = value?.[key];
|
||||
return value ?? "";
|
||||
}
|
||||
|
||||
function invoiceSetValue(invoice, field, item, value) {
|
||||
const keys = field.split(".");
|
||||
let target = keys[0] === "items" ? invoice.items[item] : invoice;
|
||||
if (keys[0] === "items") keys.shift();
|
||||
for (const key of keys.slice(0, -1)) target = target[key] ||= {};
|
||||
target[keys.at(-1)] = value;
|
||||
}
|
||||
|
||||
function invoiceIssues(issues) {
|
||||
return (issues || []).map(issue => typeof issue === "string" ? issue : [issue.field, issue.message || issue.code].filter(Boolean).join(":")).join("\n");
|
||||
}
|
||||
|
||||
function renderInvoiceSettings() {
|
||||
const context = settingsElements.Context, content = settingsElements.Content;
|
||||
const draft = invoiceCurrent();
|
||||
const sources = [...new Map(invoicePanel.entries.map(entry => [invoiceSourceKey(entry.source), entry.source])).entries()];
|
||||
if (sources.length) settingsSelect(context, "来源", "InvoiceSource", sources.map(([key, source]) => [key, source.fileLoc || source.id || source.kind]), draft?.entry ? invoiceSourceKey(draft.entry.source) : null, value => {
|
||||
invoicePanel.key = invoicePanel.entries.find(entry => invoiceSourceKey(entry.source) === value).key;
|
||||
loadDocumentSettings();
|
||||
});
|
||||
const candidates = invoicePanel.entries.filter(entry => !draft?.entry || invoiceSourceKey(entry.source) === invoiceSourceKey(draft.entry.source));
|
||||
const choices = candidates.map(entry => [entry.key, entry.invoice.number || entry.invoice.code || `${entry.invoice.kind} · ${entry.index + 1}`]);
|
||||
if (invoicePanel.drafts.has("new")) choices.push(["new", "新建"]);
|
||||
if (choices.length) settingsSelect(context, "发票", "InvoiceEntry", choices, invoicePanel.key, value => { invoicePanel.key = value; loadDocumentSettings(); });
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "invoice-actions";
|
||||
context.append(actions);
|
||||
if (state.editing) {
|
||||
settingsButton("新建", newInvoiceDraft, false, "InvoiceNew", actions);
|
||||
settingsButton("导入", () => readInvoiceFile(async text => {
|
||||
const value = await newInvoiceDraft();
|
||||
if (!value) return;
|
||||
value.xml = text;
|
||||
value.importing = true;
|
||||
value.dirty.add("xml");
|
||||
invoicePanel.tab = "xml";
|
||||
renderDocumentSettings();
|
||||
}), false, "InvoiceImport", actions);
|
||||
}
|
||||
if (!draft) {
|
||||
settingsElements.Status.textContent = invoiceIssues(invoicePanel.issues) || "暂无发票";
|
||||
return;
|
||||
}
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "invoice-tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
tabs.setAttribute("aria-label", "发票信息");
|
||||
for (const [key, label, id] of [["basic", "基本", "Basic"], ["items", "明细", "Items"], ["fields", "字段", "Fields"], ["appearance", "票面", "Appearance"], ["xml", "原文", "XML"]]) {
|
||||
const button = settingsButton(label, () => { invoicePanel.tab = key; renderDocumentSettings(); }, false, `InvoiceTab${id}`, tabs);
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(invoicePanel.tab === key));
|
||||
button.setAttribute("aria-controls", "invoiceTabContent");
|
||||
button.tabIndex = invoicePanel.tab === key ? 0 : -1;
|
||||
button.addEventListener("keydown", event => {
|
||||
const buttons = [...tabs.children];
|
||||
const index = buttons.indexOf(button);
|
||||
const next = event.key === "ArrowRight" ? (index + 1) % buttons.length : event.key === "ArrowLeft" ? (index + buttons.length - 1) % buttons.length : event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : -1;
|
||||
if (next >= 0) {
|
||||
event.preventDefault();
|
||||
const target = buttons[next].id;
|
||||
buttons[next].click();
|
||||
document.getElementById(target)?.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
content.append(tabs);
|
||||
const body = document.createElement("div");
|
||||
body.id = "invoiceTabContent";
|
||||
body.setAttribute("role", "tabpanel");
|
||||
body.setAttribute("aria-labelledby", tabs.querySelector('[aria-selected="true"]').id);
|
||||
content.append(body);
|
||||
if (invoicePanel.tab === "basic") renderInvoiceBasic(body, draft);
|
||||
else if (invoicePanel.tab === "items") renderInvoiceItems(body, draft);
|
||||
else if (invoicePanel.tab === "fields") renderInvoiceFields(body, draft);
|
||||
else if (invoicePanel.tab === "appearance") renderInvoiceAppearance(body, draft);
|
||||
else renderInvoiceXML(body, draft);
|
||||
settingsButton("校验", () => validateInvoiceDraft(draft), false, "InvoiceValidate");
|
||||
settingsButton("导出", () => downloadBytes(new TextEncoder().encode(JSON.stringify(draft.invoice, null, 2)), "application/json", "invoice.json"), false, "InvoiceExportJSON").title = "导出JSON";
|
||||
if (state.editing) {
|
||||
settingsButton("应用", () => applyInvoiceDraft(draft), !draft.dirty.size, "InvoiceApply");
|
||||
settingsButton("还原", () => resetInvoiceDraft(draft), !draft.dirty.size, "InvoiceReset");
|
||||
if (draft.entry) settingsButton("删除", async () => {
|
||||
if (draft.dirty.size) throw new Error("请先应用或还原草稿");
|
||||
if (window.confirm("删除此来源中的发票数据?页面将保留" + invoiceOtherDraftWarning(draft))) await commitInvoice(draft, { operation: "delete" });
|
||||
}, !draft.detail.capabilities?.delete, "InvoiceDelete");
|
||||
}
|
||||
settingsElements.Status.textContent = draft.status || invoiceIssues(draft.detail.issues) || "";
|
||||
}
|
||||
|
||||
async function newInvoiceDraft() {
|
||||
if (!state.editing) return;
|
||||
if (invoicePanel.drafts.get("new")?.dirty.size && !window.confirm("放弃尚未应用的新建发票?")) return;
|
||||
const format = invoicePanel.formats.find(value => value.create);
|
||||
if (!format) throw new Error("暂无可新建票种");
|
||||
const invoice = { kind: format.kind, standard: format.standard, version: format.version, buyer: {}, seller: {}, items: [], facts: [] };
|
||||
const scope = invoiceScope();
|
||||
const sequence = settings.sequence;
|
||||
const detail = await callWASM("ofdgoInvoice", JSON.stringify({ invoice, ...scope }));
|
||||
if (sequence !== settings.sequence || !settingsElements.Panel.open) return;
|
||||
invoicePanel.drafts.set("new", invoiceDraft(detail, null, scope));
|
||||
invoicePanel.key = "new";
|
||||
invoicePanel.tab = "basic";
|
||||
renderDocumentSettings();
|
||||
return invoiceCurrent();
|
||||
}
|
||||
|
||||
function renderInvoiceBasic(container, draft) {
|
||||
const fields = (draft.detail.fields || []).filter(field => field.item < 0 && !field.field.startsWith("facts.") && !field.field.startsWith("items."));
|
||||
const labels = {
|
||||
kind: "票种", standard: "规范", version: "版本", number: "号码", code: "代码", date: "日期",
|
||||
currency: "币种", status: "状态", amount: "金额", taxAmount: "税额", totalAmount: "合计", remarks: "备注",
|
||||
name: "名称", taxId: "税号", address: "地址", phone: "电话", bank: "银行", account: "账号"
|
||||
};
|
||||
for (const [group, label] of [["basic", "票据"], ["buyer", "购方"], ["seller", "销方"]]) {
|
||||
const title = document.createElement("h3"), grid = document.createElement("div");
|
||||
title.className = "invoice-group-title";
|
||||
title.textContent = label;
|
||||
grid.className = "settings-grid";
|
||||
container.append(title, grid);
|
||||
for (const field of fields.filter(field => field.group === group)) {
|
||||
invoiceInput(grid, labels[field.field.split(".").at(-1)] || field.label, `Field_${field.field.replaceAll(".", "_")}`, invoiceValue(draft.invoice, field.field), value => {
|
||||
invoiceSetValue(draft.invoice, field.field, -1, value);
|
||||
invoiceDirty(draft, "model");
|
||||
}, !state.editing || !(field.editable || !draft.entry && field.field === "currency" && draft.invoice.standard !== "EInvoice") || !!draft.entry && !draft.detail.capabilities?.edit).title = field.label;
|
||||
}
|
||||
}
|
||||
if (draft.entry && state.editing) invoiceInput(container, "同步", "Synchronize", draft.synchronize, value => { draft.synchronize = value; }, !draft.detail.capabilities?.sync, "checkbox").title = draft.detail.capabilities?.reasons?.sync || "同步票面";
|
||||
if (!draft.entry) renderInvoiceAdvanced(container, draft);
|
||||
}
|
||||
|
||||
function renderInvoiceAdvanced(container, draft) {
|
||||
const details = document.createElement("details"), summary = document.createElement("summary");
|
||||
summary.textContent = "高级";
|
||||
details.className = "invoice-advanced";
|
||||
details.open = draft.advanced || false;
|
||||
details.addEventListener("toggle", () => { draft.advanced = details.open; });
|
||||
details.append(summary);
|
||||
container.append(details);
|
||||
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);
|
||||
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 => {
|
||||
try {
|
||||
const format = formats[Number(value)];
|
||||
Object.assign(draft.invoice, { kind: format.kind, standard: format.standard, version: format.version });
|
||||
invoiceDirty(draft, "model");
|
||||
await invoiceRefreshFields(draft);
|
||||
if (invoiceCurrent() === draft) renderDocumentSettings();
|
||||
} catch (error) {
|
||||
settingsElements.Status.textContent = error.message;
|
||||
}
|
||||
});
|
||||
invoiceInput(details, "名称", "Name", draft.options.name, value => { draft.options.name = value; invoiceDirty(draft, "model"); });
|
||||
const encoding = draft.options.encoding;
|
||||
if (draft.invoice.standard !== "EInvoice") {
|
||||
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", "止日"]]) {
|
||||
invoiceInput(details, label, `Context_${key}`, encoding.context[key], value => { encoding.context[key] = value; invoiceDirty(draft, "model"); }).title = `context.${key}`;
|
||||
}
|
||||
for (const [index, unit] of encoding.units.entries()) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "invoice-unit";
|
||||
details.append(row);
|
||||
for (const [key, label] of [["id", "标识"], ["namespace", "空间"], ["name", "单位"]]) {
|
||||
invoiceInput(row, label, `Unit_${index}_${key}`, unit[key], value => { unit[key] = value; invoiceDirty(draft, "model"); }).title = key === "namespace" ? "命名空间" : label;
|
||||
}
|
||||
settingsButton("删除", () => {
|
||||
encoding.units.splice(index, 1);
|
||||
invoiceDirty(draft, "model");
|
||||
renderDocumentSettings();
|
||||
}, false, `InvoiceUnitDelete${index}`, row);
|
||||
}
|
||||
settingsButton("添加", () => {
|
||||
encoding.units.push({ id: "", namespace: "", name: "" });
|
||||
invoiceDirty(draft, "model");
|
||||
renderDocumentSettings();
|
||||
}, false, "InvoiceUnitAdd", details).title = "添加单位";
|
||||
}
|
||||
for (const [key, label] of [["headerXML", "票头"], ["sellerAuthenticationXML", "认证"], ["taxSupervisionXML", "监制"], ["taxBureauSignatureXML", "签名"]]) {
|
||||
invoiceInput(details, label, `Fragment_${key}`, draft.fragments[key], value => {
|
||||
draft.fragments[key] = value;
|
||||
invoiceDirty(draft, "model");
|
||||
}, false, "textarea");
|
||||
settingsButton("导入", () => readInvoiceFile(text => {
|
||||
draft.fragments[key] = text;
|
||||
invoiceDirty(draft, "model");
|
||||
renderDocumentSettings();
|
||||
}), false, `InvoiceFragmentImport_${key}`, details).title = `导入${label}XML`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderInvoiceItems(container, draft) {
|
||||
const items = draft.invoice.items ||= [];
|
||||
const editable = state.editing && (!draft.entry ? invoicePanel.formats.find(value => value.kind === draft.invoice.kind && value.standard === draft.invoice.standard && value.version === draft.invoice.version)?.items : draft.detail.capabilities?.items);
|
||||
draft.item = Math.max(0, Math.min(draft.item, items.length - 1));
|
||||
const columns = [["name", "名称"], ["specification", "规格"], ["unit", "单位"], ["quantity", "数量"], ["unitPrice", "单价"], ["amount", "金额"], ["taxRate", "税率"], ["taxAmount", "税额"]];
|
||||
const scroll = document.createElement("div"), table = document.createElement("table");
|
||||
const head = table.createTHead().insertRow();
|
||||
scroll.className = "invoice-table-scroll";
|
||||
table.className = "invoice-table";
|
||||
table.id = "settingsInvoiceItems";
|
||||
for (const label of ["选择", ...columns.map(entry => entry[1])]) {
|
||||
const cell = document.createElement("th");
|
||||
cell.scope = "col";
|
||||
cell.textContent = label;
|
||||
head.append(cell);
|
||||
}
|
||||
const body = table.createTBody();
|
||||
for (const [index, item] of items.entries()) {
|
||||
const row = body.insertRow(), radio = document.createElement("input");
|
||||
radio.type = "radio";
|
||||
radio.name = "invoiceItem";
|
||||
radio.checked = index === draft.item;
|
||||
radio.setAttribute("aria-label", `明细${index + 1}`);
|
||||
radio.addEventListener("change", () => {
|
||||
draft.item = index;
|
||||
renderDocumentSettings();
|
||||
});
|
||||
row.insertCell().append(radio);
|
||||
for (const [key, label] of columns) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.value = item[key] || "";
|
||||
input.id = `settingsInvoiceItem_${index}_${key}`;
|
||||
input.setAttribute("aria-label", `${label}${index + 1}`);
|
||||
const descriptor = draft.detail.fields?.find(field => field.field === `items.${key}` && field.item === index);
|
||||
input.disabled = !editable || !descriptor?.editable;
|
||||
input.addEventListener("input", () => {
|
||||
item[key] = input.value;
|
||||
invoiceDirty(draft, "model");
|
||||
});
|
||||
const cell = row.insertCell();
|
||||
if (!state.editing) {
|
||||
const text = document.createElement("span");
|
||||
text.className = "settings-readonly";
|
||||
text.textContent = item[key] || "-";
|
||||
cell.append(text);
|
||||
} else cell.append(input);
|
||||
}
|
||||
}
|
||||
scroll.append(table);
|
||||
container.append(scroll);
|
||||
if (state.editing) {
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "invoice-actions";
|
||||
container.append(actions);
|
||||
const reorder = async (direction) => {
|
||||
if (draft.changes.size) throw new Error("请先应用原字段修改,再调整明细顺序");
|
||||
const next = draft.item + direction;
|
||||
[items[next], items[draft.item]] = [items[draft.item], items[next]];
|
||||
draft.item = next;
|
||||
invoiceDirty(draft, "model");
|
||||
await invoiceRefreshFields(draft);
|
||||
renderDocumentSettings();
|
||||
};
|
||||
settingsButton("添加", async () => {
|
||||
const item = {};
|
||||
items.push(item);
|
||||
draft.addedItems.add(item);
|
||||
draft.item = items.length - 1;
|
||||
invoiceDirty(draft, "model");
|
||||
await invoiceRefreshFields(draft);
|
||||
renderDocumentSettings();
|
||||
}, !editable, "InvoiceItemAdd", actions);
|
||||
settingsButton("删除", async () => {
|
||||
if (draft.changes.size) throw new Error("请先应用原字段修改,再删除明细");
|
||||
items.splice(draft.item, 1);
|
||||
invoiceDirty(draft, "model");
|
||||
await invoiceRefreshFields(draft);
|
||||
renderDocumentSettings();
|
||||
}, !editable || !items.length, "InvoiceItemDelete", actions);
|
||||
settingsButton("上移", () => reorder(-1), !editable || draft.item === 0 || !items.length, "InvoiceItemUp", actions);
|
||||
settingsButton("下移", () => reorder(1), !editable || draft.item >= items.length - 1, "InvoiceItemDown", actions);
|
||||
}
|
||||
}
|
||||
|
||||
async function invoiceRefreshFields(draft) {
|
||||
const invoice = structuredClone(draft.invoice);
|
||||
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;
|
||||
}
|
||||
|
||||
function renderInvoiceFields(container, draft) {
|
||||
const facts = (draft.invoice.facts || []).map((fact, index) => ({ fact, field: `facts.${index}`, item: -1 }));
|
||||
for (const [item, value] of (draft.invoice.items || []).entries()) {
|
||||
for (const [index, fact] of (value.fields || []).entries()) {
|
||||
facts.push({ fact, field: `items.fields.${index}`, item });
|
||||
}
|
||||
}
|
||||
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}`]] : [])];
|
||||
if (state.editing && owners.length) {
|
||||
if (!owners.some(([index]) => index === draft.factOwner)) draft.factOwner = owners[0][0];
|
||||
settingsSelect(container, "归属", "InvoiceFactOwner", owners, draft.factOwner, value => { draft.factOwner = Number(value); });
|
||||
settingsButton("添加", () => {
|
||||
const owner = draft.factOwner;
|
||||
const target = owner < 0 ? draft.invoice.facts ||= [] : draft.invoice.items[owner].fields ||= [];
|
||||
target.push({ name: "", namespace: "", value: "", context: "", unit: "" });
|
||||
draft.fact = owner < 0 ? target.length - 1 : (draft.invoice.facts || []).length + draft.invoice.items.slice(0, owner).reduce((sum, item) => sum + (item.fields || []).length, 0) + target.length - 1;
|
||||
invoiceDirty(draft, "model");
|
||||
renderDocumentSettings();
|
||||
}, false, "InvoiceFactAdd", container);
|
||||
}
|
||||
if (!options.length) return;
|
||||
draft.fact = Math.min(draft.fact || 0, facts.length - 1);
|
||||
settingsSelect(container, "字段", "InvoiceFact", options, draft.fact, value => {
|
||||
draft.fact = Number(value);
|
||||
renderDocumentSettings();
|
||||
});
|
||||
const { fact, field, item } = facts[draft.fact];
|
||||
const patchKey = invoiceSourceKey(fact.source);
|
||||
const change = draft.changes.get(patchKey);
|
||||
const descriptor = draft.detail.fields?.find(value => value.field === field && value.item === item);
|
||||
const fresh = !draft.entry || item >= 0 && draft.addedItems.has(draft.invoice.items[item]);
|
||||
const writable = state.editing && (fresh || descriptor?.editable && !!fact.source?.nodePath);
|
||||
for (const [key, label] of [["name", "名称"], ["namespace", "空间"], ["context", "语境"], ["unit", "单位"], ["decimals", "小数"], ["precision", "精度"]]) {
|
||||
invoiceInput(container, label, `FactMeta_${key}`, fact[key], value => {
|
||||
fact[key] = value;
|
||||
invoiceDirty(draft, "model");
|
||||
}, !fresh || !state.editing).title = key === "namespace" ? "命名空间" : key === "context" ? "上下文标识" : label;
|
||||
}
|
||||
if (fact.source?.nodePath) invoiceInput(container, "路径", "FactPath", fact.source.nodePath, () => {}, true);
|
||||
const update = (key, value) => {
|
||||
if (fresh) {
|
||||
fact[key] = value;
|
||||
if (key === "nil" && value) {
|
||||
fact.value = "";
|
||||
document.querySelector("#settingsInvoiceFactValue").value = "";
|
||||
}
|
||||
if (key === "value") {
|
||||
fact.nil = false;
|
||||
document.querySelector("#settingsInvoiceFactNil").checked = false;
|
||||
}
|
||||
invoiceDirty(draft, "model");
|
||||
return;
|
||||
}
|
||||
const patch = draft.changes.get(patchKey) || {
|
||||
source: structuredClone(fact.source), value: fact.value || "", nil: !!fact.nil,
|
||||
expectedValue: fact.value || "", expectedNil: !!fact.nil
|
||||
};
|
||||
patch[key] = value;
|
||||
if ((key === "nil" || key === "delete") && value) {
|
||||
patch.value = "";
|
||||
document.querySelector("#settingsInvoiceFactValue").value = "";
|
||||
}
|
||||
if (key === "nil" && value) {
|
||||
patch.delete = false;
|
||||
document.querySelector("#settingsInvoiceFactDelete").checked = false;
|
||||
}
|
||||
if (key === "delete" && value) {
|
||||
patch.nil = false;
|
||||
document.querySelector("#settingsInvoiceFactNil").checked = false;
|
||||
}
|
||||
if (key === "value") {
|
||||
patch.nil = false;
|
||||
patch.delete = false;
|
||||
document.querySelector("#settingsInvoiceFactNil").checked = false;
|
||||
document.querySelector("#settingsInvoiceFactDelete").checked = false;
|
||||
}
|
||||
draft.changes.set(patchKey, patch);
|
||||
invoiceDirty(draft, "fields");
|
||||
};
|
||||
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");
|
||||
if (!fresh) invoiceInput(container, "删除", "FactDelete", change?.delete || false, value => update("delete", value), !writable, "checkbox");
|
||||
else if (state.editing) settingsButton("删除", () => {
|
||||
const index = Number(field.split(".").at(-1));
|
||||
(item < 0 ? draft.invoice.facts : draft.invoice.items[item].fields).splice(index, 1);
|
||||
invoiceDirty(draft, "model");
|
||||
renderDocumentSettings();
|
||||
}, false, "InvoiceFactDelete", container);
|
||||
}
|
||||
|
||||
function renderInvoiceAppearance(container, draft) {
|
||||
const capabilities = draft.detail.capabilities || {};
|
||||
const writable = state.editing && !!draft.entry;
|
||||
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}`]);
|
||||
if (!choices.some(([key]) => key === draft.field)) {
|
||||
draft.field = choices[0]?.[0] || "";
|
||||
const binding = draft.bindings.find(binding => JSON.stringify([binding.field, binding.item]) === draft.field);
|
||||
draft.binding = 0;
|
||||
draft.page = binding?.page ?? state.pageIndex;
|
||||
draft.object = binding?.objectId || "";
|
||||
}
|
||||
settingsSelect(container, "字段", "InvoiceBindingField", choices, draft.field, value => {
|
||||
draft.field = value;
|
||||
draft.binding = 0;
|
||||
const binding = draft.bindings.find(binding => JSON.stringify([binding.field, binding.item]) === value);
|
||||
draft.page = binding?.page ?? state.pageIndex;
|
||||
draft.object = binding?.objectId || "";
|
||||
renderDocumentSettings();
|
||||
});
|
||||
const bound = draft.bindings.filter(binding => JSON.stringify([binding.field, binding.item]) === draft.field);
|
||||
if (bound.length) {
|
||||
draft.binding = Math.min(draft.binding || 0, bound.length - 1);
|
||||
settingsSelect(container, "引用", "InvoiceBinding", bound.map((binding, index) => [index, `第${binding.page + 1}页 · ${binding.objectId}`]), draft.binding, value => {
|
||||
draft.binding = Number(value);
|
||||
draft.page = bound[draft.binding].page;
|
||||
draft.object = bound[draft.binding].objectId;
|
||||
renderDocumentSettings();
|
||||
});
|
||||
}
|
||||
settingsPages(container, "InvoicePage", draft.page, value => {
|
||||
draft.page = Number(value);
|
||||
draft.object = "";
|
||||
renderDocumentSettings();
|
||||
});
|
||||
const object = settingsSelect(container, "图元", "InvoiceObject", [[draft.object, draft.object || "选择图元"]], draft.object, value => {
|
||||
draft.object = value;
|
||||
renderDocumentSettings();
|
||||
});
|
||||
object.disabled = !writable || !capabilities.bind;
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "invoice-actions";
|
||||
container.append(actions);
|
||||
settingsButton("定位", async () => {
|
||||
if (!draft.object) return;
|
||||
settings.page = draft.page;
|
||||
await locateSettingsObjects([draft.object]);
|
||||
}, !draft.object, "InvoiceLocate", actions);
|
||||
if (state.editing) {
|
||||
settingsButton("选取", async () => {
|
||||
settings.page = draft.page;
|
||||
await beginSettingsPick("invoice");
|
||||
}, !writable || !capabilities.bind, "InvoicePick", actions, capabilities.reasons?.bind);
|
||||
settingsButton("绑定", () => {
|
||||
if (!draft.object || !draft.field) throw new Error("请选择字段和图元");
|
||||
const [field, item] = JSON.parse(draft.field);
|
||||
if (draft.bindings.some(binding => binding.page === draft.page && binding.objectId === draft.object && (binding.field !== field || binding.item !== item))) throw new Error("此图元已绑定其他字段");
|
||||
draft.bindings = draft.bindings.filter(binding => binding.field !== field || binding.item !== item);
|
||||
draft.bindings.push({ field, item, page: draft.page, objectId: draft.object });
|
||||
invoiceDirty(draft, "bindings");
|
||||
renderDocumentSettings();
|
||||
}, !writable || !capabilities.bind || !draft.field, "InvoiceBind", actions, capabilities.reasons?.bind);
|
||||
settingsButton("解绑", () => {
|
||||
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");
|
||||
renderDocumentSettings();
|
||||
}, !writable || !bound.length, "InvoiceUnbind", actions);
|
||||
settingsButton("清空", () => {
|
||||
draft.bindings = [];
|
||||
invoiceDirty(draft, "bindings");
|
||||
renderDocumentSettings();
|
||||
}, !writable || !draft.bindings.length, "InvoiceUnbindAll", actions).title = "解除全部绑定";
|
||||
}
|
||||
if (writable) {
|
||||
const details = document.createElement("details"), summary = document.createElement("summary");
|
||||
summary.textContent = "生成";
|
||||
details.className = "invoice-advanced";
|
||||
details.append(summary);
|
||||
container.append(details);
|
||||
details.open = draft.layoutOpen || false;
|
||||
details.addEventListener("toggle", () => { draft.layoutOpen = details.open; });
|
||||
const fonts = state.doc.fonts || [];
|
||||
const libraryFonts = fontManager.editorFonts().filter(font => !font.disabled);
|
||||
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.layout.font = value.startsWith("file:") ? "" : value;
|
||||
renderDocumentSettings();
|
||||
});
|
||||
settingsButton("添加", () => openFontFile(el.fontInput), false, "InvoiceAddFont", details).title = "添加字体";
|
||||
settingsButton("嵌入", async () => {
|
||||
const font = fonts.find(value => value.id === draft.layout.font);
|
||||
if (!font) throw new Error("请选择字体");
|
||||
const before = invoiceScope();
|
||||
const refresh = () => {
|
||||
const next = invoiceScope();
|
||||
if (next.documentIndex !== before.documentIndex || next.version !== before.version) return;
|
||||
for (const value of invoicePanel.drafts.values()) {
|
||||
if (value.scope.documentIndex === before.documentIndex && value.scope.version === before.version && value.scope.revision === before.revision) {
|
||||
value.scope.revision = next.revision;
|
||||
}
|
||||
}
|
||||
if (settingsElements.Panel.open && settingsElements.Kind.value === "invoices") renderDocumentSettings();
|
||||
};
|
||||
await openFontBinding(font);
|
||||
if (fontBindingElements.Panel.open) fontBindingElements.Panel.addEventListener("close", refresh, { once: true });
|
||||
else refresh();
|
||||
}, !draft.layout.font || !!fonts.find(font => font.id === draft.layout.font)?.embedded, "InvoiceEmbedFont", details);
|
||||
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");
|
||||
input.min = key === "margin" ? "0" : "0.01";
|
||||
}
|
||||
settingsButton("生成", async () => {
|
||||
if (draft.dirty.size) throw new Error("请先应用草稿");
|
||||
if (!draft.layout.font && !draft.fontChoice?.startsWith("file:")) throw new Error("请选择字体");
|
||||
let fontData;
|
||||
if (draft.fontChoice?.startsWith("file:")) {
|
||||
const font = libraryFonts.find(value => `file:${value.id}` === draft.fontChoice);
|
||||
if (!font) throw new Error("所选字体不可用");
|
||||
fontData = invoiceBase64(await readTextFont(font));
|
||||
}
|
||||
await commitInvoice(draft, { operation: "generate", layout: draft.layout, fontData });
|
||||
}, !capabilities.generate, "InvoiceGenerate", details, capabilities.reasons?.generate || (draft.bindings.length ? "请先解除绑定" : ""));
|
||||
settingsButton("同步", async () => {
|
||||
if (draft.dirty.size) throw new Error("请先应用草稿");
|
||||
await commitInvoice(draft, { operation: "sync" });
|
||||
}, !capabilities.sync, "InvoiceSync", actions, capabilities.reasons?.sync);
|
||||
}
|
||||
loadInvoiceObjects(draft).catch(error => { if (invoiceCurrent() === draft) settingsElements.Status.textContent = error.message; });
|
||||
}
|
||||
|
||||
async function loadInvoiceObjects(draft) {
|
||||
const page = draft.page, sequence = settings.sequence;
|
||||
const field = document.querySelector("#settingsInvoiceObject");
|
||||
if (!field || !state.doc.pages[page]) return;
|
||||
const data = await loadPageData(page);
|
||||
if (invoiceCurrent() !== draft || draft.page !== page || sequence !== settings.sequence || !field.isConnected) return;
|
||||
const objects = (data.objects || []).filter(item => !item.scoped && item.type === "TextObject");
|
||||
field.replaceChildren(new Option("选择图元", ""), ...objects.map(item => new Option(`${item.id} · ${(item.text || "文字").slice(0, 60)}`, item.id)));
|
||||
field.value = draft.object;
|
||||
}
|
||||
|
||||
function invoiceOtherDraftWarning(draft) {
|
||||
return [...invoicePanel.drafts.values()].some(value => value !== draft && value.entry && value.dirty.size && invoiceSourceKey(value.entry.source) === invoiceSourceKey(draft.entry.source)) ? "\n该来源其他发票的未应用修改也将放弃" : "";
|
||||
}
|
||||
|
||||
function resetInvoiceDraft(draft) {
|
||||
const section = { xml: "xml", fields: draft.entry ? "fields" : "model", appearance: "bindings" }[invoicePanel.tab] || "model";
|
||||
if (!draft.dirty.has(section) || !window.confirm("放弃当前编辑项的未应用修改?")) return;
|
||||
if (section === "xml") draft.xml = draft.detail.xml || "";
|
||||
else if (section === "fields") draft.changes.clear();
|
||||
else if (section === "bindings") draft.bindings = structuredClone(draft.detail.bindings || []);
|
||||
else {
|
||||
draft.invoice = structuredClone(draft.detail.invoice);
|
||||
draft.options = { name: "", encoding: { schemaRef: "", context: {}, units: [] } };
|
||||
draft.fragments = {};
|
||||
}
|
||||
draft.dirty.delete(section);
|
||||
draft.status = "";
|
||||
renderDocumentSettings();
|
||||
}
|
||||
|
||||
function renderInvoiceXML(container, draft) {
|
||||
invoiceInput(container, "原文", "XML", draft.xml, value => {
|
||||
draft.xml = value;
|
||||
invoiceDirty(draft, "xml");
|
||||
}, !state.editing, "textarea").rows = 14;
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "invoice-actions";
|
||||
container.append(actions);
|
||||
if (state.editing) settingsButton("导入", () => readInvoiceFile(text => {
|
||||
draft.xml = text;
|
||||
if (!draft.entry) draft.importing = true;
|
||||
invoiceDirty(draft, "xml");
|
||||
renderDocumentSettings();
|
||||
}), false, "InvoiceLoadXML", actions);
|
||||
settingsButton("导出", async () => {
|
||||
const result = await callWASM("ofdgoExportInvoice", JSON.stringify({ ...draft.scope, source: draft.entry.source, index: draft.entry.index }));
|
||||
downloadBytes(result.bytes, "application/xml", (draft.entry.source.fileLoc?.split("/").at(-1) || "invoice.xml"));
|
||||
}, !draft.entry, "InvoiceExportXML", actions).title = "导出来源原文";
|
||||
}
|
||||
|
||||
function readInvoiceFile(accept) {
|
||||
const sequence = settings.sequence, openSeq = state.openSeq;
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".xml,.xbrl,application/xml,text/xml";
|
||||
input.addEventListener("change", async () => {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const text = new TextDecoder("utf-8", { fatal: true }).decode(await file.arrayBuffer());
|
||||
if (sequence !== settings.sequence || openSeq !== state.openSeq || !settingsElements.Panel.open) return;
|
||||
await accept(text);
|
||||
} catch (error) {
|
||||
settingsElements.Status.textContent = error.message;
|
||||
}
|
||||
});
|
||||
input.click();
|
||||
}
|
||||
|
||||
async function validateInvoiceDraft(draft) {
|
||||
const request = draft.dirty.size ? buildInvoiceRequest(draft) : { invoice: draft.invoice };
|
||||
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;
|
||||
draft.status = invoiceIssues([...(result.issues || []), ...(result.appearance || [])]);
|
||||
settingsElements.Status.textContent = draft.status;
|
||||
}
|
||||
|
||||
async function applyInvoiceDraft(draft) {
|
||||
if (!state.editing || !draft.dirty.size) return;
|
||||
const request = buildInvoiceRequest(draft);
|
||||
if (request.operation === "replace" && !window.confirm("替换此来源中的全部发票原文?" + invoiceOtherDraftWarning(draft))) return;
|
||||
await commitInvoice(draft, request);
|
||||
}
|
||||
|
||||
function buildInvoiceRequest(draft) {
|
||||
if (draft.dirty.has("xml") && draft.dirty.size > 1) throw new Error("原文替换与其他修改冲突,草稿已保留,请先还原其中一项");
|
||||
const section = draft.dirty.has("xml") ? "xml" : "model";
|
||||
const request = { synchronize: draft.synchronize };
|
||||
if (section === "xml") {
|
||||
Object.assign(request, { operation: draft.entry ? "replace" : "import", xml: draft.xml, options: { name: draft.options.name } });
|
||||
} else {
|
||||
const options = structuredClone(draft.options);
|
||||
for (const [key, text] of Object.entries(draft.fragments)) {
|
||||
options.encoding[key] = invoiceBase64(new TextEncoder().encode(text));
|
||||
}
|
||||
if (!draft.entry) Object.assign(request, { operation: "create", invoice: draft.invoice, options });
|
||||
else {
|
||||
const operations = [];
|
||||
if (draft.dirty.has("model")) operations.push({ operation: "set", index: draft.entry.index, invoice: draft.invoice });
|
||||
if (draft.dirty.has("fields")) operations.push({ operation: "patch", index: draft.entry.index, changes: [...draft.changes.values()] });
|
||||
if (draft.dirty.has("bindings")) operations.push({ operation: "bindings", index: draft.entry.index, bindings: draft.bindings });
|
||||
Object.assign(request, operations.length === 1 ? operations[0] : { operation: "batch", operations });
|
||||
}
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
function invoiceBase64(bytes) {
|
||||
let value = "";
|
||||
for (let offset = 0; offset < bytes.length; offset += 8192) value += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
|
||||
return btoa(value);
|
||||
}
|
||||
|
||||
async function commitInvoice(draft, request) {
|
||||
if (!state.editing || settings.busy || document.body.hasAttribute("aria-busy")) return;
|
||||
settings.busy = true;
|
||||
settingsElements.Form.inert = true;
|
||||
settingsElements.Status.textContent = "正在保存修改";
|
||||
try {
|
||||
invoicePanel.result = null;
|
||||
if (!await changeDocument("ofdgoChangeInvoice", null, JSON.stringify({ ...draft.scope, source: draft.entry?.source, index: draft.entry?.index || 0, digest: draft.detail.digest, ...request }))) {
|
||||
if (settingsElements.Status.textContent === "正在保存修改") settingsElements.Status.textContent = el.statusText.textContent;
|
||||
return;
|
||||
}
|
||||
const result = invoicePanel.result;
|
||||
invoicePanel.drafts.delete(invoicePanel.key);
|
||||
const currentScope = invoiceScope();
|
||||
for (const [key, value] of invoicePanel.drafts) {
|
||||
if (value.scope.documentIndex !== currentScope.documentIndex || value.scope.version !== currentScope.version) continue;
|
||||
value.scope.revision = currentScope.revision;
|
||||
if (value.entry && draft.entry && invoiceSourceKey(value.entry.source) === invoiceSourceKey(draft.entry.source)) {
|
||||
if (!value.dirty.size) invoicePanel.drafts.delete(key);
|
||||
else value.status = "同源数据已修改,应用时将检查来源冲突";
|
||||
}
|
||||
}
|
||||
if (request.operation === "delete" || request.operation === "replace") {
|
||||
for (const [key, value] of invoicePanel.drafts) {
|
||||
if (value.entry && invoiceSourceKey(value.entry.source) === invoiceSourceKey(draft.entry.source)) {
|
||||
invoicePanel.drafts.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result?.source?.kind) invoicePanel.key = JSON.stringify([invoiceSourceKey(result.source), draft.entry?.index || 0]);
|
||||
await loadDocumentSettings();
|
||||
const warning = invoiceIssues(result?.issues);
|
||||
if (warning) {
|
||||
if (invoiceCurrent()) invoiceCurrent().status = warning;
|
||||
settingsElements.Status.textContent = warning;
|
||||
}
|
||||
} finally {
|
||||
settings.busy = false;
|
||||
settingsElements.Form.inert = false;
|
||||
}
|
||||
}
|
||||
|
||||
function settingsPages(container, id, value, change) {
|
||||
return settingsSelect(container, "页码", id, state.doc.pages.map((page, index) => [index, `第${index + 1}页`]), value, change);
|
||||
}
|
||||
@@ -6098,13 +6864,17 @@ function suspendSettings(kind) {
|
||||
async function pickSettingsObject(event, picking) {
|
||||
if (picking.settings === "locate") { finishResourcePick(); return; }
|
||||
const available = canvasEditor.itemsAt(event).filter(item => !item.scoped && !item.draft && item.type !== "Annotation"
|
||||
&& (picking.settings !== "invoice" || item.type === "TextObject")
|
||||
&& (picking.settings !== "style" || ["TextObject", "PathObject", "CompositeObject", "CompositeGraphicUnit"].includes(item.type))
|
||||
&& (picking.settings !== "layer" || item.index === settings.page && !settings.data.page.layers[settings.selected]?.ObjectIDs?.includes(item.id)));
|
||||
if (!available.length) return;
|
||||
const choose = async item => {
|
||||
if (resourceManager.pick !== picking || picking.sequence !== state.openSeq) return;
|
||||
finishResourcePick();
|
||||
if (picking.settings === "action") {
|
||||
if (picking.settings === "invoice") {
|
||||
const draft = invoiceCurrent();
|
||||
if (draft) { draft.page = item.index; draft.object = item.id; renderDocumentSettings(); }
|
||||
} else if (picking.settings === "action") {
|
||||
settings.page = item.index;
|
||||
settings.object = item.id;
|
||||
settings.selected = -1;
|
||||
|
||||
@@ -114,6 +114,33 @@ type editorOutlineInfo struct {
|
||||
OutlinePath []int `json:"outlinePath"`
|
||||
}
|
||||
|
||||
// invoiceRequest 发票操作参数
|
||||
type invoiceRequest struct {
|
||||
Operation string `json:"operation"`
|
||||
Operations []invoiceRequest `json:"operations"`
|
||||
Source ofdgo.InvoiceSource `json:"source"`
|
||||
Index int `json:"index"`
|
||||
Digest string `json:"digest"`
|
||||
Invoice ofdgo.Invoice `json:"invoice"`
|
||||
Changes []ofdgo.InvoiceFieldChange `json:"changes"`
|
||||
Bindings []ofdgo.InvoiceBinding `json:"bindings"`
|
||||
XML string `json:"xml"`
|
||||
Options ofdgo.InvoiceCreateOptions `json:"options"`
|
||||
Layout ofdgo.InvoiceLayout `json:"layout"`
|
||||
FontData []byte `json:"fontData"`
|
||||
Synchronize bool `json:"synchronize"`
|
||||
DocumentIndex *int `json:"documentIndex"`
|
||||
Version *string `json:"version"`
|
||||
Revision *uint64 `json:"revision"`
|
||||
indexProvided bool
|
||||
}
|
||||
|
||||
// invoiceEditorInfo 发票操作结果与编辑状态
|
||||
type invoiceEditorInfo struct {
|
||||
editorInfo
|
||||
InvoiceResult ofdgo.InvoiceUpdateResult `json:"invoiceResult"`
|
||||
}
|
||||
|
||||
// annotationOptions 保存注解面板的输入,资源字节单独传输
|
||||
type annotationOptions struct {
|
||||
Kind, ID, Text, Old, Creator, Pages, URI, Color string
|
||||
@@ -199,6 +226,11 @@ func RunWASM() {
|
||||
registerAsyncCallback("ofdgoExportVersion", exportVersion)
|
||||
registerCallback("ofdgoAnnotationInfos", annotationInfos)
|
||||
registerCallback("ofdgoDocumentNavigation", documentNavigation)
|
||||
registerCallback("ofdgoInvoices", documentInvoices)
|
||||
registerCallback("ofdgoInvoice", documentInvoice)
|
||||
registerCallback("ofdgoExportInvoice", exportDocumentInvoice)
|
||||
registerCallback("ofdgoValidateInvoice", validateDocumentInvoice)
|
||||
registerCallback("ofdgoChangeInvoice", changeDocumentInvoice)
|
||||
registerCallback("ofdgoChangeBookmark", changeBookmark)
|
||||
registerCallback("ofdgoActionList", actionList)
|
||||
registerCallback("ofdgoChangeActions", changeActions)
|
||||
@@ -854,6 +886,516 @@ func resourceTargets(args []js.Value) (any, error) {
|
||||
return currentEditor.ResourceTargets(context.Background(), args[1].String(), args[2].String(), args[0].Int())
|
||||
}
|
||||
|
||||
// documentInvoices 获取发票列表与格式能力
|
||||
// 入参: args 空参数
|
||||
// 返回: any 发票提取结果与格式列表, error 读取错误
|
||||
func documentInvoices(args []js.Value) (any, error) {
|
||||
if currentSession == nil || len(args) != 0 {
|
||||
return nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
r, close, err := currentInvoiceReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close()
|
||||
report, err := r.Invoices()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"report": report, "formats": ofdgo.InvoiceFormats()}, nil
|
||||
}
|
||||
|
||||
// currentInvoiceReader 获取当前发票读取器
|
||||
// 返回: *ofdgo.Reader 当前读取器, func() 快照释放函数, error 快照错误
|
||||
func currentInvoiceReader() (*ofdgo.Reader, func(), error) {
|
||||
if currentEditor != nil {
|
||||
r, err := currentEditor.Reader()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return r, func() { _ = r.Close() }, nil
|
||||
}
|
||||
if currentSession == nil {
|
||||
return nil, nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
return currentSession.Reader, func() {}, nil
|
||||
}
|
||||
|
||||
// decodeInvoiceRequest 解析发票参数并校验文档状态
|
||||
// 入参: args 单个JSON操作对象
|
||||
// 返回: invoiceRequest 操作参数, error 参数或过期状态错误
|
||||
func decodeInvoiceRequest(args []js.Value) (invoiceRequest, error) {
|
||||
var request invoiceRequest
|
||||
if currentSession == nil || len(args) != 1 || args[0].Type() != js.TypeString {
|
||||
return request, fmt.Errorf("invalid invoice arguments")
|
||||
}
|
||||
data := args[0].String()
|
||||
if len(data) > 64<<20 {
|
||||
return request, fmt.Errorf("invoice request exceeds size limit")
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &request); err != nil {
|
||||
return request, err
|
||||
}
|
||||
var presence struct {
|
||||
Operations []struct {
|
||||
Index *int `json:"index"`
|
||||
} `json:"operations"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &presence); err != nil {
|
||||
return request, err
|
||||
}
|
||||
for i := range presence.Operations {
|
||||
request.Operations[i].indexProvided = presence.Operations[i].Index != nil
|
||||
}
|
||||
index := currentSession.Reader.DocumentIndex()
|
||||
if currentEditor != nil {
|
||||
index = currentEditor.DocumentIndex()
|
||||
}
|
||||
if request.DocumentIndex != nil && *request.DocumentIndex != index {
|
||||
return request, fmt.Errorf("invoice document has changed")
|
||||
}
|
||||
if request.Version != nil {
|
||||
version, err := currentSession.Reader.DocumentVersionID()
|
||||
if currentEditor != nil {
|
||||
version, err = currentEditor.DocumentVersionID()
|
||||
}
|
||||
if err != nil {
|
||||
return request, err
|
||||
}
|
||||
if *request.Version != version {
|
||||
return request, fmt.Errorf("invoice document version has changed")
|
||||
}
|
||||
}
|
||||
if request.Revision != nil && (currentEditor == nil || *request.Revision != currentEditor.Revision()) {
|
||||
return request, fmt.Errorf("invoice editing revision has changed")
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// documentInvoice 获取发票详情与票面关联
|
||||
// 入参: args 来源及来源内发票索引,新建时提供发票模型
|
||||
// 返回: any 发票详情, error 来源、索引或读取错误
|
||||
func documentInvoice(args []js.Value) (any, error) {
|
||||
request, err := decodeInvoiceRequest(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
if request.Source.Kind == "" {
|
||||
return map[string]any{"invoice": request.Invoice, "fields": ofdgo.InvoiceFields(request.Invoice)}, nil
|
||||
}
|
||||
r, close, err := currentInvoiceReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close()
|
||||
data, err := r.ExportInvoice(ctx, request.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report, err := ofdgo.DecodeInvoices(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if request.Index < 0 || request.Index >= len(report.Invoices) {
|
||||
return nil, fmt.Errorf("invoice index out of range")
|
||||
}
|
||||
value := report.Invoices[request.Index]
|
||||
capabilities, err := r.InvoiceCapabilities(ctx, request.Source, request.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindings, bindingErr := r.InvoiceBindings(ctx, request.Source, request.Index)
|
||||
if bindingErr != nil {
|
||||
report.Issues = append(report.Issues, ofdgo.InvoiceIssue{Code: "binding-error", Message: bindingErr.Error(), Source: request.Source})
|
||||
}
|
||||
return map[string]any{"invoice": value, "fields": ofdgo.InvoiceFields(value), "capabilities": capabilities, "bindings": bindings, "digest": ofdgo.InvoiceDigest(data), "xml": string(data), "issues": report.Issues}, nil
|
||||
}
|
||||
|
||||
// exportDocumentInvoice 导出发票原文
|
||||
// 入参: args 发票来源
|
||||
// 返回: any 原始字节, error 来源或读取错误
|
||||
func exportDocumentInvoice(args []js.Value) (any, error) {
|
||||
request, err := decodeInvoiceRequest(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, close, err := currentInvoiceReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close()
|
||||
data, err := r.ExportInvoice(context.Background(), request.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
|
||||
}
|
||||
|
||||
// validateDocumentInvoice 校验发票草稿
|
||||
// 入参: args 发票模型或来源、草稿操作与原始摘要
|
||||
// 返回: any 字段与票面诊断, error 校验或读取错误
|
||||
func validateDocumentInvoice(args []js.Value) (any, error) {
|
||||
request, err := decodeInvoiceRequest(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
if request.Operation == "" {
|
||||
switch {
|
||||
case len(request.Operations) != 0:
|
||||
request.Operation = "batch"
|
||||
case len(request.Changes) != 0:
|
||||
request.Operation = "patch"
|
||||
case request.XML != "":
|
||||
request.Operation = "replace"
|
||||
if request.Source.Kind == "" {
|
||||
request.Operation = "import"
|
||||
}
|
||||
}
|
||||
}
|
||||
if request.Operation != "" {
|
||||
return validateInvoiceDraft(ctx, request)
|
||||
}
|
||||
issues, err := ofdgo.ValidateInvoice(ctx, request.Invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var appearance []ofdgo.InvoiceIssue
|
||||
if request.Source.Kind != "" {
|
||||
r, close, err := currentInvoiceReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close()
|
||||
appearance, err = r.CheckInvoiceAppearance(ctx, request.Source, request.Index)
|
||||
if err != nil {
|
||||
appearance = append(appearance, ofdgo.InvoiceIssue{Code: "appearance-error", Message: err.Error(), Source: request.Source})
|
||||
}
|
||||
}
|
||||
return map[string]any{"issues": issues, "appearance": appearance}, nil
|
||||
}
|
||||
|
||||
// validateInvoiceDraft 在独立快照中校验发票修改
|
||||
// 入参: ctx 取消上下文, request 已核对当前文档状态的操作
|
||||
// 返回: any 草稿发票及诊断, error 演练或读取错误
|
||||
func validateInvoiceDraft(ctx context.Context, request invoiceRequest) (any, error) {
|
||||
r, close, err := currentInvoiceReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close()
|
||||
editor, err := r.EditorContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Operations = append([]invoiceRequest(nil), request.Operations...)
|
||||
for i := range request.Operations {
|
||||
operation := &request.Operations[i]
|
||||
if operation.Revision != nil && (currentEditor == nil || *operation.Revision != currentEditor.Revision()) {
|
||||
return nil, fmt.Errorf("invoice editing revision has changed")
|
||||
}
|
||||
operation.Revision = nil
|
||||
}
|
||||
var result ofdgo.InvoiceUpdateResult
|
||||
if err := editor.Transaction(func(next *ofdgo.Editor) error {
|
||||
var err error
|
||||
result, err = applyInvoiceRequest(ctx, next, request)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issues := append([]ofdgo.InvoiceIssue(nil), result.Issues...)
|
||||
var appearance []ofdgo.InvoiceIssue
|
||||
if request.Operation == "delete" {
|
||||
return map[string]any{"issues": issues, "appearance": appearance}, nil
|
||||
}
|
||||
source := result.Source
|
||||
if source.Kind == "" {
|
||||
source = request.Source
|
||||
}
|
||||
data, err := editor.ExportInvoice(ctx, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report, err := ofdgo.DecodeInvoices(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issues = append(issues, report.Issues...)
|
||||
view, err := editor.ReaderContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer view.Close()
|
||||
for index, invoice := range report.Invoices {
|
||||
validation, err := ofdgo.ValidateInvoice(ctx, invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range validation {
|
||||
validation[i].Source = source
|
||||
}
|
||||
issues = append(issues, validation...)
|
||||
checks, err := view.CheckInvoiceAppearance(ctx, source, index)
|
||||
if err != nil {
|
||||
appearance = append(appearance, ofdgo.InvoiceIssue{Code: "appearance-error", Message: err.Error(), Source: source})
|
||||
} else {
|
||||
appearance = append(appearance, checks...)
|
||||
}
|
||||
}
|
||||
return map[string]any{"issues": issues, "appearance": appearance, "invoices": report.Invoices}, nil
|
||||
}
|
||||
|
||||
// changeDocumentInvoice 提交发票修改
|
||||
// 入参: args 操作及业务参数
|
||||
// 返回: any 编辑状态与签名诊断, error 校验或提交错误
|
||||
func changeDocumentInvoice(args []js.Value) (any, error) {
|
||||
request, err := decodeInvoiceRequest(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
var result ofdgo.InvoiceUpdateResult
|
||||
info, err := changeAtomicObjects(func() error {
|
||||
return currentEditor.Transaction(func(editor *ofdgo.Editor) error {
|
||||
var err error
|
||||
result, err = applyInvoiceRequest(ctx, editor, request)
|
||||
return err
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return invoiceEditorInfo{editorInfo: info.(editorInfo), InvoiceResult: result}, nil
|
||||
}
|
||||
|
||||
// applyInvoiceRequest 校验来源摘要并执行发票操作
|
||||
// 入参: ctx 取消上下文, editor 事务编辑器, request 操作参数
|
||||
// 返回: ofdgo.InvoiceUpdateResult 提交结果, error 校验或写入错误
|
||||
func applyInvoiceRequest(ctx context.Context, editor *ofdgo.Editor, request invoiceRequest) (ofdgo.InvoiceUpdateResult, error) {
|
||||
var result ofdgo.InvoiceUpdateResult
|
||||
if request.Operation == "batch" {
|
||||
return applyInvoiceBatch(ctx, editor, request)
|
||||
}
|
||||
if len(request.Operations) != 0 {
|
||||
return result, fmt.Errorf("invoice operations require batch action")
|
||||
}
|
||||
err := func() error {
|
||||
if request.Operation != "create" && request.Operation != "import" {
|
||||
data, err := editor.ExportInvoice(ctx, request.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Digest == "" || ofdgo.InvoiceDigest(data) != request.Digest {
|
||||
return fmt.Errorf("invoice source has changed")
|
||||
}
|
||||
}
|
||||
switch request.Operation {
|
||||
case "create", "import":
|
||||
var source ofdgo.InvoiceSource
|
||||
var err error
|
||||
if request.Operation == "create" {
|
||||
source, err = editor.CreateInvoice(ctx, request.Invoice, request.Options)
|
||||
} else {
|
||||
source, err = editor.ImportInvoice(ctx, []byte(request.XML), request.Options)
|
||||
}
|
||||
result.Source = source
|
||||
return err
|
||||
case "set":
|
||||
var err error
|
||||
result, err = editor.SetInvoice(ctx, request.Source, request.Digest, request.Index, request.Invoice, request.Synchronize)
|
||||
return err
|
||||
case "patch":
|
||||
for i := range request.Changes {
|
||||
request.Changes[i].Source.Kind = request.Source.Kind
|
||||
request.Changes[i].Source.ID = request.Source.ID
|
||||
request.Changes[i].Source.FileLoc = request.Source.FileLoc
|
||||
if request.Source.NodePath != "" {
|
||||
request.Changes[i].Source.NodePath = request.Source.NodePath + request.Changes[i].Source.NodePath
|
||||
}
|
||||
}
|
||||
var err error
|
||||
result, err = editor.EditInvoice(ctx, request.Source, request.Digest, request.Changes, request.Synchronize)
|
||||
return err
|
||||
case "replace":
|
||||
var err error
|
||||
result, err = editor.UpdateInvoice(ctx, request.Source, request.Digest, []byte(request.XML), request.Synchronize)
|
||||
return err
|
||||
case "delete":
|
||||
return editor.DeleteInvoice(ctx, request.Source, request.Digest)
|
||||
case "bindings":
|
||||
return editor.SetInvoiceBindings(ctx, request.Source, request.Index, request.Bindings)
|
||||
case "generate":
|
||||
if len(request.FontData) != 0 {
|
||||
file := ofdgo.FontFile{Data: request.FontData}
|
||||
if request.Layout.Font == "" {
|
||||
id, err := editor.AddFont(file, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Layout.Font = id
|
||||
} else if err := editor.EmbedFont(ctx, request.Layout.Font, file, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return editor.GenerateInvoicePages(ctx, request.Source, request.Index, request.Layout)
|
||||
case "sync":
|
||||
data, err := editor.ExportInvoice(ctx, request.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err = editor.UpdateInvoice(ctx, request.Source, request.Digest, data, true)
|
||||
return err
|
||||
default:
|
||||
return fmt.Errorf("unsupported invoice action")
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return ofdgo.InvoiceUpdateResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// applyInvoiceBatch 合并发票修改并同步票面
|
||||
// 入参: ctx 取消上下文, editor 事务编辑器, request 批次及原始来源
|
||||
// 返回: ofdgo.InvoiceUpdateResult 提交结果, error 冲突或写入错误
|
||||
func applyInvoiceBatch(ctx context.Context, editor *ofdgo.Editor, request invoiceRequest) (ofdgo.InvoiceUpdateResult, error) {
|
||||
var result ofdgo.InvoiceUpdateResult
|
||||
data, err := editor.ExportInvoice(ctx, request.Source)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if request.Digest == "" || ofdgo.InvoiceDigest(data) != request.Digest {
|
||||
return result, fmt.Errorf("invoice source has changed")
|
||||
}
|
||||
if len(request.Operations) == 0 || len(request.Operations) > 256 {
|
||||
return result, fmt.Errorf("invalid invoice batch size")
|
||||
}
|
||||
report, err := ofdgo.DecodeInvoices(ctx, data)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if request.Index < 0 || request.Index >= len(report.Invoices) {
|
||||
return result, fmt.Errorf("invoice index out of range")
|
||||
}
|
||||
paths := make(map[string]bool)
|
||||
for _, fact := range report.Invoices[request.Index].Facts {
|
||||
paths[fact.Source.NodePath] = true
|
||||
}
|
||||
for _, item := range report.Invoices[request.Index].Items {
|
||||
for _, fact := range item.Fields {
|
||||
paths[fact.Source.NodePath] = true
|
||||
}
|
||||
}
|
||||
request.Operations = append([]invoiceRequest(nil), request.Operations...)
|
||||
for i := range request.Operations {
|
||||
operation := &request.Operations[i]
|
||||
if (operation.indexProvided || operation.Index != 0) && operation.Index != request.Index {
|
||||
return result, fmt.Errorf("invoice batch cannot span candidates")
|
||||
}
|
||||
operation.Index = request.Index
|
||||
}
|
||||
document := editor.DocumentIndex()
|
||||
version, err := editor.DocumentVersionID()
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
revision := editor.Revision()
|
||||
models, bindings := make(map[int]bool), make(map[int]bool)
|
||||
patched := make(map[string]bool)
|
||||
hasSet, hasPatch := false, false
|
||||
for _, operation := range request.Operations {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if len(operation.Operations) != 0 || operation.Operation == "batch" || operation.Synchronize {
|
||||
return result, fmt.Errorf("nested invoice batch or synchronization is not supported")
|
||||
}
|
||||
if operation.Source != (ofdgo.InvoiceSource{}) && operation.Source != request.Source {
|
||||
return result, fmt.Errorf("invoice batch cannot span sources")
|
||||
}
|
||||
if operation.Digest != "" && operation.Digest != request.Digest {
|
||||
return result, fmt.Errorf("invoice batch requires one original digest")
|
||||
}
|
||||
if operation.DocumentIndex != nil && *operation.DocumentIndex != document || operation.Version != nil && *operation.Version != version || operation.Revision != nil && *operation.Revision != revision {
|
||||
return result, fmt.Errorf("invoice batch scope has changed")
|
||||
}
|
||||
switch operation.Operation {
|
||||
case "set":
|
||||
if models[operation.Index] {
|
||||
return result, fmt.Errorf("duplicate invoice model update")
|
||||
}
|
||||
models[operation.Index], hasSet = true, true
|
||||
case "patch":
|
||||
if len(operation.Changes) == 0 {
|
||||
return result, fmt.Errorf("empty invoice field update")
|
||||
}
|
||||
hasPatch = true
|
||||
for _, change := range operation.Changes {
|
||||
if change.Source.Kind != "" && change.Source.Kind != request.Source.Kind || change.Source.ID != "" && change.Source.ID != request.Source.ID || change.Source.FileLoc != "" && change.Source.FileLoc != request.Source.FileLoc {
|
||||
return result, fmt.Errorf("invoice patch cannot span sources")
|
||||
}
|
||||
if change.Source.NodePath == "" || patched[change.Source.NodePath] {
|
||||
return result, fmt.Errorf("duplicate or missing invoice field path")
|
||||
}
|
||||
if !paths[change.Source.NodePath] {
|
||||
return result, fmt.Errorf("invoice patch cannot span candidates")
|
||||
}
|
||||
if change.Revision != "" && change.Revision != request.Digest {
|
||||
return result, fmt.Errorf("invoice field revision has changed")
|
||||
}
|
||||
patched[change.Source.NodePath] = true
|
||||
}
|
||||
case "bindings":
|
||||
if bindings[operation.Index] {
|
||||
return result, fmt.Errorf("duplicate invoice binding update")
|
||||
}
|
||||
bindings[operation.Index] = true
|
||||
default:
|
||||
return result, fmt.Errorf("unsupported invoice batch action")
|
||||
}
|
||||
}
|
||||
if hasSet && hasPatch {
|
||||
return result, fmt.Errorf("invoice batch cannot mix model and raw field updates")
|
||||
}
|
||||
var changes []ofdgo.InvoiceFieldChange
|
||||
for _, operation := range request.Operations {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
switch operation.Operation {
|
||||
case "set":
|
||||
data, err = ofdgo.UpdateInvoiceXML(ctx, data, operation.Index, operation.Invoice)
|
||||
case "patch":
|
||||
changes = append(changes, operation.Changes...)
|
||||
}
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
if len(changes) != 0 {
|
||||
for i := range changes {
|
||||
changes[i].Revision = request.Digest
|
||||
changes[i].Source.Kind = ""
|
||||
changes[i].Source.ID = ""
|
||||
changes[i].Source.FileLoc = ""
|
||||
}
|
||||
data, err = ofdgo.PatchInvoice(ctx, data, changes)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
for _, operation := range request.Operations {
|
||||
if operation.Operation == "bindings" {
|
||||
if err := editor.SetInvoiceBindings(ctx, request.Source, operation.Index, operation.Bindings); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return editor.UpdateInvoice(ctx, request.Source, request.Digest, data, request.Synchronize)
|
||||
}
|
||||
|
||||
// resourceLocations 定位当前文档与版本中的资源引用实例
|
||||
// 入参: args 资源类型、标识和引用索引
|
||||
// 返回: any 页面图元范围, error 引用或度量错误
|
||||
|
||||
+152
-6
@@ -45,6 +45,19 @@ type InvoiceUpdateResult struct {
|
||||
Issues []InvoiceIssue `json:"issues,omitempty"`
|
||||
}
|
||||
|
||||
// InvoiceCapabilities 发票操作能力
|
||||
// Create表示同格式创建能力,Delete表示整源删除,Reasons按操作小写名称索引
|
||||
type InvoiceCapabilities struct {
|
||||
Create bool `json:"create"`
|
||||
Edit bool `json:"edit"`
|
||||
Items bool `json:"items"`
|
||||
Bind bool `json:"bind"`
|
||||
Generate bool `json:"generate"`
|
||||
Sync bool `json:"sync"`
|
||||
Delete bool `json:"delete"`
|
||||
Reasons map[string]string `json:"reasons"`
|
||||
}
|
||||
|
||||
// invoiceBindingRecord 保存当前文档内的来源选择器,资源路径变化时仍以入口和摘要核对载荷
|
||||
type invoiceBindingRecord struct {
|
||||
Kind string `json:"kind"`
|
||||
@@ -69,6 +82,138 @@ type invoiceBindingEntry struct {
|
||||
bindings []InvoiceBinding
|
||||
}
|
||||
|
||||
// InvoiceCapabilities 获取当前文档版本的发票操作能力
|
||||
// 入参: ctx 取消上下文, source 精确来源, index 来源内从0开始的发票索引
|
||||
// 返回: InvoiceCapabilities 操作能力, error 来源、索引或读取错误
|
||||
func (r *Reader) InvoiceCapabilities(ctx context.Context, source InvoiceSource, index int) (InvoiceCapabilities, error) {
|
||||
ctx = invoiceWriteContext(ctx)
|
||||
result := InvoiceCapabilities{Reasons: make(map[string]string)}
|
||||
data, err := r.ExportInvoice(ctx, source)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
report, err := parseInvoiceXML(ctx, data, source)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if index < 0 || index >= len(report.Invoices) {
|
||||
return result, fmt.Errorf("invoice index out of range")
|
||||
}
|
||||
format, known := invoiceWriteFormat(report.Invoices[index])
|
||||
result.Create = known && format.Create
|
||||
result.Edit = known && format.Edit
|
||||
result.Items = known && format.Items
|
||||
result.Bind = known
|
||||
result.Generate = known
|
||||
result.Delete = true
|
||||
if !result.Create {
|
||||
result.Reasons["create"] = "当前格式不支持创建"
|
||||
}
|
||||
if !result.Edit {
|
||||
result.Reasons["edit"] = "当前格式不支持编辑"
|
||||
}
|
||||
if !result.Items {
|
||||
result.Reasons["items"] = "当前格式不支持明细增删"
|
||||
}
|
||||
if !known {
|
||||
result.Reasons["bind"] = "当前格式未适配"
|
||||
result.Reasons["generate"] = "当前格式未适配"
|
||||
}
|
||||
entries, err := r.invoiceBindings(ctx, source, InvoiceDigest(data), true)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return result, ctx.Err()
|
||||
}
|
||||
result.Sync = false
|
||||
result.Generate = false
|
||||
result.Bind = false
|
||||
for _, operation := range []string{"sync", "generate", "bind"} {
|
||||
result.Reasons[operation] = "票面关联损坏,请先修复关联"
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.record.Invoice != index {
|
||||
continue
|
||||
}
|
||||
result.Generate = false
|
||||
result.Reasons["generate"] = "请先解除已有票面关联"
|
||||
result.Sync = result.Edit && len(entry.bindings) > 0
|
||||
if result.Sync {
|
||||
editor, err := r.EditorContext(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
for _, binding := range entry.bindings {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
_, fieldErr := invoiceBindingValue(report.Invoices[index], binding)
|
||||
object, objectErr := editor.Object(binding.Page, binding.ObjectID)
|
||||
if fieldErr != nil || objectErr != nil || object.Type != "TextObject" {
|
||||
result.Sync = false
|
||||
result.Reasons["sync"] = "票面关联字段或图元已失效"
|
||||
}
|
||||
}
|
||||
if entry.record.Layout != nil {
|
||||
ids := make([]string, len(entry.record.Pages))
|
||||
for i, page := range entry.record.Pages {
|
||||
ids[i] = page.ID
|
||||
}
|
||||
current, err := editor.invoiceOwnedPages(ctx, ids)
|
||||
if err != nil || len(ids) == 0 || !slices.Equal(current, entry.record.Pages) || editor.source.document.Signatures != "" {
|
||||
result.Sync = false
|
||||
result.Reasons["sync"] = "生成票面已修改或签名,请重新绑定"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !result.Sync && result.Reasons["sync"] == "" {
|
||||
result.Reasons["sync"] = "当前发票没有可同步的票面关联"
|
||||
}
|
||||
return result, ctx.Err()
|
||||
}
|
||||
|
||||
// GenerateInvoicePages 为已有发票生成信息页并保存字段关联,失败时整体回滚
|
||||
// 入参: ctx 取消上下文, source 精确来源, index 来源内发票索引, layout 排版选项
|
||||
// 返回: error 来源、关联、排版或提交错误
|
||||
func (e *Editor) GenerateInvoicePages(ctx context.Context, source InvoiceSource, index int, layout InvoiceLayout) error {
|
||||
ctx = invoiceWriteContext(ctx)
|
||||
return e.Transaction(func(next *Editor) error {
|
||||
r, err := next.ReaderContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
capabilities, err := r.InvoiceCapabilities(ctx, source, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !capabilities.Generate {
|
||||
return fmt.Errorf("invoice appearance cannot be generated: %s", capabilities.Reasons["generate"])
|
||||
}
|
||||
data, err := r.ExportInvoice(ctx, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report, err := parseInvoiceXML(ctx, data, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bindings, err := next.AppendInvoicePages(ctx, report.Invoices[index], layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := next.SetInvoiceBindings(ctx, source, index, bindings); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := next.markInvoicePages(ctx, source, index, layout); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Err()
|
||||
})
|
||||
}
|
||||
|
||||
// Invoices 获取编辑状态的发票提取结果,不修改文档
|
||||
// 返回: InvoiceReport 发票及诊断, error 快照或解析错误
|
||||
func (e *Editor) Invoices() (InvoiceReport, error) {
|
||||
@@ -159,7 +304,7 @@ func (e *Editor) ImportInvoice(ctx context.Context, data []byte, options Invoice
|
||||
if err := next.SetInvoiceBindings(ctx, source, 0, bindings); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := next.markInvoicePages(ctx, source, *options.Layout); err != nil {
|
||||
if err := next.markInvoicePages(ctx, source, 0, *options.Layout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -894,9 +1039,9 @@ func (e *Editor) invoiceOwnedPages(ctx context.Context, ids []string) ([]invoice
|
||||
}
|
||||
|
||||
// markInvoicePages 保存新建票面的排版选项和页面摘要,供明细增删后安全重排
|
||||
// 入参: ctx 取消上下文, source 新增来源, layout 排版选项
|
||||
// 入参: ctx 取消上下文, source 来源, index 来源内发票索引, layout 排版选项
|
||||
// 返回: error 关联或提交错误
|
||||
func (e *Editor) markInvoicePages(ctx context.Context, source InvoiceSource, layout InvoiceLayout) error {
|
||||
func (e *Editor) markInvoicePages(ctx context.Context, source InvoiceSource, index int, layout InvoiceLayout) error {
|
||||
r, err := e.ReaderContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -911,10 +1056,11 @@ func (e *Editor) markInvoicePages(ctx context.Context, source InvoiceSource, lay
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
return fmt.Errorf("generated invoice requires one binding record")
|
||||
position := slices.IndexFunc(entries, func(entry invoiceBindingEntry) bool { return entry.record.Invoice == index })
|
||||
if position < 0 {
|
||||
return fmt.Errorf("generated invoice binding record not found")
|
||||
}
|
||||
entry := entries[0]
|
||||
entry := entries[position]
|
||||
var ids []string
|
||||
for _, binding := range entry.bindings {
|
||||
page, err := e.Page(binding.Page)
|
||||
|
||||
@@ -41,6 +41,20 @@ type InvoiceFormat struct {
|
||||
Items bool `json:"items"`
|
||||
}
|
||||
|
||||
// InvoiceFieldDescriptor 发票字段描述
|
||||
// Item为从0开始的明细索引,非明细字段为-1
|
||||
// Type为text、decimal、date或datetime,Editable表示字段支持编辑,提交时仍需校验
|
||||
type InvoiceFieldDescriptor struct {
|
||||
Field string `json:"field"`
|
||||
Label string `json:"label"`
|
||||
Group string `json:"group"`
|
||||
Type string `json:"type"`
|
||||
Item int `json:"item"`
|
||||
Value string `json:"value"`
|
||||
Editable bool `json:"editable"`
|
||||
Bindable bool `json:"bindable"`
|
||||
}
|
||||
|
||||
// InvoiceFieldChange 修改已有叶节点,Revision为源XML的SHA256十六进制摘要
|
||||
// 未提供Revision时必须提供ExpectedValue,ExpectedNil缺省表示原节点非空值
|
||||
type InvoiceFieldChange struct {
|
||||
@@ -97,6 +111,100 @@ type invoiceXMLPatch struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
// InvoiceFields 获取发票字段描述,原始事实通过来源补丁修改
|
||||
// 入参: value 发票数据
|
||||
// 返回: []InvoiceFieldDescriptor 字段描述列表
|
||||
func InvoiceFields(value Invoice) []InvoiceFieldDescriptor {
|
||||
format, supported := invoiceWriteFormat(value)
|
||||
probe := Invoice{Items: []InvoiceItem{{}}}
|
||||
if value.Standard == "EInvoice" {
|
||||
probe.Number, probe.Date = "editable", "editable"
|
||||
for _, group := range invoicePlainGroups() {
|
||||
for _, name := range strings.Fields(group.value) {
|
||||
if p, _ := invoicePlainField(&probe, nil, "/EInvoice/EInvoiceData/"+group.name+"/"+name); p != nil {
|
||||
*p = "editable"
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range strings.Fields("ItemName SpecMod MeaUnits Quantity UnPrice Amount TaxRate ComTaxAm") {
|
||||
if p, _ := invoicePlainField(&probe, &probe.Items[0], "/EInvoice/EInvoiceData/IssuItemInformation/"+name); p != nil {
|
||||
*p = "editable"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, name := range invoiceWriteHeader(value) {
|
||||
if p := invoiceXMLField(&probe, value.Kind, name); p != nil {
|
||||
*p = "editable"
|
||||
}
|
||||
}
|
||||
for _, name := range invoiceWriteItemNames(value.Kind) {
|
||||
if p := invoiceItemField(&probe.Items[0], value.Kind, name); p != nil {
|
||||
*p = "editable"
|
||||
}
|
||||
}
|
||||
}
|
||||
var result []InvoiceFieldDescriptor
|
||||
for _, field := range invoiceLayoutFields(value) {
|
||||
if strings.HasPrefix(field.field, "facts.") || strings.HasPrefix(field.field, "items.fields.") {
|
||||
continue
|
||||
}
|
||||
d := InvoiceFieldDescriptor{Field: field.field, Label: field.label, Group: "basic", Type: "text", Item: field.item, Value: field.value, Bindable: true}
|
||||
binding := InvoiceBinding{Field: field.field, Item: field.item}
|
||||
if field.item >= 0 {
|
||||
binding.Item = 0
|
||||
d.Group = "items"
|
||||
}
|
||||
if strings.HasPrefix(field.field, "buyer.") {
|
||||
d.Group = "buyer"
|
||||
}
|
||||
if strings.HasPrefix(field.field, "seller.") {
|
||||
d.Group = "seller"
|
||||
}
|
||||
mapped, _ := invoiceBindingValue(probe, binding)
|
||||
d.Editable = supported && format.Edit && mapped == "editable"
|
||||
switch field.field {
|
||||
case "date":
|
||||
d.Type = "date"
|
||||
if value.Kind == "einv" {
|
||||
d.Type = "datetime"
|
||||
}
|
||||
case "amount", "taxAmount", "totalAmount", "items.quantity", "items.unitPrice", "items.amount", "items.taxAmount":
|
||||
d.Type = "decimal"
|
||||
case "items.taxRate":
|
||||
if value.Standard == "EInvoice" {
|
||||
d.Type = "decimal"
|
||||
}
|
||||
}
|
||||
result = append(result, d)
|
||||
}
|
||||
appendFact := func(fact InvoiceFact, field string, item int) {
|
||||
d := InvoiceFieldDescriptor{Field: field, Label: fact.Name, Group: "facts", Type: "text", Item: item, Value: fact.Value, Bindable: true}
|
||||
if item >= 0 {
|
||||
d.Group = "items"
|
||||
}
|
||||
d.Editable = supported && format.Edit && fact.Source.NodePath != "" && fact.Namespace != invoiceXBRL
|
||||
if invoiceNumeric(value.Kind, fact.Name) {
|
||||
d.Type = "decimal"
|
||||
}
|
||||
if invoiceWriteDateFact(value.Kind, fact.Name) {
|
||||
d.Type = "date"
|
||||
}
|
||||
if value.Kind == "einv" && (fact.Name == "RequestTime" || fact.Name == "IssueTime") {
|
||||
d.Type = "datetime"
|
||||
}
|
||||
result = append(result, d)
|
||||
}
|
||||
for i, fact := range value.Facts {
|
||||
appendFact(fact, fmt.Sprintf("facts.%d", i), -1)
|
||||
}
|
||||
for i, item := range value.Items {
|
||||
for j, fact := range item.Fields {
|
||||
appendFact(fact, fmt.Sprintf("items.fields.%d", j), i)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// InvoiceFormats 返回独立格式能力副本,历史航空格式仅允许读取和原位修改
|
||||
// 返回: 已适配格式列表
|
||||
func InvoiceFormats() []InvoiceFormat {
|
||||
|
||||
Reference in new issue
Block a user