From 8f8a9366935b9512e6c2306b1e7c0fc1d72e58f0 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Wed, 22 Apr 2026 11:52:25 +0000 Subject: [PATCH] first commit --- jey_wizard/api.py | 62 +++++++++++ jey_wizard/hooks.py | 5 + jey_wizard/public/js/jey_setup.js | 176 ++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 jey_wizard/api.py create mode 100644 jey_wizard/public/js/jey_setup.js diff --git a/jey_wizard/api.py b/jey_wizard/api.py new file mode 100644 index 0000000..373fc7b --- /dev/null +++ b/jey_wizard/api.py @@ -0,0 +1,62 @@ +from datetime import date + +import frappe +from frappe import _ + + +LANG_CODE_TO_NAME = { + "en": "English", + "az": "Azərbaycan dili", + "ru": "Русский", +} + + +@frappe.whitelist() +def finalize(language, company_name): + """Run setup_complete with hardcoded AZ defaults plus two fields from the UI. + + For the skeleton test only. Once the real multi-step wizard is in place, this will + grow into a Session-backed finalizer with rollback. + """ + if frappe.session.user != "Administrator": + frappe.throw(_("Only Administrator may run setup"), frappe.PermissionError) + + if frappe.is_setup_complete(): + return {"status": "ok", "already_complete": True} + + language_name = LANG_CODE_TO_NAME.get(language, "English") + company_name = (company_name or "").strip() or "Test Company AZ" + year = date.today().year + + args = { + "language": language_name, + "country": "Azerbaijan", + "timezone": "Asia/Baku", + "currency": "AZN", + "enable_telemetry": 0, + # Leave email blank → create_or_update_user is a no-op and Administrator stays. + "full_name": "", + "email": "", + "password": "", + "company_name": company_name, + "company_abbr": _make_abbr(company_name), + "chart_of_accounts": "az_chart_of_accounts", + "fy_start_date": f"{year}-01-01", + "fy_end_date": f"{year}-12-31", + "setup_demo": 0, + } + + from frappe.desk.page.setup_wizard.setup_wizard import setup_complete + + return setup_complete(args) + + +def _make_abbr(name: str) -> str: + parts = [p for p in name.split() if p] + if len(parts) >= 2: + abbr = "".join(p[0] for p in parts[:3]) + elif parts: + abbr = parts[0][:3] + else: + abbr = "CO" + return abbr.upper()[:10] diff --git a/jey_wizard/hooks.py b/jey_wizard/hooks.py index c46e8cb..3ea1677 100644 --- a/jey_wizard/hooks.py +++ b/jey_wizard/hooks.py @@ -28,6 +28,11 @@ app_license = "unlicense" # app_include_css = "/assets/jey_wizard/css/jey_wizard.css" # app_include_js = "/assets/jey_wizard/js/jey_wizard.js" +# Replace the default setup wizard UI. This JS is loaded inside /app/setup-wizard +# page bootstrap, after frappe/setup_wizard.js but before the wizard instance is +# created — so overriding frappe.setup.SetupWizard in it swaps the UI entirely. +setup_wizard_requires = ["/assets/jey_wizard/js/jey_setup.js"] + # include js, css files in header of web template # web_include_css = "/assets/jey_wizard/css/jey_wizard.css" # web_include_js = "/assets/jey_wizard/js/jey_wizard.js" diff --git a/jey_wizard/public/js/jey_setup.js b/jey_wizard/public/js/jey_setup.js new file mode 100644 index 0000000..558fb40 --- /dev/null +++ b/jey_wizard/public/js/jey_setup.js @@ -0,0 +1,176 @@ +// Loaded via setup_wizard_requires hook. At the time this runs: +// - frappe/setup_wizard.js has already defined frappe.setup.SetupWizard and the default +// slides_settings + the `frappe.pages["setup-wizard"].on_page_load` handler. +// - The handler called frappe.require([...this file...], cb). After we finish loading, +// cb runs and does `new frappe.setup.SetupWizard(wizard_settings)`. +// So overriding the class here replaces the default UI entirely for this pageload. + +frappe.provide("jey_wizard"); + +// 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. +frappe.setup.slides = []; +frappe.setup.slides_settings = []; +frappe.setup.events = {}; + +frappe.setup.SetupWizard = class JeySetupWizard { + constructor(settings = {}) { + this.parent = settings.parent; + this.values = {}; + this.data = { + language: "az", + company_name: "Test Company AZ", + }; + this.current_step = 0; + this.steps = ["language", "company", "confirm"]; + this.render(); + } + + // Frappe's on_page_show calls frappe.wizard.show_slide — no-op for us. + show_slide() {} + + render() { + const $parent = $(this.parent).empty(); + this.$wrap = $(` +
+

Jey Setup

+
+
+
+
+
+ `).appendTo($parent); + this.show_current_step(); + } + + show_current_step() { + const step = this.steps[this.current_step]; + 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}` + ); + this.$wrap.find(".jey-status").empty(); + + if (step === "language") { + $body.html(` + + + `); + $body.find(".jey-language").val(this.data.language); + } else if (step === "company") { + $body.html(` + + + `); + $body.find(".jey-company-name").val(this.data.company_name); + } else if (step === "confirm") { + $body.html(` +

Confirm

+ + `); + } + + if (this.current_step > 0) { + $(``) + .appendTo($nav) + .on("click", () => this.prev()); + } + if (this.current_step < this.steps.length - 1) { + $(``) + .appendTo($nav) + .on("click", () => this.next()); + } else { + $(``) + .appendTo($nav) + .on("click", () => this.finalize()); + } + } + + capture_current() { + const step = this.steps[this.current_step]; + if (step === "language") { + this.data.language = this.$wrap.find(".jey-language").val(); + } 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"); + return false; + } + this.data.company_name = name; + } + return true; + } + + next() { + if (!this.capture_current()) return; + this.current_step++; + this.show_current_step(); + } + + prev() { + this.current_step--; + this.show_current_step(); + } + + finalize() { + this.$wrap.find(".jey-body").html( + `

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

` + ); + this.$wrap.find(".jey-nav").empty(); + this.$wrap.find(".jey-status").empty(); + + frappe.call({ + method: "jey_wizard.api.finalize", + args: { + language: this.data.language, + company_name: this.data.company_name, + }, + freeze: true, + callback: (r) => { + if (r.message && r.message.status === "ok") { + this.$wrap.find(".jey-body").html( + `

Setup complete! Redirecting...

` + ); + setTimeout(() => { + window.location.href = "/app"; + }, 1500); + } else { + this.show_error( + "Setup returned unexpected response: " + + JSON.stringify(r.message || {}) + ); + } + }, + error: () => { + const msg = + (frappe.last_response && frappe.last_response.setup_wizard_failure_message) || + "Setup failed — check server logs."; + this.show_error(msg); + }, + }); + } + + show_error(msg) { + this.$wrap.find(".jey-status").text(msg); + this.$wrap.find(".jey-nav") + .empty() + .html(``) + .find(".jey-retry") + .on("click", () => window.location.reload()); + } +};