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
# Apply inventory valuation method before any stock loaders touch the Company.
# Company-level wins over Stock Settings in erpnext.stock.utils.get_valuation_method.
# Apply inventory valuation method. Company.valuation_method wins over Stock
# 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"
if chosen_vm in ("FIFO", "Moving Average"):
frappe.db.set_value(
"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
@ -250,20 +259,25 @@ def _materialize_native_banks(company):
def _find_bank_parent_account(company):
"""'Bank hesablaşma hesabları' group (account_number=223) under this company.
Falls back to any group with the AZ name prefix."""
parent = frappe.db.get_value(
"Account",
"""Find the CoA group to place bank accounts under. In the AZ chart 223
'Bank hesablaşma hesabları' is a leaf (not a group) and its numbered
siblings 223.1/223.2/... all live under 22 'Pul vəsaitləri və onların
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},
"name",
)
if parent:
return parent
return frappe.db.get_value(
"Account",
{"company": company, "account_name": ("like", "Bank hesablaşma%"), "is_group": 1},
"name",
)
{"company": company, "account_number": "22", "is_group": 1},
{"company": company, "account_name": ("like", "Pul vəsaitləri%"), "is_group": 1},
):
parent = frappe.db.get_value("Account", filters, "name")
if parent:
return parent
return None
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):
"""Remove the stock 'AZN AZXXX…', 'USD AZXXX…' template accounts from AZ CoA
once real banks have been materialised. Fresh-setup only nothing references
these placeholders yet, so deletion is safe. Per-row failures logged.
"""Remove the stock template accounts from AZ CoA once real banks have been
materialised. Covers both `<CUR> AZXX` and `Kart XXXX` patterns anything
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",
filters={
"company": company,
"parent_account": parent_account,
"account_name": ("like", "% AZXX%"),
"account_type": "Bank",
"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:
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:
frappe.log_error(
f"Placeholder delete failed: {name}: {exc}",
f"Placeholder delete failed: {row.name}: {exc}",
"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
// 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.1.4";
const JEY_WIZARD_VERSION = "0.1.5";
// 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.
@ -48,6 +48,8 @@ frappe.setup.SetupWizard = class JeySetupWizard {
asan_login_name: "",
asan_voen: "",
company_name: "",
company_abbr: "",
company_abbr_user_edited: false,
fy_start_date: `${year}-01-01`,
fy_end_date: `${year}-12-31`,
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>
<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>
<div style="display:flex;gap:16px;max-width:420px">
<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-end").val(this.data.fy_end_date);
$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) {
@ -428,7 +476,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
<h3>${__("Confirm")}</h3>
<ul style="line-height:1.8">
<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
? `<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;
}
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 fyEnd = (this.$wrap.find(".jey-fy-end").val() || "").trim();
if (!fyStart || !fyEnd) {
@ -1043,12 +1102,13 @@ frappe.setup.SetupWizard = class JeySetupWizard {
build_setup_args() {
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 = (
parts.length >= 2
? parts.slice(0, 3).map((p) => p[0]).join("")
: (parts[0] || "CO").slice(0, 3)
).toUpperCase().slice(0, 10);
(this.data.company_abbr || "").toUpperCase().replace(/\s+/g, "") ||
this.derive_abbr(company) ||
"CO"
).slice(0, 10);
const admin = frappe.boot.user || {};
return {

View File

@ -79,6 +79,10 @@ 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ı
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)
Fiscal Year,Maliyyə ili
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,Сотрудники выбраны
"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 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)
Fiscal Year,Финансовый год
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 Оценка запасов