fixed merge conflict

This commit is contained in:
Nabin Hait
2019-03-28 11:35:39 +05:30
54 changed files with 4877 additions and 773 deletions

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Account Subtype', {
refresh: function() {
}
});

View File

@@ -0,0 +1,134 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:account_subtype",
"beta": 0,
"creation": "2018-10-25 15:46:08.054586",
"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": "account_subtype",
"fieldtype": "Data",
"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": "Account Subtype",
"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": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-10-25 15:47:03.841390",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Subtype",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}

View File

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

View File

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Account Subtype", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Account Subtype
() => frappe.tests.make('Account Subtype', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestAccountSubtype(unittest.TestCase):
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Account Type', {
refresh: function() {
}
});

View File

@@ -0,0 +1,134 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:account_type",
"beta": 0,
"creation": "2018-10-25 15:45:45.789963",
"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": "account_type",
"fieldtype": "Data",
"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": "Account Type",
"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": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-10-25 15:46:51.042604",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Type",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}

View File

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

View File

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Account Type", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Account Type
() => frappe.tests.make('Account Type', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestAccountType(unittest.TestCase):
pass

View File

@@ -2,7 +2,29 @@
// For license information, please see license.txt
frappe.ui.form.on('Bank', {
onload: function(frm) {
add_fields_to_mapping_table(frm);
},
refresh: function(frm) {
add_fields_to_mapping_table(frm);
}
});
let add_fields_to_mapping_table = function (frm) {
let options = [];
frappe.model.with_doctype("Bank Transaction", function() {
let meta = frappe.get_meta("Bank Transaction");
meta.fields.forEach(value => {
if (!["Section Break", "Column Break"].includes(value.fieldtype)) {
options.push(value.fieldname);
}
});
});
frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field",
frm.doc.name).options = options;
frm.fields_dict.bank_transaction_mapping.grid.refresh();
};

View File

@@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
@@ -15,6 +16,7 @@
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -42,6 +44,134 @@
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "data_import_configuration_section",
"fieldtype": "Section 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,
"label": "Data Import Configuration",
"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": "bank_transaction_mapping",
"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": "Bank Transaction Mapping",
"length": 0,
"no_copy": 0,
"options": "Bank Transaction Mapping",
"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": "section_break_4",
"fieldtype": "Section 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": "plaid_access_token",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Plaid Access Token",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
@@ -55,7 +185,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-07 17:00:21.246202",
"modified": "2018-11-27 16:12:13.938776",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank",
@@ -64,7 +194,6 @@
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
@@ -90,5 +219,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
"track_seen": 0,
"track_views": 0
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Bank Account', {
@@ -24,5 +24,13 @@ frappe.ui.form.on('Bank Account', {
else {
frappe.contacts.render_address_and_contact(frm);
}
if (frm.doc.integration_id) {
frm.add_custom_button(__("Unlink external integrations"), function() {
frappe.confirm(__("This action will unlink this account from any external service integrating ERPNext with your bank accounts. It cannot be undone. Are you certain ?"), function() {
frm.set_value("integration_id", "");
});
});
}
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@ class BankAccount(Document):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)
def autoname(self):
self.name = self.account_name + " - " + self.bank
def on_trash(self):
delete_contact_and_address('BankAccount', self.name)

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Bank Transaction', {
onload: function(frm) {
frm.set_query('payment_document', 'payment_entries', function() {
return {
"filters": {
"name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]]
}
};
});
}
});

View File

@@ -0,0 +1,744 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "naming_series:",
"beta": 0,
"creation": "2018-10-22 18:19:02.784533",
"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,
"default": "ACC-BTN-.YYYY.-",
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Series",
"length": 0,
"no_copy": 1,
"options": "ACC-BTN-.YYYY.-",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 1,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "date",
"fieldtype": "Date",
"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": "Date",
"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": "column_break_2",
"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,
"default": "Settled",
"fieldname": "status",
"fieldtype": "Select",
"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": "Status",
"length": 0,
"no_copy": 0,
"options": "\nPending\nSettled",
"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": "bank_account",
"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": "Bank Account",
"length": 0,
"no_copy": 0,
"options": "Bank Account",
"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": "section_break_4",
"fieldtype": "Section 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": "debit",
"fieldtype": "Currency",
"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": "Debit",
"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": "credit",
"fieldtype": "Currency",
"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": "Credit",
"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": "column_break_7",
"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": "currency",
"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": "Currency",
"length": 0,
"no_copy": 0,
"options": "Currency",
"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": "section_break_10",
"fieldtype": "Section 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": "description",
"fieldtype": "Small Text",
"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": "Description",
"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": "section_break_14",
"fieldtype": "Section 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": "reference_number",
"fieldtype": "Data",
"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": "Reference Number",
"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": "transaction_id",
"fieldtype": "Data",
"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": "Transaction ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"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,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"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": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Bank Transaction",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"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": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-11-28 11:05:05.087606",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "date",
"sort_order": "DESC",
"title_field": "bank_account",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}

View File

