feat(bank-integration): optional registry loading + "Load Data" button

The Excel import dialog on the Bank Transaction list gains an "Also load
counterparties & purposes" checkbox (default on — preserves current
behaviour). When unchecked, only Bank Transactions are created and the
Bank Integration Customer/Supplier/Purpose registries are left alone.

The Bank Integration form gains a "Load Data" button: pick an Excel
preset + file and choose which registries to populate (Customers /
Suppliers / Purposes checkboxes, like the standard data import). It runs
the same upsert logic but creates no Bank Transactions — the file-based
counterpart to kapital_bank's API-driven counterparty/purpose loaders.
Backed by jey_erp.bank_integration.import_api.load_registries_from_excel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-05-11 13:49:10 +00:00
parent e600a77806
commit 1d6bdc8424
3 changed files with 169 additions and 15 deletions

View File

@ -4,7 +4,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import cint, flt
from jey_erp.bank_integration.excel_parser import parse_excel from jey_erp.bank_integration.excel_parser import parse_excel
@ -48,8 +48,14 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun
@frappe.whitelist() @frappe.whitelist()
def import_bulk_bt(txn_list, bank_integration, bank_account): def import_bulk_bt(txn_list, bank_integration, bank_account, load_registries=1):
"""Enqueue bulk Bank Transaction creation as a background job.""" """Enqueue bulk Bank Transaction creation as a background job.
load_registries: when truthy, also upsert counterparties ( Bank Integration
Customer/Supplier by direction) and purpose keywords ( Bank Integration
Purpose) with status=New, exactly as before. When falsy, only Bank
Transactions are created.
"""
if isinstance(txn_list, str): if isinstance(txn_list, str):
txn_list = json.loads(txn_list) txn_list = json.loads(txn_list)
@ -61,6 +67,7 @@ def import_bulk_bt(txn_list, bank_integration, bank_account):
bank_integration=bank_integration, bank_integration=bank_integration,
bank_account=bank_account, bank_account=bank_account,
user=user, user=user,
load_registries=cint(load_registries),
queue="default", queue="default",
timeout=600, timeout=600,
) )
@ -68,8 +75,8 @@ def import_bulk_bt(txn_list, bank_integration, bank_account):
return {"success": True, "enqueued": True, "total": len(txn_list)} return {"success": True, "enqueued": True, "total": len(txn_list)}
def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user): def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, load_registries=1):
"""Background job: create BT + upsert registry rows for each transaction.""" """Background job: create BT (+ optionally upsert registry rows) for each transaction."""
frappe.set_user(user) frappe.set_user(user)
bi = frappe.get_doc("Bank Integration", bank_integration) bi = frappe.get_doc("Bank Integration", bank_integration)
@ -102,17 +109,18 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user):
drcr = (txn.get("drcr") or "D").upper() drcr = (txn.get("drcr") or "D").upper()
purpose = (txn.get("purpose") or "").strip() purpose = (txn.get("purpose") or "").strip()
if counterparty: if load_registries:
if drcr == "C": if counterparty:
if _upsert_customer(counterparty, contr_voen, contr_iban, bank_integration): if drcr == "C":
new_customers += 1 if _upsert_customer(counterparty, contr_voen, contr_iban, bank_integration):
else: new_customers += 1
if _upsert_supplier(counterparty, contr_voen, contr_iban, bank_integration): else:
new_suppliers += 1 if _upsert_supplier(counterparty, contr_voen, contr_iban, bank_integration):
new_suppliers += 1
if purpose: if purpose:
if _upsert_purpose(purpose, drcr, bank_integration): if _upsert_purpose(purpose, drcr, bank_integration):
new_purposes += 1 new_purposes += 1
ref_no = (txn.get("ref_no") or "").strip() ref_no = (txn.get("ref_no") or "").strip()
amount = abs(flt(txn.get("amount", 0))) amount = abs(flt(txn.get("amount", 0)))
@ -258,3 +266,48 @@ def update_bank_account_bi_default(bank_account, bi_type, bi_name):
}, update_modified=False) }, update_modified=False)
frappe.db.commit() frappe.db.commit()
return {"success": True} return {"success": True}
@frappe.whitelist()
def load_registries_from_excel(file_url, preset_name, bank_integration,
load_customers=1, load_suppliers=1, load_purposes=1):
"""Parse an Excel statement and upsert ONLY the selected reference registries
(Bank Integration Customer / Supplier / Purpose) no Bank Transactions.
This is the "Load Data" action on the Bank Integration form, the file-based
counterpart to kapital_bank's API-driven counterparty/purpose loaders."""
load_customers = cint(load_customers)
load_suppliers = cint(load_suppliers)
load_purposes = cint(load_purposes)
if not (load_customers or load_suppliers or load_purposes):
return {"success": False, "message": _("Select at least one type to load.")}
transactions = parse_excel(file_url, preset_name)
new_c = new_s = new_p = 0
for txn in transactions:
counterparty = (txn.get("counterparty") or "").strip()
contr_voen = (txn.get("contr_voen") or "").strip()
contr_iban = (txn.get("contr_iban") or "").strip()
drcr = (txn.get("drcr") or "D").upper()
purpose = (txn.get("purpose") or "").strip()
if counterparty:
if drcr == "C" and load_customers:
if _upsert_customer(counterparty, contr_voen, contr_iban, bank_integration):
new_c += 1
elif drcr == "D" and load_suppliers:
if _upsert_supplier(counterparty, contr_voen, contr_iban, bank_integration):
new_s += 1
if purpose and load_purposes:
if _upsert_purpose(purpose, drcr, bank_integration):
new_p += 1
frappe.db.commit()
return {
"success": True,
"total_rows": len(transactions),
"new_customers": new_c,
"new_suppliers": new_s,
"new_purposes": new_p,
}

