From 31cb24f48d6d57cd8ab6991622c174390f6c2c51 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 29 Nov 2018 19:55:34 +0000 Subject: [PATCH] Bank reconciliation WIP --- .../bank_transaction/bank_transaction.js | 10 +- .../bank_reconciliation.js | 137 ++++++++--- .../bank_reconciliation.py | 218 ++++++++++++++---- .../bank_transaction_row.html | 2 + .../linked_payment_row.html | 18 +- 5 files changed, 302 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js index 6960570be36..4d40751b1de 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js @@ -2,7 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Bank Transaction', { - refresh: function(frm) { - + onload: function(frm) { + frm.set_query('payment_document', 'payment_entries', function(doc, cdt, cdn) { + return { + "filters": { + "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice"]] + } + }; + }); } }); diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js index 0aaf48891b8..9059689a794 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js @@ -78,7 +78,6 @@ erpnext.accounts.bankReconciliation = class BankReconciliation { make_reconciliation_tool() { const me = this; - console.log(me) frappe.model.with_doctype("Bank Transaction", () => { new erpnext.accounts.ReconciliationTool({ parent: me.parent, @@ -116,7 +115,6 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { no_socketio: true, sample_url: "e.g. http://example.com/somefile.csv", callback: function(attachment, r) { - console.log(r) if (!r.exc && r.message) { me.data = r.message; me.setup_transactions_dom(); @@ -432,16 +430,23 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { 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} - ) - .then((result) => { + {bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")} + ).then((result) => { + console.log(result) me.make_dialog(result) }) } make_dialog(data) { const me = this; + me.selected_payment = null; + const fields = [ { fieldtype: 'Section Break', @@ -459,18 +464,59 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { }, { fieldtype: 'Link', - fieldname: 'payment_entry', - options: 'Payment Entry', - label: 'Payment Entry', + fieldname: 'payment_doctype', + options: 'DocType', + label: 'Payment DocType', get_query: () => { return { - filters : [ - ["Payment Entry", "ifnull(clearance_date, '')", "=", ""], - ["Payment Entry", "docstatus", "=", 1] - ] + filters : { + "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice"]] + } } } }, + { + 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 { + filters : [ + ["Payment Entry", "ifnull(clearance_date, '')", "=", ""], + ["Payment Entry", "docstatus", "=", 1] + ] + } + } else if (dt === "Journal Entry") { + return { + query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.journal_entry_query", + filters : { + "bank_account": this.data.bank_account + } + } + } else if (dt === "Sales Invoice") { + return { + query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.sales_invoices_query" + } + } + }, + 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' @@ -479,11 +525,12 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { me.dialog = new frappe.ui.Dialog({ title: __("Choose a corresponding payment"), - fields: fields + fields: fields, + size: "large" }); const proposals_wrapper = me.dialog.fields_dict.payment_proposals.$wrapper; - if (data.length > 0) { + if (data && data.length > 0) { data.map(value => { proposals_wrapper.append(frappe.render_template("linked_payment_row", value)) }) @@ -494,26 +541,62 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { $(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_entry: payment_entry}) + {bank_transaction: me.bank_entry, payment_doctype: payment_doctype, payment_entry: payment_entry}) .then((result) => { erpnext.accounts.ReconciliationTool.trigger_list_update(); me.dialog.hide(); }) }) - $(me.dialog.body).on('blur', '.input-with-feedback', (e) => { - if (e.target.value) { - e.preventDefault(); - me.dialog.fields_dict['payment_details'].$wrapper.empty(); - frappe.db.get_doc("Payment Entry", e.target.value) - .then(doc => { - const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper; - details_wrapper.append(frappe.render_template("linked_payment_row", doc)); - }) - } - - }); 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 = [] + if (dt === "Payment Entry") { + doc.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency; + displayed_docs.push(doc); + } else if (dt === "Journal Entry") { + doc.accounts.forEach(payment => { + if (payment.account === me.gl_account) { + 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.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; + displayed_docs.forEach(values => { + details_wrapper.append(frappe.render_template("linked_payment_row", values)); + }) + }) + } + + } } \ No newline at end of file diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py index caccc9911b4..9a05448eb49 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py @@ -8,76 +8,125 @@ from frappe import _ import difflib from operator import itemgetter from frappe.utils import flt +from six import iteritems @frappe.whitelist() -def reconcile(bank_transaction, payment_entry): +def reconcile(bank_transaction, payment_doctype, payment_entry): transaction = frappe.get_doc("Bank Transaction", bank_transaction) - payment_entry = frappe.get_doc("Payment Entry", payment_entry) + payment_entry = frappe.get_doc(payment_doctype, payment_entry) - if transaction.payment_entry: - frappe.throw(_("This bank transaction is already linked to a payment entry")) + if transaction.unallocated_amount == 0: + frappe.throw(_("This bank transaction is already fully reconciled")) + """ if transaction.credit > 0 and payment_entry.payment_type == "Pay": frappe.throw(_("The selected payment entry should be linked with a debitor bank transaction")) if transaction.debit > 0 and payment_entry.payment_type == "Receive": frappe.throw(_("The selected payment entry should be linked with a creditor bank transaction")) + """ - add_payment_to_transaction(transaction, payment_entry) - clear_payment_entry(transaction, payment_entry) + add_payment_to_transaction(transaction, payment_doctype, payment_entry) + #clear_payment_entry(transaction, payment_doctype, payment_entry) return 'reconciled' -def add_payment_to_transaction(transaction, payment_entry): - transaction.append("payment_entries", {"payment_entry": payment_entry.name}) +def add_payment_to_transaction(transaction, payment_doctype, payment_entry): + transaction.append("payment_entries", {"payment_document": payment_doctype, "payment_entry": payment_entry.name}) transaction.save() -def clear_payment_entry(transaction, payment_entry): - linked_bank_transactions = frappe.get_all("Bank Transaction", filters={"payment_entry": payment_entry, "docstatus": 1}, +def clear_payment_entry(transaction, payment_doctype, payment_entry): + pass + """ + linked_bank_transactions = frappe.get_all("Bank Transaction Payments", filters={"payment_entry": payment_entry, "docstatus": 1}, fields=["sum(debit) as debit", "sum(credit) as credit"]) cleared_amount = (flt(linked_bank_transactions[0].credit) - flt(linked_bank_transactions[0].debit)) if cleared_amount == payment_entry.paid_amount: - frappe.db.set_value("Payment Entry", payment_entry.name, "clearance_date", transaction.date) + frappe.db.set_value(payment_doctype, payment_entry.name, "clearance_date", transaction.date) + """ @frappe.whitelist() def get_linked_payments(bank_transaction): - transaction = frappe.get_doc("Bank Transaction", bank_transaction) + bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") # Get all payment entries with a matching amount - amount_matching = check_matching_amount(transaction) + amount_matching = check_matching_amount(bank_account, transaction) print(amount_matching) # Get some data from payment entries linked to a corresponding bank transaction - description_matching = check_matching_descriptions(transaction) + description_matching = get_matching_descriptions_data(bank_account, transaction) print(description_matching) - """ if amount_matching: - match = check_amount_vs_description(amount_matching, description_matching) - if match: - return match - else: - return merge_matching_lists(amount_matching, description_matching) + return check_amount_vs_description(amount_matching, description_matching) else: - linked_payments = get_matching_transactions_payments(description_matching) - return linked_payments - """ + print("else") + #linked_payments = get_matching_transactions_payments(description_matching) + #return linked_payments -def check_matching_amount(transaction): +def check_matching_amount(bank_account, transaction): + payments = [] amount = transaction.credit if transaction.credit > 0 else transaction.debit - payment_type = "Receive" if transaction.credit > 0 else "Pay" - payments = frappe.get_all("Payment Entry", fields=["name", "paid_amount", "payment_type", "reference_no", "reference_date", - "party", "party_type", "posting_date", "paid_to_account_currency"], filters=[["paid_amount", "like", "{0}%".format(amount)], - ["docstatus", "=", "1"], ["payment_type", "=", payment_type], ["ifnull(clearance_date, '')", "=", ""]]) + payment_type = "Receive" if transaction.credit > 0 else "Pay" + account_from_to = "paid_to" if transaction.credit > 0 else "paid_from" + currency_field = "paid_to_account_currency as currency" if transaction.credit > 0 else "paid_from_account_currency as currency" + payment_entries = frappe.get_all("Payment Entry", fields=["'Payment Entry' as doctype", "name", "paid_amount", "payment_type", "reference_no", "reference_date", + "party", "party_type", "posting_date", "{0}".format(currency_field)], filters=[["paid_amount", "like", "{0}%".format(amount)], + ["docstatus", "=", "1"], ["payment_type", "=", payment_type], ["ifnull(clearance_date, '')", "=", ""], ["{0}".format(account_from_to), "=", "{0}".format(bank_account)]]) + + payment_field = "jea.debit_in_account_currency" if transaction.credit > 0 else "jea.credit_in_account_currency" + journal_entries = frappe.db.sql(""" + SELECT + 'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no, + je.pay_to_recd_from as party, je.cheque_date as reference_date, %s as paid_amount + FROM + `tabJournal Entry Account` as jea + JOIN + `tabJournal Entry` as je + ON + jea.parent = je.name + WHERE + (je.clearance_date is null or je.clearance_date='0000-00-00') + AND + jea.account = '%s' + AND + %s like '%s' + AND + je.docstatus = 1 + """ % (payment_field, bank_account, payment_field, amount), as_dict=True) + + sales_invoices = frappe.db.sql(""" + SELECT + 'Sales Invoice' as doctype, si.name, si.customer as party, + si.posting_date, sip.amount as paid_amount + FROM + `tabSales Invoice Payment` as sip + JOIN + `tabSales Invoice` as si + ON + sip.parent = si.name + WHERE + (sip.clearance_date is null or sip.clearance_date='0000-00-00') + AND + sip.account = '%s' + AND + sip.amount like '%s' + AND + si.docstatus = 1 + """ % (bank_account, amount), as_dict=True) + + for data in [payment_entries, journal_entries, sales_invoices]: + if data: + payments.extend(data) return payments -def check_matching_descriptions(transaction): +def get_matching_descriptions_data(bank_account, transaction): bank_transactions = frappe.db.sql(""" SELECT bt.name, bt.description, bt.date, btp.payment_document, btp.payment_entry @@ -104,33 +153,42 @@ def check_matching_descriptions(transaction): document_types = set([x["payment_document"] for x in selection]) + links = {} for document_type in document_types: - print(document_type) + links[document_type] = [x["payment_entry"] for x in selection if x["payment_document"]==document_type] - return selection + data = [] + for key, value in iteritems(links): + if key == "Payment Entry": + data.extend(frappe.get_all("Payment Entry", filters=[["name", "in", value]], fields=["'Payment Entry' as doctype", "posting_date", "party", "reference_no", "reference_date", "paid_amount"])) + if key == "Journal Entry": + data.extend(frappe.get_all("Journal Entry", filters=[["name", "in", value]], fields=["'Journal Entry' as doctype", "posting_date", "paid_to_recd_from as party", "cheque_no as reference_no", "cheque_date as reference_date"])) + if key == "Sales Invoice": + data.extend(frappe.get_all("Sales Invoice", filters=[["name", "in", value]], fields=["'Sales Invoice' as doctype", "posting_date", "customer as party"])) + #if key == "Purchase Invoice": + # data.append(frappe.get_all("Purchase Invoice", filters=[["name", "in", value]], fields=["posting_date", "customer as party"])) + + return data def check_amount_vs_description(amount_matching, description_matching): result = [] - print(description_matching) - print(amount_matching) - for match in amount_matching: - m = [match for x in description_matching.payment_entries if match["name"]==x["payment_entry"]] - result.append(m) - print(result) - return result -def merge_matching_lists(amount_matching, description_matching): - - for match in amount_matching: - if match["name"] in map(itemgetter('payment_entry'), description_matching): - index = map(itemgetter('payment_entry'), description_matching).index(match["name"]) - del description_matching[index] + if description_matching: + for am_match in amount_matching: + for des_match in description_matching: + if am_match["party"] == des_match["party"]: + result.append(am_match) + continue - linked_payments = get_matching_transactions_payments(description_matching) + if hasattr(am_match, "reference_no") and hasattr(des_match, "reference_no"): + if difflib.SequenceMatcher(lambda x: x == " ", am_match["reference_no"], des_match["reference_no"]) > 70: + result.append(am_match) - result = amount_matching.append(linked_payments) - return sorted(result, key = lambda x: x["posting_date"], reverse=True) + return sorted(result, key = lambda x: x["posting_date"], reverse=True) + + else: + return sorted(amount_matching, key = lambda x: x["posting_date"], reverse=True) def get_matching_transactions_payments(description_matching): payments = [x["payment_entry"] for x in description_matching] @@ -144,4 +202,68 @@ def get_matching_transactions_payments(description_matching): return sorted(reference_payment_list, key=lambda x: payment_by_ratio[x["name"]]) else: - return [] \ No newline at end of file + return [] + +def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): + account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") + + return frappe.db.sql(""" + SELECT + jea.parent, je.pay_to_recd_from, + if(jea.debit_in_account_currency > 0, jea.debit_in_account_currency, jea.credit_in_account_currency) + FROM + `tabJournal Entry Account` as jea + LEFT JOIN + `tabJournal Entry` as je + ON + jea.parent = je.name + WHERE + (je.clearance_date is null or je.clearance_date='0000-00-00') + AND + jea.account = %(account)s + AND + jea.parent like %(txt)s + AND + je.docstatus = 1 + ORDER BY + if(locate(%(_txt)s, jea.parent), locate(%(_txt)s, jea.parent), 99999), + jea.parent + LIMIT + %(start)s, %(page_len)s""".format(**{ + 'key': searchfield, + }), { + 'txt': "%%%s%%" % txt, + '_txt': txt.replace("%", ""), + 'start': start, + 'page_len': page_len, + 'account': account + } + ) + +def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters): + return frappe.db.sql(""" + SELECT + sip.parent, si.customer, sip.amount, sip.mode_of_payment + FROM + `tabSales Invoice Payment` as sip + LEFT JOIN + `tabSales Invoice` as si + ON + sip.parent = si.name + WHERE + (sip.clearance_date is null or sip.clearance_date='0000-00-00') + AND + sip.parent like %(txt)s + ORDER BY + if(locate(%(_txt)s, sip.parent), locate(%(_txt)s, sip.parent), 99999), + sip.parent + LIMIT + %(start)s, %(page_len)s""".format(**{ + 'key': searchfield, + }), { + 'txt': "%%%s%%" % txt, + '_txt': txt.replace("%", ""), + 'start': start, + 'page_len': page_len + } + ) \ No newline at end of file diff --git a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html b/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html index ab83ebec311..7f63168f80f 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html +++ b/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html @@ -23,6 +23,8 @@ diff --git a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html b/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html index deeca942412..eb9648bee9b 100644 --- a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html +++ b/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html @@ -4,16 +4,22 @@
-

{{ __("Amount:") }}

{{ format_currency(paid_amount, paid_to_account_currency) }}
-

{{ __("Party:") }}

{{ party }}
-

{{ __("Reference:") }}

{{ reference_no }}
+

{{ __("Amount") }}

{{ format_currency(paid_amount, currency) }}
+ {% if (typeof party !== "undefined") %} +

{{ __("Party") }}

{{ party }}
+ {% endif %} + {% if (typeof reference_no !== "undefined") %} +

{{ __("Reference") }}

{{ reference_no }}
+ {% endif %}
- +