@@ -0,0 +1,25 @@
# -*- 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
from frappe.utils import flt
class BankTransaction(Document):
def after_insert(self):
self.unallocated_amount = abs(flt(self.credit) - flt(self.debit))
def on_update_after_submit(self):
allocated_amount = reduce(lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries])
if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.credit) - flt(self.debit)) - flt(allocated_amount))
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,75 @@
# -*- 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
import json
from frappe.utils import getdate
from frappe.utils.dateutils import parse_date
@frappe.whitelist()
def upload_bank_statement():
if getattr(frappe, "uploaded_file", None):
with open(frappe.uploaded_file, "rb") as upfile:
fcontent = upfile.read()
else:
from frappe.utils.file_manager import get_uploaded_content
fname, fcontent = get_uploaded_content()
if frappe.safe_encode(fname).lower().endswith("csv"):
from frappe.utils.csvutils import read_csv_content
rows = read_csv_content(fcontent, False)
elif frappe.safe_encode(fname).lower().endswith("xlsx"):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
columns = rows[0]
rows.pop(0)
data = rows
return {"columns": columns, "data": data}
@frappe.whitelist()
def create_bank_entries(columns, data, bank_account):
header_map = get_header_mapping(columns, bank_account)
count = 0
for d in json.loads(data):
if all(item is None for item in d) is True:
continue
fields = {}
for key, value in header_map.iteritems():
fields.update({key: d[int(value)-1]})
bank_transaction = frappe.get_doc({
"doctype": "Bank Transaction"
})
bank_transaction.update(fields)
bank_transaction.date = getdate(parse_date(bank_transaction.date))
bank_transaction.bank_account = bank_account
bank_transaction.insert()
bank_transaction.submit()
count = count + 1
return count
def get_header_mapping(columns, bank_account):
mapping = get_bank_mapping(bank_account)
header_map = {}
for column in json.loads(columns):
if column["content"] in mapping:
header_map.update({mapping[column["content"]]: column["colIndex"]})
return header_map
def get_bank_mapping(bank_account):
bank_name = frappe.db.get_value("Bank Account", bank_account, "bank")
bank = frappe.get_doc("Bank", bank_name)
mapping = {row.file_field:row.bank_transaction_field for row in bank.bank_transaction_mapping}
return mapping

