Bank reconciliation wip

This commit is contained in:
Charles-Henri Decultot
2018-11-29 09:09:30 +00:00
parent 6025e498f2
commit 590d8d3d3e
8 changed files with 398 additions and 87 deletions

View File

@@ -495,6 +495,134 @@
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_entries",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payment Entries",
"length": 0,
"no_copy": 0,
"options": "Bank Transaction Payments",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_17",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allocated Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "unallocated_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Unallocated Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
@@ -526,39 +654,6 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_entry",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payment Entry",
"length": 0,
"no_copy": 0,
"options": "Payment Entry",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
@@ -571,7 +666,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-11-27 13:26:53.794350",
"modified": "2018-11-28 11:05:05.087606",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@@ -640,7 +735,7 @@
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_field": "date",
"sort_order": "DESC",
"title_field": "bank_account",
"track_changes": 0,

View File

@@ -4,7 +4,25 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt
class BankTransaction(Document):
pass
def after_insert(self):
self.unallocated_amount = abs(flt(self.credit) - flt(self.debit))
def on_update_after_submit(self):
linked_payments = [x.payment_entry for x in self.payment_entries]
if linked_payments:
allocated_amount = frappe.get_all("Payment Entry", filters=[["name", "in", linked_payments]], fields=["sum(paid_amount) as total"])
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount[0].total))
frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.credit) - flt(self.debit)) - flt(allocated_amount[0].total))
else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.credit) - flt(self.debit)))
self.reload()

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.listview_settings['Bank Transaction'] = {
add_fields: ["unallocated_amount"],
get_indicator: function(doc) {
if(flt(doc.unallocated_amount)>0) {
return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]
} else if(flt(doc.unallocated_amount)==0) {
return [__("Reconciled"), "green", "unallocated_amount,=,0"];
}
}
};

View File

@@ -0,0 +1,109 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-11-28 08:55:40.815355",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_document",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Document",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "payment_entry",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Payment Entry",
"length": 0,
"no_copy": 0,
"options": "payment_document",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-11-28 12:34:41.685571",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction Payments",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class BankTransactionPayments(Document):
pass

View File

