Wipe placeholders unconditionally, then number real banks 223.X
Replaces the per-currency-match repurposing scheme with a clean-slate approach that handles every shape of e-taxes data uniformly: 1. Detach Company.default_bank_account if it points at a placeholder (otherwise we'd hit LinkExistsError trying to delete that row). 2. Wipe every XXXX-marked Account under the bank parent. Same delete-or-disable fallback as before. 3. Sort the e-taxes accounts by currency preference (AZN→USD→EUR→RUB→GBP→others) so the resulting numbering looks familiar even though the source order from the API is arbitrary. 4. Allocate sequential 223.X account_numbers (next-free integer under the bank parent), creating each GL with an explicit account_number so ERPNext's autoname yields "223.X - <CUR> <IBAN> - <abbr>". 5. Repoint Company.default_bank_account to the first AZN-preferred GL (or first created if no preferred currency present). Handles cleanly: - standard 1-per-currency setups - multiples of one currency (each gets its own 223.X) - exotic currencies like TRY (still gets a 223.X prefix) - zero-bank tenants (placeholders gone, default_bank_account left None) - idempotent reruns (existing GL with matching account_name reused without consuming a number) Removed now-unused helpers: _index_placeholders_by_currency, _parse_placeholder_currency, _claim_placeholder, _peek_placeholder, _convert_placeholder_to_real, _delete_bank_placeholders.
This commit is contained in:
parent
830d621a87
commit
b739f06c98
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.10"
|
||||
__version__ = "0.1.11"
|
||||
|
|
|
|||
|
|
@ -308,41 +308,50 @@ 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)
|
||||
# Clean-slate strategy: detach Company.default_bank_account if it points at
|
||||
# a placeholder (frees the placeholder for deletion), wipe every XXXX row
|
||||
# under the bank parent, then create real banks with explicit "223.X"
|
||||
# numbering. Works uniformly for any currency mix (incl. multiples of one
|
||||
# currency, exotic currencies like TRY, or zero banks) — every materialised
|
||||
# account ends up with the canonical "223.X - <CUR> <IBAN> - <ABBR>" name.
|
||||
_detach_placeholder_default_bank(company)
|
||||
deleted_placeholders = _delete_all_placeholders(company, parent)
|
||||
|
||||
# Sort by currency preference so the numbering ends up familiar:
|
||||
# AZN=223.1, USD=223.2, EUR=223.3, RUB=223.4, GBP=223.5, then anything else.
|
||||
active_accounts = [a for a in accounts if (a.get("status") or "").upper() != "C"]
|
||||
active_accounts.sort(key=_currency_sort_key)
|
||||
skipped_closed = sum(1 for a in accounts if (a.get("status") or "").upper() == "C")
|
||||
|
||||
next_num = _next_bank_account_number(company, parent)
|
||||
|
||||
created = 0
|
||||
skipped_closed = 0
|
||||
created_by_currency = {}
|
||||
created_gl_names = []
|
||||
bank_trace = [] # per-bank: action taken + result, dumped to Error Log at end
|
||||
for acc in accounts:
|
||||
if (acc.get("status") or "").upper() == "C":
|
||||
skipped_closed += 1
|
||||
bank_trace.append({"iban": acc.get("number"), "action": "skipped_closed"})
|
||||
continue
|
||||
bank_trace = []
|
||||
for acc in active_accounts:
|
||||
cur_for_log = (acc.get("currency") or "AZN").strip() or "AZN"
|
||||
ph_available_before = bool(_peek_placeholder(placeholder_index, cur_for_log))
|
||||
try:
|
||||
gl = _materialize_one_bank(company, parent, acc, placeholder_index)
|
||||
if gl:
|
||||
created_gl_names.append(gl)
|
||||
created_by_currency.setdefault(cur_for_log, []).append(gl)
|
||||
result = _materialize_one_bank(company, parent, acc, next_num)
|
||||
if not result:
|
||||
bank_trace.append({"iban": acc.get("number"), "currency": cur_for_log, "action": "skipped_incomplete"})
|
||||
continue
|
||||
gl_name, consumed_number = result
|
||||
if consumed_number:
|
||||
next_num += 1
|
||||
created_gl_names.append(gl_name)
|
||||
created_by_currency.setdefault(cur_for_log, []).append(gl_name)
|
||||
created += 1
|
||||
bank_trace.append({
|
||||
"iban": acc.get("number"),
|
||||
"currency": cur_for_log,
|
||||
"placeholder_was_available": ph_available_before,
|
||||
"resulting_gl": gl,
|
||||
"action": "reused_placeholder" if (ph_available_before and gl) else "created_fresh",
|
||||
"resulting_gl": gl_name,
|
||||
"action": "created" if consumed_number else "existed",
|
||||
})
|
||||
except Exception as exc:
|
||||
bank_trace.append({
|
||||
"iban": acc.get("number"),
|
||||
"currency": cur_for_log,
|
||||
"placeholder_was_available": ph_available_before,
|
||||
"action": "error",
|
||||
"error": str(exc)[:300],
|
||||
})
|
||||
|
|
@ -352,19 +361,15 @@ def _materialize_native_banks(company):
|
|||
)
|
||||
|
||||
_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.
|
||||
frappe.log_error(
|
||||
json.dumps({
|
||||
"company": company,
|
||||
"materialised": created,
|
||||
"skipped_closed": skipped_closed,
|
||||
"deleted_placeholders": deleted_placeholders,
|
||||
"input_rows": len(accounts),
|
||||
"parent": parent,
|
||||
"placeholder_index_keys": sorted(list(placeholder_index.keys())),
|
||||
"placeholder_index_counts": {cur: len(items) for cur, items in placeholder_index.items()},
|
||||
"banks": bank_trace,
|
||||
}, ensure_ascii=False, indent=2),
|
||||
"Jey Wizard trace: bank native done",
|
||||
|
|
@ -393,11 +398,18 @@ def _find_bank_parent_account(company):
|
|||
return None
|
||||
|
||||
|
||||
def _materialize_one_bank(company, parent_account, row, placeholder_index=None):
|
||||
def _materialize_one_bank(company, parent_account, row, account_number_n):
|
||||
"""Create Bank + GL Account (numbered "223.<account_number_n>") + Bank
|
||||
Account for one e-taxes row. Idempotent on the GL: if an Account with the
|
||||
same account_name already exists for this company, reuse it without
|
||||
consuming the proposed number.
|
||||
|
||||
Returns: (gl_name, consumed) — `consumed` is True when we allocated a new
|
||||
"223.X" and the caller should advance its counter; False when we reused an
|
||||
existing GL. Returns None when the row is incomplete (no bank/IBAN).
|
||||
"""
|
||||
# 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
|
||||
# Company.default_bank_account before deleting placeholders), None otherwise.
|
||||
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"
|
||||
|
|
@ -412,33 +424,29 @@ def _materialize_one_bank(company, parent_account, row, placeholder_index=None):
|
|||
bank_doc.flags.ignore_if_duplicate = True
|
||||
bank_doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
# 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.
|
||||
# 2. GL Account with explicit "223.<n>" numbering, falls through to reuse
|
||||
# if the same account_name already exists (idempotent reruns).
|
||||
account_name = f"{currency} {iban}"
|
||||
gl_name = frappe.db.get_value(
|
||||
"Account",
|
||||
{"company": company, "account_name": account_name},
|
||||
"name",
|
||||
)
|
||||
consumed = False
|
||||
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,
|
||||
"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
|
||||
gl_doc = frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"account_name": account_name,
|
||||
"account_number": f"223.{account_number_n}",
|
||||
"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
|
||||
consumed = True
|
||||
|
||||
# 3. Bank Account (links Bank + GL + Company)
|
||||
ba_exists = frappe.db.exists(
|
||||
|
|
@ -463,7 +471,7 @@ def _materialize_one_bank(company, parent_account, row, placeholder_index=None):
|
|||
})
|
||||
ba_doc.insert(ignore_permissions=True)
|
||||
|
||||
return gl_name
|
||||
return gl_name, consumed
|
||||
|
||||
|
||||
def _repoint_company_default_bank(company, by_currency, created_gl_names):
|
||||
|
|
@ -498,12 +506,24 @@ def _repoint_company_default_bank(company, by_currency, created_gl_names):
|
|||
)
|
||||
|
||||
|
||||
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.
|
||||
def _detach_placeholder_default_bank(company):
|
||||
"""ERPNext's setup_company → set_default_accounts pointed
|
||||
Company.default_bank_account at "223.1 - AZN AZXX… - JS" (the AZN
|
||||
placeholder). Clear that field if it currently references a placeholder so
|
||||
the placeholder can be deleted; _repoint_company_default_bank reassigns it
|
||||
to a real bank afterwards.
|
||||
"""
|
||||
current = frappe.db.get_value("Company", company, "default_bank_account")
|
||||
if current and "XXXX" in current:
|
||||
frappe.db.set_value(
|
||||
"Company", company, "default_bank_account", None, update_modified=False
|
||||
)
|
||||
|
||||
|
||||
def _delete_all_placeholders(company, parent_account):
|
||||
"""Wipe every Account under the bank parent whose account_name carries the
|
||||
'XXXX' placeholder marker. Returns the number deleted (or disabled, on
|
||||
LinkExistsError) for trace logging.
|
||||
"""
|
||||
candidates = frappe.get_all(
|
||||
"Account",
|
||||
|
|
@ -515,6 +535,7 @@ def _delete_bank_placeholders(company, parent_account, placeholder_index=None):
|
|||
},
|
||||
fields=["name", "account_name"],
|
||||
)
|
||||
processed = 0
|
||||
for row in candidates:
|
||||
if "XXXX" not in (row.account_name or ""):
|
||||
continue
|
||||
|
|
@ -522,17 +543,17 @@ def _delete_bank_placeholders(company, parent_account, placeholder_index=None):
|
|||
frappe.delete_doc(
|
||||
"Account", row.name, ignore_permissions=True, delete_permanently=True
|
||||
)
|
||||
processed += 1
|
||||
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.
|
||||
# Some other doc still references this placeholder. Clear the
|
||||
# popup that frappe.throw queued, then disable instead of leaving
|
||||
# the user with an error after a successful setup.
|
||||
frappe.clear_last_message()
|
||||
try:
|
||||
frappe.db.set_value(
|
||||
"Account", row.name, "disabled", 1, update_modified=False
|
||||
)
|
||||
processed += 1
|
||||
except Exception as exc:
|
||||
frappe.log_error(
|
||||
f"Placeholder disable fallback failed: {row.name}: {exc}",
|
||||
|
|
@ -543,6 +564,52 @@ def _delete_bank_placeholders(company, parent_account, placeholder_index=None):
|
|||
f"Placeholder delete failed: {row.name}: {exc}",
|
||||
"Jey Wizard materialize (bank placeholder)",
|
||||
)
|
||||
return processed
|
||||
|
||||
|
||||
def _next_bank_account_number(company, parent_account):
|
||||
"""Return the next free integer N such that account_number "223.N" is not
|
||||
used by any sibling under `parent_account`. After _delete_all_placeholders
|
||||
on a fresh install nothing matches, so this returns 1 — but on idempotent
|
||||
reruns it skips over the numbers already taken by real banks.
|
||||
"""
|
||||
rows = frappe.db.sql(
|
||||
"""
|
||||
SELECT account_number FROM tabAccount
|
||||
WHERE company = %s AND parent_account = %s
|
||||
AND account_number LIKE '223.%%'
|
||||
""",
|
||||
(company, parent_account),
|
||||
as_dict=True,
|
||||
)
|
||||
used = set()
|
||||
for r in rows:
|
||||
num = (r.account_number or "")
|
||||
if not num.startswith("223."):
|
||||
continue
|
||||
try:
|
||||
used.add(int(num.split(".", 1)[1]))
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
n = 1
|
||||
while n in used:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
_CURRENCY_PRIORITY = ["AZN", "USD", "EUR", "RUB", "GBP"]
|
||||
|
||||
|
||||
def _currency_sort_key(acc):
|
||||
"""Sort accounts so the canonical AZ ordering is preserved when we hand
|
||||
out 223.X numbers. Known currencies come first in the priority order;
|
||||
anything else is alphabetical after them.
|
||||
"""
|
||||
cur = (acc.get("currency") or "AZN").strip() or "AZN"
|
||||
try:
|
||||
return (0, _CURRENCY_PRIORITY.index(cur), cur)
|
||||
except ValueError:
|
||||
return (1, 0, cur)
|
||||
|
||||
|
||||
def _materialize_amas_employees(company_name):
|
||||
|
|
@ -671,99 +738,6 @@ 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 _peek_placeholder(placeholder_index, currency):
|
||||
"""Like _claim_placeholder but doesn't mutate. Used by trace logging."""
|
||||
if not placeholder_index:
|
||||
return None
|
||||
for ph in placeholder_index.get(currency, []):
|
||||
if not ph["consumed"]:
|
||||
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. Delegates the rename
|
||||
to ERPNext's canonical update_account_number helper — that routine handles
|
||||
the tree-doctype rename, child-company sync, and final frappe.rename_doc
|
||||
with force=True. Currency is updated separately via direct db write so we
|
||||
don't go through Account.validate (which on a doc.save would re-fire all
|
||||
hooks and historically choked on the AZ-CoA placeholder shape).
|
||||
"""
|
||||
if currency:
|
||||
frappe.db.set_value(
|
||||
"Account", old_name, "account_currency", currency, update_modified=False
|
||||
)
|
||||
from erpnext.accounts.doctype.account.account import update_account_number
|
||||
|
||||
new_name = update_account_number(old_name, new_account_name, account_number)
|
||||
return new_name or 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
|
||||
|
|
|
|||
|
|
@ -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.10";
|
||||
const JEY_WIZARD_VERSION = "0.1.11";
|
||||
|
||||
// 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