Fix silent bank materialize, set Stock Settings too, editable abbr

Bank materialize was finding no parent account and skipping everything:
the AZ CoA keeps "Bank hesablaşma hesabları" (223) as a LEAF and the
223.1/223.2/... currency placeholders as siblings under the real
group "Pul vəsaitləri və onların ekvivalentləri" (22). The finder now
walks 223 → 22 → name-prefix and lands on 22, which is where real
bank GL accounts need to go. Placeholder sweep widened from "% AZXX%"
to any leaf under the bank parent whose name contains XXXX — catches
the "Kart XXXX…AZN" template too.

Valuation was only being written to Company.valuation_method, which
wins in erpnext.stock.utils.get_valuation_method but is invisible on
the Stock Settings page users actually open to verify. Now we write
both via frappe.db.set_single_value.

Company abbreviation is editable on the company step with a live-
suggested default. Deriver strips every quote variant and collapses
the AZ legal-form boilerplate (MƏHDUD MƏSULİYYƏTLİ CƏMİYYƏTİ → MMC,
AÇIQ/QAPALI SƏHMDAR CƏMİYYƏTİ → ASC/QSC, SƏHMDAR CƏMİYYƏTİ → SC,
KOOPERATİV → K) before tokenising, so "XYZ MMC" produces XM instead
of the old deriver's M-with-a-stray-quote. Manual edits stick.
Validated A-Z0-9, 1–10 chars.
This commit is contained in:
Ali 2026-04-24 14:09:35 +00:00
parent 45e9e54535
commit 4ed716637d
5 changed files with 120 additions and 32 deletions

View File

@ -1 +1 @@
__version__ = "0.1.4" __version__ = "0.1.5"

View File

@ -168,13 +168,22 @@ def materialize_after_setup(args):
) )
return return
# Apply inventory valuation method before any stock loaders touch the Company. # Apply inventory valuation method. Company.valuation_method wins over Stock
# Company-level wins over Stock Settings in erpnext.stock.utils.get_valuation_method. # Settings in erpnext.stock.utils.get_valuation_method, but users typically
# inspect the Stock Settings page to verify setup, so we set both to avoid the
# "I picked Moving Average but Stock Settings still shows FIFO" surprise.
chosen_vm = (args or {}).get("valuation_method") or "FIFO" chosen_vm = (args or {}).get("valuation_method") or "FIFO"
if chosen_vm in ("FIFO", "Moving Average"): if chosen_vm in ("FIFO", "Moving Average"):
frappe.db.set_value( frappe.db.set_value(
"Company", company_name, "valuation_method", chosen_vm, update_modified=False "Company", company_name, "valuation_method", chosen_vm, update_modified=False
) )
try:
frappe.db.set_single_value("Stock Settings", "valuation_method", chosen_vm)
except Exception as exc:
frappe.log_error(
f"Stock Settings valuation_method set failed: {exc}",
"Jey Wizard materialize",
)
from invoice_az import company_api from invoice_az import company_api
@ -250,20 +259,25 @@ def _materialize_native_banks(company):
def _find_bank_parent_account(company): def _find_bank_parent_account(company):
"""'Bank hesablaşma hesabları' group (account_number=223) under this company. """Find the CoA group to place bank accounts under. In the AZ chart 223
Falls back to any group with the AZ name prefix.""" 'Bank hesablaşma hesabları' is a leaf (not a group) and its numbered
parent = frappe.db.get_value( siblings 223.1/223.2/... all live under 22 'Pul vəsaitləri və onların
"Account", ekvivalentləri' — which is a group. Real bank accounts go there too.
Preference order:
1. 223 if it's ever been converted to a group.
2. 22 (Pul vəsaitləri), the actual AZ CoA group.
3. Any group with name starting with 'Pul vəsaitləri'.
"""
for filters in (
{"company": company, "account_number": "223", "is_group": 1}, {"company": company, "account_number": "223", "is_group": 1},
"name", {"company": company, "account_number": "22", "is_group": 1},
) {"company": company, "account_name": ("like", "Pul vəsaitləri%"), "is_group": 1},
if parent: ):
return parent parent = frappe.db.get_value("Account", filters, "name")
return frappe.db.get_value( if parent:
"Account", return parent
{"company": company, "account_name": ("like", "Bank hesablaşma%"), "is_group": 1}, return None
"name",
)
def _materialize_one_bank(company, parent_account, row): def _materialize_one_bank(company, parent_account, row):
@ -323,26 +337,32 @@ def _materialize_one_bank(company, parent_account, row):
def _delete_bank_placeholders(company, parent_account): def _delete_bank_placeholders(company, parent_account):
"""Remove the stock 'AZN AZXXX…', 'USD AZXXX…' template accounts from AZ CoA """Remove the stock template accounts from AZ CoA once real banks have been
once real banks have been materialised. Fresh-setup only nothing references materialised. Covers both `<CUR> AZXX` and `Kart XXXX` patterns anything
these placeholders yet, so deletion is safe. Per-row failures logged. with a run of four or more X characters in the name, under the bank parent.
Fresh-setup only nothing references these placeholders yet, so deletion
is safe. Per-row failures logged.
""" """
placeholders = frappe.get_all( candidates = frappe.get_all(
"Account", "Account",
filters={ filters={
"company": company, "company": company,
"parent_account": parent_account, "parent_account": parent_account,
"account_name": ("like", "% AZXX%"), "account_type": "Bank",
"is_group": 0, "is_group": 0,
}, },
pluck="name", fields=["name", "account_name"],
) )
for name in placeholders: for row in candidates:
if "XXXX" not in (row.account_name or ""):
continue
try: try:
frappe.delete_doc("Account", name, ignore_permissions=True, delete_permanently=True) frappe.delete_doc(
"Account", row.name, ignore_permissions=True, delete_permanently=True
)
except Exception as exc: except Exception as exc:
frappe.log_error( frappe.log_error(
f"Placeholder delete failed: {name}: {exc}", f"Placeholder delete failed: {row.name}: {exc}",
"Jey Wizard materialize (bank placeholder)", "Jey Wizard materialize (bank placeholder)",
) )

