diff --git a/jey_wizard/__init__.py b/jey_wizard/__init__.py index 43a0e4e..c8ec146 100644 --- a/jey_wizard/__init__.py +++ b/jey_wizard/__init__.py @@ -1 +1 @@ -__version__ = "0.1.25" +__version__ = "0.1.26" diff --git a/jey_wizard/etaxes.py b/jey_wizard/etaxes.py index 527759b..f2c23fd 100644 --- a/jey_wizard/etaxes.py +++ b/jey_wizard/etaxes.py @@ -219,6 +219,252 @@ def fetch_company_profile(): 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() def get_cached(kind): """Return parsed cached data for a given kind. Used by future wizard steps that diff --git a/jey_wizard/public/js/jey_setup.js b/jey_wizard/public/js/jey_setup.js index c848cdc..9b41439 100644 --- a/jey_wizard/public/js/jey_setup.js +++ b/jey_wizard/public/js/jey_setup.js @@ -10,7 +10,7 @@ frappe.provide("jey_wizard"); // 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 // 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 // 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") { const summary = this.asan_fetch_summary || {}; const errors = this.asan_fetch_errors || {}; - const labels = { - objects_json: __("Objects"), - cash_registers_json: __("Cash registers"), - pos_terminals_json: __("POS terminals"), - bank_accounts_json: __("Bank accounts"), - obligation_pacts_json: __("Obligation pacts"), - presented_certs_json: __("Presented certificates"), - }; - const rows = Object.keys(labels).map((k) => { - const label = labels[k]; - if (errors[k]) { - return `
${__("Pulled from the tax portal:")}