View File

@ -5,6 +5,11 @@ frappe.ui.form.on('Bank Integration', {
refresh(frm) { refresh(frm) {
if (frm.is_new()) return; if (frm.is_new()) return;
// === Load Data (file-based, registry-only) ===
frm.add_custom_button(__('Load Data'), () => {
_show_load_data_dialog(frm);
});
// === Accounts group === // === Accounts group ===
frm.add_custom_button(__('Match accounts to Bank Accounts'), () => { frm.add_custom_button(__('Match accounts to Bank Accounts'), () => {
_safe(frm, () => _call(frm, 'jey_erp.bank_integration.matching.match_similar_accounts', _safe(frm, () => _call(frm, 'jey_erp.bank_integration.matching.match_similar_accounts',
@ -133,3 +138,91 @@ function _call(frm, method, args, onSuccess) {
}, },
}); });
} }
function _show_load_data_dialog(frm) {
const d = new frappe.ui.Dialog({
title: __('Load Data from Excel'),
fields: [
{
fieldname: 'preset',
fieldtype: 'Link',
options: 'Bank Integration Excel Preset',
label: __('Excel Preset'),
reqd: 1,
},
{
fieldname: 'file_url',
fieldtype: 'Attach',
label: __('Excel File'),
reqd: 1,
},
{ fieldname: 'sec_what', fieldtype: 'Section Break', label: __('What to load') },
{
fieldname: 'load_customers',
fieldtype: 'Check',
label: __('Customers (incoming counterparties)'),
default: 1,
},
{
fieldname: 'load_suppliers',
fieldtype: 'Check',
label: __('Suppliers (outgoing counterparties)'),
default: 1,
},
{
fieldname: 'load_purposes',
fieldtype: 'Check',
label: __('Purpose keywords'),
default: 1,
},
],
primary_action_label: __('Load'),
primary_action(values) {
if (!values.load_customers && !values.load_suppliers && !values.load_purposes) {
frappe.msgprint({
title: __('Nothing Selected'),
indicator: 'orange',
message: __('Pick at least one type to load.'),
});
return;
}
d.disable_primary_action();
frappe.call({
method: 'jey_erp.bank_integration.import_api.load_registries_from_excel',
args: {
file_url: values.file_url,
preset_name: values.preset,
bank_integration: frm.doc.name,
load_customers: values.load_customers ? 1 : 0,
load_suppliers: values.load_suppliers ? 1 : 0,
load_purposes: values.load_purposes ? 1 : 0,
},
callback(r) {
d.enable_primary_action();
if (!r.message || r.message.success === false) {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: (r.message && r.message.message) || __('Unknown error'),
});
return;
}
d.hide();
const res = r.message;
frappe.msgprint({
title: __('Data Loaded'),
indicator: 'green',
message: __('From {0} rows — new customers: {1}, suppliers: {2}, purposes: {3}',
[res.total_rows, res.new_customers, res.new_suppliers, res.new_purposes]),
});
frm.reload_doc();
},
error() {
d.enable_primary_action();
frappe.msgprint({ title: __('Error'), indicator: 'red', message: __('Network error') });
},
});
},
});
d.show();
}

View File

@ -84,6 +84,13 @@ const BIExcelImport = {
label: __('Excel File'), label: __('Excel File'),
reqd: 1, reqd: 1,
}, },
{
fieldname: 'load_registries',
fieldtype: 'Check',
label: __('Also load counterparties & purposes'),
default: 1,
description: __('Extract unique counterparties and purpose keywords from the file into the Bank Integration registries (status: New).'),
},
], ],
primary_action_label: __('Parse'), primary_action_label: __('Parse'),
primary_action(values) { primary_action(values) {
@ -251,6 +258,7 @@ const BIExcelImport = {
txn_list: JSON.stringify(selected), txn_list: JSON.stringify(selected),
bank_integration: values.bank_integration, bank_integration: values.bank_integration,
bank_account: values.bank_account, bank_account: values.bank_account,
load_registries: values.load_registries ? 1 : 0,
}, },
error() { error() {
frappe.realtime.off('bi_bt_import_progress'); frappe.realtime.off('bi_bt_import_progress');