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 = $(` +
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()); + } +};