From 31d93fb887217da816fd827259d5e2eda0cd7be6 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Mon, 18 May 2026 13:50:03 +0000 Subject: [PATCH] feat(bank-statement-importer): cascade delete with confirmation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stock delete on Bank Statement Importer hits Frappe's link-check (registry rows + Bank Account dynamic-link) and refuses to proceed, leaving users unable to remove an importer they no longer need without hand-cleaning every dependent row. New flow: - cascade_delete.py adds two whitelisted endpoints: get_dependents (counts) and cascade_delete (unlink Bank Accounts → delete Customer/ Supplier/Purpose registry rows → delete the importer) - bank_statement_importer.js overrides the Menu → Delete action: shows a confirm dialog listing exactly how many records will be deleted vs just unlinked, with a red primary button to drive the choice home. After success the user is sent to the list view. Bank Accounts are intentionally unlinked rather than deleted — they hold business data (Bank Transactions, reconciliations) that has nothing to do with the importer being removed. --- jey_erp/bank_integration/cascade_delete.py | 86 +++++++++++++ .../bank_statement_importer.js | 115 ++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 jey_erp/bank_integration/cascade_delete.py diff --git a/jey_erp/bank_integration/cascade_delete.py b/jey_erp/bank_integration/cascade_delete.py new file mode 100644 index 0000000..24cbae6 --- /dev/null +++ b/jey_erp/bank_integration/cascade_delete.py @@ -0,0 +1,86 @@ +"""Cascade-delete endpoint for Bank Statement Importer. + +By default Frappe refuses to delete a Bank Statement Importer that still has +dependent registry rows (Customer/Supplier/Purpose) or Bank Accounts pointing +at it via the bank_integration Dynamic Link. This module exposes: + +- `get_dependents(name)` — preview counts so the UI can warn the user +- `cascade_delete(name)` — unlinks Bank Accounts and deletes registry rows, + then deletes the importer itself. Caller is expected to have shown a + confirm dialog with the counts from get_dependents. +""" + +import frappe +from frappe import _ + +DOCTYPE = "Bank Statement Importer" +_REGISTRY_DOCTYPES = ( + ("Bank Integration Customer", "Customers"), + ("Bank Integration Supplier", "Suppliers"), + ("Bank Integration Purpose", "Purposes"), +) + + +@frappe.whitelist() +def get_dependents(name): + """Return counts of records that would be touched by a cascade delete.""" + if not name or not frappe.db.exists(DOCTYPE, name): + frappe.throw(_("Bank Statement Importer '{0}' not found").format(name)) + + counts = {} + for dt, _label in _REGISTRY_DOCTYPES: + counts[dt] = frappe.db.count(dt, {"parent_bank_integration": name}) + + counts["Bank Account"] = frappe.db.count("Bank Account", { + "bank_integration_type": DOCTYPE, + "bank_integration": name, + }) + return counts + + +@frappe.whitelist() +def cascade_delete(name): + """Delete a Bank Statement Importer and everything that depends on it. + + Steps (executed in this order so Frappe's link-check doesn't block the + final delete_doc on the importer): + + 1. Unlink Bank Accounts: clear bank_integration_type / bank_integration + 2. Delete registry rows (Customer / Supplier / Purpose) by parent_bank_integration + 3. Delete the importer itself (its mapping child tables go with it) + """ + if not name or not frappe.db.exists(DOCTYPE, name): + frappe.throw(_("Bank Statement Importer '{0}' not found").format(name)) + + frappe.only_for(("System Manager", "Accounts Manager")) + + # 1. Unlink Bank Accounts + unlinked_accounts = 0 + ba_names = frappe.get_all("Bank Account", filters={ + "bank_integration_type": DOCTYPE, + "bank_integration": name, + }, pluck="name") + for ba in ba_names: + frappe.db.set_value("Bank Account", ba, { + "bank_integration_type": None, + "bank_integration": None, + }, update_modified=False) + unlinked_accounts += 1 + + # 2. Delete registries + deleted = {} + for dt, _label in _REGISTRY_DOCTYPES: + rows = frappe.get_all(dt, filters={"parent_bank_integration": name}, pluck="name") + for r in rows: + frappe.delete_doc(dt, r, ignore_permissions=True, force=True, delete_permanently=True) + deleted[dt] = len(rows) + + # 3. Delete the importer (child mapping tables cascade automatically) + frappe.delete_doc(DOCTYPE, name, ignore_permissions=True, force=True, delete_permanently=True) + + frappe.db.commit() + return { + "success": True, + "deleted": deleted, + "unlinked_bank_accounts": unlinked_accounts, + } diff --git a/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.js b/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.js index 44741f2..5dd91bd 100644 --- a/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.js +++ b/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.js @@ -20,6 +20,11 @@ frappe.ui.form.on('Bank Statement Importer', { if (frm.is_new()) return; + // Replace the standard Menu → Delete with a cascading version. The + // stock action would just error out on the link-check (registry rows + // + Bank Account dynamic-link) and leave the user stuck. + BSICascadeDelete.installMenuOverride(frm); + // === Load Data (file-based, registry-only) === frm.add_custom_button(__('Load Data'), () => { _show_load_data_dialog(frm); @@ -629,3 +634,113 @@ function _bi_list_header(label, total, refresh_id) { ${__('Refresh')} `; } + +// ═══════════════════════════════════════════════════════════════════════════════ +// CASCADE DELETE — replace Menu → Delete with a confirm dialog that explains +// what registry rows and Bank Account links will be cleaned up along with +// this importer. +// ═══════════════════════════════════════════════════════════════════════════════ + +const BSICascadeDelete = { + installMenuOverride(frm) { + // frappe.PageMenu renders the items right after refresh; defer slightly. + setTimeout(() => this._wireMenuItem(frm), 50); + }, + + _wireMenuItem(frm) { + // Each Standard menu item is an in the + // page menu. We replace the "Delete" one's click handler. + const $menu = frm.page.menu; + if (!$menu || !$menu.length) return; + const $delete = $menu.find('a.grey-link').filter(function () { + const label = ($(this).find('.menu-item-label').text() || $(this).text() || '').trim(); + return label === __('Delete'); + }); + if (!$delete.length) return; + if ($delete.data('bsiCascadeWired')) return; + $delete.data('bsiCascadeWired', true); + $delete.off('click.bsiCascade').on('click.bsiCascade', (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.run(frm); + }); + }, + + run(frm) { + if (frm.is_new()) return; + frappe.call({ + method: 'jey_erp.bank_integration.cascade_delete.get_dependents', + args: { name: frm.doc.name }, + callback: (r) => { + if (!r.message) return; + this._showConfirm(frm, r.message); + }, + }); + }, + + _showConfirm(frm, counts) { + const items = [ + [__('Customers (registry)'), counts['Bank Integration Customer'] || 0], + [__('Suppliers (registry)'), counts['Bank Integration Supplier'] || 0], + [__('Purposes (registry)'), counts['Bank Integration Purpose'] || 0], + [__('Bank Accounts (will be unlinked, not deleted)'), counts['Bank Account'] || 0], + ]; + const total = items.reduce((s, [, n]) => s + n, 0); + const rows = items + .filter(([, n]) => n > 0) + .map(([label, n]) => `
  • ${frappe.utils.escape_html(label)}: ${n}
  • `) + .join(''); + + const body = total === 0 + ? `

    ${__('No dependent records — deleting this importer is safe.')}

    ` + : `

    ${__('Deleting {0} will also affect:', [frappe.utils.escape_html(frm.doc.name)])}

    + +

    ${__('Registry rows will be deleted. Bank Accounts will only have their Bank Statement Importer link cleared — the accounts themselves stay.')}

    +

    ${__('This cannot be undone.')}

    `; + + const d = new frappe.ui.Dialog({ + title: __('Delete Bank Statement Importer?'), + fields: [{ fieldname: 'body', fieldtype: 'HTML', options: body }], + primary_action_label: __('Delete Everything'), + primary_action: () => { + d.disable_primary_action(); + this._doDelete(frm.doc.name).then(() => { + d.hide(); + frappe.set_route('List', 'Bank Statement Importer'); + }).catch(() => { + d.enable_primary_action(); + }); + }, + secondary_action_label: __('Cancel'), + secondary_action: () => d.hide(), + }); + // Primary button red since this is destructive. + d.show(); + d.get_primary_btn().removeClass('btn-primary').addClass('btn-danger'); + }, + + _doDelete(name) { + return new Promise((resolve, reject) => { + frappe.call({ + method: 'jey_erp.bank_integration.cascade_delete.cascade_delete', + args: { name }, + freeze: true, + freeze_message: __('Deleting…'), + callback: (r) => { + if (!r.message || r.message.success === false) { + frappe.msgprint({ + title: __('Delete Failed'), + indicator: 'red', + message: (r.message && r.message.message) || __('Unknown error'), + }); + reject(); + return; + } + frappe.show_alert({ message: __('Deleted.'), indicator: 'green' }, 4); + resolve(); + }, + error: reject, + }); + }); + }, +};