758 lines
25 KiB
JavaScript
758 lines
25 KiB
JavaScript
const KBBTImport = {
|
|
loadingErrors: [],
|
|
};
|
|
|
|
// ======= ERROR MANAGEMENT =======
|
|
KBBTImport.errors = {
|
|
add: function(txnData, errorType, errorMessage) {
|
|
const errorEntry = {
|
|
ref_no: txnData.ref_no || 'Unknown',
|
|
date: txnData.date || 'Unknown',
|
|
counterparty: txnData.counterparty || 'Unknown',
|
|
amount: txnData.amount || 0,
|
|
source_label: txnData.source_label || '',
|
|
error_type: errorType,
|
|
error_message: errorMessage,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
KBBTImport.loadingErrors.push(errorEntry);
|
|
console.error('KB BT Import Error:', errorEntry);
|
|
},
|
|
|
|
clear: function() {
|
|
frappe.confirm(
|
|
__('Are you sure you want to clear the error log?'),
|
|
function() {
|
|
KBBTImport.loadingErrors = [];
|
|
frappe.show_alert({
|
|
message: __('Error log cleared'),
|
|
indicator: 'green'
|
|
}, 2);
|
|
}
|
|
);
|
|
},
|
|
|
|
showDetails: function() {
|
|
if (KBBTImport.loadingErrors.length === 0) {
|
|
frappe.msgprint(__('No errors to display'));
|
|
return;
|
|
}
|
|
|
|
let errorsHtml = '<div class="error-log-container">';
|
|
|
|
KBBTImport.loadingErrors.forEach(function(error) {
|
|
let documentDate = 'No date';
|
|
if (error.date && error.date !== 'Unknown') {
|
|
try {
|
|
documentDate = moment(error.date).format('DD.MM.YYYY');
|
|
} catch (e) {
|
|
documentDate = 'Invalid date';
|
|
}
|
|
}
|
|
|
|
errorsHtml += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
|
|
|
|
// Header
|
|
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
|
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
|
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
|
'<strong>' + error.ref_no + '</strong></h5>';
|
|
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
|
errorsHtml += '</div>';
|
|
errorsHtml += '<small style="color: var(--text-muted);">' + error.counterparty + '</small>';
|
|
if (error.source_label) {
|
|
errorsHtml += ' <small style="color: var(--text-muted);">(' + error.source_label + ')</small>';
|
|
}
|
|
errorsHtml += '</div>';
|
|
|
|
// Error message
|
|
errorsHtml += '<div class="error-message" style="margin-bottom: 10px;">';
|
|
errorsHtml += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
|
|
errorsHtml += '</div>';
|
|
|
|
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
|
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
|
errorsHtml += '</div>';
|
|
|
|
errorsHtml += '</div>';
|
|
});
|
|
|
|
errorsHtml += '</div>';
|
|
|
|
// Statistics
|
|
const errorTypes = {};
|
|
KBBTImport.loadingErrors.forEach(function(error) {
|
|
errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
|
|
});
|
|
|
|
let statsHtml = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
|
|
statsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
|
statsHtml += '<div>';
|
|
statsHtml += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + KBBTImport.loadingErrors.length + '<br>';
|
|
statsHtml += '<span style="color: var(--text-muted);">';
|
|
Object.keys(errorTypes).forEach(function(type, index) {
|
|
if (index > 0) statsHtml += ' • ';
|
|
statsHtml += type + ': ' + errorTypes[type];
|
|
});
|
|
statsHtml += '</span>';
|
|
statsHtml += '</div>';
|
|
statsHtml += '<div>';
|
|
statsHtml += '<button class="btn btn-default btn-sm" id="kb-bt-export-csv-dialog-btn" style="margin-right: 5px;">' +
|
|
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>';
|
|
statsHtml += '<button class="btn btn-warning btn-sm" id="kb-bt-clear-log-dialog-btn">' +
|
|
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
|
|
statsHtml += '</div></div>';
|
|
|
|
const d = new frappe.ui.Dialog({
|
|
title: __('Error Details') + ' (' + KBBTImport.loadingErrors.length + ')',
|
|
size: 'large',
|
|
fields: [
|
|
{
|
|
fieldname: 'stats_html',
|
|
fieldtype: 'HTML',
|
|
options: statsHtml
|
|
},
|
|
{
|
|
fieldname: 'errors_html',
|
|
fieldtype: 'HTML',
|
|
options: '<div style="max-height: 500px; overflow-y: auto;">' + errorsHtml + '</div>'
|
|
}
|
|
],
|
|
primary_action_label: __('Close'),
|
|
primary_action: function() {
|
|
d.hide();
|
|
}
|
|
});
|
|
|
|
d.show();
|
|
|
|
setTimeout(function() {
|
|
$('#kb-bt-export-csv-dialog-btn').off('click').on('click', function() {
|
|
KBBTImport.errors.exportCSV();
|
|
});
|
|
|
|
$('#kb-bt-clear-log-dialog-btn').off('click').on('click', function() {
|
|
KBBTImport.errors.clear();
|
|
d.hide();
|
|
});
|
|
}, 200);
|
|
},
|
|
|
|
exportCSV: function() {
|
|
if (KBBTImport.loadingErrors.length === 0) {
|
|
frappe.msgprint(__('No errors to export'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let csvContent = "data:text/csv;charset=utf-8,";
|
|
csvContent += "Ref No,Date,Counterparty,Amount,Source,Error Type,Error Message,Timestamp\n";
|
|
|
|
KBBTImport.loadingErrors.forEach(function(error) {
|
|
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
|
const cleanCounterparty = (error.counterparty || '').replace(/"/g, '""');
|
|
|
|
const row = [
|
|
error.ref_no,
|
|
error.date,
|
|
cleanCounterparty,
|
|
error.amount,
|
|
error.source_label || '',
|
|
error.error_type,
|
|
cleanMessage,
|
|
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
|
|
].map(function(field) {
|
|
return '"' + (field || '') + '"';
|
|
}).join(',');
|
|
|
|
csvContent += row + "\n";
|
|
});
|
|
|
|
const encodedUri = encodeURI(csvContent);
|
|
const link = document.createElement("a");
|
|
link.setAttribute("href", encodedUri);
|
|
link.setAttribute("download", "kb_bt_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
frappe.show_alert({
|
|
message: __('Error log exported successfully'),
|
|
indicator: 'green'
|
|
}, 3);
|
|
} catch (e) {
|
|
console.error('Export error:', e);
|
|
frappe.show_alert({
|
|
message: __('Failed to export error log'),
|
|
indicator: 'red'
|
|
}, 3);
|
|
}
|
|
},
|
|
|
|
showSummary: function(totalTransactions, processedCount, errorsCount) {
|
|
if (errorsCount === 0) {
|
|
frappe.msgprint({
|
|
title: __('Import Completed Successfully'),
|
|
indicator: 'green',
|
|
message: __('All ') + totalTransactions + __(' transactions were imported successfully.')
|
|
});
|
|
} else {
|
|
const title = errorsCount === totalTransactions ? __('Import Failed') : __('Import Completed with Errors');
|
|
const indicator = processedCount > 0 ? 'orange' : 'red';
|
|
|
|
let message = '<div style="margin-bottom: 15px;">';
|
|
if (processedCount > 0) {
|
|
message += __('Successfully imported: ') + '<strong>' + processedCount + '</strong>' + __(' transactions') + '<br>';
|
|
}
|
|
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' transactions');
|
|
message += '</div>';
|
|
|
|
message += '<div style="text-align: center;">' +
|
|
'<button class="btn btn-primary btn-sm" id="kb-bt-view-error-details-btn" style="margin-right: 10px;">' +
|
|
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
|
|
'<button class="btn btn-default btn-sm" id="kb-bt-export-error-log-btn">' +
|
|
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
|
|
'</div>';
|
|
|
|
const summaryDialog = frappe.msgprint({
|
|
title: title,
|
|
indicator: indicator,
|
|
message: message
|
|
});
|
|
|
|
setTimeout(function() {
|
|
$('#kb-bt-view-error-details-btn').off('click').on('click', function() {
|
|
summaryDialog.hide();
|
|
KBBTImport.errors.showDetails();
|
|
});
|
|
|
|
$('#kb-bt-export-error-log-btn').off('click').on('click', function() {
|
|
KBBTImport.errors.exportCSV();
|
|
});
|
|
}, 200);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ======= IMPORT MODULE =======
|
|
KBBTImport.import = {
|
|
showDialog: function() {
|
|
// Load sources: try settings mappings first, fall back to registry doctypes
|
|
frappe.call({
|
|
method: 'frappe.client.get',
|
|
args: { doctype: 'Kapital Bank Settings', name: 'Kapital Bank Settings' },
|
|
callback: function(r) {
|
|
if (!r.message) {
|
|
frappe.msgprint({ title: __('Error'), indicator: 'red', message: __('Could not load Kapital Bank Settings') });
|
|
return;
|
|
}
|
|
|
|
const accountMappings = r.message.account_mappings || [];
|
|
const cardMappings = r.message.card_mappings || [];
|
|
|
|
if (accountMappings.length > 0 || cardMappings.length > 0) {
|
|
// Use mappings as source list
|
|
KBBTImport.import._showDialogWithSources(
|
|
accountMappings.map(function(m) {
|
|
return { type: 'account', id: m.iban, label: m.account_label ? (m.iban + ' — ' + m.account_label) : m.iban };
|
|
}).filter(function(s) { return s.id; }),
|
|
cardMappings.map(function(m) {
|
|
return { type: 'card', id: m.account_number, label: m.card_label ? (m.account_number + ' — ' + m.card_label) : m.account_number };
|
|
}).filter(function(s) { return s.id; })
|
|
);
|
|
} else {
|
|
// Fall back to registry doctypes
|
|
KBBTImport.import._loadRegistrySources();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
_loadRegistrySources: function() {
|
|
let accounts = [];
|
|
let cards = [];
|
|
let pending = 2;
|
|
|
|
function onDone() {
|
|
pending--;
|
|
if (pending > 0) return;
|
|
|
|
if (accounts.length === 0 && cards.length === 0) {
|
|
frappe.msgprint({
|
|
title: __('No Sources'),
|
|
indicator: 'orange',
|
|
message: __('No bank accounts or cards found. Please load registry data in Kapital Bank Settings first.')
|
|
});
|
|
return;
|
|
}
|
|
|
|
KBBTImport.import._showDialogWithSources(accounts, cards);
|
|
}
|
|
|
|
frappe.call({
|
|
method: 'frappe.client.get_list',
|
|
args: {
|
|
doctype: 'Kapital Bank Account',
|
|
fields: ['iban', 'account_desc', 'currency'],
|
|
filters: { status: 'Active' },
|
|
limit_page_length: 0
|
|
},
|
|
callback: function(r) {
|
|
if (r.message) {
|
|
accounts = r.message.map(function(a) {
|
|
const label = a.account_desc ? (a.iban + ' — ' + a.account_desc) : a.iban;
|
|
return { type: 'account', id: a.iban, label: label };
|
|
});
|
|
}
|
|
onDone();
|
|
},
|
|
error: function() { onDone(); }
|
|
});
|
|
|
|
frappe.call({
|
|
method: 'frappe.client.get_list',
|
|
args: {
|
|
doctype: 'Kapital Bank Card',
|
|
fields: ['account_number', 'card_type', 'pan', 'currency'],
|
|
filters: { status: 'Active' },
|
|
limit_page_length: 0
|
|
},
|
|
callback: function(r) {
|
|
if (r.message) {
|
|
cards = r.message.map(function(c) {
|
|
const label = c.pan ? (c.account_number + ' — ' + c.pan) : c.account_number;
|
|
return { type: 'card', id: c.account_number, label: label };
|
|
});
|
|
}
|
|
onDone();
|
|
},
|
|
error: function() { onDone(); }
|
|
});
|
|
},
|
|
|
|
_showDialogWithSources: function(accounts, cards) {
|
|
let sourcesHtml = '<div style="max-height: 300px; overflow-y: auto;">';
|
|
sourcesHtml += '<div style="margin-bottom: 10px;">';
|
|
sourcesHtml += '<label><input type="checkbox" class="kb-bt-select-all-sources"> <strong>' + __('Select All') + '</strong></label>';
|
|
sourcesHtml += '</div>';
|
|
|
|
if (accounts.length > 0) {
|
|
sourcesHtml += '<div style="margin-bottom: 8px; font-weight: 600; color: var(--text-color);">' + __('Accounts') + '</div>';
|
|
accounts.forEach(function(a) {
|
|
sourcesHtml += '<div style="margin-left: 20px; margin-bottom: 4px;">';
|
|
sourcesHtml += '<label><input type="checkbox" class="kb-bt-source-check" data-type="' + a.type + '" data-id="' + a.id + '"> ' + a.label + '</label>';
|
|
sourcesHtml += '</div>';
|
|
});
|
|
}
|
|
|
|
if (cards.length > 0) {
|
|
sourcesHtml += '<div style="margin-bottom: 8px; margin-top: 10px; font-weight: 600; color: var(--text-color);">' + __('Cards') + '</div>';
|
|
cards.forEach(function(c) {
|
|
sourcesHtml += '<div style="margin-left: 20px; margin-bottom: 4px;">';
|
|
sourcesHtml += '<label><input type="checkbox" class="kb-bt-source-check" data-type="' + c.type + '" data-id="' + c.id + '"> ' + c.label + '</label>';
|
|
sourcesHtml += '</div>';
|
|
});
|
|
}
|
|
|
|
sourcesHtml += '</div>';
|
|
|
|
const today = frappe.datetime.get_today();
|
|
const first_day = frappe.datetime.month_start(today);
|
|
|
|
const d = new frappe.ui.Dialog({
|
|
title: __('Import Bank Transactions'),
|
|
fields: [
|
|
{ fieldname: 'sec_period', fieldtype: 'Section Break', label: __('Period') },
|
|
{ fieldname: 'date_from', fieldtype: 'Date', label: __('Date From'), reqd: 1, default: first_day },
|
|
{ fieldname: 'col_break', fieldtype: 'Column Break' },
|
|
{ fieldname: 'date_to', fieldtype: 'Date', label: __('Date To'), reqd: 1, default: today },
|
|
{ fieldname: 'sec_sources', fieldtype: 'Section Break', label: __('Sources') },
|
|
{
|
|
fieldname: 'sources_html',
|
|
fieldtype: 'HTML',
|
|
options: sourcesHtml
|
|
}
|
|
],
|
|
primary_action_label: __('Search'),
|
|
primary_action: function() {
|
|
const values = d.get_values();
|
|
const selectedSources = [];
|
|
|
|
d.$wrapper.find('.kb-bt-source-check:checked').each(function() {
|
|
selectedSources.push({
|
|
type: $(this).data('type'),
|
|
id: $(this).data('id')
|
|
});
|
|
});
|
|
|
|
if (selectedSources.length === 0) {
|
|
frappe.msgprint({
|
|
title: __('Warning'),
|
|
indicator: 'orange',
|
|
message: __('Please select at least one account or card')
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Card statements are limited to 90 days by the API
|
|
const hasCards = selectedSources.some(s => s.type === 'card');
|
|
if (hasCards) {
|
|
const diffDays = frappe.datetime.get_diff(values.date_to, values.date_from);
|
|
if (diffDays > 90) {
|
|
frappe.msgprint({
|
|
title: __('Date Range Too Large'),
|
|
indicator: 'orange',
|
|
message: __('Card statements are limited to 90 days. Please shorten the date range or deselect cards.')
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
d.hide();
|
|
KBBTImport.import.loadTransactionsMulti(values.date_from, values.date_to, selectedSources);
|
|
}
|
|
});
|
|
d.show();
|
|
|
|
// Select All handler
|
|
d.$wrapper.find('.kb-bt-select-all-sources').on('change', function() {
|
|
const isChecked = $(this).prop('checked');
|
|
d.$wrapper.find('.kb-bt-source-check').prop('checked', isChecked);
|
|
});
|
|
},
|
|
|
|
loadTransactionsMulti: function(fromDate, toDate, sources) {
|
|
const totalSources = sources.length;
|
|
let currentSourceIdx = 0;
|
|
let allTransactions = [];
|
|
let totalSkipped = 0;
|
|
let seenRefNos = new Set();
|
|
let crossAccountDuplicates = 0;
|
|
let skippedCrossCurrency = 0;
|
|
|
|
function fetchNext() {
|
|
if (currentSourceIdx >= totalSources) {
|
|
frappe.hide_progress();
|
|
|
|
if (allTransactions.length === 0) {
|
|
let msg = __('No new transactions found for the specified period.');
|
|
if (totalSkipped > 0) {
|
|
msg += ' ' + __('({0} already imported)', [totalSkipped]);
|
|
}
|
|
frappe.msgprint({
|
|
title: __('Information'),
|
|
indicator: 'blue',
|
|
message: msg
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (totalSkipped > 0 || crossAccountDuplicates > 0 || skippedCrossCurrency > 0) {
|
|
let parts = [];
|
|
if (totalSkipped > 0) parts.push(__('already imported: {0}', [totalSkipped]));
|
|
if (crossAccountDuplicates > 0) parts.push(__('cross-account duplicates: {0}', [crossAccountDuplicates]));
|
|
if (skippedCrossCurrency > 0) parts.push(__('other currency account: {0}', [skippedCrossCurrency]));
|
|
frappe.show_alert({
|
|
message: __('Skipped: {0}', [parts.join(', ')]),
|
|
indicator: 'blue'
|
|
}, 5);
|
|
}
|
|
|
|
KBBTImport.import.showTransactionSelection(allTransactions);
|
|
return;
|
|
}
|
|
|
|
const source = sources[currentSourceIdx];
|
|
const sourceLabel = source.type === 'account'
|
|
? source.id
|
|
: __('Card: {0}', [source.id]);
|
|
|
|
frappe.show_progress(
|
|
__('Fetching Statements'),
|
|
currentSourceIdx,
|
|
totalSources,
|
|
__('Fetching statement {0} of {1}: {2}', [currentSourceIdx + 1, totalSources, sourceLabel])
|
|
);
|
|
|
|
const method = source.type === 'account'
|
|
? 'kapital_bank.bank_api.get_statement_transactions'
|
|
: 'kapital_bank.bank_api.get_card_statement_transactions';
|
|
|
|
const args = source.type === 'account'
|
|
? { from_date: fromDate, to_date: toDate, account_iban: source.id }
|
|
: { from_date: fromDate, to_date: toDate, card_account_number: source.id };
|
|
|
|
frappe.call({
|
|
method: method,
|
|
args: args,
|
|
callback: function(r) {
|
|
if (r.message && r.message.success) {
|
|
const txns = (r.message.transactions || []).map(function(txn) {
|
|
txn.source_id = source.id;
|
|
return txn;
|
|
});
|
|
// Deduplicate across multiple statements (cross-account transfers)
|
|
txns.forEach(function(txn) {
|
|
if (txn.ref_no && seenRefNos.has(txn.ref_no)) {
|
|
crossAccountDuplicates++;
|
|
} else {
|
|
if (txn.ref_no) seenRefNos.add(txn.ref_no);
|
|
allTransactions.push(txn);
|
|
}
|
|
});
|
|
totalSkipped += (r.message.skipped_duplicates || 0);
|
|
skippedCrossCurrency += (r.message.skipped_cross_currency || 0);
|
|
} else {
|
|
frappe.show_alert({
|
|
message: __('Error fetching {0}: {1}', [sourceLabel, r.message ? r.message.message : 'unknown']),
|
|
indicator: 'red'
|
|
}, 5);
|
|
}
|
|
currentSourceIdx++;
|
|
setTimeout(fetchNext, 100);
|
|
},
|
|
error: function() {
|
|
frappe.show_alert({
|
|
message: __('Network error fetching {0}', [sourceLabel]),
|
|
indicator: 'red'
|
|
}, 5);
|
|
currentSourceIdx++;
|
|
setTimeout(fetchNext, 100);
|
|
}
|
|
});
|
|
}
|
|
|
|
fetchNext();
|
|
},
|
|
|
|
showTransactionSelection: function(txns) {
|
|
const hasCardTxns = txns.some(t => t.source_type === 'card');
|
|
|
|
let txnTable = '';
|
|
if (hasCardTxns) {
|
|
txnTable += '<div style="margin-bottom: 10px; padding: 10px 12px; background: var(--red-50, #fff5f5); border: 1px solid var(--red-200, #fecaca); border-radius: var(--border-radius); font-size: var(--text-base); color: var(--red-600, #dc2626);">';
|
|
txnTable += '⚠️ ' + __('Card transactions may already be imported from other accounts. The bank uses different reference numbers for card and account statements, so duplicates cannot be detected automatically.');
|
|
txnTable += '</div>';
|
|
}
|
|
|
|
txnTable += '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered kb-bt-txn-table" style="width: 100%; table-layout: fixed;">';
|
|
txnTable += '<thead style="position: sticky; top: 0; z-index: 1; background: var(--bg-color, #fff);"><tr>' +
|
|
'<th style="width: 4%;"><input type="checkbox" class="kb-bt-select-all-txns"></th>' +
|
|
'<th style="width: 14%;">' + __('Ref No') + '</th>' +
|
|
'<th style="width: 10%;">' + __('Date') + '</th>' +
|
|
'<th style="width: 14%;">' + __('Account') + '</th>' +
|
|
'<th style="width: 20%;">' + __('Counterparty') + '</th>' +
|
|
'<th style="width: 12%; text-align: right;">' + __('Amount') + '</th>' +
|
|
'<th style="width: 6%; text-align: center;">' + __('Type') + '</th>' +
|
|
'<th style="width: 20%;">' + __('Purpose') + '</th>' +
|
|
'</tr></thead><tbody>';
|
|
|
|
txns.forEach(function(txn, idx) {
|
|
const displayDate = txn.date ? moment(txn.date).format('DD.MM.YYYY') : '';
|
|
const amountFmt = parseFloat(txn.amount || 0).toFixed(2);
|
|
const drcrLabel = txn.drcr === 'D'
|
|
? '<span class="label label-danger">' + __('Pay') + '</span>'
|
|
: '<span class="label label-success">' + __('Receive') + '</span>';
|
|
const sourceLabel = txn.source_label || '';
|
|
|
|
txnTable += '<tr>' +
|
|
'<td><input type="checkbox" class="kb-bt-select-txn" data-idx="' + idx + '"></td>' +
|
|
'<td style="word-break: break-word;">' + (txn.ref_no || '') + '</td>' +
|
|
'<td>' + displayDate + '</td>' +
|
|
'<td style="word-break: break-word; font-size: 0.85em;">' + sourceLabel + '</td>' +
|
|
'<td style="word-break: break-word;">' + (txn.counterparty || '') + '</td>' +
|
|
'<td style="text-align: right;">' + amountFmt + ' ' + (txn.currency || '') + '</td>' +
|
|
'<td style="text-align: center;">' + drcrLabel + '</td>' +
|
|
'<td style="word-break: break-word; font-size: 0.9em;">' + (txn.full_description || txn.purpose || '') + '</td>' +
|
|
'</tr>';
|
|
});
|
|
|
|
txnTable += '</tbody></table></div>';
|
|
|
|
const infoMessage = '<div class="alert alert-info">' +
|
|
__('Transactions found: ') + txns.length +
|
|
'</div>';
|
|
|
|
const d = new frappe.ui.Dialog({
|
|
title: __('Select Transactions to Import'),
|
|
size: 'large',
|
|
fields: [
|
|
{
|
|
fieldname: 'info_html',
|
|
fieldtype: 'HTML',
|
|
options: infoMessage
|
|
},
|
|
{
|
|
fieldname: 'txns_html',
|
|
fieldtype: 'HTML',
|
|
options: txnTable
|
|
}
|
|
],
|
|
primary_action_label: __('Load selected'),
|
|
primary_action: function() {
|
|
const selectedTxns = [];
|
|
d.$wrapper.find('.kb-bt-select-txn:checked').each(function() {
|
|
const idx = $(this).data('idx');
|
|
selectedTxns.push(txns[idx]);
|
|
});
|
|
|
|
if (selectedTxns.length === 0) {
|
|
frappe.msgprint({
|
|
title: __('Warning'),
|
|
indicator: 'orange',
|
|
message: __('No transactions selected')
|
|
});
|
|
return;
|
|
}
|
|
|
|
d.hide();
|
|
KBBTImport.import.loadSelectedTransactions(selectedTxns);
|
|
},
|
|
secondary_action_label: __('Cancel'),
|
|
secondary_action: function() {
|
|
d.hide();
|
|
}
|
|
});
|
|
|
|
d.$wrapper.find('.modal-dialog').css({
|
|
'max-width': '90%',
|
|
'width': '90%',
|
|
'margin': '30px auto'
|
|
});
|
|
|
|
d.$wrapper.find('.modal-body').css({
|
|
'padding': '15px'
|
|
});
|
|
|
|
d.show();
|
|
|
|
d.$wrapper.find('.kb-bt-select-all-txns').on('change', function() {
|
|
const isChecked = $(this).prop('checked');
|
|
d.$wrapper.find('.kb-bt-select-txn').prop('checked', isChecked);
|
|
});
|
|
},
|
|
|
|
loadSelectedTransactions: function(selectedTxns) {
|
|
KBBTImport.loadingErrors = [];
|
|
const total = selectedTxns.length;
|
|
|
|
frappe.show_progress(
|
|
__('Importing Transactions'), 0, total,
|
|
__('Starting import of {0} transactions...', [total])
|
|
);
|
|
|
|
// Listen for realtime progress from background job
|
|
frappe.realtime.off('kb_bt_import_progress');
|
|
frappe.realtime.on('kb_bt_import_progress', function(data) {
|
|
frappe.show_progress(
|
|
__('Importing Transactions'), data.current, data.total,
|
|
__('Importing transaction {0} of {1}', [data.current, data.total])
|
|
);
|
|
});
|
|
|
|
// Listen for completion from background job
|
|
frappe.realtime.off('kb_bt_import_complete');
|
|
frappe.realtime.on('kb_bt_import_complete', function(data) {
|
|
frappe.realtime.off('kb_bt_import_progress');
|
|
frappe.realtime.off('kb_bt_import_complete');
|
|
KBBTImport.import._onBulkComplete(data);
|
|
});
|
|
|
|
frappe.call({
|
|
method: 'kapital_bank.bank_api.import_bulk_bank_transactions',
|
|
args: { txn_list: JSON.stringify(selectedTxns) },
|
|
callback: function(r) {
|
|
// Job enqueued — progress & completion come via realtime events
|
|
if (!r.message || !r.message.enqueued) {
|
|
frappe.realtime.off('kb_bt_import_progress');
|
|
frappe.realtime.off('kb_bt_import_complete');
|
|
KBBTImport.import._closeProgress();
|
|
frappe.msgprint({
|
|
title: __('Error'),
|
|
indicator: 'red',
|
|
message: __('Failed to start import job')
|
|
});
|
|
}
|
|
},
|
|
error: function() {
|
|
frappe.realtime.off('kb_bt_import_progress');
|
|
frappe.realtime.off('kb_bt_import_complete');
|
|
KBBTImport.import._closeProgress();
|
|
frappe.msgprint({
|
|
title: __('Error'),
|
|
indicator: 'red',
|
|
message: __('Network error starting import')
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
_onBulkComplete: function(data) {
|
|
KBBTImport.import._closeProgress();
|
|
|
|
// Collect errors for the error panel
|
|
(data.errors || []).forEach(function(err) {
|
|
const txnData = err.txn_data || {};
|
|
const errorType = err.error_type === 'unmapped_bank_account' ? 'Unmapped Bank Account'
|
|
: 'Import Error';
|
|
KBBTImport.errors.add(txnData, errorType, err.message);
|
|
});
|
|
|
|
KBBTImport.errors.showSummary(data.total, data.imported, data.errors ? data.errors.length : 0);
|
|
|
|
if (cur_list) {
|
|
cur_list.refresh();
|
|
}
|
|
},
|
|
|
|
_closeProgress: function() {
|
|
if (frappe.cur_progress) {
|
|
try {
|
|
frappe.cur_progress.$wrapper.modal('hide');
|
|
frappe.cur_progress.$wrapper.remove();
|
|
} catch(e) {}
|
|
frappe.cur_progress = null;
|
|
}
|
|
if ($('.modal:visible').length === 0) {
|
|
$('.modal-backdrop').remove();
|
|
$('body').removeClass('modal-open');
|
|
}
|
|
}
|
|
};
|
|
|
|
// ───────────────────────────────────────────────────────────────────────────────
|
|
// Register Kapital Bank as a bank-import source in jey_erp's shared "Import
|
|
// From..." menu (Bank Transaction list, Payment Entry list, Bank Reconciliation
|
|
// Tool). Loaded globally via app_include_js, so KBBTImport is available wherever
|
|
// the menu is shown — including the BRT, where the per-doctype script is not.
|
|
// ───────────────────────────────────────────────────────────────────────────────
|
|
|
|
window.KBBTImport = KBBTImport;
|
|
|
|
frappe.provide('jey_erp');
|
|
jey_erp.bank_sources = jey_erp.bank_sources || [];
|
|
if (!jey_erp.bank_sources.some((s) => s.key === 'kapital')) {
|
|
jey_erp.bank_sources.push({
|
|
key: 'kapital',
|
|
label: __('Kapital Bank'),
|
|
importLabel: __('Kapital Bank (API)'),
|
|
settingsLabel: __('Kapital Bank Settings'),
|
|
runImport(ctx) {
|
|
ctx = ctx || {};
|
|
// Let the caller (e.g. the Bank Reconciliation Tool) react when the
|
|
// API import finishes — KBBTImport runs its own progress/list refresh,
|
|
// but the BRT needs to reload its reconciliation table afterwards.
|
|
if (typeof ctx.onComplete === 'function') {
|
|
const handler = (d) => {
|
|
frappe.realtime.off('kb_bt_import_complete', handler);
|
|
ctx.onComplete({ imported: (d && d.imported) || 0, errors: (d && d.errors) || [], transactions: [] });
|
|
};
|
|
frappe.realtime.on('kb_bt_import_complete', handler);
|
|
}
|
|
KBBTImport.import.showDialog();
|
|
},
|
|
goToSettings() { frappe.set_route('Form', 'Kapital Bank Settings'); },
|
|
});
|
|
}
|