reconsiliation tool modified
This commit is contained in:
parent
7be2112e2a
commit
bced451359
|
|
@ -0,0 +1,274 @@
|
||||||
|
// Kapital Bank — Bank Reconciliation Tool integration
|
||||||
|
// Adds checkbox column + "Create Purpose Mapping" toolbar
|
||||||
|
|
||||||
|
frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||||
|
render: function (frm) {
|
||||||
|
// DTM is created synchronously but its datatable is built after an async API call.
|
||||||
|
// Poll until datatable is ready, then enhance once.
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 = $(`
|
||||||
|
<div id="kb-brt-mapping-toolbar"
|
||||||
|
style="display:none; align-items:center; gap:10px; margin-bottom:8px;
|
||||||
|
padding:8px 12px; background:var(--fg-color);
|
||||||
|
border:1px solid var(--border-color); border-radius:6px;">
|
||||||
|
<span class="kb-brt-selected-count text-muted"></span>
|
||||||
|
<button class="btn btn-primary btn-sm kb-brt-create-mapping-btn">
|
||||||
|
${__("Create Purpose Mapping")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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, ...]
|
||||||
|
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 = $("<div>").html(row[2] || "").text().trim();
|
||||||
|
const withdrawal = parseFloat(row[5]) || 0;
|
||||||
|
txns.push({
|
||||||
|
purpose: description,
|
||||||
|
drcr: withdrawal > 0 ? "D" : "C",
|
||||||
|
party_type: partyType,
|
||||||
|
party: partyName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deduplicate for preview
|
||||||
|
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 = '<div style="max-height:260px; overflow-y:auto; margin-bottom:12px;">' +
|
||||||
|
'<table class="table table-bordered" style="width:100%;">' +
|
||||||
|
"<thead><tr>" +
|
||||||
|
"<th>" + __("Description") + "</th>" +
|
||||||
|
'<th style="width:90px;">' + __("Type") + "</th>" +
|
||||||
|
'<th style="width:140px;">' + __("Party") + "</th>" +
|
||||||
|
'<th style="width:60px; text-align:center;">' + __("Rows") + "</th>" +
|
||||||
|
"</tr></thead><tbody>";
|
||||||
|
purposeMap.forEach(function (e) {
|
||||||
|
const badge = e.payment_type === "Pay"
|
||||||
|
? '<span class="label label-danger">Pay</span>'
|
||||||
|
: '<span class="label label-success">Receive</span>';
|
||||||
|
previewHtml += "<tr>" +
|
||||||
|
'<td style="word-break:break-word;font-size:0.9em;">' + frappe.utils.escape_html(e.purpose) + "</td>" +
|
||||||
|
"<td>" + badge + "</td>" +
|
||||||
|
"<td>" + (e.party ? frappe.utils.escape_html(e.party) : '<span class="text-muted">—</span>') + "</td>" +
|
||||||
|
'<td style="text-align:center;">' + e.count + "</td></tr>";
|
||||||
|
});
|
||||||
|
previewHtml += "</tbody></table></div>";
|
||||||
|
if (skippedCount > 0) {
|
||||||
|
previewHtml += '<div class="alert alert-warning">' +
|
||||||
|
__("{0} row(s) skipped — no description text.", [skippedCount]) + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __("Create Purpose Mappings") + " (" + purposeMap.size + ")",
|
||||||
|
size: "large",
|
||||||
|
fields: [
|
||||||
|
{ 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",
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "use_custom_purpose",
|
||||||
|
fieldtype: "Check",
|
||||||
|
label: __("Use custom purpose keyword"),
|
||||||
|
onchange: function () {
|
||||||
|
const on = d.get_value("use_custom_purpose");
|
||||||
|
d.set_df_property("custom_purpose", "hidden", on ? 0 : 1);
|
||||||
|
d.set_df_property("custom_purpose", "reqd", on ? 1 : 0);
|
||||||
|
d.get_field("custom_purpose").refresh();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "custom_purpose",
|
||||||
|
fieldtype: "Data",
|
||||||
|
label: __("Custom Purpose Keyword"),
|
||||||
|
hidden: 1,
|
||||||
|
description: __("All selected rows will be mapped with this purpose 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)"), reqd: 1,
|
||||||
|
},
|
||||||
|
{ fieldname: "col_break", fieldtype: "Column Break" },
|
||||||
|
{
|
||||||
|
fieldname: "paid_to", fieldtype: "Link", options: "Account",
|
||||||
|
label: __("Paid To (Account)"), reqd: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action_label: __("Create Mappings"),
|
||||||
|
primary_action: function () {
|
||||||
|
const values = d.get_values();
|
||||||
|
if (!values) return;
|
||||||
|
|
||||||
|
let finalTxns = txns;
|
||||||
|
if (values.use_custom_purpose && (values.custom_purpose || "").trim()) {
|
||||||
|
const cp = values.custom_purpose.trim();
|
||||||
|
const seen = new Set();
|
||||||
|
finalTxns = [];
|
||||||
|
txns.forEach(function (t) {
|
||||||
|
const key = cp + "|" + t.drcr;
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
finalTxns.push(Object.assign({}, t, { purpose: cp }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
KBBRTMapping.submitMappings(d, finalTxns, values.paid_from, values.paid_to, values.document_type);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
d.show();
|
||||||
|
|
||||||
|
// 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_df_property("custom_purpose", "hidden", 0);
|
||||||
|
d.set_df_property("custom_purpose", "reqd", 1);
|
||||||
|
d.set_value("use_custom_purpose", 1);
|
||||||
|
d.set_value("custom_purpose", filterVal);
|
||||||
|
d.get_field("custom_purpose").refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submitMappings: function (dialog, txns, paid_from, paid_to, document_type) {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
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) parts.push(__("Created: {0}", [res.created]));
|
||||||
|
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]));
|
||||||
|
frappe.show_alert({
|
||||||
|
message: parts.join(" • ") || __("No changes"),
|
||||||
|
indicator: res.created > 0 ? "green" : "blue",
|
||||||
|
}, 5);
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
dialog.enable_primary_action();
|
||||||
|
frappe.msgprint({ title: __("Error"), indicator: "red",
|
||||||
|
message: __("Network error creating mappings") });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
const KBMapping = {};
|
||||||
|
|
||||||
|
KBMapping.init = function(dialog, txns) {
|
||||||
|
const $container = dialog.$wrapper.find('.kb-bt-txn-table').closest('div');
|
||||||
|
if ($container.length === 0) return;
|
||||||
|
|
||||||
|
// Search input + button row
|
||||||
|
const $toolbar = $(`
|
||||||
|
<div class="kb-mapping-toolbar" style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
|
||||||
|
<input type="text" class="form-control kb-mapping-search"
|
||||||
|
placeholder="${__('Search by purpose...')}"
|
||||||
|
style="flex: 1; max-width: 300px;">
|
||||||
|
<button class="btn btn-primary btn-sm kb-mapping-btn" style="display: none; white-space: nowrap;">
|
||||||
|
${__('Create Purpose Mapping')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
$container.prepend($toolbar);
|
||||||
|
|
||||||
|
const $searchInput = $toolbar.find('.kb-mapping-search');
|
||||||
|
const $mappingBtn = $toolbar.find('.kb-mapping-btn');
|
||||||
|
|
||||||
|
// Search: filter rows by purpose column (8th td, index 7)
|
||||||
|
$searchInput.on('input', function() {
|
||||||
|
const query = $(this).val().toLowerCase();
|
||||||
|
dialog.$wrapper.find('.kb-bt-txn-table tbody tr').each(function() {
|
||||||
|
const purposeText = $(this).find('td').eq(7).text().toLowerCase();
|
||||||
|
$(this).toggle(purposeText.indexOf(query) !== -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle mapping button visibility based on checked rows
|
||||||
|
function toggleMappingBtn() {
|
||||||
|
const anyChecked = dialog.$wrapper.find('.kb-bt-select-txn:checked').length > 0;
|
||||||
|
$mappingBtn.toggle(anyChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.$wrapper.on('change', '.kb-bt-select-txn, .kb-bt-select-all-txns', toggleMappingBtn);
|
||||||
|
|
||||||
|
// Button click -> show mapping dialog
|
||||||
|
$mappingBtn.on('click', function() {
|
||||||
|
KBMapping.showMappingDialog(dialog, txns);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
KBMapping.showMappingDialog = function(parentDialog, txns) {
|
||||||
|
// Collect checked transactions
|
||||||
|
const selectedTxns = [];
|
||||||
|
parentDialog.$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build unique purposes: key = "purpose|payment_type"
|
||||||
|
const purposeMap = new Map();
|
||||||
|
let skippedCount = 0;
|
||||||
|
|
||||||
|
selectedTxns.forEach(function(txn) {
|
||||||
|
const purpose = (txn.purpose || '').trim();
|
||||||
|
if (!purpose) {
|
||||||
|
skippedCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const paymentType = txn.drcr === 'D' ? 'Pay' : 'Receive';
|
||||||
|
const key = purpose + '|' + paymentType;
|
||||||
|
if (purposeMap.has(key)) {
|
||||||
|
purposeMap.get(key).count++;
|
||||||
|
} else {
|
||||||
|
purposeMap.set(key, { purpose: purpose, payment_type: paymentType, count: 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (purposeMap.size === 0 && skippedCount > 0) {
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __('No Purposes'),
|
||||||
|
indicator: 'orange',
|
||||||
|
message: __('All selected transactions have no purpose text.'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview table
|
||||||
|
let previewHtml = '<div style="max-height: 300px; overflow-y: auto; margin-bottom: 15px;">';
|
||||||
|
previewHtml += '<table class="table table-bordered" style="width: 100%;">';
|
||||||
|
previewHtml += '<thead><tr>' +
|
||||||
|
'<th>' + __('Purpose') + '</th>' +
|
||||||
|
'<th style="width: 100px;">' + __('Type') + '</th>' +
|
||||||
|
'<th style="width: 80px; text-align: center;">' + __('Txns') + '</th>' +
|
||||||
|
'</tr></thead><tbody>';
|
||||||
|
|
||||||
|
purposeMap.forEach(function(entry) {
|
||||||
|
const typeLabel = entry.payment_type === 'Pay'
|
||||||
|
? '<span class="label label-danger">Pay</span>'
|
||||||
|
: '<span class="label label-success">Receive</span>';
|
||||||
|
previewHtml += '<tr>' +
|
||||||
|
'<td style="word-break: break-word; font-size: 0.9em;">' + entry.purpose + '</td>' +
|
||||||
|
'<td>' + typeLabel + '</td>' +
|
||||||
|
'<td style="text-align: center;">' + entry.count + '</td>' +
|
||||||
|
'</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
previewHtml += '</tbody></table></div>';
|
||||||
|
|
||||||
|
if (skippedCount > 0) {
|
||||||
|
previewHtml += '<div class="alert alert-warning">' +
|
||||||
|
__('{0} transaction(s) skipped — no purpose text.', [skippedCount]) +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __('Create Purpose Mappings') + ' (' + purposeMap.size + ')',
|
||||||
|
size: 'large',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: 'preview_html',
|
||||||
|
fieldtype: 'HTML',
|
||||||
|
options: previewHtml,
|
||||||
|
},
|
||||||
|
{ fieldname: 'sec_accounts', fieldtype: 'Section Break', label: __('GL Accounts') },
|
||||||
|
{
|
||||||
|
fieldname: 'paid_from',
|
||||||
|
fieldtype: 'Link',
|
||||||
|
options: 'Account',
|
||||||
|
label: __('Paid From (Account)'),
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{ fieldname: 'col_break', fieldtype: 'Column Break' },
|
||||||
|
{
|
||||||
|
fieldname: 'paid_to',
|
||||||
|
fieldtype: 'Link',
|
||||||
|
options: 'Account',
|
||||||
|
label: __('Paid To (Account)'),
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action_label: __('Create Mappings'),
|
||||||
|
primary_action: function() {
|
||||||
|
const values = d.get_values();
|
||||||
|
if (!values) return;
|
||||||
|
KBMapping._submitMappings(d, selectedTxns, values.paid_from, values.paid_to);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
d.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
KBMapping._submitMappings = function(dialog, txns, paid_from, paid_to) {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
let parts = [];
|
||||||
|
if (res.created) parts.push(__('Created: {0}', [res.created]));
|
||||||
|
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]));
|
||||||
|
|
||||||
|
frappe.show_alert({
|
||||||
|
message: parts.join(' • ') || __('No changes'),
|
||||||
|
indicator: res.created > 0 ? 'green' : 'blue',
|
||||||
|
}, 5);
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
dialog.enable_primary_action();
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __('Error'),
|
||||||
|
indicator: 'red',
|
||||||
|
message: __('Network error creating mappings'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -13,6 +13,7 @@ after_migrate = "kapital_bank.setup.after_migrate"
|
||||||
doctype_js = {
|
doctype_js = {
|
||||||
"Payment Entry": "client/payment_entry.js",
|
"Payment Entry": "client/payment_entry.js",
|
||||||
"Bank Transaction": "client/bank_transaction.js",
|
"Bank Transaction": "client/bank_transaction.js",
|
||||||
|
"Bank Reconciliation Tool": "client/bank_reconciliation_tool.js",
|
||||||
}
|
}
|
||||||
|
|
||||||
doctype_list_js = {
|
doctype_list_js = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2026-02-26 00:00:00.000000",
|
"creation": "2026-02-26 00:00:00",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"document_type",
|
"document_type",
|
||||||
|
|
@ -43,7 +44,7 @@
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Counterparty Type",
|
"label": "Counterparty Type",
|
||||||
"options": "\nKapital Bank Customer\nKapital Bank Supplier"
|
"options": "\nCustomer\nSupplier"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "counterparty",
|
"fieldname": "counterparty",
|
||||||
|
|
@ -83,17 +84,15 @@
|
||||||
"label": "Notes"
|
"label": "Notes"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 0,
|
|
||||||
"allow_bulk_edit": 1,
|
|
||||||
"editable_grid": 1,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-05 00:00:00.000000",
|
"modified": "2026-03-10 17:54:47.738883",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Kapital Bank",
|
"module": "Kapital Bank",
|
||||||
"name": "Kapital Bank Transaction Mapping",
|
"name": "Kapital Bank Transaction Mapping",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_purpose_mappings(transactions, paid_from, paid_to, document_type="Payment Entry"):
|
||||||
|
"""Bulk-create transaction mappings from selected BRT transactions."""
|
||||||
|
try:
|
||||||
|
txn_list = json.loads(transactions) if isinstance(transactions, str) else transactions
|
||||||
|
|
||||||
|
settings = frappe.get_single("Kapital Bank Settings")
|
||||||
|
existing = {
|
||||||
|
(row.purpose_keyword, row.payment_type)
|
||||||
|
for row in settings.transaction_mappings
|
||||||
|
if row.purpose_keyword and row.payment_type
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deduplicate within batch; first occurrence wins for party info
|
||||||
|
unique_purposes = {}
|
||||||
|
skipped_no_data = 0
|
||||||
|
|
||||||
|
for txn in txn_list:
|
||||||
|
purpose = (txn.get("purpose") or "").strip()
|
||||||
|
if not purpose:
|
||||||
|
skipped_no_data += 1
|
||||||
|
continue
|
||||||
|
payment_type = "Pay" if txn.get("drcr") == "D" else "Receive"
|
||||||
|
key = (purpose, payment_type)
|
||||||
|
if key not in unique_purposes:
|
||||||
|
unique_purposes[key] = {
|
||||||
|
"count": 0,
|
||||||
|
# party_type is already "Customer"/"Supplier" from ERPNext BRT
|
||||||
|
"counterparty_type": (txn.get("party_type") or "").strip(),
|
||||||
|
"counterparty": (txn.get("party") or "").strip(),
|
||||||
|
}
|
||||||
|
unique_purposes[key]["count"] += 1
|
||||||
|
|
||||||
|
created = 0
|
||||||
|
already_mapped = 0
|
||||||
|
for (purpose_text, payment_type), data in unique_purposes.items():
|
||||||
|
purpose_name = _ensure_purpose(purpose_text)
|
||||||
|
|
||||||
|
if (purpose_name, payment_type) in existing:
|
||||||
|
already_mapped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
settings.append("transaction_mappings", {
|
||||||
|
"purpose_keyword": purpose_name,
|
||||||
|
"payment_type": payment_type,
|
||||||
|
"paid_from": paid_from,
|
||||||
|
"paid_to": paid_to,
|
||||||
|
"document_type": document_type,
|
||||||
|
"counterparty_type": data["counterparty_type"] or None,
|
||||||
|
"counterparty": data["counterparty"] or None,
|
||||||
|
})
|
||||||
|
existing.add((purpose_name, payment_type))
|
||||||
|
created += 1
|
||||||
|
|
||||||
|
if created > 0:
|
||||||
|
settings.save(ignore_permissions=True)
|
||||||
|
for (purpose_text, _payment_type) in unique_purposes:
|
||||||
|
purpose_name = frappe.db.get_value(
|
||||||
|
"Kapital Bank Purpose", {"purpose_keyword": purpose_text}, "name"
|
||||||
|
)
|
||||||
|
if purpose_name:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Kapital Bank Purpose", purpose_name, "status", "Mapped",
|
||||||
|
update_modified=False,
|
||||||
|
)
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"created": created,
|
||||||
|
"skipped_no_data": skipped_no_data,
|
||||||
|
"already_mapped": already_mapped,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(
|
||||||
|
f"create_purpose_mappings: {e}\n{frappe.get_traceback()}",
|
||||||
|
"Kapital Bank Mapping",
|
||||||
|
)
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_purpose(purpose_text):
|
||||||
|
"""Return the name of an existing or newly created Kapital Bank Purpose."""
|
||||||
|
existing = frappe.db.get_value(
|
||||||
|
"Kapital Bank Purpose", {"purpose_keyword": purpose_text}, "name"
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = frappe.new_doc("Kapital Bank Purpose")
|
||||||
|
doc.purpose_keyword = purpose_text
|
||||||
|
doc.direction = "Both"
|
||||||
|
doc.status = "New"
|
||||||
|
doc.insert(ignore_permissions=True)
|
||||||
|
return doc.name
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"Kapital Bank Purpose", {"purpose_keyword": purpose_text}, "name"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(
|
||||||
|
f"Insert purpose failed for {purpose_text}: {e}",
|
||||||
|
"Kapital Bank Purpose Loading",
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
@ -5,3 +5,4 @@ kapital_bank.patches.rename_purpose_mapping_to_transaction_mapping
|
||||||
|
|
||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
# Patches added in this section will be executed after doctypes are migrated
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
|
kapital_bank.patches.migrate_counterparty_type
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"""Rename counterparty_type values from 'Kapital Bank Customer/Supplier' to 'Customer/Supplier'.
|
||||||
|
The counterparty values (KB registry names) are no longer valid for the new Dynamic Link target,
|
||||||
|
so they are cleared."""
|
||||||
|
frappe.db.sql("""
|
||||||
|
UPDATE `tabKapital Bank Transaction Mapping`
|
||||||
|
SET counterparty_type = 'Customer', counterparty = NULL
|
||||||
|
WHERE counterparty_type = 'Kapital Bank Customer'
|
||||||
|
""")
|
||||||
|
frappe.db.sql("""
|
||||||
|
UPDATE `tabKapital Bank Transaction Mapping`
|
||||||
|
SET counterparty_type = 'Supplier', counterparty = NULL
|
||||||
|
WHERE counterparty_type = 'Kapital Bank Supplier'
|
||||||
|
""")
|
||||||
|
frappe.db.commit()
|
||||||
Loading…
Reference in New Issue