Bank reconciliation WIP

This commit is contained in:
Charles-Henri Decultot
2018-11-29 19:55:34 +00:00
parent 590d8d3d3e
commit 31cb24f48d
5 changed files with 302 additions and 83 deletions

View File

@@ -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"]]
}
};
});
}
});

View File

@@ -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));
})
})
}
}
}

View File

@@ -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 []
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
}
)

View File

@@ -23,6 +23,8 @@
<span class="caret"></span>
</a>
<ul class="dropdown-menu reports-dropdown" style="max-height: 300px; overflow-y: auto; right: 0px; left: auto;">
<li><a class="new-reconciliation" data-name={{ name }}>{{ __("Reconcile") }}</a></li>
<li class="divider"></li>
<li><a class="new-payment" data-name={{ name }}>{{ __("New Payment") }}</a></li>
<li><a class="new-invoice" data-name={{ name }}>{{ __("New Invoice") }}</a></li>
</ul>

View File

@@ -4,16 +4,22 @@
<label class="control-label">{{ name }}</label>
</div>
<div class="col-xs-5 ellipsis hidden-xs">
<h4>{{ __("Date:") }}</h4><h6> {{ posting_date }}</h6>
<h4>{{ __("Reference Date:") }}</h4><h6>{{ reference_date }}</h6>
<h4>{{ __("Date") }}</h4><h6> {%= frappe.datetime.str_to_user(posting_date) %}</h6>
{% if (typeof reference_date !== "undefined") %}
<h4>{{ __("Reference Date") }}</h4><h6>{%= frappe.datetime.str_to_user(reference_date) %}</h6>
{% endif %}
</div>
<div class="col-xs-7 ellipsis list-subject">
<h4>{{ __("Amount:") }}</h4><h6>{{ format_currency(paid_amount, paid_to_account_currency) }}</h6>
<h4>{{ __("Party:") }}</h4><h6>{{ party }}</h6>
<h4>{{ __("Reference:") }}</h4><h6>{{ reference_no }}</h6>
<h4>{{ __("Amount") }}</h4><h6>{{ format_currency(paid_amount, currency) }}</h6>
{% if (typeof party !== "undefined") %}
<h4>{{ __("Party") }}</h4><h6>{{ party }}</h6>
{% endif %}
{% if (typeof reference_no !== "undefined") %}
<h4>{{ __("Reference") }}</h4><h6>{{ reference_no }}</h6>
{% endif %}
</div>
<div class="text-right margin-bottom">
<button class="btn btn-primary btn-xs reconciliation-btn" data-name={{ name }}>{{ __("Reconcile") }}</button>
<button class="btn btn-primary btn-xs reconciliation-btn" data-doctype={{ doctype }} data-name={{ name }}>{{ __("Reconcile") }}</button>
</div>
</div>
</div>