feat(bank-integration): populate the Data tab subtabs

The Accounts / Customers / Suppliers / Purposes subtabs under "Data"
were stuck on "Loading..." — nothing rendered into them. Added
get_bi_reference_data_list (paginated rows for each registry, scoped to
the Bank Integration record) and the client-side rendering in
bank_integration.js (tables with status pills, links to the underlying
records and their ERPNext mappings, plus a Refresh button per subtab) —
mirroring the kapital_bank_settings Data tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-05-12 10:40:23 +00:00
parent 2dba6908b0
commit 7e862e48e5
2 changed files with 210 additions and 0 deletions

View File

@ -252,6 +252,69 @@ def _upsert_purpose(purpose, drcr, bank_integration):
return False
@frappe.whitelist()
def get_bi_reference_data_list(bank_integration, data_type, limit=100, offset=0):
"""Paginated rows for the Bank Integration "Data" tab subtabs.
data_type {accounts, customers, suppliers, purposes}.
"""
limit = cint(limit)
offset = cint(offset)
try:
if data_type == "accounts":
total = frappe.db.count("Bank Integration Account Mapping", filters={
"parent": bank_integration, "parenttype": "Bank Integration"
})
data = frappe.get_all(
"Bank Integration Account Mapping",
filters={"parent": bank_integration, "parenttype": "Bank Integration"},
fields=["name", "iban", "account_label", "currency", "bank_account", "gl_account"],
limit=limit, start=offset, order_by="idx asc",
)
elif data_type == "customers":
total = frappe.db.count("Bank Integration Customer", filters={"parent_bank_integration": bank_integration})
data = frappe.get_all(
"Bank Integration Customer",
filters={"parent_bank_integration": bank_integration},
fields=["name", "customer_name", "tax_id", "iban", "status", "mapped_customer", "creation"],
limit=limit, start=offset, order_by="customer_name asc",
)
elif data_type == "suppliers":
total = frappe.db.count("Bank Integration Supplier", filters={"parent_bank_integration": bank_integration})
data = frappe.get_all(
"Bank Integration Supplier",
filters={"parent_bank_integration": bank_integration},
fields=["name", "supplier_name", "tax_id", "iban", "status", "mapped_supplier", "creation"],
limit=limit, start=offset, order_by="supplier_name asc",
)
elif data_type == "purposes":
total = frappe.db.count("Bank Integration Purpose", filters={"parent_bank_integration": bank_integration})
data = frappe.get_all(
"Bank Integration Purpose",
filters={"parent_bank_integration": bank_integration},
fields=["name", "purpose_keyword", "direction", "status"],
limit=limit, start=offset, order_by="purpose_keyword asc",
)
else:
return {"success": False, "message": f"Unknown data_type: {data_type}"}
return {
"success": True,
"data": data,
"total_count": total,
"has_more": (offset + limit) < total,
}
except Exception as e:
frappe.log_error(f"get_bi_reference_data_list({data_type}): {e}\n{frappe.get_traceback()}", "BI Reference Data")
return {"success": False, "message": str(e)}
@frappe.whitelist()
def update_bank_account_bi_default(bank_account, bi_type, bi_name):
"""Save the user's BRT-time choice to the BA's hidden fields."""

View File

