diff --git a/jey_wizard/__init__.py b/jey_wizard/__init__.py index 034f46c..6526deb 100644 --- a/jey_wizard/__init__.py +++ b/jey_wizard/__init__.py @@ -1 +1 @@ -__version__ = "0.0.6" +__version__ = "0.0.7" diff --git a/jey_wizard/amas.py b/jey_wizard/amas.py new file mode 100644 index 0000000..b936049 --- /dev/null +++ b/jey_wizard/amas.py @@ -0,0 +1,66 @@ +"""Thin wizard-side wrappers for ƏMAS (e-social.gov.az) integration. + +All heavy lifting (MyGovID auth, AMAS session, employee report, bulk import) already +lives in `invoice_az.amas_api`. The wizard calls those directly from JS; only two +things are wizard-specific and land here: + +1. Caching the user's employee selection between the wizard's employees step and the + setup_wizard_complete hook — because Company doesn't exist yet and the background + import job needs it. +2. Clearing that cache when the user chooses to skip. +""" + +import json + +import frappe +from frappe import _ + + +@frappe.whitelist() +def cache_selected_employees(employees_json, create_designation=0): + """Store the selected ƏMAS employees in the wizard cache so materialize_after_setup + can enqueue the bulk import with the freshly-created Company. + """ + _only_admin() + + if isinstance(employees_json, str): + parsed = json.loads(employees_json) + else: + parsed = employees_json or [] + + try: + create_designation = int(create_designation) + except (ValueError, TypeError): + create_designation = 0 + + payload = { + "employees": parsed, + "create_designation": create_designation, + } + + cache = frappe.get_single("Jey Wizard Etaxes Cache") + cache.amas_selected_employees_json = json.dumps(payload, ensure_ascii=False) + cache.flags.ignore_permissions = True + cache.save() + frappe.db.commit() + + return {"ok": True, "count": len(parsed)} + + +@frappe.whitelist() +def clear_amas_cache(): + """Called when the user skips the AMAS step. Leaves e-taxes cache alone.""" + _only_admin() + + cache = frappe.get_single("Jey Wizard Etaxes Cache") + cache.amas_selected_employees_json = "" + cache.flags.ignore_permissions = True + cache.save() + frappe.db.commit() + + return {"ok": True} + + +def _only_admin(): + if frappe.session.user != "Administrator": + frappe.throw(_("Only Administrator may run setup"), frappe.PermissionError) diff --git a/jey_wizard/etaxes.py b/jey_wizard/etaxes.py index d6a5ba6..475e785 100644 --- a/jey_wizard/etaxes.py +++ b/jey_wizard/etaxes.py @@ -189,6 +189,56 @@ def materialize_after_setup(args): "Jey Wizard materialize", ) + _materialize_amas_employees(company_name) + + +def _materialize_amas_employees(company_name): + """If the user loaded ƏMAS employees during the wizard, enqueue bulk import now + that Company exists. Skipped silently when the cache is empty (i.e. user skipped + the AMAS step).""" + cache = frappe.get_single("Jey Wizard Etaxes Cache") + raw = (cache.amas_selected_employees_json or "").strip() + if not raw: + return + + try: + payload = json.loads(raw) + except Exception as exc: + frappe.log_error( + f"materialize_after_setup: bad amas cache JSON: {exc}\n{traceback.format_exc()}", + "Jey Wizard materialize", + ) + return + + employees = payload.get("employees") or [] + if not employees: + return + + create_designation = payload.get("create_designation", 0) + + asan_login = frappe.db.get_value("Asan Login", {"is_default": 1}, "name") + if not asan_login: + frappe.log_error( + "materialize_after_setup: no default Asan Login — cannot import employees", + "Jey Wizard materialize", + ) + return + + try: + from invoice_az.amas_api import import_bulk_employees + + import_bulk_employees( + asan_login_name=asan_login, + employees_data=json.dumps(employees, ensure_ascii=False), + company=company_name, + create_designation=create_designation, + ) + except Exception as exc: + frappe.log_error( + f"materialize_after_setup: amas import enqueue failed: {exc}\n{traceback.format_exc()}", + "Jey Wizard materialize", + ) + # ---------- internals ---------- diff --git a/jey_wizard/jey_wizard/doctype/jey_wizard_etaxes_cache/jey_wizard_etaxes_cache.json b/jey_wizard/jey_wizard/doctype/jey_wizard_etaxes_cache/jey_wizard_etaxes_cache.json index 90d0cf5..e7a0384 100644 --- a/jey_wizard/jey_wizard/doctype/jey_wizard_etaxes_cache/jey_wizard_etaxes_cache.json +++ b/jey_wizard/jey_wizard/doctype/jey_wizard_etaxes_cache/jey_wizard_etaxes_cache.json @@ -15,7 +15,9 @@ "pos_terminals_json", "bank_accounts_json", "obligation_pacts_json", - "presented_certs_json" + "presented_certs_json", + "amas_section", + "amas_selected_employees_json" ], "fields": [ { @@ -88,6 +90,18 @@ "label": "Presented Certificates", "options": "JSON", "read_only": 1 + }, + { + "fieldname": "amas_section", + "fieldtype": "Section Break", + "label": "ƏMAS" + }, + { + "fieldname": "amas_selected_employees_json", + "fieldtype": "Code", + "label": "Selected ƏMAS Employees", + "options": "JSON", + "read_only": 1 } ], "index_web_pages_for_search": 0, diff --git a/jey_wizard/public/js/jey_setup.js b/jey_wizard/public/js/jey_setup.js index 6f93467..b22cbee 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.0.6"; +const JEY_WIZARD_VERSION = "0.0.7"; // 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. @@ -19,14 +19,26 @@ frappe.setup.slides_settings = []; frappe.setup.events = {}; // Asan polling: 20 attempts × 5s = ~100s window for the user to confirm on phone. -// Mirrors invoice_az/asan_login.js timing. const ASAN_POLL_INTERVAL_MS = 5000; const ASAN_POLL_MAX_ATTEMPTS = 20; +// MyGovID's /asanSignLogin is a server-held long-poll (up to 120s). We let the call +// run for 130s before giving up client-side. +const MYGOVID_TIMEOUT_MS = 130000; + +// Language code -> full name. load_messages and Frappe's setup_complete both accept +// the full name form; the language step uses codes internally so the field is compact. +const LANG_NAME = { + en: "English", + az: "Azərbaycan dili", + ru: "Русский", +}; + frappe.setup.SetupWizard = class JeySetupWizard { constructor(settings = {}) { this.parent = settings.parent; this.values = {}; + const year = new Date().getFullYear(); this.data = { language: "az", asan_phone: "", @@ -34,16 +46,31 @@ frappe.setup.SetupWizard = class JeySetupWizard { asan_login_name: "", asan_voen: "", company_name: "", + fy_start_date: `${year}-01-01`, + fy_end_date: `${year}-12-31`, + // AMAS + amas_skipped: false, + amas_selected_employees: [], + amas_create_designation: 1, + amas_organization_name: "", }; - // Sub-state for the asan step: input -> polling -> certs -> done + + // Sub-state machines per step. `input` / `intro` is the always-safe fallback + // that Back can return to without worrying about dangling network state. this.asan_state = "input"; this.asan_certificates = []; this.asan_bearer_token = ""; this.asan_poll_attempts = 0; this.asan_poll_timer = null; + this.amas_state = "intro"; + this.amas_accounts = []; + this.amas_employees = []; + this.amas_existing_map = {}; + this.amas_verification_code = ""; + this.current_step = 0; - this.steps = ["language", "asan", "company", "confirm"]; + this.steps = ["language", "asan", "employees", "company", "confirm"]; this.render(); } @@ -53,13 +80,14 @@ frappe.setup.SetupWizard = class JeySetupWizard { render() { const $parent = $(this.parent).empty(); this.$wrap = $(` -
+
v${JEY_WIZARD_VERSION}

