Materialize ERPNext Bank / Bank Account / GL Account in wizard

Keeps invoice_az's loader untouched (it still only fills the read-only
E-Taxes Bank Account cache) and does the ERPNext-native side here
instead. After materialize_after_setup runs the bank loader, we walk
the cache rows linked to the new Company and create a Bank (global,
one per unique bank_name), a GL Account under parent "223 Bank
hesablaşma hesabları" (account_type=Bank, account_currency from
e-taxes, named "<CUR> <IBAN>"), and a Bank Account tying them all
together. Closed accounts (status=C) are skipped entirely — cache row
still exists.

Stock AZ CoA placeholders matching "<CUR> AZXX…" under the bank group
are deleted at the end, since nothing references them on a fresh
setup and they'd otherwise clutter the chart of accounts alongside
the real entries.

Per-row failures are logged under "Jey Wizard materialize (bank …)"
and never abort the run. Idempotent on re-entry.
This commit is contained in:
Ali 2026-04-24 13:10:17 +00:00
parent 3137fc3caf
commit 45e9e54535
3 changed files with 149 additions and 2 deletions

View File

@ -1 +1 @@
__version__ = "0.1.3"
__version__ = "0.1.4"

View File

@ -197,9 +197,156 @@ def materialize_after_setup(args):
"Jey Wizard materialize",
)
# invoice_az's bank loader only populates the E-Taxes Bank Account cache doctype.
# For the wizard we also want native ERPNext Bank / Bank Account / GL Account so
# the user can post transactions on day one, plus we clear out unused CoA
# placeholders. Runs off the just-populated cache, not another e-taxes fetch.
_materialize_native_banks(company_name)
_materialize_amas_employees(company_name)
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.
"""
cache_rows = frappe.get_all(
"E-Taxes Bank Account",
filters={"company": company},
fields=[
"bank_name",
"number",
"currency",
"status",
"account_type",
],
)
if not cache_rows:
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",
)
return
for row in cache_rows:
if (row.get("status") or "").upper() == "C":
continue
try:
_materialize_one_bank(company, parent, row)
except Exception as exc:
frappe.log_error(
f"{row.get('number')}: {exc}\n{traceback.format_exc()}",
"Jey Wizard materialize (bank native)",
)
_delete_bank_placeholders(company, parent)
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",
{"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",
)
def _materialize_one_bank(company, parent_account, row):
bank_name = (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()
if not bank_name or not iban:
return
# 1. Bank (global, unique on bank_name)
if not frappe.db.exists("Bank", bank_name):
bank_doc = frappe.get_doc({"doctype": "Bank", "bank_name": bank_name})
bank_doc.flags.ignore_permissions = True
bank_doc.flags.ignore_if_duplicate = True
bank_doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
# 2. GL Account under parent 223
account_name = f"{currency} {iban}"
gl_name = frappe.db.get_value(
"Account",
{"company": company, "account_name": account_name},
"name",
)
if not gl_name:
gl_doc = frappe.get_doc({
"doctype": "Account",
"account_name": account_name,
"parent_account": parent_account,
"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)
ba_exists = frappe.db.exists(
"Bank Account",
{"bank": bank_name, "account_name": account_name, "company": company},
)
if not ba_exists:
ba_doc = frappe.get_doc({
"doctype": "Bank Account",
"account_name": account_name,
"bank": bank_name,
"account": gl_name,
"company": company,
"is_company_account": 1,
"bank_account_no": iban,
"iban": iban,
"account_type": etx_account_type or None,
})
ba_doc.insert(ignore_permissions=True)
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.
"""
placeholders = frappe.get_all(
"Account",
filters={
"company": company,
"parent_account": parent_account,
"account_name": ("like", "% AZXX%"),
"is_group": 0,
},
pluck="name",
)
for name in placeholders:
try:
frappe.delete_doc("Account", name, ignore_permissions=True, delete_permanently=True)
except Exception as exc:
frappe.log_error(
f"Placeholder delete failed: {name}: {exc}",
"Jey Wizard materialize (bank placeholder)",
)
def _materialize_amas_employees(company_name):
"""If the user loaded ƏMAS employees during the wizard, enqueue bulk import now
that Company exists. Skipped silently when the cache is empty (i.e. user skipped

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