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 = {
|
||||
"Payment Entry": "client/payment_entry.js",
|
||||
"Bank Transaction": "client/bank_transaction.js",
|
||||
"Bank Reconciliation Tool": "client/bank_reconciliation_tool.js",
|
||||
}
|
||||
|
||||
doctype_list_js = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2026-02-26 00:00:00.000000",
|
||||
"creation": "2026-02-26 00:00:00",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Counterparty Type",
|
||||
"options": "\nKapital Bank Customer\nKapital Bank Supplier"
|
||||
"options": "\nCustomer\nSupplier"
|
||||
},
|
||||
{
|
||||
"fieldname": "counterparty",
|
||||
|
|
@ -83,17 +84,15 @@
|
|||
"label": "Notes"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 0,
|
||||
"allow_bulk_edit": 1,
|
||||
"editable_grid": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-05 00:00:00.000000",
|
||||
"modified": "2026-03-10 17:54:47.738883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Transaction Mapping",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"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]
|
||||
# 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