Jey Setup

+
`).appendTo($parent); this.show_current_step(); @@ -71,19 +99,15 @@ frappe.setup.SetupWizard = class JeySetupWizard { const $body = this.$wrap.find(".jey-body").empty(); const $nav = this.$wrap.find(".jey-nav").empty(); this.$wrap.find(".jey-progress").text( - `Step ${this.current_step + 1} of ${this.steps.length}` + __("Step {0} of {1}", [this.current_step + 1, this.steps.length]) ); this.$wrap.find(".jey-status").empty(); - if (step === "language") { - this.render_language(($body)); - } else if (step === "asan") { - this.render_asan($body); - } else if (step === "company") { - this.render_company($body); - } else if (step === "confirm") { - this.render_confirm($body); - } + if (step === "language") this.render_language($body); + else if (step === "asan") this.render_asan($body); + else if (step === "employees") this.render_employees($body); + else if (step === "company") this.render_company($body); + else if (step === "confirm") this.render_confirm($body); this.render_nav($nav, step); } @@ -92,7 +116,8 @@ frappe.setup.SetupWizard = class JeySetupWizard { render_language($body) { $body.html(` - +

${__("Language")}

+ `); $body.find(".jey-language").val(this.data.language); + // Apply language immediately on change so subsequent screens pick it up. + $body.find(".jey-language").on("change", (e) => { + const code = $(e.currentTarget).val(); + this.change_language(code); + }); } render_asan($body) { - // Sub-state machine: input -> polling -> certs -> done. - // All four sub-states render into the same $body slot. if (this.asan_state === "input") { $body.html(` -

Asan Imza authentication

+

${__("Asan Imza authentication")}

- - + +
- - + +
- + `); $body.find(".jey-asan-phone").val(this.data.asan_phone); $body.find(".jey-asan-userid").val(this.data.asan_user_id); @@ -124,49 +152,48 @@ frappe.setup.SetupWizard = class JeySetupWizard { } else if (this.asan_state === "polling") { const code = this.asan_verification_code || ""; $body.html(` -

Confirm on your phone

-

Open the Asan Imza app on your phone and confirm the request.

+

${__("Confirm on your phone")}

+

${__("Open the Asan Imza app on your phone and confirm the request.")}

${code ? `
-
Verification code
+
${__("Verification code")}
${frappe.utils.escape_html(String(code))}
` : ""}
-
Waiting for confirmation...
+
${__("Waiting for confirmation...")}
- `); } else if (this.asan_state === "certs") { const rows = (this.asan_certificates || []).map((c, i) => { const legal = c.legalInfo || {}; const name = legal.name || ""; const voen = legal.voen || legal.tin || ""; - const liquidated = c.liquidated ? " (liquidated)" : ""; + const liquidated = c.liquidated ? ` (${__("liquidated")})` : ""; return ` ${frappe.utils.escape_html(name)}${liquidated} ${frappe.utils.escape_html(voen)} - + `; }).join(""); $body.html(` -

Select your company

+

${__("Select your company")}

${rows ? ` - - + + ${rows}
CompanyVÖEN${__("Company")}${__("VÖEN")}
` - : `

No legal-entity certificates available on this Asan ID. Use a phone/ID linked to a company.

`} + : `

${__("No legal-entity certificates available on this Asan ID. Use a phone/ID linked to a company.")}

`} `); $body.find(".jey-cert-pick").on("click", (e) => { const idx = parseInt($(e.currentTarget).attr("data-idx"), 10); @@ -174,29 +201,28 @@ frappe.setup.SetupWizard = class JeySetupWizard { }); } else if (this.asan_state === "fetching") { $body.html(` -

Loading data from tax portal...

-

Pulling objects, cash registers, POS terminals, bank accounts, obligation pacts and presented certificates. This usually takes 10-30 seconds.

+

${__("Loading data from tax portal...")}

+

${__("Pulling objects, cash registers, POS terminals, bank accounts, obligation pacts and presented certificates. This usually takes 10-30 seconds.")}

