From bb13e293c9969c08cb4ae092406f3b970bd1fe12 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Fri, 24 Apr 2026 13:01:33 +0000 Subject: [PATCH] Materialize ERPNext Bank / Bank Account / GL Account from e-taxes load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit load_company_bank_accounts now makes every non-closed e-taxes bank record usable inside ERPNext, not just a read-only E-Taxes Bank Account cache row: - Bank (global, one per unique bankName) — idempotent insert. - GL Account under parent "223 Bank hesablaşma hesabları", named " ", account_type=Bank, account_currency from e-taxes. Parent discovered by account_number=223 with a name-prefix fallback. - Bank Account linking Bank + GL Account + Company, is_company_account=1, iban + bank_account_no = the AZ IBAN. Closed accounts (status=C) are left at cache-row only; no native records are created for them. After the loop, unused AZ CoA placeholder accounts (names like "AZN AZXXXXXXXXXXXXXXXXXXXXXXXXXX" under the bank group) are deleted so the chart isn't cluttered with unmatched templates. Per-record failures go to the "E-Taxes Bank Native Materialize" error log and don't abort the overall load. --- invoice_az/company_api.py | 131 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/invoice_az/company_api.py b/invoice_az/company_api.py index 5f2d133..ff6a51c 100644 --- a/invoice_az/company_api.py +++ b/invoice_az/company_api.py @@ -471,6 +471,23 @@ def load_company_bank_accounts(company): created += c updated += u + # Phase 2: also materialise ERPNext-native Bank / GL Account / Bank Account + # for non-closed accounts so users can actually post transactions against them. + # Status "C" = closed — skip entirely (cache row above still written). + if (acc.get("status") or "").upper() != "C": + try: + _materialize_native_bank(company, acc) + except Exception as e: + frappe.log_error( + f"{acc.get('number')}: {e}\n{frappe.get_traceback()}", + "E-Taxes Bank Native Materialize", + ) + + # Clean up unused AZ CoA bank placeholders (entries named " AZXX…" under + # parent 223). On a fresh setup they've never been referenced, so deletion + # is safe. Per-row failures logged but don't abort. + _delete_bank_placeholders(company) + frappe.db.commit() return _summarize(created, updated, len(accounts), "Bank accounts loaded") @@ -482,6 +499,120 @@ def load_company_bank_accounts(company): return {"success": False, "message": f"Error: {e}"} +def _find_bank_parent_account(company): + """Resolve the 'Bank hesablaşma hesabları' (account_number=223) group under this + company. Falls back to any Asset group containing bank-typed children.""" + parent = frappe.db.get_value( + "Account", + {"company": company, "account_number": "223", "is_group": 1}, + "name", + ) + if parent: + return parent + parent = frappe.db.get_value( + "Account", + {"company": company, "account_name": ("like", "Bank hesablaşma%"), "is_group": 1}, + "name", + ) + return parent + + +def _materialize_native_bank(company, acc): + """Create Bank (global) + GL Account (per-company) + Bank Account (linking) for + a single e-taxes bank record. Idempotent — safe to call repeatedly. + """ + bank_name = (acc.get("bankName") or "").strip() + iban = (acc.get("number") or "").strip() + currency = (acc.get("currency") or "").strip() or "AZN" + etx_account_type = (acc.get("type") or "").strip() + + if not bank_name or not iban: + return # can't build records without these + + # 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 (Bank hesablaşma hesabları) + parent = _find_bank_parent_account(company) + if not parent: + frappe.log_error( + f"No bank parent account found for company '{company}'; skipping native GL account for {iban}", + "E-Taxes Bank Native Materialize", + ) + return + + account_name = f"{currency} {iban}" + existing_gl = frappe.db.get_value( + "Account", + {"company": company, "account_name": account_name}, + "name", + ) + if existing_gl: + gl_name = existing_gl + else: + gl_doc = frappe.get_doc({ + "doctype": "Account", + "account_name": account_name, + "parent_account": parent, + "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): + """AZ CoA seeds placeholders like 'AZN AZXXXXXXXXXXXXXXXXXXXXXXXXXX' under the + bank group. Remove any that remain after real accounts have been materialised. + """ + parent = _find_bank_parent_account(company) + if not parent: + return + placeholders = frappe.get_all( + "Account", + filters={ + "company": company, + "parent_account": parent, + "account_name": ("like", "% AZXX%"), + "is_group": 0, + }, + pluck="name", + ) + for p in placeholders: + try: + frappe.delete_doc("Account", p, ignore_permissions=True, delete_permanently=True) + except Exception as e: + frappe.log_error( + f"Placeholder delete failed: {p}: {e}", + "E-Taxes Bank Native Materialize", + ) + + # ======= OBLIGATION PACTS (sub-contractor obl-pact-list) ======= @frappe.whitelist()