View File

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Bank Transaction", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Bank Transaction
() => frappe.tests.make('Bank Transaction', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
def setUp(self):
add_transactions()
add_payments()
def tearDown(self):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel()
doc.delete()
# Delete directly in DB to avoid validation errors for countries not allowing deletion
frappe.db.sql("""delete from `tabPayment Entry Reference`""")
frappe.db.sql("""delete from `tabPayment Entry`""")
frappe.flags.test_bank_transactions_created = False
frappe.flags.test_payments_created = False
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
linked_payments = get_linked_payments(bank_transaction.name)
self.assertTrue(linked_payments[0].party == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
reconcile(bank_transaction.name, "Payment Entry", payment.name)
unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount")
self.assertTrue(unallocated_amount == 0)
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertTrue(clearance_date is not None)
# Check if ERPNext can correctly fetch a linked payment based on the party
def test_linked_payments_based_on_party(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
linked_payments = get_linked_payments(bank_transaction.name)
self.assertTrue(len(linked_payments)==1)
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
linked_payments = get_linked_payments(bank_transaction.name)
self.assertTrue(linked_payments[0].payment_type == "Pay")
# Check error if already reconciled
def test_already_reconciled(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
reconcile(bank_transaction.name, "Payment Entry", payment.name)
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
# Raise an error if creditor transaction vs creditor payment
def test_invalid_creditor_reconcilation(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
payment = frappe.get_doc("Payment Entry", dict(party="Conrad Electronic", paid_amount=690))
self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
# Raise an error if debitor transaction vs debitor payment
def test_invalid_debitor_reconcilation(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
payment = frappe.get_doc("Payment Entry", dict(party="Fayva", paid_amount=109080))
self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
# Raise an error if debitor transaction vs debitor payment
def test_clear_sales_invoice(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
reconcile(bank_transaction.name, "Sales Invoice", payment.name)
self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
try:
frappe.get_doc({
"doctype": "Bank",
"bank_name":"Citi Bank",
}).insert()
except frappe.DuplicateEntryError:
pass
try:
frappe.get_doc({
"doctype": "Bank Account",
"account_name":"Checking Account",
"bank": "Citi Bank",
"account": "_Test Bank - _TC"
}).insert()
except frappe.DuplicateEntryError:
pass
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": "2018-10-23",
"debit": 1200,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
doc.submit()
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
"date": "2018-10-23",
"debit": 1700,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
doc.submit()
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
"date": "2018-10-26",
"debit": 690,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
doc.submit()
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
"date": "2018-10-27",
"debit": 3900,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
doc.submit()
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
"date": "2018-10-27",
"credit": 109080,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
doc.submit()
frappe.flags.test_bank_transactions_created = True
def add_payments():
if frappe.flags.test_payments_created:
return
frappe.set_user("Administrator")
try:
frappe.get_doc({
"doctype": "Supplier",
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Conrad Electronic"
}).insert()
except frappe.DuplicateEntryError:
pass
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()
try:
frappe.get_doc({
"doctype": "Supplier",
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Mr G"
}).insert()
except frappe.DuplicateEntryError:
pass
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Herr G Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Herr G Nov 18"
pe.reference_date = "2018-11-01"
pe.insert()
pe.submit()
try:
frappe.get_doc({
"doctype": "Supplier",
"supplier_group":"All Supplier Groups",
"supplier_type": "Company",
"supplier_name": "Poore Simon's"
}).insert()
except frappe.DuplicateEntryError:
pass
try:
frappe.get_doc({
"doctype": "Customer",
"customer_group":"All Customer Groups",
"customer_type": "Company",
"customer_name": "Poore Simon's"
}).insert()
except frappe.DuplicateEntryError:
pass
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.insert()
pe.submit()
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.insert()
pe.submit()
try:
frappe.get_doc({
"doctype": "Customer",
"customer_group":"All Customer Groups",
"customer_type": "Company",
"customer_name": "Fayva"
}).insert()
except frappe.DuplicateEntryError:
pass
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Fayva Oct 18"
pe.reference_date = "2018-10-29"
pe.insert()
pe.submit()
company = frappe.db.get_single_value('Global Defaults', 'default_company')
frappe.get_doc({
"doctype": "Mode of Payment",
"name": "Cash"
}).append("accounts", {
"company": company,
"default_account": "_Test Bank - _TC"
}).save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_submit=1)
si.is_pos = 1
si.append("payments", {
"mode_of_payment": "Cash",
"account": "_Test Bank - _TC",
"amount": 109080
})
si.save()
si.submit()
frappe.flags.test_payments_created = True

View File

@@ -0,0 +1,107 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-10-24 15:24:56.713277",
"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": "bank_transaction_field",
"fieldtype": "Select",
"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": "Field in Bank Transaction",
"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": 1,
"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": "file_field",
"fieldtype": "Data",
"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": "Column in Bank File",
"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": 1,
"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-10-24 15:24:56.713277",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction Mapping",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}

View File

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

View File

@@ -0,0 +1,141 @@
{
"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": 1,
"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": 1,
"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": 1,
"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": 1,
"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-12-06 10:57:02.635141",
"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,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.model.document import Document
class BankTransactionPayments(Document):
pass

View File

@@ -3401,6 +3401,38 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "clearance_date",
"fieldtype": "Date",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Clearance Date",
"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,
@@ -4726,7 +4758,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-01-07 16:51:59.800081",
"modified": "2019-01-07 18:00:41.508642",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -0,0 +1,540 @@
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 = $(`<div class="reconciliation page-main-content"></div>`).appendTo(me.page.main);
const empty_state = __("Upload a bank statement, link or reconcile a bank account")
me.$main_section.append(`<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;"><h5 class="text-muted">${empty_state}</h5></div>`)
me.page.add_field({
fieldtype: 'Link',
label: __('Bank Account'),
fieldname: 'bank_account',
options: "Bank Account",
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;
frappe.upload.make({
args: {
method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
allow_multiple: 0
},
no_socketio: true,
sample_url: "e.g. http://example.com/somefile.csv",
callback: 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(`<div class="transactions-table"></div>`)
}
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.
<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() {
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 = __("{0} bank transaction(s) created", [result])
let result_msg = `
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5>
</div>`
me.parent.page.clear_primary_action();
me.parent.$main_section.empty();
me.parent.$main_section.append(result_msg);
frappe.show_alert({message:__("All bank transactions have been created"), indicator:'green'});
})
}
}
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.plaid_configuration')
.then(result => {
me.plaid_env = result.plaid_env;
me.plaid_public_key = result.plaid_public_key;
me.client_name = result.client_name;
me.sync_transactions()
})
}
sync_transactions() {
const me = this;
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
bank: v['bank'],
bank_account: me.parent.bank_account,
freeze: true
})
.then((result) => {
let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized")
let result_msg = `
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5>
</div>`
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(`<span>${__('Refreshing')}...</span>`);
}
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 = $('<div class="list-row-container">').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_invoice() {
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 {
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"
}
} else if (dt === "Purchase Invoice") {
return {
filters : [
["Purchase Invoice", "ifnull(clearance_date, '')", "=", ""],
["Purchase Invoice", "docstatus", "=", 1]
]
}
} else if (dt === "Expense Claim") {
return {
filters : [
["Expense Claim", "ifnull(clearance_date, '')", "=", ""],
["Expense Claim", "docstatus", "=", 1]
]
}
}
},
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(`<div class="text-center"><h5 class="text-muted">${empty_data_msg}</h5></div>`)
}
$(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 = []
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;
details_wrapper.append(frappe.render_template("linked_payment_header"));
displayed_docs.forEach(values => {
details_wrapper.append(frappe.render_template("linked_payment_row", values));
})
})
}
}
}

View File

