frappe.provide("erpnext.accounts"); frappe.pages['bank-reconciliation'].on_page_load = function(wrapper) { new erpnext.accounts.bankReconciliation(wrapper); } erpnext.accounts.bankReconciliation = class BankReconciliation { constructor(wrapper) { this.page = frappe.ui.make_app_page({ parent: wrapper, title: __("Bank Reconciliation"), single_column: true }); this.parent = wrapper; this.page = this.parent.page; this.check_plaid_status(); this.make(); } make() { const me = this; me.$main_section = $(`
`).appendTo(me.page.main); const empty_state = __("Upload a bank statement, link or reconcile a bank account") me.$main_section.append(`
${empty_state}
`) me.page.add_field({ fieldtype: 'Link', label: __('Company'), fieldname: 'company', options: "Company", onchange: function() { if (this.value) { me.company = this.value; } else { me.company = null; me.bank_account = null; } } }) me.page.add_field({ fieldtype: 'Link', label: __('Bank Account'), fieldname: 'bank_account', options: "Bank Account", get_query: function() { if(!me.company) { frappe.throw(__("Please select company first")); return } return { filters: { "company": me.company } } }, onchange: function() { if (this.value) { me.bank_account = this.value; me.add_actions(); } else { me.bank_account = null; me.page.hide_actions_menu(); } } }) } check_plaid_status() { const me = this; frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { if (r && r.enabled === "1") { me.plaid_status = "active" } else { me.plaid_status = "inactive" } }) } add_actions() { const me = this; me.page.show_menu() me.page.add_menu_item(__("Upload a statement"), function() { me.clear_page_content(); new erpnext.accounts.bankTransactionUpload(me); }, true) if (me.plaid_status==="active") { me.page.add_menu_item(__("Synchronize this account"), function() { me.clear_page_content(); new erpnext.accounts.bankTransactionSync(me); }, true) } me.page.add_menu_item(__("Reconcile this account"), function() { me.clear_page_content(); me.make_reconciliation_tool(); }, true) } clear_page_content() { const me = this; $(me.page.body).find('.frappe-list').remove(); me.$main_section.empty(); } make_reconciliation_tool() { const me = this; frappe.model.with_doctype("Bank Transaction", () => { erpnext.accounts.ReconciliationList = new erpnext.accounts.ReconciliationTool({ parent: me.parent, doctype: "Bank Transaction" }); }) } } erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { constructor(parent) { this.parent = parent; this.data = []; const assets = [ "/assets/frappe/css/frappe-datatable.css", "/assets/frappe/js/lib/clusterize.min.js", "/assets/frappe/js/lib/Sortable.min.js", "/assets/frappe/js/lib/frappe-datatable.js" ]; frappe.require(assets, () => { this.make(); }); } make() { const me = this; new frappe.ui.FileUploader({ method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', allow_multiple: 0, on_success: function(attachment, r) { if (!r.exc && r.message) { me.data = r.message; me.setup_transactions_dom(); me.create_datatable(); me.add_primary_action(); } } }) } setup_transactions_dom() { const me = this; me.parent.$main_section.append(`
`) } create_datatable() { try { this.datatable = new DataTable('.transactions-table', { columns: this.data.columns, data: this.data.data }) } catch(err) { let msg = __(`Your file could not be processed by ERPNext.
It should be a standard CSV or XLSX file.
The headers should be in the first row.`) frappe.throw(msg) } } add_primary_action() { const me = this; me.parent.page.set_primary_action(__("Submit"), function() { me.add_bank_entries() }, null, __("Creating bank entries...")) } add_bank_entries() { const me = this; frappe.xcall('erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.create_bank_entries', {columns: this.datatable.datamanager.columns, data: this.datatable.datamanager.data, bank_account: me.parent.bank_account} ).then((result) => { let result_title = result.errors == 0 ? __("{0} bank transaction(s) created", [result.success]) : __("{0} bank transaction(s) created and {1} errors", [result.success, result.errors]) let result_msg = `
${result_title}
` me.parent.page.clear_primary_action(); me.parent.$main_section.empty(); me.parent.$main_section.append(result_msg); if (result.errors == 0) { frappe.show_alert({message:__("All bank transactions have been created"), indicator:'green'}); } else { frappe.show_alert({message:__("Please check the error log for details about the import errors"), indicator:'red'}); } }) } } erpnext.accounts.bankTransactionSync = class bankTransactionSync { constructor(parent) { this.parent = parent; this.data = []; this.init_config() } init_config() { const me = this; frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration') .then(result => { me.plaid_env = result.plaid_env; me.client_name = result.client_name; me.link_token = result.link_token; me.sync_transactions(); }) } sync_transactions() { const me = this; frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { bank: r.bank, bank_account: me.parent.bank_account, freeze: true }) .then((result) => { let result_title = (result && result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized"); let result_msg = `
${result_title}
` this.parent.$main_section.append(result_msg) frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' }); }) }) } } erpnext.accounts.ReconciliationTool = class ReconciliationTool extends frappe.views.BaseList { constructor(opts) { super(opts); this.show(); } setup_defaults() { super.setup_defaults(); this.page_title = __("Bank Reconciliation"); this.doctype = 'Bank Transaction'; this.fields = ['date', 'description', 'debit', 'credit', 'currency'] } setup_view() { this.render_header(); } setup_side_bar() { // } make_standard_filters() { // } freeze() { this.$result.find('.list-count').html(`${__('Refreshing')}...`); } get_args() { const args = super.get_args(); return Object.assign({}, args, { ...args.filters.push(["Bank Transaction", "docstatus", "=", 1], ["Bank Transaction", "unallocated_amount", ">", 0]) }); } update_data(r) { let data = r.message || []; if (this.start === 0) { this.data = data; } else { this.data = this.data.concat(data); } } render() { const me = this; this.$result.find('.list-row-container').remove(); $('[data-fieldname="name"]').remove(); me.data.map((value) => { const row = $('
').data("data", value).appendTo(me.$result).get(0); new erpnext.accounts.ReconciliationRow(row, value); }) } render_header() { const me = this; if ($(this.wrapper).find('.transaction-header').length === 0) { me.$result.append(frappe.render_template("bank_transaction_header")); } } } erpnext.accounts.ReconciliationRow = class ReconciliationRow { constructor(row, data) { this.data = data; this.row = row; this.make(); this.bind_events(); } make() { $(this.row).append(frappe.render_template("bank_transaction_row", this.data)) } bind_events() { const me = this; $(me.row).on('click', '.clickable-section', function() { me.bank_entry = $(this).attr("data-name"); me.show_dialog($(this).attr("data-name")); }) $(me.row).on('click', '.new-reconciliation', function() { me.bank_entry = $(this).attr("data-name"); me.show_dialog($(this).attr("data-name")); }) $(me.row).on('click', '.new-payment', function() { me.bank_entry = $(this).attr("data-name"); me.new_payment(); }) $(me.row).on('click', '.new-invoice', function() { me.bank_entry = $(this).attr("data-name"); me.new_invoice(); }) $(me.row).on('click', '.new-expense', function() { me.bank_entry = $(this).attr("data-name"); me.new_expense(); }) } new_payment() { const me = this; const paid_amount = me.data.credit > 0 ? me.data.credit : me.data.debit; const payment_type = me.data.credit > 0 ? "Receive": "Pay"; const party_type = me.data.credit > 0 ? "Customer": "Supplier"; frappe.new_doc("Payment Entry", {"payment_type": payment_type, "paid_amount": paid_amount, "party_type": party_type, "paid_from": me.data.bank_account}) } new_invoice() { const me = this; const invoice_type = me.data.credit > 0 ? "Sales Invoice" : "Purchase Invoice"; frappe.new_doc(invoice_type) } new_expense() { frappe.new_doc("Expense Claim") } show_dialog(data) { const me = this; frappe.db.get_value("Bank Account", me.data.bank_account, "account", (r) => { me.gl_account = r.account; }) frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") } ).then((result) => { me.make_dialog(result) }) } make_dialog(data) { const me = this; me.selected_payment = null; const fields = [ { fieldtype: 'Section Break', fieldname: 'section_break_1', label: __('Automatic Reconciliation') }, { fieldtype: 'HTML', fieldname: 'payment_proposals' }, { fieldtype: 'Section Break', fieldname: 'section_break_2', label: __('Search for a payment') }, { fieldtype: 'Link', fieldname: 'payment_doctype', options: 'DocType', label: 'Payment DocType', get_query: () => { return { filters : { "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]] } } }, }, { fieldtype: 'Column Break', fieldname: 'column_break_1', }, { fieldtype: 'Dynamic Link', fieldname: 'payment_entry', options: 'payment_doctype', label: 'Payment Document', get_query: () => { let dt = this.dialog.fields_dict.payment_doctype.value; if (dt === "Payment Entry") { return { query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.payment_entry_query", filters : { "bank_account": this.data.bank_account, "company": this.data.company } } } else if (dt === "Journal Entry") { return { query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.journal_entry_query", filters : { "bank_account": this.data.bank_account, "company": this.data.company } } } else if (dt === "Sales Invoice") { return { query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.sales_invoices_query" } } else if (dt === "Purchase Invoice") { return { filters : [ ["Purchase Invoice", "ifnull(clearance_date, '')", "=", ""], ["Purchase Invoice", "docstatus", "=", 1], ["Purchase Invoice", "company", "=", this.data.company] ] } } else if (dt === "Expense Claim") { return { filters : [ ["Expense Claim", "ifnull(clearance_date, '')", "=", ""], ["Expense Claim", "docstatus", "=", 1], ["Expense Claim", "company", "=", this.data.company] ] } } }, onchange: function() { if (me.selected_payment !== this.value) { me.selected_payment = this.value; me.display_payment_details(this); } } }, { fieldtype: 'Section Break', fieldname: 'section_break_3' }, { fieldtype: 'HTML', fieldname: 'payment_details' }, ]; me.dialog = new frappe.ui.Dialog({ title: __("Choose a corresponding payment"), fields: fields, size: "large" }); const proposals_wrapper = me.dialog.fields_dict.payment_proposals.$wrapper; if (data && data.length > 0) { proposals_wrapper.append(frappe.render_template("linked_payment_header")); data.map(value => { proposals_wrapper.append(frappe.render_template("linked_payment_row", value)) }) } else { const empty_data_msg = __("ERPNext could not find any matching payment entry") proposals_wrapper.append(`
${empty_data_msg}
`) } $(me.dialog.body).on('click', '.reconciliation-btn', (e) => { const payment_entry = $(e.target).attr('data-name'); const payment_doctype = $(e.target).attr('data-doctype'); frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.reconcile', {bank_transaction: me.bank_entry, payment_doctype: payment_doctype, payment_name: payment_entry}) .then((result) => { setTimeout(function(){ erpnext.accounts.ReconciliationList.refresh(); }, 2000); me.dialog.hide(); }) }) me.dialog.show(); } display_payment_details(event) { const me = this; if (event.value) { let dt = me.dialog.fields_dict.payment_doctype.value; me.dialog.fields_dict['payment_details'].$wrapper.empty(); frappe.db.get_doc(dt, event.value) .then(doc => { let displayed_docs = [] let payment = [] if (dt === "Payment Entry") { payment.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency; payment.doctype = dt payment.posting_date = doc.posting_date; payment.party = doc.party; payment.reference_no = doc.reference_no; payment.reference_date = doc.reference_date; payment.paid_amount = doc.paid_amount; payment.name = doc.name; displayed_docs.push(payment); } else if (dt === "Journal Entry") { doc.accounts.forEach(payment => { if (payment.account === me.gl_account) { payment.doctype = dt; payment.posting_date = doc.posting_date; payment.party = doc.pay_to_recd_from; payment.reference_no = doc.cheque_no; payment.reference_date = doc.cheque_date; payment.currency = payment.account_currency; payment.paid_amount = payment.credit > 0 ? payment.credit : payment.debit; payment.name = doc.name; displayed_docs.push(payment); } }) } else if (dt === "Sales Invoice") { doc.payments.forEach(payment => { if (payment.clearance_date === null || payment.clearance_date === "") { payment.doctype = dt; payment.posting_date = doc.posting_date; payment.party = doc.customer; payment.reference_no = doc.remarks; payment.currency = doc.currency; payment.paid_amount = payment.amount; payment.name = doc.name; displayed_docs.push(payment); } }) } const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper; details_wrapper.append(frappe.render_template("linked_payment_header")); displayed_docs.forEach(payment => { details_wrapper.append(frappe.render_template("linked_payment_row", payment)); }) }) } } }