Read banks from wizard cache; save Stock Settings; quoted-core abbr
Materialising banks off E-Taxes Bank Account made us hostage to
invoice_az's finalize-time re-fetch. If that re-fetch 401'd or hit
a network error the doctype stayed empty, and everything downstream
(Bank / GL Account / Bank Account, placeholder cleanup) silently
no-op'd. Now we read the e-taxes response straight from
Jey Wizard Etaxes Cache.bank_accounts_json which is already
populated during the Asan step, so materialisation runs off a
guaranteed-present payload and is decoupled from the retry-prone
loader call. Logs both entry ("applying valuation_method=…") and
exit ("materialised=N skipped_closed=M parent=…") via
frappe.logger() so we can confirm from the server log that the
hook actually ran end-to-end on a given setup.
Stock Settings valuation_method write switched from
frappe.db.set_single_value to the load-and-.save() pattern that
erpnext itself uses in install_fixtures.update_stock_settings, and
followed by an explicit frappe.db.commit() — should survive any
subsequent cache fetch of the Single doc.
Abbreviation deriver rewritten around the canonical AZ registered
name shape `"<Core>" <LEGAL FORM>`. When quotes are present the core
gets initials and the legal form collapses to its short code
(MMC / ASC / QSC / SC / K / FS), joined with a space — so
"TEST SOFT" MƏHDUD MƏSULİYYƏTLİ CƏMİYYƏTİ now yields "TS MMC"
instead of "″SMM". The quote character class widened to include
U+2033 DOUBLE PRIME (the typography used on the actual reference
site) plus prime, guillemet single ‹›, and other variants. Abbr
validation relaxed to allow a single internal space so the space
between core initials and legal-form code survives setup_complete.
This commit is contained in:
parent
4ed716637d
commit
3812a06c3c
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.5"
|
||||
__version__ = "0.1.6"
|
||||
|
|
|
|||
|
|
@ -172,18 +172,29 @@ def materialize_after_setup(args):
|
|||
# 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.
|
||||
# Mirrors erpnext's own pattern in install_fixtures.update_stock_settings —
|
||||
# loading the doc and saving, instead of set_single_value — so the write
|
||||
# definitely sticks past any subsequent cache fetch.
|
||||
chosen_vm = (args or {}).get("valuation_method") or "FIFO"
|
||||
if chosen_vm in ("FIFO", "Moving Average"):
|
||||
frappe.logger().info(
|
||||
f"materialize_after_setup: applying valuation_method={chosen_vm} to {company_name}"
|
||||
)
|
||||
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)
|
||||
stock_settings = frappe.get_doc("Stock Settings")
|
||||
stock_settings.valuation_method = chosen_vm
|
||||
stock_settings.flags.ignore_permissions = True
|
||||
stock_settings.flags.ignore_validate_update_after_submit = True
|
||||
stock_settings.save(ignore_permissions=True)
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"Stock Settings valuation_method set failed: {exc}",
|
||||
f"Stock Settings valuation_method set failed: {exc}\n{traceback.format_exc()}",
|
||||
"Jey Wizard materialize",
|
||||
)
|
||||
frappe.db.commit()
|
||||
|
||||
from invoice_az import company_api
|
||||
|
||||
|
|
@ -216,46 +227,74 @@ def materialize_after_setup(args):
|
|||
|
||||
|
||||
def _materialize_native_banks(company):
|
||||
"""Read E-Taxes Bank Account rows just populated for `company` and create
|
||||
ERPNext-native Bank / GL Account / Bank Account records for each non-closed
|
||||
entry. Idempotent — safe to re-run. Closed accounts (status=C) are ignored.
|
||||
After the loop, unused AZ CoA bank placeholders (names like `<CUR> AZXX…`
|
||||
under account_number=223) are deleted.
|
||||
"""Create ERPNext-native Bank / GL Account / Bank Account for each non-closed
|
||||
bank from the wizard cache. Reads from Jey Wizard Etaxes Cache.bank_accounts_json
|
||||
(populated during the Asan step) rather than the E-Taxes Bank Account doctype —
|
||||
that way the materialization still runs even if the finalize-time invoice_az
|
||||
re-fetch happens to fail. Closed (status=C) accounts are ignored. Idempotent.
|
||||
"""
|
||||
cache_rows = frappe.get_all(
|
||||
"E-Taxes Bank Account",
|
||||
filters={"company": company},
|
||||
fields=[
|
||||
"bank_name",
|
||||
"number",
|
||||
"currency",
|
||||
"status",
|
||||
"account_type",
|
||||
],
|
||||
)
|
||||
if not cache_rows:
|
||||
cache = frappe.get_single("Jey Wizard Etaxes Cache")
|
||||
raw = (cache.bank_accounts_json or "").strip()
|
||||
if not raw:
|
||||
frappe.log_error(
|
||||
f"materialize_native_banks: empty bank_accounts_json cache for '{company}'",
|
||||
"Jey Wizard materialize (bank native)",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"materialize_native_banks: cache JSON parse failed: {exc}",
|
||||
"Jey Wizard materialize (bank native)",
|
||||
)
|
||||
return
|
||||
|
||||
# Cached payload is the raw e-taxes response shape: {"bankAccounts": [...]}.
|
||||
accounts = []
|
||||
if isinstance(data, dict):
|
||||
accounts = data.get("bankAccounts") or []
|
||||
elif isinstance(data, list):
|
||||
accounts = data
|
||||
|
||||
if not accounts:
|
||||
frappe.log_error(
|
||||
f"materialize_native_banks: no bankAccounts in cache for '{company}'",
|
||||
"Jey Wizard materialize (bank native)",
|
||||
)
|
||||
return
|
||||
|
||||
parent = _find_bank_parent_account(company)
|
||||
if not parent:
|
||||
frappe.log_error(
|
||||
f"No bank parent account found for company '{company}' — skipping native bank materialize",
|
||||
"Jey Wizard materialize",
|
||||
f"materialize_native_banks: no bank parent (22/223) for company '{company}'",
|
||||
"Jey Wizard materialize (bank native)",
|
||||
)
|
||||
return
|
||||
|
||||
for row in cache_rows:
|
||||
if (row.get("status") or "").upper() == "C":
|
||||
created = 0
|
||||
skipped_closed = 0
|
||||
for acc in accounts:
|
||||
if (acc.get("status") or "").upper() == "C":
|
||||
skipped_closed += 1
|
||||
continue
|
||||
try:
|
||||
_materialize_one_bank(company, parent, row)
|
||||
_materialize_one_bank(company, parent, acc)
|
||||
created += 1
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"{row.get('number')}: {exc}\n{traceback.format_exc()}",
|
||||
f"{acc.get('number')}: {exc}\n{traceback.format_exc()}",
|
||||
"Jey Wizard materialize (bank native)",
|
||||
)
|
||||
|
||||
_delete_bank_placeholders(company, parent)
|
||||
frappe.db.commit()
|
||||
# Success log is loud on purpose — makes it easy to verify the hook ran end-to-end.
|
||||
frappe.logger().info(
|
||||
f"materialize_native_banks: company={company} materialised={created} "
|
||||
f"skipped_closed={skipped_closed} parent={parent}"
|
||||
)
|
||||
|
||||
|
||||
def _find_bank_parent_account(company):
|
||||
|
|
@ -281,10 +320,12 @@ def _find_bank_parent_account(company):
|
|||
|
||||
|
||||
def _materialize_one_bank(company, parent_account, row):
|
||||
bank_name = (row.get("bank_name") or "").strip()
|
||||
# Accepts either the raw e-taxes payload shape (bankName/number/type…) or
|
||||
# the E-Taxes Bank Account doctype shape (bank_name/number/account_type).
|
||||
bank_name = (row.get("bankName") or row.get("bank_name") or "").strip()
|
||||
iban = (row.get("number") or "").strip()
|
||||
currency = (row.get("currency") or "").strip() or "AZN"
|
||||
etx_account_type = (row.get("account_type") or "").strip()
|
||||
etx_account_type = (row.get("type") or row.get("account_type") or "").strip()
|
||||
|
||||
if not bank_name or not iban:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -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.5";
|
||||
const JEY_WIZARD_VERSION = "0.1.6";
|
||||
|
||||
// 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.
|
||||
|
|
@ -440,31 +440,78 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
|||
$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);
|
||||
const v = ($(e.currentTarget).val() || "").toUpperCase().slice(0, 10);
|
||||
this.data.company_abbr = v;
|
||||
$(e.currentTarget).val(v);
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
// Azerbaijani registered names follow the pattern
|
||||
// "<Core Name>" <LEGAL FORM>
|
||||
// where the core is always quoted and the legal form is spelled out in
|
||||
// full ("Məhdud Məsuliyyətli Cəmiyyəti"). The nice abbreviation follows
|
||||
// that split: initials of the core + short code of the legal form
|
||||
// ("TEST SOFT" MMC → "TS MMC"). If no quotes are present we fall back
|
||||
// to a simpler deriver.
|
||||
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);
|
||||
const QUOTE_CHARS = "\"“”„‟«»‘’‚‛'`´″′‹›";
|
||||
const stripEdgeQuotes = (s) =>
|
||||
String(s || "").replace(new RegExp(`^[${QUOTE_CHARS}\\s]+|[${QUOTE_CHARS}\\s]+$`, "g"), "");
|
||||
|
||||
const legalFormCode = (text) => {
|
||||
// Treat Ə/ə the same as E/e and match common full-word AZ legal forms.
|
||||
const ascii = String(text || "").replace(/Ə/g, "E").replace(/ə/g, "e");
|
||||
const rules = [
|
||||
[/\bMEHDUD\s+MESULIYYETLI\s+CEMIYYETI\b/i, "MMC"],
|
||||
[/\bA[CÇ]IQ\s+SEHMDAR\s+CEMIYYETI\b/i, "ASC"],
|
||||
[/\bQAPALI\s+SEHMDAR\s+CEMIYYETI\b/i, "QSC"],
|
||||
[/\bSEHMDAR\s+CEMIYYETI\b/i, "SC"],
|
||||
[/\bKOOPERATIV\b/i, "K"],
|
||||
[/\bFERDI\s+SAHIBKAR\b/i, "FS"],
|
||||
];
|
||||
for (const [re, code] of rules) if (re.test(ascii)) return code;
|
||||
return "";
|
||||
};
|
||||
|
||||
const initialsOf = (text, max = 4) => {
|
||||
const parts = stripEdgeQuotes(text).split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) return "";
|
||||
if (parts.length === 1) return parts[0].slice(0, 3).toUpperCase();
|
||||
return parts.slice(0, max).map((p) => p[0]).join("").toUpperCase();
|
||||
};
|
||||
|
||||
const raw = String(rawName);
|
||||
// Find the first quoted segment using any recognised quote character.
|
||||
const quoted = raw.match(new RegExp(`[${QUOTE_CHARS}]([^${QUOTE_CHARS}]+?)[${QUOTE_CHARS}]`));
|
||||
if (quoted) {
|
||||
const core = quoted[1];
|
||||
const outside = (raw.slice(0, quoted.index) + " " + raw.slice(quoted.index + quoted[0].length))
|
||||
.trim();
|
||||
const coreAbbr = initialsOf(core, 4);
|
||||
const legalCode = legalFormCode(outside);
|
||||
const combined = [coreAbbr, legalCode].filter(Boolean).join(" ").slice(0, 10);
|
||||
if (combined) return combined;
|
||||
}
|
||||
|
||||
// Unquoted fallback: strip quotes, collapse known legal forms inline, take
|
||||
// first letters.
|
||||
let clean = raw.replace(new RegExp(`[${QUOTE_CHARS}]`, "g"), " ");
|
||||
const legal = legalFormCode(clean);
|
||||
if (legal) {
|
||||
// Remove the legal form text from the core, then combine.
|
||||
clean = clean
|
||||
.replace(/Ə/g, "E").replace(/ə/g, "e")
|
||||
.replace(/\bMEHDUD\s+MESULIYYETLI\s+CEMIYYETI\b/gi, " ")
|
||||
.replace(/\bA[CÇ]IQ\s+SEHMDAR\s+CEMIYYETI\b/gi, " ")
|
||||
.replace(/\bQAPALI\s+SEHMDAR\s+CEMIYYETI\b/gi, " ")
|
||||
.replace(/\bSEHMDAR\s+CEMIYYETI\b/gi, " ")
|
||||
.replace(/\bKOOPERATIV\b/gi, " ")
|
||||
.replace(/\bFERDI\s+SAHIBKAR\b/gi, " ");
|
||||
const core = initialsOf(clean, 4);
|
||||
return [core, legal].filter(Boolean).join(" ").slice(0, 10);
|
||||
}
|
||||
return initialsOf(clean, 4).slice(0, 10);
|
||||
}
|
||||
|
||||
render_confirm($body) {
|
||||
|
|
@ -541,14 +588,19 @@ 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 abbrRaw = (this.$wrap.find(".jey-company-abbr").val() || "")
|
||||
.toUpperCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
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 1–10 letters/digits (A–Z, 0–9)."));
|
||||
// Allow letters, digits and a single space — e.g. "TS MMC" for
|
||||
// the Azerbaijani quoted-core + legal-form pattern.
|
||||
if (!/^[A-Z0-9 ]{1,10}$/.test(abbr)) {
|
||||
this.$wrap.find(".jey-status").text(__("Abbreviation must be 1–10 letters/digits (A–Z, 0–9, spaces allowed)."));
|
||||
return false;
|
||||
}
|
||||
this.data.company_abbr = abbr;
|
||||
|
|
@ -1103,9 +1155,10 @@ frappe.setup.SetupWizard = class JeySetupWizard {
|
|||
build_setup_args() {
|
||||
const company = (this.data.company_name || "").trim() || "Test Company AZ";
|
||||
// Prefer the user's (possibly edited) abbr captured in capture_current;
|
||||
// fall back to the deriver if something bypassed validation.
|
||||
// fall back to the deriver if something bypassed validation. Collapse
|
||||
// runs of whitespace but keep a single space (e.g. "TS MMC").
|
||||
const abbr = (
|
||||
(this.data.company_abbr || "").toUpperCase().replace(/\s+/g, "") ||
|
||||
(this.data.company_abbr || "").toUpperCase().replace(/\s+/g, " ").trim() ||
|
||||
this.derive_abbr(company) ||
|
||||
"CO"
|
||||
).slice(0, 10);
|
||||
|
|
|
|||
Loading…
Reference in New Issue