View File

@ -10,7 +10,7 @@ frappe.provide("jey_wizard");
// Bump this string in every commit that changes wizard code. Displayed in the badge so // 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 // 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. // sync with __version__ in jey_wizard/__init__.py.
const JEY_WIZARD_VERSION = "0.1.4"; const JEY_WIZARD_VERSION = "0.1.5";
// Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners // 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. // don't try to mutate a wizard that isn't slide-based anymore.
@ -48,6 +48,8 @@ frappe.setup.SetupWizard = class JeySetupWizard {
asan_login_name: "", asan_login_name: "",
asan_voen: "", asan_voen: "",
company_name: "", company_name: "",
company_abbr: "",
company_abbr_user_edited: false,
fy_start_date: `${year}-01-01`, fy_start_date: `${year}-01-01`,
fy_end_date: `${year}-12-31`, fy_end_date: `${year}-12-31`,
valuation_method: "FIFO", valuation_method: "FIFO",
@ -391,6 +393,11 @@ frappe.setup.SetupWizard = class JeySetupWizard {
? `<div style="color:#888;font-size:12px;margin-top:6px">${__("VÖEN {0} (from Asan)", [frappe.utils.escape_html(this.data.asan_voen)])}</div>` ? `<div style="color:#888;font-size:12px;margin-top:6px">${__("VÖEN {0} (from Asan)", [frappe.utils.escape_html(this.data.asan_voen)])}</div>`
: ""} : ""}
</div> </div>
<div style="margin-bottom:12px">
<label style="display:block;margin-bottom:4px">${__("Company abbreviation")}</label>
<input type="text" class="jey-company-abbr form-control" style="max-width:140px;text-transform:uppercase" maxlength="10" />
<div style="color:#888;font-size:12px;margin-top:6px">${__("Short code suffixed to account names. Auto-suggested from the company name — you can edit it.")}</div>
</div>
<h4 style="margin-top:20px;margin-bottom:10px">${__("Fiscal Year")}</h4> <h4 style="margin-top:20px;margin-bottom:10px">${__("Fiscal Year")}</h4>
<div style="display:flex;gap:16px;max-width:420px"> <div style="display:flex;gap:16px;max-width:420px">
<div style="flex:1"> <div style="flex:1">
@ -417,6 +424,47 @@ frappe.setup.SetupWizard = class JeySetupWizard {
$body.find(".jey-fy-start").val(this.data.fy_start_date); $body.find(".jey-fy-start").val(this.data.fy_start_date);
$body.find(".jey-fy-end").val(this.data.fy_end_date); $body.find(".jey-fy-end").val(this.data.fy_end_date);
$body.find(".jey-valuation-method").val(this.data.valuation_method || "FIFO"); $body.find(".jey-valuation-method").val(this.data.valuation_method || "FIFO");
// If user has never touched the abbr field, keep it in sync with the name.
// Once they edit it manually we stop auto-suggesting so their choice sticks.
const $nameEl = $body.find(".jey-company-name");
const $abbrEl = $body.find(".jey-company-abbr");
const fillAbbrDefault = () => {
if (!this.data.company_abbr_user_edited) {
const suggested = this.derive_abbr($nameEl.val() || "");
this.data.company_abbr = suggested;
$abbrEl.val(suggested);
}
};
$abbrEl.val(this.data.company_abbr || this.derive_abbr(this.data.company_name || ""));
$nameEl.on("input", fillAbbrDefault);
$abbrEl.on("input", (e) => {
this.data.company_abbr_user_edited = true;
this.data.company_abbr = ($(e.currentTarget).val() || "").toUpperCase().slice(0, 10);
$(e.currentTarget).val(this.data.company_abbr);
});
}
derive_abbr(rawName) {
// Normalise Azerbaijani company-form boilerplate before tokenising so the
// auto-suggest doesn't spell out each word of "Məhdud Məsuliyyətli Cəmiyyəti"
// as separate initials. Also strip every quote character — the stock
// ERPNext deriver chokes on them.
if (!rawName) return "";
let name = String(rawName).replace(/["“”„‟«»‘’‚‛'`´]/g, " ");
name = name.replace(/[ƏE]/gi, (ch) => (ch === ch.toUpperCase() ? "E" : "e"));
const rules = [
[/\bMEHDUD\s+MESULIYYETLI\s+CEMIYYETI\b/gi, "MMC"],
[/\bA[CÇ]IQ\s+SEHMDAR\s+CEMIYYETI\b/gi, "ASC"],
[/\bQAPALI\s+SEHMDAR\s+CEMIYYETI\b/gi, "QSC"],
[/\bSEHMDAR\s+CEMIYYETI\b/gi, "SC"],
[/\bKOOPERATIV\b/gi, "K"],
];
for (const [re, rep] of rules) name = name.replace(re, rep);
const parts = name.split(/\s+/).filter(Boolean);
if (parts.length === 0) return "";
if (parts.length === 1) return parts[0].slice(0, 3).toUpperCase();
return parts.slice(0, 4).map((p) => p[0]).join("").toUpperCase().slice(0, 10);
} }
render_confirm($body) { render_confirm($body) {
@ -428,7 +476,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
<h3>${__("Confirm")}</h3> <h3>${__("Confirm")}</h3>
<ul style="line-height:1.8"> <ul style="line-height:1.8">
<li><b>${__("Language")}:</b> ${LANG_NAME[this.data.language] || this.data.language}</li> <li><b>${__("Language")}:</b> ${LANG_NAME[this.data.language] || this.data.language}</li>
<li><b>${__("Company")}:</b> ${frappe.utils.escape_html(this.data.company_name)}</li> <li><b>${__("Company")}:</b> ${frappe.utils.escape_html(this.data.company_name)} <span style="color:#888">(${frappe.utils.escape_html(this.data.company_abbr || "")})</span></li>
${this.data.asan_voen ${this.data.asan_voen
? `<li><b>${__("VÖEN")}:</b> ${frappe.utils.escape_html(this.data.asan_voen)}</li>` ? `<li><b>${__("VÖEN")}:</b> ${frappe.utils.escape_html(this.data.asan_voen)}</li>`
: ""} : ""}
@ -493,6 +541,17 @@ frappe.setup.SetupWizard = class JeySetupWizard {
return false; return false;
} }
this.data.company_name = name; this.data.company_name = name;
const abbrRaw = (this.$wrap.find(".jey-company-abbr").val() || "").toUpperCase().replace(/\s+/g, "");
const abbr = abbrRaw || this.derive_abbr(name);
if (!abbr) {
this.$wrap.find(".jey-status").text(__("Company abbreviation is required"));
return false;
}
if (!/^[A-Z0-9]{1,10}$/.test(abbr)) {
this.$wrap.find(".jey-status").text(__("Abbreviation must be 110 letters/digits (AZ, 09)."));
return false;
}
this.data.company_abbr = abbr;
const fyStart = (this.$wrap.find(".jey-fy-start").val() || "").trim(); const fyStart = (this.$wrap.find(".jey-fy-start").val() || "").trim();
const fyEnd = (this.$wrap.find(".jey-fy-end").val() || "").trim(); const fyEnd = (this.$wrap.find(".jey-fy-end").val() || "").trim();
if (!fyStart || !fyEnd) { if (!fyStart || !fyEnd) {
@ -1043,12 +1102,13 @@ frappe.setup.SetupWizard = class JeySetupWizard {
build_setup_args() { build_setup_args() {
const company = (this.data.company_name || "").trim() || "Test Company AZ"; const company = (this.data.company_name || "").trim() || "Test Company AZ";
const parts = company.split(/\s+/).filter(Boolean); // Prefer the user's (possibly edited) abbr captured in capture_current;
// fall back to the deriver if something bypassed validation.
const abbr = ( const abbr = (
parts.length >= 2 (this.data.company_abbr || "").toUpperCase().replace(/\s+/g, "") ||
? parts.slice(0, 3).map((p) => p[0]).join("") this.derive_abbr(company) ||
: (parts[0] || "CO").slice(0, 3) "CO"
).toUpperCase().slice(0, 10); ).slice(0, 10);
const admin = frappe.boot.user || {}; const admin = frappe.boot.user || {};
return { return {

View File

@ -79,6 +79,10 @@ In System,Sistemdə
Employees selected,İşçilər seçildi 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." "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ı Company name,Şirkət adı
Company abbreviation,Şirkətin qısaltması
"Short code suffixed to account names. Auto-suggested from the company name — you can edit it.","Hesab adlarına əlavə olunan qısa kod. Şirkət adından avtomatik təklif edilir — dəyişə bilərsiniz."
Company abbreviation is required,Şirkətin qısaltması tələb olunur
"Abbreviation must be 110 letters/digits (AZ, 09).","Qısaltma 110 simvol olmalıdır (AZ, 09)."
VÖEN {0} (from Asan),VÖEN {0} (Asan-dan) VÖEN {0} (from Asan),VÖEN {0} (Asan-dan)
Fiscal Year,Maliyyə ili Fiscal Year,Maliyyə ili
Stock Valuation,Ehtiyatların qiymətləndirilməsi Stock Valuation,Ehtiyatların qiymətləndirilməsi

1 Step {0} of {1} Addım {0} / {1}
79 Employees selected İşçilər seçildi
80 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.
81 Company name Şirkət adı
82 Company abbreviation Şirkətin qısaltması
83 Short code suffixed to account names. Auto-suggested from the company name — you can edit it. Hesab adlarına əlavə olunan qısa kod. Şirkət adından avtomatik təklif edilir — dəyişə bilərsiniz.
84 Company abbreviation is required Şirkətin qısaltması tələb olunur
85 Abbreviation must be 1–10 letters/digits (A–Z, 0–9). Qısaltma 1–10 simvol olmalıdır (A–Z, 0–9).
86 VÖEN {0} (from Asan) VÖEN {0} (Asan-dan)
87 Fiscal Year Maliyyə ili
88 Stock Valuation Ehtiyatların qiymətləndirilməsi

View File

@ -79,6 +79,10 @@ In System,В системе
Employees selected,Сотрудники выбраны 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 после завершения мастера." "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,Название компании Company name,Название компании
Company abbreviation,Сокращённое название
"Short code suffixed to account names. Auto-suggested from the company name — you can edit it.","Короткий код, добавляемый к названиям счетов. Подставляется автоматически — можно исправить."
Company abbreviation is required,Сокращённое название обязательно
"Abbreviation must be 110 letters/digits (AZ, 09).","Сокращение должно быть 110 символов (AZ, 09)."
VÖEN {0} (from Asan),ВÖЕН {0} (из Asan) VÖEN {0} (from Asan),ВÖЕН {0} (из Asan)
Fiscal Year,Финансовый год Fiscal Year,Финансовый год
Stock Valuation,Оценка запасов Stock Valuation,Оценка запасов

1 Step {0} of {1} Шаг {0} из {1}
79 Employees selected Сотрудники выбраны
80 Selected {0} employee(s). They will be imported into ƏMAS Employees and the standard Employee doctype after the wizard finishes. Выбрано сотрудников: {0}. Они будут импортированы в ƏMAS Employees и стандартный Employee после завершения мастера.
81 Company name Название компании
82 Company abbreviation Сокращённое название
83 Short code suffixed to account names. Auto-suggested from the company name — you can edit it. Короткий код, добавляемый к названиям счетов. Подставляется автоматически — можно исправить.
84 Company abbreviation is required Сокращённое название обязательно
85 Abbreviation must be 1–10 letters/digits (A–Z, 0–9). Сокращение должно быть 1–10 символов (A–Z, 0–9).
86 VÖEN {0} (from Asan) ВÖЕН {0} (из Asan)
87 Fiscal Year Финансовый год
88 Stock Valuation Оценка запасов