@@ -0,0 +1,29 @@
{
"content": null,
"creation": "2018-11-24 12:03:14.646669",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2018-11-24 12:03:14.646669",
"modified_by": "Administrator",
"module": "Accounts",
"name": "bank-reconciliation",
"owner": "Administrator",
"page_name": "bank-reconciliation",
"roles": [
{
"role": "System Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Bank Reconciliation"
}

View File

@@ -0,0 +1,349 @@
# -*- 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 import _
import difflib
from frappe.utils import flt
from six import iteritems
from erpnext import get_company_currency
@frappe.whitelist()
def reconcile(bank_transaction, payment_doctype, payment_name):
transaction = frappe.get_doc("Bank Transaction", bank_transaction)
payment_entry = frappe.get_doc(payment_doctype, payment_name)
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name))
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
if transaction.credit > 0 and gl_entry.credit > 0:
frappe.throw(_("The selected payment entry should be linked with a debitor bank transaction"))
if transaction.debit > 0 and gl_entry.debit > 0:
frappe.throw(_("The selected payment entry should be linked with a creditor bank transaction"))
add_payment_to_transaction(transaction, payment_entry, gl_entry)
clear_payment_entry(transaction, payment_entry, gl_entry)
return 'reconciled'
def add_payment_to_transaction(transaction, payment_entry, gl_entry):
transaction.append("payment_entries", {
"payment_document": payment_entry.doctype,
"payment_entry": payment_entry.name,
"allocated_amount": gl_entry.credit if gl_entry.credit > 0 else gl_entry.debit
})
transaction.save()
def clear_payment_entry(transaction, payment_entry, gl_entry):
linked_bank_transactions = frappe.db.sql("""
SELECT
bt.credit, bt.debit
FROM
`tabBank Transaction Payments` as btp
LEFT JOIN
`tabBank Transaction` as bt on btp.parent=bt.name
WHERE
btp.payment_document = %s
AND
btp.payment_entry = %s
AND
bt.docstatus = 1
""", (payment_entry.doctype, payment_entry.name), as_dict=True)
amount_cleared = (flt(linked_bank_transactions[0].credit) - flt(linked_bank_transactions[0].debit))
amount_to_be_cleared = (flt(gl_entry.debit) - flt(gl_entry.credit))
if payment_entry.doctype == "Payment Entry":
clear_simple_entry(amount_cleared, amount_to_be_cleared, payment_entry, transaction)
elif payment_entry.doctype == "Journal Entry":
clear_simple_entry(amount_cleared, amount_to_be_cleared, payment_entry, transaction)
elif payment_entry.doctype == "Sales Invoice":
clear_sales_invoice(amount_cleared, amount_to_be_cleared, payment_entry, transaction)
elif payment_entry.doctype == "Purchase Invoice":
clear_simple_entry(amount_cleared, amount_to_be_cleared, payment_entry, transaction)
elif payment_entry.doctype == "Expense Claim":
clear_simple_entry(amount_cleared, amount_to_be_cleared, payment_entry, transaction)
def clear_simple_entry(amount_cleared, amount_to_be_cleared, payment_entry, transaction):
if amount_cleared == amount_to_be_cleared:
frappe.db.set_value(payment_entry.doctype, payment_entry.name, "clearance_date", transaction.date)
def clear_sales_invoice(amount_cleared, amount_to_be_cleared, payment_entry, transaction):
if amount_cleared == amount_to_be_cleared:
frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.doctype,
parent=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_values("Bank Account", transaction.bank_account, ["account", "company"], as_dict=True)
# Get all payment entries with a matching amount
amount_matching = check_matching_amount(bank_account[0].account, bank_account[0].company, transaction)
# Get some data from payment entries linked to a corresponding bank transaction
description_matching = get_matching_descriptions_data(bank_account[0].account, transaction)
if amount_matching:
return check_amount_vs_description(amount_matching, description_matching)
elif description_matching:
return sorted(description_matching, key = lambda x: x["posting_date"], reverse=True)
else:
return []
def check_matching_amount(bank_account, company, transaction):
payments = []
amount = transaction.credit if transaction.credit > 0 else transaction.debit
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)]])
if transaction.credit > 0:
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, jea.debit_in_account_currency 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
jea.debit_in_account_currency like %s
AND
je.docstatus = 1
""", (bank_account, amount), as_dict=True)
else:
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, jea.credit_in_account_currency 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
jea.credit_in_account_currency like %s
AND
je.docstatus = 1
""", (bank_account, amount), as_dict=True)
if transaction.credit > 0:
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)
else:
sales_invoices = []
if transaction.debit > 0:
purchase_invoices = frappe.get_all("Purchase Invoice", fields=["'Purchase Invoice' as doctype", "name", "paid_amount",
"supplier as party", "posting_date", "currency"], filters=[["paid_amount", "like", "{0}%".format(amount)],
["docstatus", "=", "1"], ["is_paid", "=", "1"], ["ifnull(clearance_date, '')", "=", ""], ["cash_bank_account", "=", "{0}".format(bank_account)]])
mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"])]
company_currency = get_company_currency(company)
expense_claims = frappe.get_all("Expense Claim", fields=["'Expense Claim' as doctype", "name", "total_sanctioned_amount as paid_amount",
"employee as party", "posting_date", "'{0}' as currency".format(company_currency)], filters=[["total_sanctioned_amount", "like", "{0}%".format(amount)],
["docstatus", "=", "1"], ["is_paid", "=", "1"], ["ifnull(clearance_date, '')", "=", ""], ["mode_of_payment", "in", "{0}".format(tuple(mode_of_payments))]])
else:
purchase_invoices = expense_claims = []
for data in [payment_entries, journal_entries, sales_invoices, purchase_invoices, expense_claims]:
if data:
payments.extend(data)
return payments
def get_matching_descriptions_data(bank_account, transaction):
if not transaction.description :
return []
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)
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.6:
bank_transaction["ratio"] = seq.ratio()
selection.append(bank_transaction)
document_types = set([x["payment_document"] for x in selection])
links = {}
for document_type in document_types:
links[document_type] = [x["payment_entry"] for x in selection if x["payment_document"]==document_type]
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_name as party"]))
if key == "Purchase Invoice":
data.append(frappe.get_all("Purchase Invoice", filters=[["name", "in", value]], fields=["'Purchase Invoice' as doctype", "posting_date", "supplier_name as party"]))
if key == "Purchase Invoice":
data.append(frappe.get_all("Expense Claim", filters=[["name", "in", value]], fields=["'Expense Claim' as doctype", "posting_date", "employee_name as party"]))
return data
def check_amount_vs_description(amount_matching, description_matching):
result = []
if description_matching:
for am_match in amount_matching:
for des_match in description_matching:
if am_match["party"] == des_match["party"]:
if am_match not in result:
result.append(am_match)
continue
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:
if am_match not in result:
result.append(am_match)
if result:
return sorted(result, key = lambda x: x["posting_date"], reverse=True)
else:
return sorted(amount_matching, 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]
payment_by_ratio = {x["payment_entry"]: x["ratio"] for x in description_matching}
if payments:
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(reference_payment_list, key=lambda x: payment_by_ratio[x["name"]])
else:
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 or je.pay_to_recd_from 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""",
{
'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 or si.customer 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""",
{
'txt': "%%%s%%" % txt,
'_txt': txt.replace("%", ""),
'start': start,
'page_len': page_len
}
)

