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:
|
||||
stock_settings = frappe.get_doc("Stock Settings")
|
||||
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_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}\n{traceback.format_exc()}",
|
||||
f"Stock Settings valuation_method/warehouse set failed: {exc}\n{traceback.format_exc()}",
|
||||
"Jey Wizard materialize",
|
||||
)
|
||||
frappe.db.commit()
|
||||
|
|
@ -301,17 +308,25 @@ def _materialize_native_banks(company):
|
|||
)
|
||||
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
|
||||
skipped_closed = 0
|
||||
created_by_currency = {}
|
||||
created_gl_names = []
|
||||
for acc in accounts:
|
||||
if (acc.get("status") or "").upper() == "C":
|
||||
skipped_closed += 1
|
||||
continue
|
||||
try:
|
||||
gl = _materialize_one_bank(company, parent, acc)
|
||||
gl = _materialize_one_bank(company, parent, acc, placeholder_index)
|
||||
if gl:
|
||||
created_gl_names.append(gl)
|
||||
cur = (acc.get("currency") or "AZN").strip() or "AZN"
|
||||
created_by_currency.setdefault(cur, []).append(gl)
|
||||
created += 1
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
|
|
@ -319,8 +334,8 @@ def _materialize_native_banks(company):
|
|||
"Jey Wizard materialize (bank native)",
|
||||
)
|
||||
|
||||
_repoint_company_default_bank(company, created_gl_names)
|
||||
_delete_bank_placeholders(company, parent)
|
||||
_repoint_company_default_bank(company, created_by_currency, created_gl_names)
|
||||
_delete_bank_placeholders(company, parent, placeholder_index)
|
||||
frappe.db.commit()
|
||||
# Success breadcrumb in Error Log so we can verify end-to-end run from the same
|
||||
# place where loader/cache traces live.
|
||||
|
|
@ -353,7 +368,7 @@ def _find_bank_parent_account(company):
|
|||
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
|
||||
# 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
|
||||
|
|
@ -372,7 +387,9 @@ def _materialize_one_bank(company, parent_account, row):
|
|||
bank_doc.flags.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}"
|
||||
gl_name = frappe.db.get_value(
|
||||
"Account",
|
||||
|
|
@ -380,6 +397,12 @@ def _materialize_one_bank(company, parent_account, row):
|
|||
"name",
|
||||
)
|
||||
if not gl_name:
|
||||
ph = _claim_placeholder(placeholder_index, currency) if placeholder_index else None
|
||||
if ph:
|
||||
gl_name = _convert_placeholder_to_real(
|
||||
ph["name"], ph["account_number"], company, account_name, currency
|
||||
)
|
||||
else:
|
||||
gl_doc = frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"account_name": account_name,
|
||||
|
|
@ -418,34 +441,44 @@ def _materialize_one_bank(company, parent_account, row):
|
|||
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
|
||||
account_type='Bank' Account it finds and writes it to
|
||||
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
|
||||
(LinkExistsError pointing back at Company). Repoint the default to the first
|
||||
real bank GL we just created so the placeholder is free to remove.
|
||||
(223.1 - AZN AZXX… - JS). When we reuse the AZN placeholder via rename,
|
||||
frappe.rename_doc auto-updates this link to the real account. But if the
|
||||
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
|
||||
pattern — never overwrites a value the user explicitly set.
|
||||
Currency preference order: AZN → USD → EUR → RUB → GBP → first created.
|
||||
Never overwrites a value the user explicitly set (anything without 'XXXX').
|
||||
"""
|
||||
if not created_gl_names:
|
||||
return
|
||||
current = frappe.db.get_value("Company", company, "default_bank_account")
|
||||
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
|
||||
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(
|
||||
"Company", company, "default_bank_account",
|
||||
created_gl_names[0], update_modified=False,
|
||||
chosen, update_modified=False,
|
||||
)
|
||||
|
||||
|
||||
def _delete_bank_placeholders(company, parent_account):
|
||||
"""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.
|
||||
def _delete_bank_placeholders(company, parent_account, placeholder_index=None):
|
||||
"""Remove unconsumed placeholders. Same-currency placeholders that we
|
||||
repurposed via rename in _convert_placeholder_to_real are no longer "XXXX"
|
||||
rows so the sweep won't touch them. Anything left under the bank parent
|
||||
with `XXXX` in account_name (Kart placeholder, extra-currency rows) is
|
||||
either deleted or — if some other doc still links to it — disabled.
|
||||
"""
|
||||
candidates = frappe.get_all(
|
||||
"Account",
|
||||
|
|
@ -613,6 +646,120 @@ def _count_items(data, paginated):
|
|||
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():
|
||||
if frappe.session.user != "Administrator":
|
||||
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
|
||||
// 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.8";
|
||||
const JEY_WIZARD_VERSION = "0.1.9";
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue