first commit

This commit is contained in:
Ali 2026-04-22 11:52:25 +00:00
parent 1bb4ecfb6c
commit 8f8a936693
3 changed files with 243 additions and 0 deletions

62
jey_wizard/api.py Normal file
View File

@ -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]

View File

@ -28,6 +28,11 @@ app_license = "unlicense"
# app_include_css = "/assets/jey_wizard/css/jey_wizard.css" # app_include_css = "/assets/jey_wizard/css/jey_wizard.css"
# app_include_js = "/assets/jey_wizard/js/jey_wizard.js" # 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 # include js, css files in header of web template
# web_include_css = "/assets/jey_wizard/css/jey_wizard.css" # web_include_css = "/assets/jey_wizard/css/jey_wizard.css"
# web_include_js = "/assets/jey_wizard/js/jey_wizard.js" # web_include_js = "/assets/jey_wizard/js/jey_wizard.js"

View File

@ -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 = $(`
<div class="jey-wizard" style="max-width:560px;margin:80px auto;padding:32px;font-family:system-ui,sans-serif">
<h1 style="margin-bottom:8px">Jey Setup</h1>
<div class="jey-progress" style="color:#888;margin-bottom:24px"></div>
<div class="jey-body"></div>
<div class="jey-nav" style="margin-top:24px;display:flex;gap:8px;justify-content:flex-end"></div>
<div class="jey-status" style="margin-top:16px;color:#c00"></div>
</div>
`).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(`
<label style="display:block;margin-bottom:8px">Wizard & site language</label>
<select class="jey-language form-control" style="max-width:280px">
<option value="az">Azərbaycan</option>
<option value="ru">Русский</option>
<option value="en">English</option>
</select>
`);
$body.find(".jey-language").val(this.data.language);
} else if (step === "company") {
$body.html(`
<label style="display:block;margin-bottom:8px">Company name</label>
<input type="text" class="jey-company-name form-control" style="max-width:380px" />
`);
$body.find(".jey-company-name").val(this.data.company_name);
} else if (step === "confirm") {
$body.html(`
<h3>Confirm</h3>
<ul style="line-height:1.8">
<li><b>Language:</b> ${this.data.language}</li>
<li><b>Company:</b> ${frappe.utils.escape_html(this.data.company_name)}</li>
<li><b>Country:</b> Azerbaijan <i>(hardcoded)</i></li>
<li><b>Currency:</b> AZN <i>(hardcoded)</i></li>
<li><b>Timezone:</b> Asia/Baku <i>(hardcoded)</i></li>
<li><b>Chart of Accounts:</b> az_chart_of_accounts <i>(hardcoded)</i></li>
<li><b>Fiscal Year:</b> current year <i>(hardcoded)</i></li>
<li><b>Telemetry:</b> off <i>(hardcoded)</i></li>
<li><b>Demo data:</b> off <i>(hardcoded)</i></li>
</ul>
`);
}
if (this.current_step > 0) {
$(`<button class="btn btn-default jey-back">Back</button>`)
.appendTo($nav)
.on("click", () => this.prev());
}
if (this.current_step < this.steps.length - 1) {
$(`<button class="btn btn-primary jey-next">Next</button>`)
.appendTo($nav)
.on("click", () => this.next());
} else {
$(`<button class="btn btn-primary jey-finish">Finish</button>`)
.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(
`<p>Setting up your system. This can take a minute...</p>`
);
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(
`<p>Setup complete! Redirecting...</p>`
);
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(`<button class="btn btn-default jey-retry">Retry</button>`)
.find(".jey-retry")
.on("click", () => window.location.reload());
}
};