@ -103,6 +103,9 @@ frappe.ui.form.on('Bank Integration', {
}, 5),
));
}, __('Purposes'));
// === Data tab rendering ===
_bi_load_data_tabs(frm);
},
});
@ -226,3 +229,147 @@ function _show_load_data_dialog(frm) {
});
d.show();
}
// ═══════════════════════════════════════════════════════════════════════════════
// DATA TAB — registry overviews (Accounts / Customers / Suppliers / Purposes)
// ═══════════════════════════════════════════════════════════════════════════════
function _bi_load_data_tabs(frm) {
const name = frm.doc.name;
_bi_load_tab(name, 'accounts', 'bi-accounts-container', _bi_render_accounts);
_bi_load_tab(name, 'customers', 'bi-customers-container', _bi_render_customers);
_bi_load_tab(name, 'suppliers', 'bi-suppliers-container', _bi_render_suppliers);
_bi_load_tab(name, 'purposes', 'bi-purposes-container', _bi_render_purposes);
}
function _bi_load_tab(bank_integration, data_type, container_id, renderer) {
$('#' + container_id).html(_bi_loading_html());
frappe.call({
method: 'jey_erp.bank_integration.import_api.get_bi_reference_data_list',
args: { bank_integration, data_type, limit: 100, offset: 0 },
callback(r) {
if (r.message && r.message.success) {
renderer(r.message.data, r.message.total_count, () => _bi_load_tab(bank_integration, data_type, container_id, renderer));
} else {
$('#' + container_id).html(_bi_error_html(r.message ? r.message.message : __('Unknown error')));
}
},
error() { $('#' + container_id).html(_bi_error_html(__('Network error'))); },
});
}
function _bi_render_accounts(rows, total, reload) {
let html = _bi_list_header(__('Accounts'), total, 'bi-refresh-accounts');
if (rows && rows.length) {
html += `<table class="table table-bordered" style="font-size:13px;">
<thead class="grid-heading-row"><tr>
<th>${__('IBAN')}</th><th>${__('Label')}</th><th>${__('Currency')}</th>
<th>${__('Bank Account')}</th><th>${__('GL Account')}</th>
</tr></thead><tbody>`;
rows.forEach(a => {
const ba = a.bank_account ? `<a href="/app/bank-account/${encodeURIComponent(a.bank_account)}">${a.bank_account}</a>` : '-';
const gl = a.gl_account ? `<a href="/app/account/${encodeURIComponent(a.gl_account)}">${a.gl_account}</a>` : '-';
html += `<tr><td>${a.iban || '-'}</td><td>${frappe.utils.escape_html(a.account_label || '-')}</td>
<td>${a.currency || '-'}</td><td>${ba}</td><td>${gl}</td></tr>`;
});
html += '</tbody></table>';
} else {
html += _bi_empty_state(__('No account mappings yet. Add rows in the Mappings tab.'));
}
$('#bi-accounts-container').html(html);
$('#bi-refresh-accounts').on('click', reload);
}
function _bi_render_customers(rows, total, reload) {
let html = _bi_list_header(__('Customers'), total, 'bi-refresh-customers');
if (rows && rows.length) {
html += `<table class="table table-bordered" style="font-size:13px;">
<thead class="grid-heading-row"><tr>
<th>${__('Customer Name')}</th><th>${__('Tax ID (VOEN)')}</th><th>${__('IBAN')}</th>
<th>${__('Status')}</th><th>${__('Mapped To')}</th><th>${__('Created')}</th>
</tr></thead><tbody>`;
rows.forEach(c => {
const pill = c.status === 'Mapped' ? 'green' : 'red';
const mapped = c.mapped_customer ? `<a href="/app/customer/${encodeURIComponent(c.mapped_customer)}">${c.mapped_customer}</a>` : '-';
const created = c.creation ? frappe.datetime.str_to_user(c.creation) : '-';
html += `<tr>
<td><a href="/app/bank-integration-customer/${encodeURIComponent(c.name)}">${frappe.utils.escape_html(c.customer_name)}</a></td>
<td>${c.tax_id || '-'}</td><td>${c.iban || '-'}</td>
<td><span class="indicator-pill ${pill}">${__(c.status)}</span></td>
<td>${mapped}</td><td>${created}</td></tr>`;
});
html += '</tbody></table>';
} else {
html += _bi_empty_state(__('No customers registered yet. Import a statement (with "Also load counterparties & purposes") or use "Load Data".'));
}
$('#bi-customers-container').html(html);
$('#bi-refresh-customers').on('click', reload);
}
function _bi_render_suppliers(rows, total, reload) {
let html = _bi_list_header(__('Suppliers'), total, 'bi-refresh-suppliers');
if (rows && rows.length) {
html += `<table class="table table-bordered" style="font-size:13px;">
<thead class="grid-heading-row"><tr>
<th>${__('Supplier Name')}</th><th>${__('Tax ID (VOEN)')}</th><th>${__('IBAN')}</th>
<th>${__('Status')}</th><th>${__('Mapped To')}</th><th>${__('Created')}</th>
</tr></thead><tbody>`;
rows.forEach(s => {
const pill = s.status === 'Mapped' ? 'green' : 'red';
const mapped = s.mapped_supplier ? `<a href="/app/supplier/${encodeURIComponent(s.mapped_supplier)}">${s.mapped_supplier}</a>` : '-';
const created = s.creation ? frappe.datetime.str_to_user(s.creation) : '-';
html += `<tr>
<td><a href="/app/bank-integration-supplier/${encodeURIComponent(s.name)}">${frappe.utils.escape_html(s.supplier_name)}</a></td>
<td>${s.tax_id || '-'}</td><td>${s.iban || '-'}</td>
<td><span class="indicator-pill ${pill}">${__(s.status)}</span></td>
<td>${mapped}</td><td>${created}</td></tr>`;
});
html += '</tbody></table>';
} else {
html += _bi_empty_state(__('No suppliers registered yet.'));
}
$('#bi-suppliers-container').html(html);
$('#bi-refresh-suppliers').on('click', reload);
}
function _bi_render_purposes(rows, total, reload) {
let html = _bi_list_header(__('Purposes'), total, 'bi-refresh-purposes');
if (rows && rows.length) {
html += `<table class="table table-bordered" style="font-size:13px;">
<thead class="grid-heading-row"><tr>
<th>${__('Purpose Keyword')}</th><th>${__('Direction')}</th><th>${__('Status')}</th>
</tr></thead><tbody>`;
rows.forEach(p => {
const pill = p.status === 'Mapped' ? 'green' : 'red';
html += `<tr>
<td><a href="/app/bank-integration-purpose/${encodeURIComponent(p.name)}">${frappe.utils.escape_html(p.purpose_keyword || '')}</a></td>
<td>${__(p.direction || '-')}</td>
<td><span class="indicator-pill ${pill}">${__(p.status)}</span></td></tr>`;
});
html += '</tbody></table>';
} else {
html += _bi_empty_state(__('No purpose keywords registered yet.'));
}
$('#bi-purposes-container').html(html);
$('#bi-refresh-purposes').on('click', reload);
}
function _bi_loading_html() {
return `<div class="text-center text-muted" style="padding:20px;"><i class="fa fa-spinner fa-spin"></i> ${__('Loading...')}</div>`;
}
function _bi_empty_state(msg) {
return `<div class="text-center text-muted" style="padding:30px;"><p>${msg}</p></div>`;
}
function _bi_error_html(msg) {
return `<div class="alert alert-warning"><p>${__('Error loading data')}: ${frappe.utils.escape_html(String(msg))}</p></div>`;
}
function _bi_list_header(label, total, refresh_id) {
return `<div class="clearfix" style="margin-bottom:10px;">
<div class="pull-left"><h6 class="text-muted">${label} (${total} ${__('total')})</h6></div>
<div class="pull-right"><button class="btn btn-xs btn-default" id="${refresh_id}">
<i class="octicon octicon-sync"></i> ${__('Refresh')}</button></div>
</div>`;
}