Silence two post-setup popups from bank materialization

Two unrelated user-visible errors appeared after a successful wizard run:

1. 'Hesab Növü: 2 tapılmadı' (×5, one per non-closed bank)
   Bank Account.account_type is a Link to the Bank Account Type doctype,
   which is empty on a fresh install. We were stuffing the e-taxes raw
   numeric 'type' code (e.g. '2' for card) into it. Field is optional —
   stop populating it, the validation error goes away.

2. 'Hesab 223.1 - AZN AZXX… ilə əlaqəli olduğu üçün silinə bilməz' (×1)
   ERPNext's setup_company → set_default_accounts picks the first Bank-
   typed Account and writes it to Company.default_bank_account. On the
   AZ chart that's a placeholder (223.1 - AZN AZXX… - JS), so when
   _delete_bank_placeholders later tries to delete it, Frappe blocks
   the delete with LinkExistsError pointing back at Company.

   Two-part fix: before the placeholder sweep, repoint
   Company.default_bank_account to the first real bank GL we just
   materialized (only when the current value is unset or a placeholder).
   And as a defensive fallback, _delete_bank_placeholders now catches
   LinkExistsError, clears the queued popup message, and disables the
   account instead of leaving the user with a scary error.
This commit is contained in:
Ali 2026-04-28 10:19:41 +00:00
parent 322aa211b0
commit 1bc9fd6b13
3 changed files with 55 additions and 6 deletions

View File

@ -1 +1 @@
__version__ = "0.1.7" __version__ = "0.1.8"

View File

@ -303,12 +303,15 @@ def _materialize_native_banks(company):
created = 0 created = 0
skipped_closed = 0 skipped_closed = 0
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:
_materialize_one_bank(company, parent, acc) gl = _materialize_one_bank(company, parent, acc)
if gl:
created_gl_names.append(gl)
created += 1 created += 1
except Exception as exc: except Exception as exc:
frappe.log_error( frappe.log_error(
@ -316,6 +319,7 @@ def _materialize_native_banks(company):
"Jey Wizard materialize (bank native)", "Jey Wizard materialize (bank native)",
) )
_repoint_company_default_bank(company, created_gl_names)
_delete_bank_placeholders(company, parent) _delete_bank_placeholders(company, parent)
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
@ -352,13 +356,14 @@ def _find_bank_parent_account(company):
def _materialize_one_bank(company, parent_account, row): def _materialize_one_bank(company, parent_account, row):
# 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
# Company.default_bank_account before deleting placeholders), None otherwise.
bank_name = (row.get("bankName") or row.get("bank_name") or "").strip() bank_name = (row.get("bankName") or row.get("bank_name") or "").strip()
iban = (row.get("number") or "").strip() iban = (row.get("number") or "").strip()
currency = (row.get("currency") or "").strip() or "AZN" currency = (row.get("currency") or "").strip() or "AZN"
etx_account_type = (row.get("type") or row.get("account_type") or "").strip()
if not bank_name or not iban: if not bank_name or not iban:
return return None
# 1. Bank (global, unique on bank_name) # 1. Bank (global, unique on bank_name)
if not frappe.db.exists("Bank", bank_name): if not frappe.db.exists("Bank", bank_name):
@ -393,6 +398,11 @@ def _materialize_one_bank(company, parent_account, row):
{"bank": bank_name, "account_name": account_name, "company": company}, {"bank": bank_name, "account_name": account_name, "company": company},
) )
if not ba_exists: if not ba_exists:
# Bank Account.account_type is a Link to the Bank Account Type doctype
# (effectively empty on a fresh install). The e-taxes 'type' field is a
# numeric code (e.g. "2" for card) that has no Bank Account Type row to
# match — populating it raises "Hesab Növü: 2 tapılmadı" on validate.
# Field is optional, so just leave it unset.
ba_doc = frappe.get_doc({ ba_doc = frappe.get_doc({
"doctype": "Bank Account", "doctype": "Bank Account",
"account_name": account_name, "account_name": account_name,
@ -402,10 +412,33 @@ def _materialize_one_bank(company, parent_account, row):
"is_company_account": 1, "is_company_account": 1,
"bank_account_no": iban, "bank_account_no": iban,
"iban": iban, "iban": iban,
"account_type": etx_account_type or None,
}) })
ba_doc.insert(ignore_permissions=True) ba_doc.insert(ignore_permissions=True)
return gl_name
def _repoint_company_default_bank(company, 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.
Only repoints when the current value is unset or matches the placeholder
pattern never overwrites a value the user explicitly set.
"""
if not created_gl_names:
return
current = frappe.db.get_value("Company", company, "default_bank_account")
if current and "XXXX" not in current:
return
frappe.db.set_value(
"Company", company, "default_bank_account",
created_gl_names[0], update_modified=False,
)
def _delete_bank_placeholders(company, parent_account): def _delete_bank_placeholders(company, parent_account):
"""Remove the stock template accounts from AZ CoA once real banks have been """Remove the stock template accounts from AZ CoA once real banks have been
@ -431,6 +464,22 @@ def _delete_bank_placeholders(company, parent_account):
frappe.delete_doc( frappe.delete_doc(
"Account", row.name, ignore_permissions=True, delete_permanently=True "Account", row.name, ignore_permissions=True, delete_permanently=True
) )
except frappe.LinkExistsError:
# Some other doc still references this placeholder (e.g. a Company
# default field we didn't preempt). Frappe.throw queued a popup
# message before raising — clear it so the user doesn't see a
# scary error after a successful setup, then disable the account
# instead of leaving it active in the chart.
frappe.clear_last_message()
try:
frappe.db.set_value(
"Account", row.name, "disabled", 1, update_modified=False
)
except Exception as exc:
frappe.log_error(
f"Placeholder disable fallback failed: {row.name}: {exc}",
"Jey Wizard materialize (bank placeholder)",
)
except Exception as exc: except Exception as exc:
frappe.log_error( frappe.log_error(
f"Placeholder delete failed: {row.name}: {exc}", f"Placeholder delete failed: {row.name}: {exc}",

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.7"; const JEY_WIZARD_VERSION = "0.1.8";
// 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.