Add click-to-preview for fetched e-taxes data on Asan/done
Counter rows and the company name on the post-Asan screen open a modal with the actual records (6 e-taxes lists + company profile fields), so users can sanity-check what landed before finalizing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6ba1bdf53d
commit
b982825c71
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.25"
|
__version__ = "0.1.26"
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,252 @@ def fetch_company_profile():
|
||||||
return {"ok": True, "resolved": resolved, "criteria": criteria}
|
return {"ok": True, "resolved": resolved, "criteria": criteria}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_preview_data():
|
||||||
|
"""Return display-ready preview tables for everything pulled in the Asan
|
||||||
|
step. Shape per kind: {"headers": [str, ...], "rows": [[cell, ...], ...]}.
|
||||||
|
Profile uses the same shape (two-column label/value table).
|
||||||
|
|
||||||
|
The wizard's done-screen modal renders this verbatim — no business logic
|
||||||
|
in the JS. Field names mirror invoice_az.company_api so the preview
|
||||||
|
matches what eventually lands in the E-Taxes DocTypes.
|
||||||
|
"""
|
||||||
|
_only_admin()
|
||||||
|
cache = frappe.get_single("Jey Wizard Etaxes Cache")
|
||||||
|
return {
|
||||||
|
"objects": _preview_objects(cache.objects_json),
|
||||||
|
"cash_registers": _preview_cash_registers(cache.cash_registers_json),
|
||||||
|
"pos_terminals": _preview_pos_terminals(cache.pos_terminals_json),
|
||||||
|
"bank_accounts": _preview_bank_accounts(cache.bank_accounts_json),
|
||||||
|
"obligation_pacts": _preview_obligation_pacts(cache.obligation_pacts_json),
|
||||||
|
"presented_certs": _preview_presented_certs(cache.presented_certs_json),
|
||||||
|
"profile": _preview_profile(cache.company_profile_json),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PREVIEW_ROW_LIMIT = 200
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_parse(raw):
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_pages(data, list_key):
|
||||||
|
"""Paginated cache payloads land as a list of page dicts ([{list_key: [...], hasMore: ...}, ...]).
|
||||||
|
Non-paginated ones land as a single dict {list_key: [...]}.
|
||||||
|
"""
|
||||||
|
if isinstance(data, list):
|
||||||
|
out = []
|
||||||
|
for page in data:
|
||||||
|
if isinstance(page, dict):
|
||||||
|
out.extend(page.get(list_key) or [])
|
||||||
|
return out
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data.get(list_key) or []
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_preview(headers):
|
||||||
|
return {"headers": headers, "rows": []}
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_objects(raw):
|
||||||
|
headers = [_("Code"), _("Name"), _("Status"), _("Address")]
|
||||||
|
data = _safe_parse(raw)
|
||||||
|
if data is None:
|
||||||
|
return _empty_preview(headers)
|
||||||
|
items = []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
items = data.get("objectInfoShortList") or data.get("objects") or []
|
||||||
|
rows = []
|
||||||
|
for obj in items[:PREVIEW_ROW_LIMIT]:
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
continue
|
||||||
|
rows.append([
|
||||||
|
obj.get("objectCode") or "",
|
||||||
|
(obj.get("objectName") or "").strip(),
|
||||||
|
obj.get("status") or "",
|
||||||
|
obj.get("address") or "",
|
||||||
|
])
|
||||||
|
return {"headers": headers, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_cash_registers(raw):
|
||||||
|
headers = [_("Number"), _("Model"), _("Status"), _("Object"), _("Operator")]
|
||||||
|
data = _safe_parse(raw)
|
||||||
|
if data is None:
|
||||||
|
return _empty_preview(headers)
|
||||||
|
items = _flatten_pages(data, "cashRegisters")
|
||||||
|
rows = []
|
||||||
|
for reg in items[:PREVIEW_ROW_LIMIT]:
|
||||||
|
if not isinstance(reg, dict):
|
||||||
|
continue
|
||||||
|
rows.append([
|
||||||
|
reg.get("number") or "",
|
||||||
|
reg.get("model") or "",
|
||||||
|
reg.get("status") or "",
|
||||||
|
reg.get("object") or "",
|
||||||
|
reg.get("operatorName") or "",
|
||||||
|
])
|
||||||
|
return {"headers": headers, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_pos_terminals(raw):
|
||||||
|
headers = [_("Serial number"), _("Status"), _("Registration date"), _("Bank")]
|
||||||
|
data = _safe_parse(raw)
|
||||||
|
if data is None:
|
||||||
|
return _empty_preview(headers)
|
||||||
|
items = _flatten_pages(data, "posTerminals")
|
||||||
|
rows = []
|
||||||
|
for term in items[:PREVIEW_ROW_LIMIT]:
|
||||||
|
if not isinstance(term, dict):
|
||||||
|
continue
|
||||||
|
rows.append([
|
||||||
|
term.get("serialNumber") or "",
|
||||||
|
term.get("status") or "",
|
||||||
|
(term.get("registrationDate") or "")[:10],
|
||||||
|
term.get("bankName") or "",
|
||||||
|
])
|
||||||
|
return {"headers": headers, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_bank_accounts(raw):
|
||||||
|
headers = [_("IBAN"), _("Currency"), _("Status"), _("Bank")]
|
||||||
|
data = _safe_parse(raw)
|
||||||
|
if data is None:
|
||||||
|
return _empty_preview(headers)
|
||||||
|
items = []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
items = data.get("bankAccounts") or []
|
||||||
|
elif isinstance(data, list):
|
||||||
|
items = data
|
||||||
|
rows = []
|
||||||
|
for acc in items[:PREVIEW_ROW_LIMIT]:
|
||||||
|
if not isinstance(acc, dict):
|
||||||
|
continue
|
||||||
|
rows.append([
|
||||||
|
acc.get("number") or "",
|
||||||
|
acc.get("currency") or "",
|
||||||
|
acc.get("status") or "",
|
||||||
|
acc.get("bankName") or "",
|
||||||
|
])
|
||||||
|
return {"headers": headers, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_obligation_pacts(raw):
|
||||||
|
headers = [_("Code"), _("Name"), _("Valid from"), _("Expires")]
|
||||||
|
data = _safe_parse(raw)
|
||||||
|
if data is None:
|
||||||
|
return _empty_preview(headers)
|
||||||
|
items = []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
items = data.get("oblPacts") or []
|
||||||
|
rows = []
|
||||||
|
for item in items[:PREVIEW_ROW_LIMIT]:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
pact = item.get("oblPact") or {}
|
||||||
|
name_blob = pact.get("name")
|
||||||
|
# name may be {"az": "..."} or a plain string depending on tenant.
|
||||||
|
if isinstance(name_blob, dict):
|
||||||
|
name = name_blob.get("az") or name_blob.get("en") or next(iter(name_blob.values()), "")
|
||||||
|
else:
|
||||||
|
name = name_blob or ""
|
||||||
|
rows.append([
|
||||||
|
pact.get("code") or "",
|
||||||
|
name,
|
||||||
|
(item.get("validityDate") or "")[:10],
|
||||||
|
(item.get("expireDate") or "")[:10],
|
||||||
|
])
|
||||||
|
return {"headers": headers, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_presented_certs(raw):
|
||||||
|
headers = [_("Cert №"), _("Category"), _("State"), _("Counterparty"), _("VÖEN"), _("Operation date")]
|
||||||
|
data = _safe_parse(raw)
|
||||||
|
if data is None:
|
||||||
|
return _empty_preview(headers)
|
||||||
|
items = []
|
||||||
|
if isinstance(data, dict):
|
||||||
|
items = data.get("certificateList") or []
|
||||||
|
rows = []
|
||||||
|
for cert in items[:PREVIEW_ROW_LIMIT]:
|
||||||
|
if not isinstance(cert, dict):
|
||||||
|
continue
|
||||||
|
op = cert.get("operationDate") or ""
|
||||||
|
# Compact YYYYMMDDhhmmss → YYYY-MM-DD when applicable.
|
||||||
|
if isinstance(op, str) and len(op) >= 8 and op[:8].isdigit():
|
||||||
|
op = f"{op[0:4]}-{op[4:6]}-{op[6:8]}"
|
||||||
|
rows.append([
|
||||||
|
cert.get("certNo") or "",
|
||||||
|
cert.get("certCategory") or "",
|
||||||
|
cert.get("certState") or "",
|
||||||
|
cert.get("taxPayerFullName") or "",
|
||||||
|
cert.get("voen") or "",
|
||||||
|
op,
|
||||||
|
])
|
||||||
|
return {"headers": headers, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_profile(raw):
|
||||||
|
"""Two-column label/value table from the cached company profile blob.
|
||||||
|
Mirrors the most useful subset of _build_company_field_updates so the
|
||||||
|
user sees what's about to land on Company.
|
||||||
|
"""
|
||||||
|
headers = [_("Field"), _("Value")]
|
||||||
|
data = _safe_parse(raw)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return _empty_preview(headers)
|
||||||
|
profile = data.get("profile") if isinstance(data.get("profile"), dict) else data
|
||||||
|
resolved = data.get("resolved") or {}
|
||||||
|
if not isinstance(profile, dict):
|
||||||
|
return _empty_preview(headers)
|
||||||
|
|
||||||
|
def _addr_or(top, legal_obj_keys):
|
||||||
|
if top:
|
||||||
|
return top
|
||||||
|
obj = profile.get("legalAddressObject") or {}
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return ""
|
||||||
|
return ", ".join(str(obj.get(k)) for k in legal_obj_keys if obj.get(k))
|
||||||
|
|
||||||
|
pairs = [
|
||||||
|
(_("VÖEN"), profile.get("tin")),
|
||||||
|
(_("Director"), profile.get("companyDirectorName")),
|
||||||
|
(_("Director PIN"), profile.get("pin")),
|
||||||
|
(_("Main activity"), profile.get("mainActivity")),
|
||||||
|
(_("Business classification"), profile.get("criteriaOfBusinessEntity")),
|
||||||
|
(_("Tax rate"), profile.get("taxRate")),
|
||||||
|
(_("Tax authority"), resolved.get("tax_authority") or profile.get("taxAuthorityCode")),
|
||||||
|
(_("Property type"), resolved.get("property_type") or profile.get("propertyType")),
|
||||||
|
(_("Registration date"), (profile.get("taxpayerRegistrationDate") or "")[:10]),
|
||||||
|
(_("Mobile phone"), profile.get("mobilePhoneNumber")),
|
||||||
|
(_("Landline phone"), profile.get("landlinePhoneNumber")),
|
||||||
|
(_("Email"), profile.get("email")),
|
||||||
|
(_("Legal address"), _addr_or(profile.get("legalAddress"), ("region", "locality", "street", "houseNumber"))),
|
||||||
|
(_("Actual address"), profile.get("actualAddress")),
|
||||||
|
(_("Employee count"), (profile.get("employeeData") or {}).get("employeeCount") if isinstance(profile.get("employeeData"), dict) else None),
|
||||||
|
]
|
||||||
|
|
||||||
|
vat_info = profile.get("vatInfo") if isinstance(profile.get("vatInfo"), dict) else None
|
||||||
|
if vat_info:
|
||||||
|
vat_date = vat_info.get("prOperationTableOperationDate") or ""
|
||||||
|
if isinstance(vat_date, str) and len(vat_date) == 8 and vat_date.isdigit():
|
||||||
|
vat_date = f"{vat_date[0:4]}-{vat_date[4:6]}-{vat_date[6:8]}"
|
||||||
|
pairs.extend([
|
||||||
|
(_("VAT certificate №"), vat_info.get("recDocumentDocumentNumber")),
|
||||||
|
(_("VAT registration date"), vat_date),
|
||||||
|
])
|
||||||
|
|
||||||
|
rows = [[label, str(value)] for label, value in pairs if value not in (None, "", [])]
|
||||||
|
return {"headers": headers, "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_cached(kind):
|
def get_cached(kind):
|
||||||
"""Return parsed cached data for a given kind. Used by future wizard steps that
|
"""Return parsed cached data for a given kind. Used by future wizard steps that
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ frappe.provide("jey_wizard");
|
||||||
// Bump this string in every commit that changes wizard code. Displayed in the badge so
|
// Bump this string in every commit that changes wizard code. Displayed in the badge so
|
||||||
// we can tell at a glance which version is actually running on a given machine. Kept in
|
// we can tell at a glance which version is actually running on a given machine. Kept in
|
||||||
// sync with __version__ in jey_wizard/__init__.py.
|
// sync with __version__ in jey_wizard/__init__.py.
|
||||||
const JEY_WIZARD_VERSION = "0.1.25";
|
const JEY_WIZARD_VERSION = "0.1.26";
|
||||||
|
|
||||||
// Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners
|
// Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners
|
||||||
// don't try to mutate a wizard that isn't slide-based anymore.
|
// don't try to mutate a wizard that isn't slide-based anymore.
|
||||||
|
|
@ -278,23 +278,26 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
} else if (this.asan_state === "done") {
|
} else if (this.asan_state === "done") {
|
||||||
const summary = this.asan_fetch_summary || {};
|
const summary = this.asan_fetch_summary || {};
|
||||||
const errors = this.asan_fetch_errors || {};
|
const errors = this.asan_fetch_errors || {};
|
||||||
const labels = {
|
// (cache_field, preview kind for get_preview_data)
|
||||||
objects_json: __("Objects"),
|
const datasets = [
|
||||||
cash_registers_json: __("Cash registers"),
|
["objects_json", "objects", __("Objects")],
|
||||||
pos_terminals_json: __("POS terminals"),
|
["cash_registers_json", "cash_registers", __("Cash registers")],
|
||||||
bank_accounts_json: __("Bank accounts"),
|
["pos_terminals_json", "pos_terminals", __("POS terminals")],
|
||||||
obligation_pacts_json: __("Obligation pacts"),
|
["bank_accounts_json", "bank_accounts", __("Bank accounts")],
|
||||||
presented_certs_json: __("Presented certificates"),
|
["obligation_pacts_json", "obligation_pacts", __("Obligation pacts")],
|
||||||
};
|
["presented_certs_json", "presented_certs", __("Presented certificates")],
|
||||||
const rows = Object.keys(labels).map((k) => {
|
];
|
||||||
const label = labels[k];
|
const rows = datasets.map(([field, kind, label]) => {
|
||||||
if (errors[k]) {
|
if (errors[field]) {
|
||||||
return `<li><b>${label}:</b> <span style="color:#c00">${__("failed")}</span> <span style="color:#888;font-size:12px">(${frappe.utils.escape_html(errors[k])})</span></li>`;
|
return `<li><b>${label}:</b> <span style="color:#c00">${__("failed")}</span> <span style="color:#888;font-size:12px">(${frappe.utils.escape_html(errors[field])})</span></li>`;
|
||||||
}
|
}
|
||||||
const n = summary[k];
|
const n = summary[field];
|
||||||
if (n === undefined) {
|
if (n === undefined) {
|
||||||
return `<li><b>${label}:</b> <span style="color:#888">—</span></li>`;
|
return `<li><b>${label}:</b> <span style="color:#888">—</span></li>`;
|
||||||
}
|
}
|
||||||
|
if (n > 0) {
|
||||||
|
return `<li><b>${label}:</b> <a href="#" class="jey-preview-link" data-kind="${kind}" data-title="${frappe.utils.escape_html(label)}">${n}</a> <span style="color:#888;font-size:12px">${__("(click to preview)")}</span></li>`;
|
||||||
|
}
|
||||||
return `<li><b>${label}:</b> ${n}</li>`;
|
return `<li><b>${label}:</b> ${n}</li>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
|
|
@ -302,7 +305,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
<h3 style="margin-bottom:16px;color:#080">✓ ${__("Authenticated & data loaded")}</h3>
|
<h3 style="margin-bottom:16px;color:#080">✓ ${__("Authenticated & data loaded")}</h3>
|
||||||
<p>${__("Pulled from the tax portal:")}</p>
|
<p>${__("Pulled from the tax portal:")}</p>
|
||||||
<ul style="line-height:1.8">
|
<ul style="line-height:1.8">
|
||||||
<li><b>${__("Company")}:</b> ${frappe.utils.escape_html(this.data.company_name)}</li>
|
<li><b>${__("Company")}:</b> <a href="#" class="jey-preview-link" data-kind="profile" data-title="${frappe.utils.escape_html(__("Company profile"))}">${frappe.utils.escape_html(this.data.company_name)}</a> <span style="color:#888;font-size:12px">${__("(click to preview)")}</span></li>
|
||||||
<li><b>${__("VÖEN")}:</b> ${frappe.utils.escape_html(this.data.asan_voen)}</li>
|
<li><b>${__("VÖEN")}:</b> ${frappe.utils.escape_html(this.data.asan_voen)}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div style="margin-top:16px;padding:12px;background:#f7f7f7;border-radius:6px">
|
<div style="margin-top:16px;padding:12px;background:#f7f7f7;border-radius:6px">
|
||||||
|
|
@ -314,6 +317,11 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
$body.find(".jey-change-company").on("click", () => this.reset_company_selection());
|
$body.find(".jey-change-company").on("click", () => this.reset_company_selection());
|
||||||
|
$body.find(".jey-preview-link").on("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const $a = $(e.currentTarget);
|
||||||
|
this.show_preview_modal($a.data("kind"), $a.data("title"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1100,6 +1108,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
this.asan_certificates = [];
|
this.asan_certificates = [];
|
||||||
this.asan_bearer_token = "";
|
this.asan_bearer_token = "";
|
||||||
this.asan_verification_code = "";
|
this.asan_verification_code = "";
|
||||||
|
this._preview_cache = null;
|
||||||
// Kick off a fresh auth round. Phone and user_id are still on
|
// Kick off a fresh auth round. Phone and user_id are still on
|
||||||
// the Asan Login record, so handle_authentication works without
|
// the Asan Login record, so handle_authentication works without
|
||||||
// re-prompting the user.
|
// re-prompting the user.
|
||||||
|
|
@ -1123,6 +1132,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
const m = r.message || {};
|
const m = r.message || {};
|
||||||
this.asan_fetch_summary = m.summary || {};
|
this.asan_fetch_summary = m.summary || {};
|
||||||
this.asan_fetch_errors = m.errors || {};
|
this.asan_fetch_errors = m.errors || {};
|
||||||
|
this._preview_cache = null;
|
||||||
this.fetch_company_profile();
|
this.fetch_company_profile();
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
|
|
@ -1499,6 +1509,64 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_preview_modal(kind, title) {
|
||||||
|
// Lazy-load the full preview blob once per done-screen visit, then
|
||||||
|
// dispatch to the requested kind. One round-trip covers every dataset
|
||||||
|
// (the cache doc is single-row, payloads are small after row-limit).
|
||||||
|
const renderInto = ($body, table) => {
|
||||||
|
if (!table || !table.rows || !table.rows.length) {
|
||||||
|
$body.html(`<div style="color:#888;padding:24px;text-align:center">${__("Nothing to preview.")}</div>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const head = (table.headers || []).map((h) => `<th style="text-align:left;padding:6px 10px;border-bottom:1px solid #ddd;font-weight:600">${frappe.utils.escape_html(h)}</th>`).join("");
|
||||||
|
const body = table.rows.map((row) => {
|
||||||
|
const tds = (row || []).map((cell) => `<td style="padding:6px 10px;border-bottom:1px solid #f0f0f0;vertical-align:top">${frappe.utils.escape_html(String(cell == null ? "" : cell))}</td>`).join("");
|
||||||
|
return `<tr>${tds}</tr>`;
|
||||||
|
}).join("");
|
||||||
|
$body.html(`
|
||||||
|
<div style="max-height:60vh;overflow:auto">
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||||||
|
<thead><tr>${head}</tr></thead>
|
||||||
|
<tbody>${body}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;color:#888;font-size:12px">${__("Showing")} ${table.rows.length} ${__("row(s)")}.</div>
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = (table) => {
|
||||||
|
const dlg = new frappe.ui.Dialog({
|
||||||
|
title: title || __("Preview"),
|
||||||
|
size: "extra-large",
|
||||||
|
fields: [{fieldtype: "HTML", fieldname: "body"}],
|
||||||
|
});
|
||||||
|
renderInto(dlg.fields_dict.body.$wrapper, table);
|
||||||
|
dlg.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._preview_cache) {
|
||||||
|
open(this._preview_cache[kind]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frappe.dom.freeze(__("Loading preview..."));
|
||||||
|
frappe.call({
|
||||||
|
method: "jey_wizard.etaxes.get_preview_data",
|
||||||
|
callback: (r) => {
|
||||||
|
frappe.dom.unfreeze();
|
||||||
|
if (!r || !r.message) {
|
||||||
|
frappe.msgprint(__("No preview data available."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._preview_cache = r.message;
|
||||||
|
open(this._preview_cache[kind]);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
frappe.dom.unfreeze();
|
||||||
|
frappe.msgprint(__("Failed to load preview."));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
show_error(summary, detail) {
|
show_error(summary, detail) {
|
||||||
this.$wrap.find(".jey-body").html(`
|
this.$wrap.find(".jey-body").html(`
|
||||||
<h3 style="color:#c00;margin-bottom:8px">${frappe.utils.escape_html(summary)}</h3>
|
<h3 style="color:#c00;margin-bottom:8px">${frappe.utils.escape_html(summary)}</h3>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue