From cc7dfbeda8801442fea7ab36a979a47169d6f75b Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Fri, 8 May 2026 16:24:03 +0000 Subject: [PATCH] chore: move BT-list Import button into shared "Import From..." group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Import" button on the Bank Transaction list view moved from the "Kapital Bank" group into the shared "Import From..." dropdown (created by jey_erp), labelled "Load from Kapital Bank". Switched to a defensive listview_settings assignment so jey_erp's existing onload handler is preserved. Removed client/bank_reconciliation_tool.js and its doctype_js hook — the BRT extension (checkbox column, Create & Reconcile toolbar) lives in jey_erp now and works with both Kapital Bank Settings and the new Bank Integration mappings via a universal resolver. No behaviour change for kb-mapped Bank Accounts: jey_erp's after_migrate links them to Kapital Bank Settings via hidden Dynamic Link fields on Bank Account. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../client/bank_reconciliation_tool.js | 348 ------------------ kapital_bank/client/bank_transaction.js | 23 +- kapital_bank/hooks.py | 5 +- 3 files changed, 20 insertions(+), 356 deletions(-) delete mode 100644 kapital_bank/client/bank_reconciliation_tool.js diff --git a/kapital_bank/client/bank_reconciliation_tool.js b/kapital_bank/client/bank_reconciliation_tool.js deleted file mode 100644 index ef8f79e..0000000 --- a/kapital_bank/client/bank_reconciliation_tool.js +++ /dev/null @@ -1,348 +0,0 @@ -// Kapital Bank — Bank Reconciliation Tool integration -// Adds checkbox column + "Create & Reconcile" toolbar - -frappe.ui.form.on("Bank Reconciliation Tool", { - render: function (frm) { - // Only activate if at least one Kapital Bank Login exists - frappe.db.count("Kapital Bank Login").then(function (count) { - if (!count) return; - - if (frm._kb_check_interval) clearInterval(frm._kb_check_interval); - - frm._kb_check_interval = setInterval(function () { - const dtm = frm.bank_reconciliation_data_table_manager; - if (dtm && dtm.datatable && !dtm._kb_enhanced) { - clearInterval(frm._kb_check_interval); - KBBRTMapping.enhance(frm, dtm); - } - }, 200); - - setTimeout(function () { clearInterval(frm._kb_check_interval); }, 15000); - }); - }, -}); - -const KBBRTMapping = { - enhance: function (frm, dtm) { - dtm._kb_enhanced = true; - dtm._kb_frm = frm; - - // Rebuild DataTable with checkboxColumn: true - dtm.datatable.destroy(); - dtm.datatable = new frappe.DataTable(dtm.$reconciliation_tool_dt.get(0), { - columns: dtm.columns, - data: dtm.transactions, - dynamicRowHeight: true, - checkboxColumn: true, - inlineFilters: true, - }); - $(`.${dtm.datatable.style.scopeClass} .dt-scrollable`).css("max-height", "calc(100vh - 400px)"); - - // Restore ERPNext's Action-button listeners for the new datatable - dtm.set_listeners(); - - dtm._kb_scopeClass = dtm.datatable.style.scopeClass; - - KBBRTMapping.injectToolbar(dtm); - }, - - injectToolbar: function (dtm) { - $("#kb-brt-mapping-toolbar").remove(); - - const $toolbar = $(` - - `); - - dtm.$reconciliation_tool_dt.before($toolbar); - - const scopeClass = dtm.datatable.style.scopeClass; - $(document).off("click.kb_brt_mapping").on( - "click.kb_brt_mapping", - `.${scopeClass} input[type="checkbox"]`, - function () { - setTimeout(() => KBBRTMapping.updateToolbar(dtm, $toolbar), 30); - } - ); - - $toolbar.find(".kb-brt-create-mapping-btn").on("click", function () { - KBBRTMapping.showMappingDialog(dtm); - }); - }, - - updateToolbar: function (dtm, $toolbar) { - const checked = dtm.datatable.rowmanager.getCheckedRows(); - if (checked && checked.length > 0) { - $toolbar.css("display", "flex"); - $toolbar.find(".kb-brt-selected-count").text(__("{0} row(s) selected", [checked.length])); - } else { - $toolbar.hide(); - } - }, - - showMappingDialog: function (dtm) { - const checkedIndices = dtm.datatable.rowmanager.getCheckedRows(); - if (!checkedIndices || checkedIndices.length === 0) { - frappe.msgprint({ title: __("Warning"), indicator: "orange", message: __("No rows selected.") }); - return; - } - - // dtm.transactions row: [date, party_type, party(HTML), description, deposit, withdrawal, ..., reference_number, bank_transaction] - const txns = []; - let skippedCount = 0; - checkedIndices.forEach(function (rowIndex) { - const row = dtm.transactions[parseInt(rowIndex, 10)]; - if (!row) return; - const description = (row[3] || "").trim(); - if (!description) { skippedCount++; return; } - const partyType = (row[1] || "").trim(); - const partyName = $("
").html(row[2] || "").text().trim(); - const deposit = parseFloat(row[4]) || 0; - const withdrawal = parseFloat(row[5]) || 0; - // row[8] = Actions button (original) OR plain name inserted by jey_erp BRT patch - // row[9] = Actions button when jey_erp patch is active (shifted by 1) - const bankTransactionName = $(row[9] || row[8] || "").data("name") || ""; - txns.push({ - purpose: description, - drcr: withdrawal > 0 ? "D" : "C", - party_type: partyType, - party: partyName, - amount: deposit || withdrawal, - date: (row[0] || "").trim(), - reference_number: (row[7] || "").trim(), - bank_transaction_name: bankTransactionName, - }); - }); - - // Deduplicate for preview only - const purposeMap = new Map(); - txns.forEach(function (txn) { - const key = txn.purpose + "|" + txn.drcr; - if (purposeMap.has(key)) { - purposeMap.get(key).count++; - } else { - purposeMap.set(key, { - purpose: txn.purpose, - payment_type: txn.drcr === "D" ? "Pay" : "Receive", - party: txn.party, - count: 1, - }); - } - }); - - if (purposeMap.size === 0) { - frappe.msgprint({ title: __("No Purposes"), indicator: "orange", - message: __("All selected rows have no description text.") }); - return; - } - - // Preview table - let previewHtml = '
' + - '' + - "" + - "" + - '" + - '" + - '" + - ""; - purposeMap.forEach(function (e) { - const badge = e.payment_type === "Pay" - ? 'Pay' - : 'Receive'; - previewHtml += "" + - '" + - "" + - "" + - '"; - }); - previewHtml += "
" + __("Description") + "' + __("Type") + "' + __("Party") + "' + __("Rows") + "
' + frappe.utils.escape_html(e.purpose) + "" + badge + "" + (e.party ? frappe.utils.escape_html(e.party) : '') + "' + e.count + "
"; - if (skippedCount > 0) { - previewHtml += '
' + - __("{0} row(s) skipped — no description text.", [skippedCount]) + "
"; - } - - const d = new frappe.ui.Dialog({ - title: __("Create & Reconcile") + " (" + purposeMap.size + ")", - size: "large", - fields: [ - { - fieldname: "mode", - fieldtype: "Select", - label: __("Action"), - options: "Both\nMappings Only\nDocuments & Reconcile", - default: "Both", - reqd: 1, - onchange: function () { KBBRTMapping._applyModeVisibility(d); }, - }, - { fieldname: "preview_html", fieldtype: "HTML", options: previewHtml }, - { fieldname: "sec_options", fieldtype: "Section Break", label: __("Options") }, - { - fieldname: "document_type", - fieldtype: "Select", - label: __("Document Type"), - options: "Payment Entry\nJournal Entry", - default: "Payment Entry", - }, - { - fieldname: "use_original_purpose", - fieldtype: "Check", - label: __("Use original description"), - onchange: function () { - const on = d.get_value("use_original_purpose"); - d.set_df_property("custom_purpose", "hidden", on ? 1 : 0); - d.get_field("custom_purpose").refresh(); - }, - }, - { - fieldname: "custom_purpose", - fieldtype: "Data", - label: __("Custom Purpose Keyword"), - hidden: 0, - description: __("Leave empty to map by party only. All rows will use this keyword instead of their own description."), - }, - { fieldname: "sec_accounts", fieldtype: "Section Break", label: __("GL Accounts") }, - { - fieldname: "paid_from", fieldtype: "Link", options: "Account", - label: __("Paid From (Account)"), - }, - { fieldname: "col_break", fieldtype: "Column Break" }, - { - fieldname: "paid_to", fieldtype: "Link", options: "Account", - label: __("Paid To (Account)"), - }, - ], - primary_action_label: __("Create & Reconcile"), - primary_action: function () { - const isDR = d.get_value("mode") === "Documents & Reconcile"; - const values = d.get_values(true); - if (!values) return; - if (!isDR) { - const missing = []; - if (!values.paid_from) missing.push(__("Paid From (Account)")); - if (!values.paid_to) missing.push(__("Paid To (Account)")); - if (missing.length) { - frappe.msgprint({ title: __("Missing Values"), indicator: "red", - message: __("Following fields are required:") + "
" + missing.join("
") }); - return; - } - } - - let finalTxns = txns.slice(); - if (values.mode !== "Documents & Reconcile") { - if (!values.use_original_purpose) { - const cp = (values.custom_purpose || "").trim(); - if (cp) { - finalTxns = txns.map(t => Object.assign({}, t, { purpose: cp })); - } else { - finalTxns = txns.map(t => Object.assign({}, t, { purpose: "" })); - if (finalTxns.every(t => !t.party)) { - frappe.msgprint({ - title: __("Validation Error"), - indicator: "red", - message: __("Either a purpose keyword or a party is required."), - }); - return; - } - } - } - } - - KBBRTMapping.submitMappings(d, finalTxns, values.paid_from, values.paid_to, values.document_type, values.mode); - }, - }); - - d._kb_frm = dtm._kb_frm; - d.show(); - KBBRTMapping._applyModeVisibility(d); - - // Pre-fill custom purpose from the Description inline filter if active - const $fi = $(`.${dtm._kb_scopeClass} .dt-row-filter input.dt-filter[data-name="Description"]`); - const filterVal = ($fi.val() || "").trim(); - if (filterVal) { - d.set_value("custom_purpose", filterVal); - } - }, - - _applyModeVisibility: function (d) { - const isDR = d.get_value("mode") === "Documents & Reconcile"; - const toHide = ["preview_html", "sec_options", "document_type", "use_original_purpose", - "custom_purpose", "sec_accounts", "paid_from", "col_break", "paid_to"]; - toHide.forEach(fn => d.set_df_property(fn, "hidden", isDR ? 1 : 0)); - d.refresh_fields(toHide); - }, - - submitMappings: function (dialog, txns, paid_from, paid_to, document_type, mode) { - dialog.disable_primary_action(); - frappe.call({ - method: "kapital_bank.mapping.create_purpose_mappings", - args: { - transactions: JSON.stringify(txns), - paid_from: paid_from, - paid_to: paid_to, - document_type: document_type || "Payment Entry", - mode: mode || "Both", - }, - callback: function (r) { - dialog.enable_primary_action(); - if (!r.message || !r.message.success) { - frappe.msgprint({ title: __("Error"), indicator: "red", - message: r.message ? r.message.message : __("Unknown error") }); - return; - } - dialog.hide(); - const res = r.message; - const parts = []; - if (res.created_mappings) parts.push(__("Mappings created: {0}", [res.created_mappings])); - if (res.already_mapped) parts.push(__("Already mapped: {0}", [res.already_mapped])); - if (res.skipped_no_data) parts.push(__("Skipped (no purpose): {0}", [res.skipped_no_data])); - if (res.created_docs) parts.push(__("Documents created: {0}", [res.created_docs])); - if (res.reconciled) parts.push(__("Reconciled: {0}", [res.reconciled])); - if (res.errors && res.errors.length) parts.push(__("Errors: {0}", [res.errors.length])); - frappe.show_alert({ - message: parts.join(" • ") || __("No changes"), - indicator: (res.created_mappings > 0 || res.created_docs > 0) ? "green" : "blue", - }, 5); - if (dialog._kb_frm) dialog._kb_frm.refresh(); - if (res.errors && res.errors.length) { - KBBRTMapping.showErrors(res.errors); - } - }, - error: function () { - dialog.enable_primary_action(); - frappe.msgprint({ title: __("Error"), indicator: "red", - message: __("Network error creating mappings") }); - }, - }); - }, - - showErrors: function (errors) { - let html = '
'; - errors.forEach(function (err) { - const ref = err.reference_number || __("Unknown"); - html += '
'; - html += '
'; - html += '' + frappe.utils.escape_html(ref) + ''; - html += '' + __("Error") + ''; - html += '
'; - html += '
' + frappe.utils.escape_html(err.message || __("Unknown error")) + '
'; - html += '
'; - }); - html += '
'; - - const d = new frappe.ui.Dialog({ - title: __("Errors ({0})", [errors.length]), - size: "large", - fields: [{ fieldname: "errors_html", fieldtype: "HTML", options: html }], - primary_action_label: __("Close"), - primary_action: function () { d.hide(); }, - }); - d.show(); - }, -}; diff --git a/kapital_bank/client/bank_transaction.js b/kapital_bank/client/bank_transaction.js index d6ecf41..f518c9e 100644 --- a/kapital_bank/client/bank_transaction.js +++ b/kapital_bank/client/bank_transaction.js @@ -722,10 +722,19 @@ KBBTImport.import = { }; // ======= LIST VIEW SETUP ======= -frappe.listview_settings['Bank Transaction'] = { - onload(listview) { - listview.page.add_inner_button(__('Import'), function() { - KBBTImport.import.showDialog(); - }, __('Kapital Bank')); - } -}; +// Coexists with jey_erp's bank_transaction_list.js which creates the +// shared "Import From..." button group; we add our item into the same group. +(function () { + const existing = frappe.listview_settings['Bank Transaction'] || {}; + const prevOnload = existing.onload; + frappe.listview_settings['Bank Transaction'] = Object.assign({}, existing, { + onload(listview) { + if (typeof prevOnload === 'function') { + try { prevOnload(listview); } catch (e) { console.error(e); } + } + listview.page.add_inner_button(__('Load from Kapital Bank'), function () { + KBBTImport.import.showDialog(); + }, __('Import From...')); + } + }); +})(); diff --git a/kapital_bank/hooks.py b/kapital_bank/hooks.py index 251754e..0302511 100644 --- a/kapital_bank/hooks.py +++ b/kapital_bank/hooks.py @@ -11,10 +11,13 @@ after_migrate = "kapital_bank.setup.after_migrate" # JS for ERPNext forms +# Note: Bank Reconciliation Tool extension (Create & Reconcile, checkbox column, +# mapping-source select) lives in jey_erp/public/js/bank_reconciliation_tool.js +# and works with both Kapital Bank Settings and Bank Integration mappings via +# the universal resolver in jey_erp/bank_integration/mapping_resolver.py. doctype_js = { "Payment Entry": "client/payment_entry.js", "Bank Transaction": "client/bank_transaction.js", - "Bank Reconciliation Tool": "client/bank_reconciliation_tool.js", "Payment Request": "client/payment_request.js", }