View File

@@ -0,0 +1,21 @@
<div class="transaction-header">
<div class="level list-row list-row-head text-muted small">
<div class="col-sm-2 ellipsis hidden-xs">
{{ __("Date") }}
</div>
<div class="col-xs-11 col-sm-4 ellipsis list-subject">
{{ __("Description") }}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{{ __("Debit") }}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{{ __("Credit") }}
</div>
<div class="col-sm-1 ellipsis hidden-xs">
{{ __("Currency") }}
</div>
<div class="col-sm-1 ellipsis">
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<div class="list-row transaction-item">
<div>
<div class="clickable-section" data-name={{ name }}>
<div class="col-sm-2 ellipsis hidden-xs">
{%= frappe.datetime.str_to_user(date) %}
</div>
<div class="col-xs-8 col-sm-4 ellipsis list-subject">
{{ description }}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{%= format_currency(debit, currency) %}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{%= format_currency(credit, currency) %}
</div>
<div class="col-sm-1 ellipsis hidden-xs">
{{ currency }}
</div>
</div>
<div class="col-xs-3 col-sm-1">
<div class="btn-group">
<a class="dropdown-toggle btn btn-default btn-xs" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span>Actions </span>
<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>
<li><a class="new-expense" data-name={{ name }}>{{ __("New Expense") }}</a></li>
</ul>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<div class="transaction-header">
<div class="level list-row list-row-head text-muted small">
<div class="col-xs-3 col-sm-2 ellipsis">
{{ __("Payment Name") }}
</div>
<div class="col-xs-3 col-sm-2 ellipsis">
{{ __("Reference Date") }}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{{ __("Amount") }}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{{ __("Party") }}
</div>
<div class="col-xs-3 col-sm-2 ellipsis">
{{ __("Reference Number") }}
</div>
<div class="col-xs-2 col-sm-2">
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<div class="list-row">
<div>
<div class="col-xs-3 col-sm-2 ellipsis">
{{ name }}
</div>
<div class="col-xs-3 col-sm-2 ellipsis">
{% if (typeof reference_date !== "undefined") %}
{%= frappe.datetime.str_to_user(reference_date) %}
{% else %}
{% if (typeof posting_date !== "undefined") %}
{%= frappe.datetime.str_to_user(posting_date) %}
{% endif %}
{% endif %}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{{ format_currency(paid_amount, currency) }}
</div>
<div class="col-sm-2 ellipsis hidden-xs">
{% if (typeof party !== "undefined") %}
{{ party }}
{% endif %}
</div>
<div class="col-xs-3 col-sm-2 ellipsis">
{% if (typeof reference_no !== "undefined") %}
{{ reference_no }}
{% else %}
{{ "" }}
{% endif %}
</div>
<div class="col-xs-2 col-sm-2">
<div class="text-right margin-bottom">
<button class="btn btn-primary btn-xs reconciliation-btn" data-doctype="{{ doctype }}" data-name="{{ name }}">{{ __("Reconcile") }}</button>
</div>
</div>
</div>
</div>

View File

@@ -76,6 +76,14 @@ def get_data():
{
"type": "doctype",
"name": "Item",
},
{
"type": "doctype",
"name": "Bank",
},
{
"type": "doctype",
"name": "Bank Account",
}
]
},
@@ -135,6 +143,12 @@ def get_data():
"name": "Bank Reconciliation",
"description": _("Update bank payment dates with journals.")
},
{
"type": "page",
"label": _("Reconcile payments and bank transactions"),
"name": "bank-reconciliation",
"description": _("Link bank transactions with payments.")
},
{
"type": "doctype",
"label": _("Match Payments with Invoices"),

View File

@@ -35,6 +35,11 @@ def get_data():
"type": "doctype",
"name": "Amazon MWS Settings",
"description": _("Connect Amazon with ERPNext"),
},
{
"type": "doctype",
"name": "Plaid Settings",
"description": _("Connect your bank accounts to ERPNext"),
}
]
}

View File

@@ -0,0 +1,81 @@
# -*- 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 import _
import requests
from plaid import Client
from plaid.errors import APIError, ItemError
class PlaidConnector():
def __init__(self, access_token=None):
if not(frappe.conf.get("plaid_client_id") and frappe.conf.get("plaid_secret") and frappe.conf.get("plaid_public_key")):
frappe.throw(_("Please complete your Plaid API configuration before synchronizing your account"))
self.config = {
"plaid_client_id": frappe.conf.get("plaid_client_id"),
"plaid_secret": frappe.conf.get("plaid_secret"),
"plaid_public_key": frappe.conf.get("plaid_public_key"),
"plaid_env": frappe.conf.get("plaid_env")
}
self.client = Client(client_id=self.config["plaid_client_id"],
secret=self.config["plaid_secret"],
public_key=self.config["plaid_public_key"],
environment=self.config["plaid_env"]
)
self.access_token = access_token
def get_access_token(self, public_token):
if public_token is None:
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
response = self.client.Item.public_token.exchange(public_token)
access_token = response['access_token']
return access_token
def auth(self):
try:
self.client.Auth.get(self.access_token)
print("Authentication successful.....")
except ItemError as e:
if e.code == 'ITEM_LOGIN_REQUIRED':
pass
else:
pass
except APIError as e:
if e.code == 'PLANNED_MAINTENANCE':
pass
else:
pass
except requests.Timeout:
pass
except Exception as e:
print(e)
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'})
def get_transactions(self, start_date, end_date, account_id=None):
try:
self.auth()
if account_id:
account_ids = [account_id]
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids)
else:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date)
transactions = response['transactions']
while len(transactions) < response['total_transactions']:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response['transactions'])
return transactions
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

@@ -0,0 +1,104 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide("erpnext.integrations");
frappe.ui.form.on('Plaid Settings', {
link_new_account: function(frm) {
new erpnext.integrations.plaidLink(frm);
}
});
erpnext.integrations.plaidLink = class plaidLink {
constructor(parent) {
this.frm = parent;
this.product = ["transactions", "auth"];
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config();
}
init_config() {
const me = this;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration')
.then(result => {
if (result !== "disabled") {
me.plaid_env = result.plaid_env;
me.plaid_public_key = result.plaid_public_key;
me.client_name = result.client_name;
me.init_plaid();
} else {
frappe.throw(__("Please save your document before adding a new account"));
}
});
}
init_plaid() {
const me = this;
me.loadScript(me.plaidUrl)
.then(() => {
me.onScriptLoaded(me);
})
.then(() => {
if (me.linkHandler) {
me.linkHandler.open();
}
})
.catch((error) => {
me.onScriptError(error);
});
}
loadScript(src) {
return new Promise(function (resolve, reject) {
if (document.querySelector('script[src="' + src + '"]')) {
resolve();
return;
}
const el = document.createElement('script');
el.type = 'text/javascript';
el.async = true;
el.src = src;
el.addEventListener('load', resolve);
el.addEventListener('error', reject);
el.addEventListener('abort', reject);
document.head.appendChild(el);
});
}
onScriptLoaded(me) {
me.linkHandler = window.Plaid.create({
clientName: me.client_name,
env: me.plaid_env,
key: me.plaid_public_key,
onSuccess: me.plaid_success,
product: me.product
});
}
onScriptError(error) {
frappe.msgprint('There was an issue loading the link-initialize.js script');
frappe.msgprint(error);
}
plaid_success(token, response) {
const me = this;
frappe.prompt({
fieldtype:"Link",
options: "Company",
label:__("Company"),
fieldname:"company",
reqd:1
}, (data) => {
me.company = data.company;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response})
.then((result) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response,
bank: result, company: me.company});
})
.then(() => {
frappe.show_alert({message:__("Bank accounts added"), indicator:'green'});
});
}, __("Select a company"), __("Continue"));
}
};

View File

@@ -0,0 +1,161 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-10-25 10:02:48.656165",
"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": "enabled",
"fieldtype": "Check",
"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": "Enabled",
"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,
"depends_on": "eval:doc.enabled==1",
"fieldname": "automatic_sync",
"fieldtype": "Check",
"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": "Synchronize all accounts every hour",
"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,
"depends_on": "eval:(doc.enabled==1)&&(!doc.__islocal)",
"fieldname": "link_new_account",
"fieldtype": "Button",
"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": "Link a new bank account",
"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
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2018-12-14 12:51:12.331395",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}

View File

@@ -0,0 +1,198 @@
# -*- 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
import json
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
from frappe.utils import getdate, formatdate, today, add_months
class PlaidSettings(Document):
pass
@frappe.whitelist()
def plaid_configuration():
if frappe.db.get_value("Plaid Settings", None, "enabled") == "1":
return {"plaid_public_key": frappe.conf.get("plaid_public_key") or None, "plaid_env": frappe.conf.get("plaid_env") or None, "client_name": frappe.local.site }
else:
return "disabled"
@frappe.whitelist()
def add_institution(token, response):
response = json.loads(response)
plaid = PlaidConnector()
access_token = plaid.get_access_token(token)
if not frappe.db.exists("Bank", response["institution"]["name"]):
try:
bank = frappe.get_doc({
"doctype": "Bank",
"bank_name": response["institution"]["name"],
"plaid_access_token": access_token
})
bank.insert()
except Exception:
frappe.throw(frappe.get_traceback())
else:
bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token
bank.save()
return bank
@frappe.whitelist()
def add_bank_accounts(response, bank, company):
response = json.loads(response) if not "accounts" in response else response
bank = json.loads(bank)
result = []
default_gl_account = get_default_bank_cash_account(company, "Bank")
if not default_gl_account:
frappe.throw(_("Please setup a default bank account for company {0}".format(company)))
for account in response["accounts"]:
acc_type = frappe.db.get_value("Account Type", account["type"])
if not acc_type:
add_account_type(account["type"])
acc_subtype = frappe.db.get_value("Account Subtype", account["subtype"])
if not acc_subtype:
add_account_subtype(account["subtype"])
if not frappe.db.exists("Bank Account", dict(integration_id=account["id"])):
try:
new_account = frappe.get_doc({
"doctype": "Bank Account",
"bank": bank["bank_name"],
"account": default_gl_account.account,
"account_name": account["name"],
"account_type": account["type"] or "",
"account_subtype": account["subtype"] or "",
"mask": account["mask"] or "",
"integration_id": account["id"],
"is_company_account": 1,
"company": company
})
new_account.insert()
result.append(new_account.name)
except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name))
except Exception:
frappe.throw(frappe.get_traceback())
else:
result.append(frappe.db.get_value("Bank Account", dict(integration_id=account["id"]), "name"))
return result
def add_account_type(account_type):
try:
frappe.get_doc({
"doctype": "Account Type",
"account_type": account_type
}).insert()
except Exception:
frappe.throw(frappe.get_traceback())
def add_account_subtype(account_subtype):
try:
frappe.get_doc({
"doctype": "Account Subtype",
"account_subtype": account_subtype
}).insert()
except Exception:
frappe.throw(frappe.get_traceback())
@frappe.whitelist()
def sync_transactions(bank, bank_account):
last_sync_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
if last_sync_date:
start_date = formatdate(last_sync_date, "YYYY-MM-dd")
else:
start_date = formatdate(add_months(today(), -12), "YYYY-MM-dd")
end_date = formatdate(today(), "YYYY-MM-dd")
try:
transactions = get_transactions(bank=bank, bank_account=bank_account, start_date=start_date, end_date=end_date)
result = []
if transactions:
for transaction in transactions:
result.append(new_bank_transaction(transaction))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", getdate(end_date))
return result
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
access_token = None
if bank_account:
related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id
else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None
plaid = PlaidConnector(access_token)
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
return transactions
def new_bank_transaction(transaction):
result = []
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
if float(transaction["amount"]) >= 0:
debit = float(transaction["amount"])
credit = 0
else:
debit = 0
credit = abs(float(transaction["amount"]))
status = "Pending" if transaction["pending"] == "True" else "Settled"
if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])):
try:
new_transaction = frappe.get_doc({
"doctype": "Bank Transaction",
"date": getdate(transaction["date"]),
"status": status,
"bank_account": bank_account,
"debit": debit,
"credit": credit,
"currency": transaction["iso_currency_code"],
"description": transaction["name"]
})
new_transaction.insert()
new_transaction.submit()
result.append(new_transaction.name)
except Exception:
frappe.throw(frappe.get_traceback())
return result
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
if settings.enabled == 1 and settings.automatic_sync == 1:
plaid_accounts = frappe.get_all("Bank Account", filter={"integration_id": ["!=", ""]}, fields=["name", "bank"])
for plaid_account in plaid_accounts:
frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name)

View File

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Plaid Settings", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Plaid Settings
() => frappe.tests.make('Plaid Settings', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts
import json
from frappe.utils.response import json_handler
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
class TestPlaidSettings(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel()
doc.delete()
for ba in frappe.get_all("Bank Account"):
frappe.get_doc("Bank Account", ba.name).delete()
for at in frappe.get_all("Account Type"):
frappe.get_doc("Account Type", at.name).delete()
for ast in frappe.get_all("Account Subtype"):
frappe.get_doc("Account Subtype", ast.name).delete()
def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0)
self.assertTrue(plaid_configuration() == "disabled")
def test_add_account_type(self):
add_account_type("brokerage")
self.assertEqual(frappe.get_doc("Account Type", "brokerage").name, "brokerage")
def test_add_account_subtype(self):
add_account_subtype("loan")
self.assertEqual(frappe.get_doc("Account Subtype", "loan").name, "loan")
def test_default_bank_account(self):
if not frappe.db.exists("Bank", "Citi"):
frappe.get_doc({
"doctype": "Bank",
"bank_name": "Citi"
}).insert()
bank_accounts = {
'account': {
'subtype': 'checking',
'mask': '0000',
'type': 'depository',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
},
'account_id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'link_session_id': 'db673d75-61aa-442a-864f-9b3f174f3725',
'accounts': [{
'type': 'depository',
'subtype': 'checking',
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
}],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
}
}
bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler)
company = frappe.db.get_single_value('Global Defaults', 'default_company')
frappe.db.set_value("Company", company, "default_bank_account", None)
self.assertRaises(frappe.ValidationError, add_bank_accounts, response=bank_accounts, bank=bank, company=company)
def test_new_transaction(self):
if not frappe.db.exists("Bank", "Citi"):
frappe.get_doc({
"doctype": "Bank",
"bank_name": "Citi"
}).insert()
bank_accounts = {
'account': {
'subtype': 'checking',
'mask': '0000',
'type': 'depository',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
},
'account_id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'link_session_id': 'db673d75-61aa-442a-864f-9b3f174f3725',
'accounts': [{
'type': 'depository',
'subtype': 'checking',
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
}],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
}
}
bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler)
company = frappe.db.get_single_value('Global Defaults', 'default_company')
if frappe.db.get_value("Company", company, "default_bank_account") is None:
frappe.db.set_value("Company", company, "default_bank_account", get_default_bank_cash_account(company, "Cash").get("account"))
add_bank_accounts(bank_accounts, bank, company)
transactions = {
'account_owner': None,
'category': ['Food and Drink', 'Restaurants'],
'account_id': 'b4Jkp1LJDZiPgojpr1ansXJrj5Q6w9fVmv6ov',
'pending_transaction_id': None,
'transaction_id': 'x374xPa7DvUewqlR5mjNIeGK8r8rl3Sn647LM',
'unofficial_currency_code': None,
'name': 'INTRST PYMNT',
'transaction_type': 'place',
'amount': -4.22,
'location': {
'city': None,
'zip': None,
'store_number': None,
'lon': None,
'state': None,
'address': None,
'lat': None
},
'payment_meta': {
'reference_number': None,
'payer': None,
'payment_method': None,
'reason': None,
'payee': None,
'ppd_id': None,
'payment_processor': None,
'by_order_of': None
},
'date': '2017-12-22',
'category_id': '13005000',
'pending': False,
'iso_currency_code': 'USD'
}
new_bank_transaction(transactions)
self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1)

View File

@@ -229,6 +229,7 @@ scheduler_events = {
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
"erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status"
],

View File

@@ -844,6 +844,38 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "clearance_date",
"fieldtype": "Date",
"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": "Clearance Date",
"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,

View File

@@ -1,58 +1,58 @@
{
"css/erpnext.css": [
"public/less/erpnext.less",
"public/less/hub.less"
],
"css/marketplace.css": [
"public/less/hub.less"
],
"js/erpnext-web.min.js": [
"public/js/website_utils.js",
"public/js/shopping_cart.js"
],
"css/erpnext.css": [
"public/less/erpnext.less",
"public/less/hub.less"
],
"css/marketplace.css": [
"public/less/hub.less"
],
"js/erpnext-web.min.js": [
"public/js/website_utils.js",
"public/js/shopping_cart.js"
],
"css/erpnext-web.css": [
"public/less/website.less"
],
"js/marketplace.min.js": [
"public/js/hub/marketplace.js"
],
"js/erpnext.min.js": [
"public/js/conf.js",
"public/js/utils.js",
"public/js/queries.js",
"public/js/sms_manager.js",
"public/js/utils/party.js",
"public/js/templates/address_list.html",
"public/js/templates/contact_list.html",
"public/js/controllers/stock_controller.js",
"public/js/payment/payments.js",
"public/js/controllers/taxes_and_totals.js",
"public/js/controllers/transaction.js",
"public/js/pos/pos.html",
"public/js/pos/pos_bill_item.html",
"public/js/pos/pos_bill_item_new.html",
"public/js/pos/pos_selected_item.html",
"public/js/pos/pos_item.html",
"public/js/pos/pos_tax_row.html",
"public/js/pos/customer_toolbar.html",
"public/js/pos/pos_invoice_list.html",
"public/js/payment/pos_payment.html",
"public/js/payment/payment_details.html",
"public/js/templates/item_selector.html",
"js/marketplace.min.js": [
"public/js/hub/marketplace.js"
],
"js/erpnext.min.js": [
"public/js/conf.js",
"public/js/utils.js",
"public/js/queries.js",
"public/js/sms_manager.js",
"public/js/utils/party.js",
"public/js/templates/address_list.html",
"public/js/templates/contact_list.html",
"public/js/controllers/stock_controller.js",
"public/js/payment/payments.js",
"public/js/controllers/taxes_and_totals.js",
"public/js/controllers/transaction.js",
"public/js/pos/pos.html",
"public/js/pos/pos_bill_item.html",
"public/js/pos/pos_bill_item_new.html",
"public/js/pos/pos_selected_item.html",
"public/js/pos/pos_item.html",
"public/js/pos/pos_tax_row.html",
"public/js/pos/customer_toolbar.html",
"public/js/pos/pos_invoice_list.html",
"public/js/payment/pos_payment.html",
"public/js/payment/payment_details.html",
"public/js/templates/item_selector.html",
"public/js/templates/employees_to_mark_attendance.html",
"public/js/utils/item_selector.js",
"public/js/help_links.js",
"public/js/agriculture/ternary_plot.js",
"public/js/templates/item_quick_entry.html",
"public/js/utils/item_quick_entry.js",
"public/js/utils/item_selector.js",
"public/js/help_links.js",
"public/js/agriculture/ternary_plot.js",
"public/js/templates/item_quick_entry.html",
"public/js/utils/item_quick_entry.js",
"public/js/utils/customer_quick_entry.js",
"public/js/education/student_button.html",
"public/js/education/assessment_result_tool.html",
"public/js/hub/hub_factory.js"
],
"js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html",
"stock/dashboard/item_dashboard_list.html",
"stock/dashboard/item_dashboard.js"
]
"public/js/education/student_button.html",
"public/js/education/assessment_result_tool.html",
"public/js/hub/hub_factory.js"
],
"js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html",
"stock/dashboard/item_dashboard_list.html",
"stock/dashboard/item_dashboard.js"
]
}

View File

@@ -25,10 +25,10 @@
.app-icon-svg {
display: inline-block;
margin: auto;
text-align: center;
border-radius: 16px;
cursor: pointer;
margin: auto;
text-align: center;
border-radius: 16px;
cursor: pointer;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.15);
}
@@ -458,4 +458,4 @@ body[data-route="pos"] {
.list-item_content {
padding-right: 45px;
}
}
}

View File

@@ -6,4 +6,5 @@ python-stdnum
braintree
gocardless_pro
woocommerce
pandas
pandas
plaid-python