@@ -43,7 +43,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
add_plaid_btn() {
const me = this;
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
if (r.enabled == "1") {
if (r && r.enabled == "1") {
me.parent.page.add_inner_button(__('Link a new bank account'), function() {
new erpnext.accounts.plaidLink(this)
})
@@ -116,6 +116,7 @@ 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();
@@ -132,10 +133,19 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
}
create_datatable() {
this.datatable = new DataTable('.transactions-table', {
columns: this.data.columns,
data: this.data.data
})
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.
<br>It should be a standard CSV or XLSX file.
<br>The headers should be in the first row.`)
frappe.throw(msg)
}
}
add_primary_action() {
@@ -333,7 +343,7 @@ erpnext.accounts.ReconciliationTool = class ReconciliationTool extends frappe.vi
return Object.assign({}, args, {
...args.filters.push(["Bank Transaction", "docstatus", "=", 1],
["Bank Transaction", "payment_entry", "=", ""])
["Bank Transaction", "unallocated_amount", ">", 0])
});
}
@@ -366,6 +376,11 @@ erpnext.accounts.ReconciliationTool = class ReconciliationTool extends frappe.vi
me.$result.append(frappe.render_template("bank_transaction_header"));
}
}
static trigger_list_update() {
const reconciliation_list = erpnext.accounts.ReconciliationTool;
reconciliation_list && reconciliation_list.on_update();
}
}
erpnext.accounts.ReconciliationRow = class ReconciliationRow {
@@ -446,7 +461,15 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
fieldtype: 'Link',
fieldname: 'payment_entry',
options: 'Payment Entry',
label: 'Payment Entry'
label: 'Payment Entry',
get_query: () => {
return {
filters : [
["Payment Entry", "ifnull(clearance_date, '')", "=", ""],
["Payment Entry", "docstatus", "=", 1]
]
}
}
},
{
fieldtype: 'HTML',
@@ -473,18 +496,23 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
const payment_entry = $(e.target).attr('data-name');
frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.reconcile',
{bank_transaction: me.bank_entry, payment_entry: payment_entry})
.then((result) => console.log(result))
.then((result) => {
erpnext.accounts.ReconciliationTool.trigger_list_update();
me.dialog.hide();
})
})
$(me.dialog.body).on('blur', '.input-with-feedback', (e) => {
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));
})
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();
}

View File

@@ -7,25 +7,7 @@ import frappe
from frappe import _
import difflib
from operator import itemgetter
@frappe.whitelist()
def get_linked_payments(bank_transaction):
transaction = frappe.get_doc("Bank Transaction", bank_transaction)
amount_matching = check_matching_amount(transaction)
description_matching = check_matching_descriptions(transaction)
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)
else:
linked_payments = get_matching_transactions_payments(description_matching)
return linked_payments
from frappe.utils import flt
@frappe.whitelist()
def reconcile(bank_transaction, payment_entry):
@@ -41,13 +23,49 @@ def reconcile(bank_transaction, payment_entry):
if transaction.debit > 0 and payment_entry.payment_type == "Receive":
frappe.throw(_("The selected payment entry should be linked with a creditor bank transaction"))
frappe.db.set_value("Bank Transaction", bank_transaction, "payment_entry", payment_entry)
add_payment_to_transaction(transaction, payment_entry)
clear_payment_entry(transaction, payment_entry)
return 'reconciled'
def add_payment_to_transaction(transaction, payment_entry):
transaction.append("payment_entries", {"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},
fields=["sum(debit) as debit", "sum(credit) as credit"])
cleared_amount = (linked_bank_transactions[0].credit - linked_bank_transactions[0].debit)
if cleared_amount == payment_entry.total_allocated_amount:
frappe.db.set_value("Payment Entry", payment_entry, "clearance_date", transaction.date)
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.whitelist()
def get_linked_payments(bank_transaction):
transaction = frappe.get_doc("Bank Transaction", bank_transaction)
# Get all payment entries with a matching amount
amount_matching = check_matching_amount(transaction)
print(amount_matching)
# Get some data from payment entries linked to a corresponding bank transaction
description_matching = check_matching_descriptions(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)
else:
linked_payments = get_matching_transactions_payments(description_matching)
return linked_payments
"""
def check_matching_amount(transaction):
amount = transaction.credit if transaction.credit > 0 else transaction.debit
@@ -55,32 +73,52 @@ def check_matching_amount(transaction):
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], ["clearance_date", "=", ""]])
["docstatus", "=", "1"], ["payment_type", "=", payment_type], ["ifnull(clearance_date, '')", "=", ""]])
return payments
def check_matching_descriptions(transaction):
bank_transactions = frappe.get_all("Bank Transaction", fields=["name", "description", "payment_entry", "date"],
filters=[["docstatus", "=", "1"], ["payment_entry", "!=", ""]])
bank_transactions = frappe.db.sql("""
SELECT
bt.name, bt.description, bt.date, btp.payment_document, btp.payment_entry
FROM
`tabBank Transaction` as bt
LEFT JOIN
`tabBank Transaction Payments` as btp
ON
bt.name = btp.parent
WHERE
bt.allocated_amount > 0
AND
bt.docstatus = 1
""", as_dict=True)
result = []
selection = []
for bank_transaction in bank_transactions:
if bank_transaction.description:
seq=difflib.SequenceMatcher(lambda x: x == " ", transaction.description, bank_transaction.description)
if seq.ratio() > 0.5:
bank_transaction["ratio"] = seq.ratio()
result.append(bank_transaction)
selection.append(bank_transaction)
return result
document_types = set([x["payment_document"] for x in selection])
for document_type in document_types:
print(document_type)
return selection
def check_amount_vs_description(amount_matching, description_matching):
result = []
print(description_matching)
print(amount_matching)
for match in amount_matching:
result.append([match for x in description_matching if match["name"]==x["payment_entry"]])
return match
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):
@@ -100,10 +138,10 @@ def get_matching_transactions_payments(description_matching):
payment_by_ratio = {x["payment_entry"]: x["ratio"] for x in description_matching}
if payments:
payment_list = frappe.get_all("Payment Entry", fields=["name", "paid_amount", "payment_type", "reference_no", "reference_date",
reference_payment_list = 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=[["name", "in", payments]])
return sorted(payment_list, key=lambda x: payment_by_ratio[x["name"]])
return sorted(reference_payment_list, key=lambda x: payment_by_ratio[x["name"]])
else:
return []