-
Fetching...
+
${__("Fetching...")}
- `); } 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", + 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 `
  • ${label}: failed (${frappe.utils.escape_html(errors[k])})
  • `; + return `
  • ${label}: ${__("failed")} (${frappe.utils.escape_html(errors[k])})
  • `; } const n = summary[k]; if (n === undefined) { @@ -206,70 +232,275 @@ frappe.setup.SetupWizard = class JeySetupWizard { }).join(""); $body.html(` -

    ✓ Authenticated & data loaded

    -

    Pulled from the tax portal:

    +

    ✓ ${__("Authenticated & data loaded")}

    +

    ${__("Pulled from the tax portal:")}

    -
    Cached datasets
    +
    ${__("Cached datasets")}
    -

    Everything above is now available to later wizard steps. After finalization, the data will be persisted into the E-Taxes DocTypes linked to your new Company.

    `); } } + render_employees($body) { + if (this.amas_state === "intro") { + const alreadyLoaded = (this.data.amas_selected_employees || []).length; + $body.html(` +

    ${__("Load employees from ƏMAS")}

    +

    ${__("ƏMAS (e-social.gov.az) stores employment contracts. If your company has employees registered there, we can import them now.")}

    +
    + ${__("Heads up:")} ${__("You will receive a second confirmation request on your phone from ASAN Sign / MyGovID. This is separate from the one you just approved for the tax portal.")} +
    +

    ${__("This step is optional. Skip it if your company has no employees or is not registered with ƏMAS.")}

    + ${alreadyLoaded + ? `
    + ${__("Already selected:")} ${alreadyLoaded} ${__("employee(s)")}. +
    ` + : ""} +
    + + +
    + `); + $body.find(".jey-amas-load").on("click", () => this.amas_start_auth()); + $body.find(".jey-amas-skip").on("click", () => this.amas_skip()); + } else if (this.amas_state === "auth") { + const code = this.amas_verification_code || ""; + $body.html(` +

    ${__("Confirm ƏMAS request on your phone")}

    +

    ${__("A second confirmation request has been sent to your phone. Open ASAN Sign and approve it.")}

    + ${code + ? `
    +
    ${__("Verification code")}
    +
    ${frappe.utils.escape_html(String(code))}
    +
    ` + : `
    ${__("Retrieving verification code...")}
    `} +
    +
    +
    ${__("Waiting for confirmation...")}
    +
    + `); + } else if (this.amas_state === "connecting") { + $body.html(` +

    ${__("Connecting to ƏMAS...")}

    +
    +
    +
    ${__("Establishing session...")}
    +
    + `); + } else if (this.amas_state === "accounts") { + const rows = (this.amas_accounts || []).map((a, i) => { + const name = a.accountName || a.name || ""; + const voen = a.accountNumber || a.voen || a.number || ""; + const role = a.roleName || a.role || ""; + return ` + + ${frappe.utils.escape_html(name)} + ${frappe.utils.escape_html(voen)} + ${frappe.utils.escape_html(role)} + + + + + `; + }).join(""); + + $body.html(` +

    ${__("Select organization")}

    +

    ${__("We could not auto-match by VÖEN. Pick the organization you want to import employees from.")}

    + + + + + + + + ${rows} +
    ${__("Organization")}${__("VÖEN")}${__("Role")}
    + `); + $body.find(".jey-amas-acc-pick").on("click", (e) => { + const idx = parseInt($(e.currentTarget).attr("data-idx"), 10); + this.amas_pick_account(idx); + }); + } else if (this.amas_state === "loading") { + $body.html(` +

    ${__("Loading employees...")}

    +
    +
    +
    ${__("Fetching from ƏMAS...")}
    +
    + `); + } else if (this.amas_state === "select") { + const emps = this.amas_employees || []; + const existing = this.amas_existing_map || {}; + const newCount = emps.filter((e) => !existing[e.identification_number]).length; + const existingCount = emps.length - newCount; + + const rows = emps.map((e, i) => { + const fin = e.identification_number || ""; + const existingId = existing[fin]; + const statusCell = existingId + ? `${frappe.utils.escape_html(existingId)}` + : ""; + return ` + + + ${frappe.utils.escape_html(e.full_name || "")} + ${frappe.utils.escape_html(fin)} + ${frappe.utils.escape_html(e.work_position_text || "")} + ${frappe.utils.escape_html(String(e.monthly_salary || ""))} + ${frappe.utils.escape_html(e.contract_status || "")} + ${statusCell} + + `; + }).join(""); + + $body.html(` +

    ${__("Select employees from ƏMAS")}

    +
    + ${__("Organization:")} ${frappe.utils.escape_html(this.data.amas_organization_name || "")}
    + ${__("Employees found:")} ${emps.length} + (${__("new")}: ${newCount}, ${__("already in system")}: ${existingCount}) +
    + +
    + + + + + + + + + + + + + ${rows} +
    ${__("Full Name")}${__("FIN")}${__("Position")}${__("Salary")}${__("Status")}${__("In System")}
    +
    + `); + + $body.find(".jey-emp-all").on("change", (e) => { + const checked = $(e.currentTarget).prop("checked"); + $body.find(".jey-emp-pick").prop("checked", checked); + }); + } else if (this.amas_state === "done") { + const n = (this.data.amas_selected_employees || []).length; + $body.html(` +

    ✓ ${__("Employees selected")}

    +

    ${__("Selected {0} employee(s). They will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes.", [n])}

    +
    + +
    + `); + $body.find(".jey-amas-back-intro").on("click", () => { + this.amas_state = "intro"; + this.show_current_step(); + }); + } + } + render_company($body) { $body.html(` - - - ${this.data.asan_voen - ? `
    VÖEN ${frappe.utils.escape_html(this.data.asan_voen)} (from Asan)
    ` - : ""} +

    ${__("Company")}

    +
    + + + ${this.data.asan_voen + ? `
    ${__("VÖEN {0} (from Asan)", [frappe.utils.escape_html(this.data.asan_voen)])}
    ` + : ""} +
    +

    ${__("Fiscal Year")}

    +
    +
    + + +
    +
    + + +
    +
    +
    ${__("Default: current calendar year (Jan 1 – Dec 31).")}
    `); $body.find(".jey-company-name").val(this.data.company_name); + $body.find(".jey-fy-start").val(this.data.fy_start_date); + $body.find(".jey-fy-end").val(this.data.fy_end_date); } render_confirm($body) { + const empCount = (this.data.amas_selected_employees || []).length; + const empRow = this.data.amas_skipped + ? __("Skipped") + : (empCount > 0 ? __("{0} selected", [empCount]) : __("None")); $body.html(` -

    Confirm

    +

    ${__("Confirm")}

    `); } render_nav($nav, step) { - // On the asan step we don't show standard Next — auth flow drives navigation - // internally and only enables Next when the user reaches the "done" sub-state. - const showStdNext = - this.current_step < this.steps.length - 1 && - !(step === "asan" && this.asan_state !== "done"); - const showFinish = this.current_step === this.steps.length - 1; - + // Back is available on every step except the very first (language). Within a + // multi-sub-state step, Back first rewinds the sub-state to its intro; only + // Back from intro jumps to the previous wizard step. if (this.current_step > 0) { - $(``) + $(``) .appendTo($nav) .on("click", () => this.prev()); } + + // Standard Next: shown when the step has no internal flow-control, or when a + // multi-sub-state step is in its "terminal" sub-state. + let showStdNext = false; + const lastStep = this.current_step === this.steps.length - 1; + if (!lastStep) { + if (step === "asan") { + // Asan drives its own transitions. Only the terminal "done" sub-state + // surfaces a standard Next. + showStdNext = this.asan_state === "done"; + } else if (step === "employees") { + if (this.amas_state === "select") { + // Commit selection instead of plain Next. + $(``) + .appendTo($nav) + .on("click", () => this.amas_commit_selection()); + return; + } + // On intro the user uses explicit Skip / Load buttons, not Next. + // Only the "done" sub-state surfaces a standard Next to move on. + showStdNext = this.amas_state === "done"; + } else { + showStdNext = true; + } + } + if (showStdNext) { - $(``) + $(``) .appendTo($nav) .on("click", () => this.next()); } - if (showFinish) { - $(``) + if (lastStep) { + $(``) .appendTo($nav) .on("click", () => this.finalize()); } @@ -280,14 +511,26 @@ frappe.setup.SetupWizard = class JeySetupWizard { capture_current() { const step = this.steps[this.current_step]; if (step === "language") { - this.data.language = this.$wrap.find(".jey-language").val(); + this.data.language = this.$wrap.find(".jey-language").val() || "az"; } else if (step === "company") { const name = (this.$wrap.find(".jey-company-name").val() || "").trim(); if (!name) { - this.$wrap.find(".jey-status").text("Company name is required"); + this.$wrap.find(".jey-status").text(__("Company name is required")); return false; } this.data.company_name = name; + const fyStart = (this.$wrap.find(".jey-fy-start").val() || "").trim(); + const fyEnd = (this.$wrap.find(".jey-fy-end").val() || "").trim(); + if (!fyStart || !fyEnd) { + this.$wrap.find(".jey-status").text(__("Fiscal year start and end dates are required")); + return false; + } + if (fyEnd < fyStart) { + this.$wrap.find(".jey-status").text(__("Fiscal year end date must be after the start date")); + return false; + } + this.data.fy_start_date = fyStart; + this.data.fy_end_date = fyEnd; } return true; } @@ -299,43 +542,67 @@ frappe.setup.SetupWizard = class JeySetupWizard { } prev() { - // Going back to/from asan resets its sub-state so the user can re-enter cleanly. const step = this.steps[this.current_step]; - if (step === "asan") { + // If we're inside a sub-state of a multi-state step, Back returns to its intro + // rather than jumping to the previous wizard step. Lets the user re-try. + if (step === "asan" && this.asan_state !== "input" && this.asan_state !== "done") { this.asan_state = "input"; + this.show_current_step(); + return; } + if (step === "employees" && this.amas_state !== "intro" && this.amas_state !== "done") { + this.amas_state = "intro"; + this.show_current_step(); + return; + } + + // Crossing a step boundary: leave sub-state as-is so the user sees whatever + // they completed before (e.g. "done" screen). They can still rewind with a + // second Back. this.current_step--; - // Same when stepping back ONTO asan from the company step — keep it simple, - // don't try to resume polling/cert state mid-flow. - if (this.steps[this.current_step] === "asan") { - this.asan_state = "input"; - } this.show_current_step(); } + // ---------- language ---------- + + change_language(code) { + if (!code || code === this.data.language) return; + const langName = LANG_NAME[code] || "English"; + frappe._messages = {}; + frappe.call({ + method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages", + args: { language: langName }, + freeze: true, + freeze_message: __("Loading translations..."), + callback: () => { + this.data.language = code; + this.show_current_step(); + }, + }); + } + // ---------- asan flow ---------- start_asan_auth() { const phone = (this.$wrap.find(".jey-asan-phone").val() || "").trim(); const userId = (this.$wrap.find(".jey-asan-userid").val() || "").trim(); if (!phone || !userId) { - this.$wrap.find(".jey-status").text("Phone and User ID are required"); + this.$wrap.find(".jey-status").text(__("Phone and User ID are required")); return; } this.data.asan_phone = phone; this.data.asan_user_id = userId; this.$wrap.find(".jey-status").empty(); - // Step 1: ensure Asan Login record exists & is reset, then trigger SMS. frappe.call({ method: "jey_wizard.api.ensure_asan_login", args: { phone, user_id: userId }, freeze: true, - freeze_message: "Preparing...", + freeze_message: __("Preparing..."), callback: (r) => { const m = r.message || {}; if (!m.name) { - this.$wrap.find(".jey-status").text("Failed to create Asan Login record"); + this.$wrap.find(".jey-status").text(__("Failed to create Asan Login record")); return; } this.data.asan_login_name = m.name; @@ -349,13 +616,11 @@ frappe.setup.SetupWizard = class JeySetupWizard { method: "invoice_az.auth.handle_authentication", args: { asan_login_name: this.data.asan_login_name }, freeze: true, - freeze_message: "Sending request to Asan Imza...", + freeze_message: __("Sending request to Asan Imza..."), callback: (r) => { const m = r.message || {}; if (!m.success) { - this.$wrap.find(".jey-status").text( - m.message || "Failed to start authentication" - ); + this.$wrap.find(".jey-status").text(m.message || __("Failed to start authentication")); return; } this.asan_bearer_token = m.bearer_token || ""; @@ -371,21 +636,22 @@ frappe.setup.SetupWizard = class JeySetupWizard { poll_asan() { if (this.asan_poll_attempts >= ASAN_POLL_MAX_ATTEMPTS) { this.$wrap.find(".jey-asan-poll-status").html( - `Timeout — confirmation not received within ~100 seconds.` + `${__("Timeout — confirmation not received within ~100 seconds.")}` ); this.$wrap.find(".jey-spinner").remove(); - this.$wrap.find(".jey-status").empty(); this.$wrap.find(".jey-nav") .empty() - .html( - `` + - `` + .append( + $(``) + .on("click", () => this.prev()) ) - .find(".jey-back").on("click", () => this.prev()).end() - .find(".jey-asan-restart").on("click", () => { - this.asan_state = "input"; - this.show_current_step(); - }); + .append( + $(``) + .on("click", () => { + this.asan_state = "input"; + this.show_current_step(); + }) + ); return; } this.asan_poll_attempts += 1; @@ -397,7 +663,7 @@ frappe.setup.SetupWizard = class JeySetupWizard { bearer_token: this.asan_bearer_token, }, callback: (r) => { - if (this.asan_state !== "polling") return; // user navigated away + if (this.asan_state !== "polling") return; const m = r.message || {}; if (m.authenticated) { this.fetch_certificates(); @@ -420,24 +686,18 @@ frappe.setup.SetupWizard = class JeySetupWizard { fetch_certificates() { this.stop_polling(); - this.$wrap.find(".jey-asan-poll-status").text("Loading certificates..."); + this.$wrap.find(".jey-asan-poll-status").text(__("Loading certificates...")); frappe.call({ method: "invoice_az.auth.get_auth_certificates", args: { asan_login_name: this.data.asan_login_name }, callback: (r) => { const m = r.message || {}; if (!m.success) { - this.$wrap.find(".jey-status").text( - m.message || "Failed to load certificates" - ); + this.$wrap.find(".jey-status").text(m.message || __("Failed to load certificates")); return; } const all = m.certificates || []; - // Wizard scope: legal entities only (per requirement). Filter and warn - // if nothing remains so the user knows why the list is empty. - this.asan_certificates = all.filter( - (c) => c.taxpayerType === "legal" - ); + this.asan_certificates = all.filter((c) => c.taxpayerType === "legal"); this.asan_state = "certs"; this.show_current_step(); }, @@ -458,13 +718,11 @@ frappe.setup.SetupWizard = class JeySetupWizard { certificate_name: certName, }, freeze: true, - freeze_message: "Selecting certificate...", + freeze_message: __("Selecting certificate..."), callback: (r) => { const m = r.message || {}; if (!m.success) { - this.$wrap.find(".jey-status").text( - m.message || "Failed to select certificate" - ); + this.$wrap.find(".jey-status").text(m.message || __("Failed to select certificate")); return; } this.run_select_taxpayer(); @@ -477,13 +735,11 @@ frappe.setup.SetupWizard = class JeySetupWizard { method: "invoice_az.auth.select_taxpayer", args: { asan_login_name: this.data.asan_login_name }, freeze: true, - freeze_message: "Choosing taxpayer & getting main token...", + freeze_message: __("Choosing taxpayer & getting main token..."), callback: (r) => { const m = r.message || {}; if (!m.success) { - this.$wrap.find(".jey-status").text( - m.message || "Failed to choose taxpayer" - ); + this.$wrap.find(".jey-status").text(m.message || __("Failed to choose taxpayer")); return; } this.fetch_company_from_cert(); @@ -499,8 +755,6 @@ frappe.setup.SetupWizard = class JeySetupWizard { const m = r.message || {}; this.data.company_name = m.company_name || this.data.company_name; this.data.asan_voen = m.voen || ""; - // Move UI into the "fetching" sub-state so the user sees progress while - // we pull all six e-taxes lists into the cache doctype. this.asan_state = "fetching"; this.show_current_step(); this.fetch_etaxes_data(); @@ -511,7 +765,6 @@ frappe.setup.SetupWizard = class JeySetupWizard { fetch_etaxes_data() { frappe.call({ method: "jey_wizard.etaxes.fetch_all_etaxes", - // Deliberately no freeze_message — UI already shows a spinner. callback: (r) => { const m = r.message || {}; this.asan_fetch_summary = m.summary || {}; @@ -520,24 +773,254 @@ frappe.setup.SetupWizard = class JeySetupWizard { this.show_current_step(); }, error: () => { - // The fetch is best-effort; we still proceed to done and let the user - // continue. The setup_wizard_complete hook will re-fetch + persist later. this.asan_fetch_summary = {}; - this.asan_fetch_errors = { all: "fetch call failed — see server logs" }; + this.asan_fetch_errors = { all: __("fetch call failed — see server logs") }; this.asan_state = "done"; this.show_current_step(); }, }); } + // ---------- AMAS flow ---------- + + amas_skip() { + this.data.amas_skipped = true; + this.data.amas_selected_employees = []; + frappe.call({ + method: "jey_wizard.amas.clear_amas_cache", + freeze: true, + freeze_message: __("Skipping..."), + callback: () => { + this.current_step++; + this.show_current_step(); + }, + }); + } + + amas_start_auth() { + if (!this.data.asan_login_name) { + this.$wrap.find(".jey-status").text(__("Finish Asan Imza authentication first.")); + return; + } + this.data.amas_skipped = false; + this.amas_verification_code = ""; + this.amas_state = "auth"; + this.show_current_step(); + + // Long-polling: server holds the call until user confirms on phone (~120s). + frappe.call({ + method: "invoice_az.invoice_az.doctype.asan_login.asan_login.start_mygovid_login", + args: { asan_login_name: this.data.asan_login_name }, + timeout: MYGOVID_TIMEOUT_MS, + callback: (r) => { + if (this.amas_state !== "auth") return; + const m = r.message || {}; + if (!m.success) { + this.$wrap.find(".jey-status").text(m.message || __("MyGovID authentication failed.")); + this.amas_state = "intro"; + this.show_current_step(); + return; + } + this.amas_connect(); + }, + error: () => { + if (this.amas_state !== "auth") return; + this.$wrap.find(".jey-status").text(__("MyGovID request timed out.")); + this.amas_state = "intro"; + this.show_current_step(); + }, + }); + + // Fetch verification code shortly after the server started the long-poll. + setTimeout(() => { + if (this.amas_state !== "auth") return; + frappe.call({ + method: "invoice_az.invoice_az.doctype.asan_login.asan_login.get_mygovid_verification_code", + args: { asan_login_name: this.data.asan_login_name }, + callback: (r) => { + if (this.amas_state !== "auth") return; + const m = r.message || {}; + if (m.success && m.code) { + this.amas_verification_code = m.code; + this.show_current_step(); + } + }, + }); + }, 800); + } + + amas_connect() { + this.amas_state = "connecting"; + this.show_current_step(); + + frappe.call({ + method: "invoice_az.amas_api.connect_amas", + args: { asan_login_name: this.data.asan_login_name }, + callback: (r) => { + const m = r.message || {}; + if (!m.success) { + this.$wrap.find(".jey-status").text(m.message || __("Failed to connect to ƏMAS.")); + this.amas_state = "intro"; + this.show_current_step(); + return; + } + this.amas_accounts = m.accounts || []; + // Auto-pick by VOEN match with the e-taxes cert; fall back to manual list. + const targetVoen = String(this.data.asan_voen || "").trim(); + let matchIdx = -1; + if (targetVoen) { + matchIdx = this.amas_accounts.findIndex((a) => { + const voen = String(a.accountNumber || a.voen || a.number || "").trim(); + return voen && voen === targetVoen; + }); + } + if (matchIdx >= 0) { + this.amas_pick_account(matchIdx); + } else if (this.amas_accounts.length === 1) { + this.amas_pick_account(0); + } else if (this.amas_accounts.length === 0) { + this.$wrap.find(".jey-status").text(__("No organizations found in ƏMAS for this account.")); + this.amas_state = "intro"; + this.show_current_step(); + } else { + this.amas_state = "accounts"; + this.show_current_step(); + } + }, + }); + } + + amas_pick_account(idx) { + const acc = this.amas_accounts[idx]; + if (!acc) return; + const oid = acc.accountOid || acc.oid || ""; + const name = acc.accountName || acc.name || ""; + const number = acc.accountNumber || acc.voen || acc.number || ""; + const role = acc.roleName || acc.role || "chairman"; + + this.amas_state = "connecting"; + this.show_current_step(); + + frappe.call({ + method: "invoice_az.amas_api.change_amas_account", + args: { + asan_login_name: this.data.asan_login_name, + account_oid: oid, + account_name: name, + account_number: number, + role_name: role, + }, + callback: (r) => { + const m = r.message || {}; + if (!m.success) { + this.$wrap.find(".jey-status").text(m.message || __("Failed to select organization.")); + this.amas_state = "intro"; + this.show_current_step(); + return; + } + this.data.amas_organization_name = name; + this.amas_load_employees(); + }, + }); + } + + amas_load_employees() { + this.amas_state = "loading"; + this.show_current_step(); + + frappe.call({ + method: "invoice_az.amas_api.get_employees_report", + args: { + asan_login_name: this.data.asan_login_name, + offset: 0, + limit: 1000, + }, + callback: (r) => { + const m = r.message || {}; + if (!m.success) { + this.$wrap.find(".jey-status").text(m.message || __("Failed to fetch employees.")); + this.amas_state = "intro"; + this.show_current_step(); + return; + } + this.amas_employees = m.employees || []; + if (this.amas_employees.length === 0) { + this.$wrap.find(".jey-status").text(__("No employees found in ƏMAS for this organization.")); + this.amas_state = "intro"; + this.show_current_step(); + return; + } + // Check which FINs are already in the Employee doctype. + const fins = this.amas_employees + .map((e) => e.identification_number) + .filter(Boolean); + if (fins.length === 0) { + this.amas_existing_map = {}; + this.amas_state = "select"; + this.show_current_step(); + return; + } + frappe.call({ + method: "frappe.client.get_list", + args: { + doctype: "Employee", + filters: { passport_number: ["in", fins] }, + fields: ["name", "passport_number"], + limit_page_length: 0, + }, + callback: (r2) => { + const map = {}; + (r2.message || []).forEach((emp) => { + if (emp.passport_number) map[emp.passport_number] = emp.name; + }); + this.amas_existing_map = map; + this.amas_state = "select"; + this.show_current_step(); + }, + }); + }, + }); + } + + amas_commit_selection() { + const $body = this.$wrap.find(".jey-body"); + const selected = []; + $body.find(".jey-emp-pick:checked").each((i, el) => { + const idx = parseInt($(el).attr("data-idx"), 10); + if (!isNaN(idx)) selected.push(this.amas_employees[idx]); + }); + if (selected.length === 0) { + this.$wrap.find(".jey-status").text(__("Select at least one employee, or go back and skip this step.")); + return; + } + const createDesignation = $body.find(".jey-emp-create-desig").is(":checked") ? 1 : 0; + this.data.amas_create_designation = createDesignation; + + frappe.call({ + method: "jey_wizard.amas.cache_selected_employees", + args: { + employees_json: JSON.stringify(selected), + create_designation: createDesignation, + }, + freeze: true, + freeze_message: __("Saving selection..."), + callback: (r) => { + const m = r.message || {}; + if (!m.ok) { + this.$wrap.find(".jey-status").text(__("Failed to cache selection.")); + return; + } + this.data.amas_selected_employees = selected; + this.data.amas_skipped = false; + this.amas_state = "done"; + this.show_current_step(); + }, + }); + } + // ---------- finalization ---------- build_setup_args() { - const LANG_CODE_TO_NAME = { - en: "English", - az: "Azərbaycan dili", - ru: "Русский", - }; const company = (this.data.company_name || "").trim() || "Test Company AZ"; const parts = company.split(/\s+/).filter(Boolean); const abbr = ( @@ -545,13 +1028,10 @@ frappe.setup.SetupWizard = class JeySetupWizard { ? parts.slice(0, 3).map((p) => p[0]).join("") : (parts[0] || "CO").slice(0, 3) ).toUpperCase().slice(0, 10); - const year = new Date().getFullYear(); const admin = frappe.boot.user || {}; - // Mirror exactly what Frappe's default slides would submit. All AZ-specific - // values are hardcoded; only language and company_name come from the UI. return { - language: LANG_CODE_TO_NAME[this.data.language] || "English", + language: LANG_NAME[this.data.language] || "English", country: "Azerbaijan", timezone: "Asia/Baku", currency: "AZN", @@ -561,19 +1041,16 @@ frappe.setup.SetupWizard = class JeySetupWizard { password: "", company_name: company, company_abbr: abbr, - // This must be the `name` FIELD from inside the JSON chart file, not the - // filename. erpnext's get_chart() iterates verified/*.json and matches on - // the "name" key. az_locale's az_chart_of_accounts.json has name="Azerbaijan Republic". chart_of_accounts: "Azerbaijan Republic", - fy_start_date: `${year}-01-01`, - fy_end_date: `${year}-12-31`, + fy_start_date: this.data.fy_start_date, + fy_end_date: this.data.fy_end_date, setup_demo: 0, }; } finalize() { this.$wrap.find(".jey-body").html( - `

    Setting up your system. This can take a minute...

    ` + `

    ${__("Setting up your system. This can take a minute...")}

    ` ); this.$wrap.find(".jey-nav").empty(); this.$wrap.find(".jey-status").empty(); @@ -581,9 +1058,6 @@ frappe.setup.SetupWizard = class JeySetupWizard { const values = this.build_setup_args(); frappe.call({ - // Call the stock Frappe endpoint directly — same entrypoint the default - // wizard uses when the user hits Finish. This keeps typing_validations, - // sanitize_input and parse_args in the normal HTTP/whitelist code path. method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete", args: { args: values }, freeze: true, @@ -591,21 +1065,21 @@ frappe.setup.SetupWizard = class JeySetupWizard { const m = r.message || {}; if (m.status === "ok") { this.$wrap.find(".jey-body").html( - `

    Setup complete! Redirecting...

    ` + `

    ${__("Setup complete! Redirecting...")}

    ` ); setTimeout(() => { window.location.href = "/app"; }, 1500); } else if (m.status === "registered") { this.show_error( - "Setup is running in background mode, not supported by skeleton", + __("Setup is running in background mode, not supported by skeleton"), JSON.stringify(m, null, 2) ); } else if (m.fail !== undefined) { this.show_error(m.fail, JSON.stringify(m, null, 2)); } else { this.show_error( - "Setup returned unexpected response", + __("Setup returned unexpected response"), JSON.stringify(m, null, 2) ); } @@ -613,8 +1087,8 @@ frappe.setup.SetupWizard = class JeySetupWizard { error: () => { const resp = frappe.last_response || {}; this.show_error( - resp.setup_wizard_failure_message || "Setup failed", - resp.exc || resp.exception || "(check server logs)" + resp.setup_wizard_failure_message || __("Setup failed"), + resp.exc || resp.exception || __("(check server logs)") ); }, }); @@ -624,14 +1098,15 @@ frappe.setup.SetupWizard = class JeySetupWizard { this.$wrap.find(".jey-body").html(`

    ${frappe.utils.escape_html(summary)}

    ${
    -				detail ? frappe.utils.escape_html(String(detail)) : "(no details)"
    +				detail ? frappe.utils.escape_html(String(detail)) : __("(no details)")
     			}
    `); this.$wrap.find(".jey-status").empty(); this.$wrap.find(".jey-nav") .empty() - .html(``) - .find(".jey-retry") - .on("click", () => window.location.reload()); + .append( + $(``) + .on("click", () => window.location.reload()) + ); } }; diff --git a/jey_wizard/translations/az.csv b/jey_wizard/translations/az.csv new file mode 100644 index 0000000..9a77170 --- /dev/null +++ b/jey_wizard/translations/az.csv @@ -0,0 +1,124 @@ +Step {0} of {1},Addım {0} / {1} +Language,Dil +Wizard & site language,Sihirbaz və sayt dili +Asan Imza authentication,Asan İmza ilə giriş +Phone number,Telefon nömrəsi +e.g. 0501234567,məsələn 0501234567 +User ID,İstifadəçi ID +ASAN ID,ASAN ID +Send request to Asan Imza,Asan İmza-ya sorğu göndər +Confirm on your phone,Telefonunuzda təsdiqləyin +Open the Asan Imza app on your phone and confirm the request.,Telefonunuzda Asan İmza tətbiqini açın və sorğunu təsdiqləyin. +Verification code,Təsdiq kodu +Waiting for confirmation...,Təsdiq gözlənilir... +liquidated,ləğv edilib +Select,Seç +Select your company,Şirkətinizi seçin +Company,Şirkət +VÖEN,VÖEN +"No legal-entity certificates available on this Asan ID. Use a phone/ID linked to a company.","Bu Asan ID üzrə hüquqi şəxs sertifikatı yoxdur. Şirkətə bağlı telefon/ID istifadə edin." +Loading data from tax portal...,Vergi portalından məlumat yüklənir... +"Pulling objects, cash registers, POS terminals, bank accounts, obligation pacts and presented certificates. This usually takes 10-30 seconds.","Obyektlər, kassa aparatları, POS terminallar, bank hesabları, öhdəlik razılaşmaları və təqdim edilmiş sertifikatlar yüklənir. Adətən 10–30 saniyə çəkir." +Fetching...,Yüklənir... +Authenticated & data loaded,"Giriş edildi, məlumat yükləndi" +Pulled from the tax portal:,Vergi portalından yükləndi: +Objects,Obyektlər +Cash registers,Kassa aparatları +POS terminals,POS terminallar +Bank accounts,Bank hesabları +Obligation pacts,Öhdəlik razılaşmaları +Presented certificates,Təqdim edilmiş sertifikatlar +failed,uğursuz +Cached datasets,Keşlənmiş məlumatlar +Load employees from ƏMAS,ƏMAS-dan işçiləri yüklə +"ƏMAS (e-social.gov.az) stores employment contracts. If your company has employees registered there, we can import them now.","ƏMAS (e-social.gov.az) əmək müqavilələrini saxlayır. Əgər şirkətinizin orada qeydiyyatda olan işçiləri varsa, onları indi idxal edə bilərik." +Heads up:,Diqqət: +You will receive a second confirmation request on your phone from ASAN Sign / MyGovID. This is separate from the one you just approved for the tax portal.,"Telefonunuza ASAN Sign / MyGovID-dən ikinci təsdiq sorğusu gələcək. Bu, vergi portalı üçün təsdiqlədiyinizdən ayrıdır." +This step is optional. Skip it if your company has no employees or is not registered with ƏMAS.,"Bu addım isteğe bağlıdır. Şirkətinizdə işçi yoxdursa və ya ƏMAS-da qeydiyyatda deyilsə, addımı ötürün." +Already selected:,Artıq seçilib: +employee(s),işçi +Change selection,Seçimi dəyiş +Load employees,İşçiləri yüklə +Skip,Ötür +Confirm ƏMAS request on your phone,Telefonunuzda ƏMAS sorğusunu təsdiqləyin +A second confirmation request has been sent to your phone. Open ASAN Sign and approve it.,Telefonunuza ikinci təsdiq sorğusu göndərildi. ASAN Sign-ı açın və təsdiqləyin. +Retrieving verification code...,Təsdiq kodu alınır... +Connecting to ƏMAS...,ƏMAS-a qoşulur... +Establishing session...,Sessiya qurulur... +Select organization,Təşkilatı seçin +"We could not auto-match by VÖEN. Pick the organization you want to import employees from.","VÖEN üzrə avtomatik uyğunlaşdırmaq mümkün olmadı. İşçiləri idxal etmək istədiyiniz təşkilatı seçin." +Organization,Təşkilat +Role,Rol +Loading employees...,İşçilər yüklənir... +Fetching from ƏMAS...,ƏMAS-dan yüklənir... +Select employees from ƏMAS,ƏMAS-dan işçiləri seçin +Organization:,Təşkilat: +Employees found:,Tapılan işçilər: +new,yeni +already in system,artıq sistemdə +Create Designation if not found,Vəzifə tapılmazsa yaradılsın +Full Name,Tam ad +FIN,FIN +Position,Vəzifə +Salary,Maaş +Status,Status +In System,Sistemdə +Employees selected,İşçilər seçildi +"Selected {0} employee(s). They will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes.","Seçilən işçi: {0}. Sihirbaz tamamlandıqdan sonra ƏMAS Employees və standart Employee doctype-ə idxal ediləcək." +Company name,Şirkət adı +VÖEN {0} (from Asan),VÖEN {0} (Asan-dan) +Fiscal Year,Maliyyə ili +Start date,Başlama tarixi +End date,Bitmə tarixi +"Default: current calendar year (Jan 1 – Dec 31).","Standart: cari təqvim ili (1 yanvar — 31 dekabr)." +Confirm,Təsdiq +Skipped,Ötürüldü +{0} selected,Seçildi: {0} +None,Yoxdur +ƏMAS employees,ƏMAS işçiləri +Country,Ölkə +Currency,Valyuta +Timezone,Saat qurşağı +Chart of Accounts,Hesablar Planı +hardcoded,standart olaraq +Back,Geri +Next,İrəli +Finish,Tamamla +Company name is required,Şirkət adı tələb olunur +Fiscal year start and end dates are required,Maliyyə ilinin başlama və bitmə tarixləri tələb olunur +Fiscal year end date must be after the start date,Maliyyə ilinin bitmə tarixi başlama tarixindən sonra olmalıdır +Skipping...,Ötürülür... +Finish Asan Imza authentication first.,Əvvəlcə Asan İmza girişini tamamlayın. +MyGovID authentication failed.,MyGovID girişi uğursuz oldu. +MyGovID request timed out.,MyGovID sorğusu vaxtı bitdi. +Failed to connect to ƏMAS.,ƏMAS-a qoşulmaq alınmadı. +No organizations found in ƏMAS for this account.,Bu hesab üçün ƏMAS-da təşkilat tapılmadı. +Failed to select organization.,Təşkilatı seçmək alınmadı. +Failed to fetch employees.,İşçiləri yükləmək alınmadı. +No employees found in ƏMAS for this organization.,Bu təşkilat üçün ƏMAS-da işçi tapılmadı. +"Select at least one employee, or go back and skip this step.","Ən azı bir işçi seçin və ya geri qayıdıb bu addımı ötürün." +Failed to cache selection.,Seçimi yadda saxlamaq alınmadı. +Saving selection...,Seçim saxlanılır... +Loading translations...,Tərcümələr yüklənir... +Preparing...,Hazırlanır... +Failed to create Asan Login record,Asan Login yazısını yaratmaq alınmadı +Phone and User ID are required,Telefon nömrəsi və User ID tələb olunur +Sending request to Asan Imza...,Asan İmza-ya sorğu göndərilir... +Failed to start authentication,Girişə başlamaq alınmadı +Timeout — confirmation not received within ~100 seconds.,Vaxt bitdi — təsdiq ~100 saniyə ərzində alınmadı. +Try again,Yenidən cəhd edin +Loading certificates...,Sertifikatlar yüklənir... +Failed to load certificates,Sertifikatları yükləmək alınmadı +Selecting certificate...,Sertifikat seçilir... +Failed to select certificate,Sertifikatı seçmək alınmadı +Choosing taxpayer & getting main token...,Vergi ödəyicisi seçilir və əsas token alınır... +Failed to choose taxpayer,Vergi ödəyicisini seçmək alınmadı +fetch call failed — see server logs,Yükləmə sorğusu uğursuz oldu — server loglarına baxın +Setting up your system. This can take a minute...,Sisteminiz qurulur. Bu bir dəqiqə çəkə bilər... +Setup complete! Redirecting...,Quraşdırma tamamlandı! Yönləndirilir... +"Setup is running in background mode, not supported by skeleton","Quraşdırma fon rejimində işləyir — bu sihirbaz tərəfindən dəstəklənmir" +Setup returned unexpected response,Quraşdırma gözlənilməz cavab qaytardı +Setup failed,Quraşdırma uğursuz oldu +(check server logs),(server loglarına baxın) +(no details),(təfərrüat yoxdur) +Retry,Təkrar cəhd diff --git a/jey_wizard/translations/ru.csv b/jey_wizard/translations/ru.csv new file mode 100644 index 0000000..97acbdc --- /dev/null +++ b/jey_wizard/translations/ru.csv @@ -0,0 +1,124 @@ +Step {0} of {1},Шаг {0} из {1} +Language,Язык +Wizard & site language,Язык мастера и сайта +Asan Imza authentication,Аутентификация Asan İmza +Phone number,Номер телефона +e.g. 0501234567,например 0501234567 +User ID,User ID +ASAN ID,ASAN ID +Send request to Asan Imza,Отправить запрос в Asan İmza +Confirm on your phone,Подтвердите на телефоне +Open the Asan Imza app on your phone and confirm the request.,Откройте приложение Asan İmza на телефоне и подтвердите запрос. +Verification code,Код подтверждения +Waiting for confirmation...,Ожидание подтверждения... +liquidated,ликвидировано +Select,Выбрать +Select your company,Выберите компанию +Company,Компания +VÖEN,ВÖЕН +"No legal-entity certificates available on this Asan ID. Use a phone/ID linked to a company.","На этом Asan ID нет сертификатов юр. лиц. Используйте телефон/ID привязанный к компании." +Loading data from tax portal...,Загрузка данных с налогового портала... +"Pulling objects, cash registers, POS terminals, bank accounts, obligation pacts and presented certificates. This usually takes 10-30 seconds.","Загружаем объекты, кассовые аппараты, POS-терминалы, банковские счета, договоры и представленные сертификаты. Обычно занимает 10–30 секунд." +Fetching...,Загрузка... +Authenticated & data loaded,"Аутентификация пройдена, данные загружены" +Pulled from the tax portal:,Загружено с налогового портала: +Objects,Объекты +Cash registers,Кассовые аппараты +POS terminals,POS-терминалы +Bank accounts,Банковские счета +Obligation pacts,Договоры-обязательства +Presented certificates,Представленные сертификаты +failed,ошибка +Cached datasets,Закэшированные данные +Load employees from ƏMAS,Загрузить сотрудников из ƏMAS +"ƏMAS (e-social.gov.az) stores employment contracts. If your company has employees registered there, we can import them now.","ƏMAS (e-social.gov.az) хранит трудовые договоры. Если у вас есть зарегистрированные там сотрудники, мы можем их импортировать." +Heads up:,Внимание: +You will receive a second confirmation request on your phone from ASAN Sign / MyGovID. This is separate from the one you just approved for the tax portal.,"На ваш телефон придёт ещё один запрос от ASAN Sign / MyGovID — отдельно от того, что вы только что подтвердили для налогового портала." +This step is optional. Skip it if your company has no employees or is not registered with ƏMAS.,"Этот шаг необязателен. Пропустите, если у вашей компании нет сотрудников или она не зарегистрирована в ƏMAS." +Already selected:,Уже выбрано: +employee(s),сотрудник(ов) +Change selection,Изменить выбор +Load employees,Загрузить сотрудников +Skip,Пропустить +Confirm ƏMAS request on your phone,Подтвердите запрос ƏMAS на телефоне +A second confirmation request has been sent to your phone. Open ASAN Sign and approve it.,На ваш телефон отправлен второй запрос подтверждения. Откройте ASAN Sign и подтвердите его. +Retrieving verification code...,Получение кода подтверждения... +Connecting to ƏMAS...,Подключение к ƏMAS... +Establishing session...,Установка сессии... +Select organization,Выберите организацию +"We could not auto-match by VÖEN. Pick the organization you want to import employees from.","Не удалось сопоставить по ВÖЕН. Выберите организацию, из которой импортировать сотрудников." +Organization,Организация +Role,Роль +Loading employees...,Загрузка сотрудников... +Fetching from ƏMAS...,Получение данных из ƏMAS... +Select employees from ƏMAS,Выберите сотрудников из ƏMAS +Organization:,Организация: +Employees found:,Найдено сотрудников: +new,новых +already in system,уже в системе +Create Designation if not found,Создавать должность если не найдена +Full Name,ФИО +FIN,ФИН +Position,Должность +Salary,Зарплата +Status,Статус +In System,В системе +Employees selected,Сотрудники выбраны +"Selected {0} employee(s). They will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes.","Выбрано сотрудников: {0}. Они будут импортированы в ƏMAS Employees и стандартный Employee после завершения мастера." +Company name,Название компании +VÖEN {0} (from Asan),ВÖЕН {0} (из Asan) +Fiscal Year,Финансовый год +Start date,Дата начала +End date,Дата окончания +"Default: current calendar year (Jan 1 – Dec 31).","По умолчанию: текущий календарный год (1 января — 31 декабря)." +Confirm,Подтверждение +Skipped,Пропущено +{0} selected,Выбрано: {0} +None,Нет +ƏMAS employees,Сотрудники ƏMAS +Country,Страна +Currency,Валюта +Timezone,Часовой пояс +Chart of Accounts,План счетов +hardcoded,задано по умолчанию +Back,Назад +Next,Далее +Finish,Завершить +Company name is required,Название компании обязательно +Fiscal year start and end dates are required,Даты начала и конца финансового года обязательны +Fiscal year end date must be after the start date,Дата окончания финансового года должна быть позже даты начала +Skipping...,Пропуск... +Finish Asan Imza authentication first.,Сначала завершите аутентификацию Asan İmza. +MyGovID authentication failed.,Ошибка аутентификации MyGovID. +MyGovID request timed out.,Таймаут запроса MyGovID. +Failed to connect to ƏMAS.,Не удалось подключиться к ƏMAS. +No organizations found in ƏMAS for this account.,Для этого аккаунта в ƏMAS не найдено организаций. +Failed to select organization.,Не удалось выбрать организацию. +Failed to fetch employees.,Не удалось загрузить сотрудников. +No employees found in ƏMAS for this organization.,В этой организации в ƏMAS сотрудников не найдено. +"Select at least one employee, or go back and skip this step.","Выберите хотя бы одного сотрудника или вернитесь и пропустите этот шаг." +Failed to cache selection.,Не удалось сохранить выбор. +Saving selection...,Сохранение выбора... +Loading translations...,Загрузка переводов... +Preparing...,Подготовка... +Failed to create Asan Login record,Не удалось создать запись Asan Login +Phone and User ID are required,Номер телефона и User ID обязательны +Sending request to Asan Imza...,Отправка запроса в Asan İmza... +Failed to start authentication,Не удалось начать аутентификацию +Timeout — confirmation not received within ~100 seconds.,Таймаут — подтверждение не получено за ~100 секунд. +Try again,Попробовать снова +Loading certificates...,Загрузка сертификатов... +Failed to load certificates,Не удалось загрузить сертификаты +Selecting certificate...,Выбор сертификата... +Failed to select certificate,Не удалось выбрать сертификат +Choosing taxpayer & getting main token...,Выбор налогоплательщика и получение основного токена... +Failed to choose taxpayer,Не удалось выбрать налогоплательщика +fetch call failed — see server logs,Запрос загрузки не удался — смотрите логи сервера +Setting up your system. This can take a minute...,Настройка системы. Это может занять минуту... +Setup complete! Redirecting...,Настройка завершена! Перенаправление... +"Setup is running in background mode, not supported by skeleton","Настройка выполняется в фоновом режиме — не поддерживается этим мастером" +Setup returned unexpected response,Получен неожиданный ответ от сетапа +Setup failed,Ошибка настройки +(check server logs),(смотрите логи сервера) +(no details),(нет деталей) +Retry,Повторить