Repurpose CoA bank placeholders in place + polish defaults
Three small wins for the post-setup chart of accounts:
1. Replace placeholders, don't sit beside them
The AZ chart ships with 223.1 - AZN AZXX… - JS, 223.2 - USD AZXX…,
etc. Previously we created our real banks with auto-generated names
("AZN AZ96AIIB… - JS") and deleted the placeholders, so the chart
format flipped between numbered and unnumbered rows. Now we index
placeholders by leading-3-letter currency before the loop, and for
each materialized bank we rename the matching placeholder in place
(account_name + account_currency updated, then frappe.rename_doc to
"<account_number> - <CUR> <IBAN> - <abbr>"). Real banks inherit the
223.X numbering. Leftover placeholders (extra currencies, the Kart
row) get the same delete-or-disable treatment as before. As a bonus,
frappe.rename_doc auto-fixes Company.default_bank_account when the
AZN placeholder it pointed at is repurposed.
2. Prefer AZN for Company.default_bank_account
_repoint_company_default_bank now picks AZN → USD → EUR → RUB → GBP
→ first-created instead of always taking the first GL out of the
e-taxes response order (which was landing on RUB).
3. Set Stock Settings.default_warehouse
ERPNext's update_stock_settings looks the warehouse up by
`_("Stores")`, which at language=az is "Anbarlar" — but the warehouse
was created with the English name, so the lookup misses and the field
stays None. The Stock Settings save block in materialize_after_setup
now resolves the warehouse directly against the new Company.
This commit is contained in:
parent
1bc9fd6b13
commit
048ab04d7a
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.8"
|
__version__ = "0.1.9"
|
||||||
|
|
|
||||||
|
|
@ -202,12 +202,19 @@ def materialize_after_setup(args):
|
||||||
try:
|
try:
|
||||||
stock_settings = frappe.get_doc("Stock Settings")
|
stock_settings = frappe.get_doc("Stock Settings")
|
||||||
stock_settings.valuation_method = chosen_vm
|
stock_settings.valuation_method = chosen_vm
|
||||||
|
# ERPNext's update_stock_settings looks up the default warehouse by
|
||||||
|
# `_("Stores")` — at language=az that's "Anbarlar", but the warehouse
|
||||||
|
# was created with English "Stores", so the lookup misses and
|
||||||
|
# default_warehouse stays None. Resolve directly here against the
|
||||||
|
# real warehouse list for the new Company.
|
||||||
|
if not stock_settings.default_warehouse:
|
||||||
|
stock_settings.default_warehouse = _find_default_warehouse(company_name)
|
||||||
stock_settings.flags.ignore_permissions = True
|
stock_settings.flags.ignore_permissions = True
|
||||||
stock_settings.flags.ignore_validate_update_after_submit = True
|
stock_settings.flags.ignore_validate_update_after_submit = True
|
||||||
stock_settings.save(ignore_permissions=True)
|
stock_settings.save(ignore_permissions=True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
frappe.log_error(
|
frappe.log_error(
|
||||||
f"Stock Settings valuation_method set failed: {exc}\n{traceback.format_exc()}",
|
f"Stock Settings valuation_method/warehouse set failed: {exc}\n{traceback.format_exc()}",
|
||||||
"Jey Wizard materialize",
|
"Jey Wizard materialize",
|
||||||
)
|
)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
@ -301,17 +308,25 @@ def _materialize_native_banks(company):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Build a currency → [placeholders] index up front. Real banks reuse a
|
||||||
|
# matching placeholder (rename in place to keep the "223.X - <CUR> <IBAN> - <ABBR>"
|
||||||
|
# format the chart already uses); leftover placeholders get deleted at the end.
|
||||||
|
placeholder_index = _index_placeholders_by_currency(company, parent)
|
||||||
|
|
||||||
created = 0
|
created = 0
|
||||||
skipped_closed = 0
|
skipped_closed = 0
|
||||||
|
created_by_currency = {}
|
||||||
created_gl_names = []
|
created_gl_names = []
|
||||||
for acc in accounts:
|
for acc in accounts:
|
||||||
if (acc.get("status") or "").upper() == "C":
|
if (acc.get("status") or "").upper() == "C":
|
||||||
skipped_closed += 1
|
skipped_closed += 1
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
gl = _materialize_one_bank(company, parent, acc)
|
gl = _materialize_one_bank(company, parent, acc, placeholder_index)
|
||||||
if gl:
|
if gl:
|
||||||
created_gl_names.append(gl)
|
created_gl_names.append(gl)
|
||||||
|
cur = (acc.get("currency") or "AZN").strip() or "AZN"
|
||||||
|
created_by_currency.setdefault(cur, []).append(gl)
|
||||||
created += 1
|
created += 1
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
frappe.log_error(
|
frappe.log_error(
|
||||||
|
|
@ -319,8 +334,8 @@ def _materialize_native_banks(company):
|
||||||
"Jey Wizard materialize (bank native)",
|
"Jey Wizard materialize (bank native)",
|
||||||
)
|
)
|
||||||
|
|
||||||
_repoint_company_default_bank(company, created_gl_names)
|
_repoint_company_default_bank(company, created_by_currency, created_gl_names)
|
||||||
_delete_bank_placeholders(company, parent)
|
_delete_bank_placeholders(company, parent, placeholder_index)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
# Success breadcrumb in Error Log so we can verify end-to-end run from the same
|
# Success breadcrumb in Error Log so we can verify end-to-end run from the same
|
||||||
# place where loader/cache traces live.
|
# place where loader/cache traces live.
|
||||||
|
|
@ -353,7 +368,7 @@ def _find_bank_parent_account(company):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _materialize_one_bank(company, parent_account, row):
|
def _materialize_one_bank(company, parent_account, row, placeholder_index=None):
|
||||||
# Accepts either the raw e-taxes payload shape (bankName/number/type…) or
|
# Accepts either the raw e-taxes payload shape (bankName/number/type…) or
|
||||||
# the E-Taxes Bank Account doctype shape (bank_name/number/account_type).
|
# the E-Taxes Bank Account doctype shape (bank_name/number/account_type).
|
||||||
# Returns the GL account name on success (used by the caller to repoint
|
# Returns the GL account name on success (used by the caller to repoint
|
||||||
|
|
@ -372,7 +387,9 @@ def _materialize_one_bank(company, parent_account, row):
|
||||||
bank_doc.flags.ignore_if_duplicate = True
|
bank_doc.flags.ignore_if_duplicate = True
|
||||||
bank_doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
bank_doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||||
|
|
||||||
# 2. GL Account under parent 223
|
# 2. GL Account: prefer to repurpose a same-currency placeholder so the
|
||||||
|
# chart keeps its "223.X - <CUR> <IBAN> - <ABBR>" numbering. Fall back to a
|
||||||
|
# fresh insert if no placeholder is left for this currency.
|
||||||
account_name = f"{currency} {iban}"
|
account_name = f"{currency} {iban}"
|
||||||
gl_name = frappe.db.get_value(
|
gl_name = frappe.db.get_value(
|
||||||
"Account",
|
"Account",
|
||||||
|
|
@ -380,17 +397,23 @@ def _materialize_one_bank(company, parent_account, row):
|
||||||
"name",
|
"name",
|
||||||
)
|
)
|
||||||
if not gl_name:
|
if not gl_name:
|
||||||
gl_doc = frappe.get_doc({
|
ph = _claim_placeholder(placeholder_index, currency) if placeholder_index else None
|
||||||
"doctype": "Account",
|
if ph:
|
||||||
"account_name": account_name,
|
gl_name = _convert_placeholder_to_real(
|
||||||
"parent_account": parent_account,
|
ph["name"], ph["account_number"], company, account_name, currency
|
||||||
"company": company,
|
)
|
||||||
"account_type": "Bank",
|
else:
|
||||||
"account_currency": currency,
|
gl_doc = frappe.get_doc({
|
||||||
"is_group": 0,
|
"doctype": "Account",
|
||||||
})
|
"account_name": account_name,
|
||||||
gl_doc.insert(ignore_permissions=True)
|
"parent_account": parent_account,
|
||||||
gl_name = gl_doc.name
|
"company": company,
|
||||||
|
"account_type": "Bank",
|
||||||
|
"account_currency": currency,
|
||||||
|
"is_group": 0,
|
||||||
|
})
|
||||||
|
gl_doc.insert(ignore_permissions=True)
|
||||||
|
gl_name = gl_doc.name
|
||||||
|
|
||||||
# 3. Bank Account (links Bank + GL + Company)
|
# 3. Bank Account (links Bank + GL + Company)
|
||||||
ba_exists = frappe.db.exists(
|
ba_exists = frappe.db.exists(
|
||||||
|
|
@ -418,34 +441,44 @@ def _materialize_one_bank(company, parent_account, row):
|
||||||
return gl_name
|
return gl_name
|
||||||
|
|
||||||
|
|
||||||
def _repoint_company_default_bank(company, created_gl_names):
|
def _repoint_company_default_bank(company, by_currency, created_gl_names):
|
||||||
"""ERPNext's setup_company → set_default_accounts picks the first
|
"""ERPNext's setup_company → set_default_accounts picks the first
|
||||||
account_type='Bank' Account it finds and writes it to
|
account_type='Bank' Account it finds and writes it to
|
||||||
Company.default_bank_account. On a fresh AZ-CoA install that's a placeholder
|
Company.default_bank_account. On a fresh AZ-CoA install that's a placeholder
|
||||||
(223.1 - AZN AZXX… - JS), and _delete_bank_placeholders then can't delete it
|
(223.1 - AZN AZXX… - JS). When we reuse the AZN placeholder via rename,
|
||||||
(LinkExistsError pointing back at Company). Repoint the default to the first
|
frappe.rename_doc auto-updates this link to the real account. But if the
|
||||||
real bank GL we just created so the placeholder is free to remove.
|
user has no AZN bank, the link is left dangling — repoint it to the most
|
||||||
|
useful real GL we just created.
|
||||||
|
|
||||||
Only repoints when the current value is unset or matches the placeholder
|
Currency preference order: AZN → USD → EUR → RUB → GBP → first created.
|
||||||
pattern — never overwrites a value the user explicitly set.
|
Never overwrites a value the user explicitly set (anything without 'XXXX').
|
||||||
"""
|
"""
|
||||||
if not created_gl_names:
|
if not created_gl_names:
|
||||||
return
|
return
|
||||||
current = frappe.db.get_value("Company", company, "default_bank_account")
|
current = frappe.db.get_value("Company", company, "default_bank_account")
|
||||||
if current and "XXXX" not in current:
|
if current and "XXXX" not in current:
|
||||||
|
# Either user-set or already a real account (e.g. auto-updated by
|
||||||
|
# frappe.rename_doc when we repurposed the AZN placeholder).
|
||||||
return
|
return
|
||||||
|
chosen = None
|
||||||
|
for cur in ("AZN", "USD", "EUR", "RUB", "GBP"):
|
||||||
|
if by_currency.get(cur):
|
||||||
|
chosen = by_currency[cur][0]
|
||||||
|
break
|
||||||
|
if not chosen:
|
||||||
|
chosen = created_gl_names[0]
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Company", company, "default_bank_account",
|
"Company", company, "default_bank_account",
|
||||||
created_gl_names[0], update_modified=False,
|
chosen, update_modified=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _delete_bank_placeholders(company, parent_account):
|
def _delete_bank_placeholders(company, parent_account, placeholder_index=None):
|
||||||
"""Remove the stock template accounts from AZ CoA once real banks have been
|
"""Remove unconsumed placeholders. Same-currency placeholders that we
|
||||||
materialised. Covers both `<CUR> AZXX…` and `Kart XXXX…` patterns — anything
|
repurposed via rename in _convert_placeholder_to_real are no longer "XXXX"
|
||||||
with a run of four or more X characters in the name, under the bank parent.
|
rows so the sweep won't touch them. Anything left under the bank parent
|
||||||
Fresh-setup only — nothing references these placeholders yet, so deletion
|
with `XXXX` in account_name (Kart placeholder, extra-currency rows) is
|
||||||
is safe. Per-row failures logged.
|
either deleted or — if some other doc still links to it — disabled.
|
||||||
"""
|
"""
|
||||||
candidates = frappe.get_all(
|
candidates = frappe.get_all(
|
||||||
"Account",
|
"Account",
|
||||||
|
|
@ -613,6 +646,120 @@ def _count_items(data, paginated):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _index_placeholders_by_currency(company, parent_account):
|
||||||
|
"""Scan the bank parent for accounts whose account_name contains 'XXXX'
|
||||||
|
(the AZ-CoA placeholder marker) and group them by currency. Currency is
|
||||||
|
parsed from the account_name as the leading 3-letter uppercase token —
|
||||||
|
"Kart …" placeholders return None and are excluded so they're never
|
||||||
|
repurposed for a regular IBAN; the leftover sweep deletes them later.
|
||||||
|
|
||||||
|
Returns: {currency: [{"name", "account_number", "consumed": False}, ...]}
|
||||||
|
Sorted by account_number so 223.1 is consumed before 223.7 etc.
|
||||||
|
"""
|
||||||
|
rows = frappe.get_all(
|
||||||
|
"Account",
|
||||||
|
filters={
|
||||||
|
"company": company,
|
||||||
|
"parent_account": parent_account,
|
||||||
|
"account_type": "Bank",
|
||||||
|
"is_group": 0,
|
||||||
|
},
|
||||||
|
fields=["name", "account_number", "account_name"],
|
||||||
|
)
|
||||||
|
index = {}
|
||||||
|
for row in rows:
|
||||||
|
if "XXXX" not in (row.account_name or ""):
|
||||||
|
continue
|
||||||
|
currency = _parse_placeholder_currency(row.account_name)
|
||||||
|
if not currency:
|
||||||
|
continue
|
||||||
|
index.setdefault(currency, []).append({
|
||||||
|
"name": row.name,
|
||||||
|
"account_number": row.account_number or "",
|
||||||
|
"consumed": False,
|
||||||
|
})
|
||||||
|
for cur in index:
|
||||||
|
index[cur].sort(key=lambda p: p["account_number"] or "")
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_placeholder_currency(account_name):
|
||||||
|
"""Extract the 3-letter currency code from AZ-CoA placeholder names like
|
||||||
|
'AZN AZXXXXX…' or 'USD AZXXXXX…'. Returns None for 'Kart XXXX… AZN' style
|
||||||
|
patterns so they're never picked for repurposing — only the canonical
|
||||||
|
"<CUR> AZXX…" placeholders match.
|
||||||
|
"""
|
||||||
|
if not account_name:
|
||||||
|
return None
|
||||||
|
parts = account_name.strip().split()
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
first = parts[0]
|
||||||
|
if first.isalpha() and len(first) == 3 and first.isupper():
|
||||||
|
return first
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _claim_placeholder(placeholder_index, currency):
|
||||||
|
"""Return the next unconsumed placeholder for `currency`, marking it
|
||||||
|
consumed in place. Returns None if no slot is available.
|
||||||
|
"""
|
||||||
|
for ph in placeholder_index.get(currency, []):
|
||||||
|
if not ph["consumed"]:
|
||||||
|
ph["consumed"] = True
|
||||||
|
return ph
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_placeholder_to_real(old_name, account_number, company, new_account_name, currency):
|
||||||
|
"""Repurpose an existing placeholder Account in place: update its
|
||||||
|
account_name and account_currency, then rename to the canonical
|
||||||
|
"<account_number> - <account_name> - <abbr>" form. frappe.rename_doc
|
||||||
|
updates Link references in other docs (notably Company.default_bank_account
|
||||||
|
and any GL entries) to the new name automatically.
|
||||||
|
"""
|
||||||
|
abbr = frappe.db.get_value("Company", company, "abbr") or ""
|
||||||
|
doc = frappe.get_doc("Account", old_name)
|
||||||
|
doc.account_name = new_account_name
|
||||||
|
doc.account_currency = currency
|
||||||
|
doc.flags.ignore_permissions = True
|
||||||
|
doc.flags.ignore_validate_update_after_submit = True
|
||||||
|
doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
if account_number:
|
||||||
|
new_name = f"{account_number} - {new_account_name} - {abbr}"
|
||||||
|
else:
|
||||||
|
new_name = f"{new_account_name} - {abbr}"
|
||||||
|
|
||||||
|
if old_name != new_name and not frappe.db.exists("Account", new_name):
|
||||||
|
frappe.rename_doc(
|
||||||
|
"Account", old_name, new_name,
|
||||||
|
force=True, ignore_permissions=True, show_alert=False,
|
||||||
|
)
|
||||||
|
return new_name
|
||||||
|
return old_name
|
||||||
|
|
||||||
|
|
||||||
|
def _find_default_warehouse(company):
|
||||||
|
"""Pick a sensible default warehouse for Stock Settings. ERPNext creates
|
||||||
|
'Stores' for every new company (is_group=0); under language=az the
|
||||||
|
`_("Stores")` lookup in update_stock_settings misses because the warehouse
|
||||||
|
name is still English. Look up directly by company instead.
|
||||||
|
"""
|
||||||
|
wh = frappe.db.get_value(
|
||||||
|
"Warehouse",
|
||||||
|
{"company": company, "warehouse_name": ("in", ["Stores", _("Stores")])},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
if wh:
|
||||||
|
return wh
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"Warehouse",
|
||||||
|
{"company": company, "is_group": 0},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _only_admin():
|
def _only_admin():
|
||||||
if frappe.session.user != "Administrator":
|
if frappe.session.user != "Administrator":
|
||||||
frappe.throw(_("Only Administrator may run setup"), frappe.PermissionError)
|
frappe.throw(_("Only Administrator may run setup"), frappe.PermissionError)
|
||||||
|
|
|
||||||
|
|
@ -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.8";
|
const JEY_WIZARD_VERSION = "0.1.9";
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue