From 288ced24dbf771988b1c601d2e6d5f0fde07bd12 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 19:33:27 +0530 Subject: [PATCH 01/73] feat: Add Naming Series for Project DocType --- erpnext/projects/doctype/project/project.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index f3cecd9059..122a1a96f4 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -2,12 +2,13 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "autoname": "field:project_name", + "autoname": "naming_series:", "creation": "2013-03-07 11:55:07", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "naming_series", "project_name", "status", "project_type", @@ -440,13 +441,22 @@ "fieldtype": "Text", "label": "Message", "mandatory_depends_on": "collect_progress" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 1, + "label": "Series", + "options": "PROJ.####", + "set_only_once": 1 } ], "icon": "fa fa-puzzle-piece", "idx": 29, + "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2020-04-08 22:11:14.552615", + "modified": "2020-08-30 19:32:40.050707", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -488,5 +498,6 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "customer", + "title_field": "project_name", "track_seen": 1 -} +} \ No newline at end of file From f46c1c5164b44e7f8ce3ad65a90189688f291740 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 21:37:45 +0530 Subject: [PATCH 02/73] fix: Change property of field --- erpnext/projects/doctype/project/project.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 122a1a96f4..c91b01eaa9 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -445,9 +445,9 @@ { "fieldname": "naming_series", "fieldtype": "Select", - "hidden": 1, "label": "Series", "options": "PROJ.####", + "reqd": 1, "set_only_once": 1 } ], @@ -456,7 +456,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2020-08-30 19:32:40.050707", + "modified": "2020-08-30 21:36:45.915818", "modified_by": "Administrator", "module": "Projects", "name": "Project", From f51cf9f23e0e41e6db5b5538ba2c4f75b24bfe74 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 11:43:36 +0530 Subject: [PATCH 03/73] fix: Crop Cycle Test --- erpnext/agriculture/doctype/crop_cycle/crop_cycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py index cae150c428..afbd9b4e6e 100644 --- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py @@ -48,7 +48,7 @@ class CropCycle(Document): def import_disease_tasks(self, disease, start_date): disease_doc = frappe.get_doc('Disease', disease) - self.create_task(disease_doc.treatment_task, self.name, start_date) + self.create_task(disease_doc.treatment_task, self.project, start_date) def create_project(self, period, crop_tasks): project = frappe.get_doc({ From 099d6718c914198475153bf6498cc57ed6fd6396 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 11:54:55 +0530 Subject: [PATCH 04/73] fix: Dont Copy or Print Naming Series --- erpnext/projects/doctype/project/project.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index c91b01eaa9..8ed6888854 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -446,7 +446,9 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", + "no_copy": 1, "options": "PROJ.####", + "print_hide": 1, "reqd": 1, "set_only_once": 1 } @@ -456,7 +458,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2020-08-30 21:36:45.915818", + "modified": "2020-09-02 11:54:01.223620", "modified_by": "Administrator", "module": "Projects", "name": "Project", From d8c38249e03e2206c41cd86c64d31cae431845e5 Mon Sep 17 00:00:00 2001 From: Michelle Alva <50285544+michellealva@users.noreply.github.com> Date: Thu, 3 Sep 2020 09:04:21 +0530 Subject: [PATCH 05/73] fix: Change naming series Co-authored-by: Himanshu --- erpnext/projects/doctype/project/project.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 8ed6888854..3cdfcb212f 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -447,7 +447,7 @@ "fieldtype": "Select", "label": "Series", "no_copy": 1, - "options": "PROJ.####", + "options": "PROJ-.####", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -502,4 +502,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} \ No newline at end of file +} From 8b6370bb45232d0e39508a1329258388c4d47439 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:41:36 +0530 Subject: [PATCH 06/73] feat: Add accounting dimension filter doctype --- .../accounting_dimension_filter/__init__.py | 0 .../accounting_dimension_filter.js | 32 ++++++ .../accounting_dimension_filter.json | 100 ++++++++++++++++++ .../accounting_dimension_filter.py | 63 +++++++++++ .../test_accounting_dimension_filter.py | 10 ++ .../doctype/allowed_dimension/__init__.py | 0 .../allowed_dimension/allowed_dimension.json | 43 ++++++++ .../allowed_dimension/allowed_dimension.py | 10 ++ .../doctype/applicable_on_account/__init__.py | 0 .../applicable_on_account.json | 35 ++++++ .../applicable_on_account.py | 10 ++ 11 files changed, 303 insertions(+) create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/__init__.py create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py create mode 100644 erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py create mode 100644 erpnext/accounts/doctype/allowed_dimension/__init__.py create mode 100644 erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json create mode 100644 erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py create mode 100644 erpnext/accounts/doctype/applicable_on_account/__init__.py create mode 100644 erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json create mode 100644 erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py b/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js new file mode 100644 index 0000000000..6c254fcfb7 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -0,0 +1,32 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Accounting Dimension Filter', { + onload: function(frm) { + frappe.db.get_list('Accounting Dimension', + {fields: ['name']}).then((res) => { + let options = ['Cost Center', 'Project']; + + res.forEach((dimension) => { + options.push(dimension.name); + }); + + frm.set_df_property('accounting_dimension', 'options', options); + }); + }, + + accounting_dimension: function(frm) { + frm.clear_table("dimensions"); + let row = frm.add_child("dimensions"); + row.accounting_dimension = frm.doc.accounting_dimension; + frm.refresh_field("dimensions"); + }, +}); + +frappe.ui.form.on('Allowed Dimension', { + dimensions_add: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + row.accounting_dimension = frm.doc.accounting_dimension; + frm.refresh_field("dimensions"); + } +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json new file mode 100644 index 0000000000..e626a09ce0 --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -0,0 +1,100 @@ +{ + "actions": [], + "autoname": "format:{accounting_dimension}-{#####}", + "creation": "2020-11-08 18:28:11.906146", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "accounting_dimension", + "column_break_2", + "allow_or_restrict", + "section_break_4", + "accounts", + "column_break_6", + "dimensions" + ], + "fields": [ + { + "fieldname": "accounting_dimension", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Accounting Dimension", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "hide_border": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "allow_or_restrict", + "fieldtype": "Select", + "label": "Allow Or Restrict Dimension", + "options": "Allow\nRestrict", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Applicable On Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval:doc.accounting_dimension", + "fieldname": "dimensions", + "fieldtype": "Table", + "label": "Dimensions", + "options": "Allowed Dimension", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-14 18:02:02.616932", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Accounting Dimension Filter", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py new file mode 100644 index 0000000000..ccfafd96ed --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright, (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _, scrub +from frappe.model.document import Document + +class AccountingDimensionFilter(Document): + def validate(self): + self.validate_applicable_accounts() + + def validate_applicable_accounts(self): + accounts = frappe.db.sql( + """ + SELECT a.applicable_on_account as account + FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` d + WHERE d.name = a.parent + and d.name != %s + and d.accounting_dimension = %s + """, (self.name, self.accounting_dimension), as_dict=1) + + account_list = [d.account for d in accounts] + + for account in self.get('accounts'): + if account.applicable_on_account in account_list: + frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format( + account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension))) + +def get_dimension_filter_map(): + filters = frappe.db.sql( + """ SELECT + a.applicable_on_account, d.dimension_value, p.accounting_dimension, + p.allow_or_restrict, ad.fieldname + FROM + `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabAccounting Dimension Filter` p, `tabAccounting Dimension` ad + WHERE + p.name = a.parent + AND p.name = d.parent + AND (p.accounting_dimension = ad.name + OR p.accounting_dimension in ('Cost Center', 'Project')) + """, as_dict=1) + + dimension_filter_map = {} + account_filter_map = {} + + for f in filters: + if f.accounting_dimension in ('Cost Center', 'Project'): + f.fieldname = scrub(f.accounting_dimension) + + build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value, + f.allow_or_restrict) + + return dimension_filter_map + +def build_map(map_object, dimension, account, filter_value, allow_or_restrict): + map_object.setdefault((dimension, account), { + 'allowed_dimensions': [], + 'allow_or_restrict': allow_or_restrict + }) + map_object[(dimension, account)]['allowed_dimensions'].append(filter_value) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py new file mode 100644 index 0000000000..c271a25fbd --- /dev/null +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestAccountingDimensionFilter(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/allowed_dimension/__init__.py b/erpnext/accounts/doctype/allowed_dimension/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json new file mode 100644 index 0000000000..20024b0322 --- /dev/null +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "creation": "2020-11-08 18:22:36.001131", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "accounting_dimension", + "dimension_value" + ], + "fields": [ + { + "fieldname": "accounting_dimension", + "fieldtype": "Link", + "label": "Accounting Dimension", + "options": "DocType", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "dimension_value", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "options": "accounting_dimension", + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-14 19:54:03.269016", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Allowed Dimension", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py new file mode 100644 index 0000000000..c2afc1a262 --- /dev/null +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class AllowedDimension(Document): + pass diff --git a/erpnext/accounts/doctype/applicable_on_account/__init__.py b/erpnext/accounts/doctype/applicable_on_account/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json new file mode 100644 index 0000000000..8305da2ba0 --- /dev/null +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json @@ -0,0 +1,35 @@ +{ + "actions": [], + "creation": "2020-11-08 18:20:00.944449", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicable_on_account" + ], + "fields": [ + { + "fieldname": "applicable_on_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Applicable On Account", + "options": "Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-14 16:54:06.756883", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Applicable On Account", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py new file mode 100644 index 0000000000..0fccaf302f --- /dev/null +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ApplicableOnAccount(Document): + pass From 96e874bfda3e93a48613765c7433824587fb0360 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:43:01 +0530 Subject: [PATCH 07/73] fix: dimension filter query --- .../accounting_dimension.py | 14 +++- erpnext/controllers/queries.py | 47 ++++++++++++++ .../public/js/utils/dimension_tree_filter.js | 65 ++++++++++++++----- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index f888d9e038..b9d4da289e 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -203,7 +203,7 @@ def get_dimension_with_children(doctype, dimension): return all_dimensions @frappe.whitelist() -def get_dimension_filters(): +def get_dimension_filters(with_costcenter_and_project=False): dimension_filters = frappe.db.sql(""" SELECT label, fieldname, document_type FROM `tabAccounting Dimension` @@ -214,6 +214,18 @@ def get_dimension_filters(): FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p WHERE c.parent = p.name""", as_dict=1) + if with_costcenter_and_project: + dimension_filters.extend([ + { + 'fieldname': 'cost_center', + 'document_type': 'Cost Center' + }, + { + 'fieldname': 'project', + 'document_type': 'Project' + } + ]) + default_dimensions_map = {} for dimension in default_dimensions: default_dimensions_map.setdefault(dimension.company, {}) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 8fe3816c24..015807d563 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -493,6 +493,53 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): 'company': filters.get("company", "") }) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters): + from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map + dimension_filters = get_dimension_filter_map() + dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account'))) + group_condition = '' + company_condition = '' + + meta = frappe.get_meta(doctype) + + if meta.is_tree: + group_condition = 'and is_group = 0 ' + + if meta.has_field('company'): + company_condition = 'and company = %s ' % (frappe.db.escape(filters.get('company'))) + + if dimension_filters: + if dimension_filters['allow_or_restrict'] == 'Allow': + query_selector = 'in' + else: + query_selector = 'not in' + + if len(dimension_filters['allowed_dimensions']) == 1: + dimensions = tuple(dimension_filters['allowed_dimensions'] * 2) + else: + dimensions = tuple(dimension_filters['allowed_dimensions']) + + result = frappe.db.sql("""SELECT name from `tab{doctype}` where + name {query_selector} {restricted} + {group_condition} {company_condition} + and {key} LIKE %(txt)s""".format( + doctype=doctype, query_selector=query_selector, restricted=dimensions, + group_condition = group_condition, + company_condition = company_condition, + key=searchfield), { + 'txt': '%' + txt + '%' + }) + + return result + else: + return frappe.db.sql(""" + SELECT name from `tab{doctype}` where + {key} LIKE %(txt)s {group_condition} {company_condition}""" + .format(doctype=doctype, key=searchfield, + group_condition=group_condition, company_condition=company_condition), + { 'txt': '%' + txt + '%'}) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index b6720c05cb..34b563553e 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -5,14 +5,18 @@ let default_dimensions = {}; let doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Shipping Rule", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", - "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Landed Cost Item", "Asset"]; + "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Asset", "Asset Value Adjustment"]; let child_docs = ["Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", - "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan"]; + "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan", + "Sales Taxes and Charges", "Purchase Taxes and Charges"]; frappe.call({ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters", + args: { + 'with_costcenter_and_project': true + }, callback: function(r) { erpnext.dimension_filters = r.message[0]; default_dimensions = r.message[1]; @@ -24,11 +28,16 @@ doctypes_with_dimensions.forEach((doctype) => { onload: function(frm) { erpnext.dimension_filters.forEach((dimension) => { frappe.model.with_doctype(dimension['document_type'], () => { - if(frappe.meta.has_field(dimension['document_type'], 'is_group')) { - frm.set_query(dimension['fieldname'], { - "is_group": 0 - }); - } + let parent_fields = []; + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + parent_fields.push(df.fieldname); + } else if (df.fieldtype === 'Table') { + setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); + }; + + setup_account_filters(frm, dimension['fieldname'], parent_fields); + }); }); }); }, @@ -67,17 +76,41 @@ doctypes_with_dimensions.forEach((doctype) => { child_docs.forEach((doctype) => { frappe.ui.form.on(doctype, { items_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]); - }); + copy_dimension(frm, cdt, cdn, "items"); }, accounts_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]); - }); + copy_dimension(frm, cdt, cdn, "accounts"); } }); -}); \ No newline at end of file +}); + +let copy_dimension = function(frm, cdt, cdn, fieldname) { + erpnext.dimension_filters.forEach((dimension) => { + let row = frappe.get_doc(cdt, cdn); + frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); + }); +} + +let setup_child_filters = function(frm, doctype, parentfield, dimension) { + let fields = []; + + frappe.model.with_doctype(doctype, () => { + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + fields.push(df.fieldname); + } + }); + + frm.set_query(dimension, parentfield, function(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company); + }); + }); +} + +let setup_account_filters = function(frm, dimension, fields) { + frm.set_query(dimension, function(doc) { + return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); + }); +} \ No newline at end of file From 6e5748e2a3344eeead1a7b1f86258de233276d02 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:43:48 +0530 Subject: [PATCH 08/73] fix: dimension filter query --- erpnext/public/js/queries.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 560a5617da..7b7a9df1ac 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -116,6 +116,25 @@ $.extend(erpnext.queries, { ] } + }, + + get_filtered_dimensions: function(doc, child_fields, dimension, company) { + let account = ''; + + child_fields.forEach((field) => { + if (!account) { + account = doc[field]; + } + }); + + return { + query: "erpnext.controllers.queries.get_filtered_dimensions", + filters: { + 'dimension': dimension, + 'account': account, + 'company': company + } + } } }); From f916bc048ff08a4bbae87e37bd8215c35ac32f8a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:44:39 +0530 Subject: [PATCH 09/73] fix: Remove cost center query from doctypes --- erpnext/accounts/doctype/budget/budget.js | 12 ++---------- .../accounts/doctype/journal_entry/journal_entry.js | 9 --------- .../doctype/loyalty_program/loyalty_program.js | 8 -------- .../accounts/doctype/payment_entry/payment_entry.js | 9 --------- .../doctype/purchase_invoice/purchase_invoice.js | 9 --------- .../accounts/doctype/sales_invoice/sales_invoice.js | 9 --------- .../accounts/doctype/shipping_rule/shipping_rule.js | 8 -------- erpnext/assets/doctype/asset/asset.js | 8 -------- erpnext/hr/doctype/expense_claim/expense_claim.js | 9 --------- .../payroll/doctype/payroll_entry/payroll_entry.js | 9 +-------- erpnext/public/js/controllers/accounts.js | 9 --------- erpnext/public/js/controllers/transaction.js | 10 ---------- 12 files changed, 3 insertions(+), 106 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index cadf1e7e0c..48cc493522 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -3,14 +3,6 @@ frappe.ui.form.on('Budget', { onload: function(frm) { - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company - } - } - }) - frm.set_query("project", function() { return { filters: { @@ -18,7 +10,7 @@ frappe.ui.form.on('Budget', { } } }) - + frm.set_query("account", "accounts", function() { return { filters: { @@ -28,7 +20,7 @@ frappe.ui.form.on('Budget', { } } }) - + frm.set_query("monthly_distribution", function() { return { filters: { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index ff12967155..d60a7b76cc 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -222,15 +222,6 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ return erpnext.journal_entry.account_query(me.frm); }); - me.frm.set_query("cost_center", "accounts", function(doc, cdt, cdn) { - return { - filters: { - company: me.frm.doc.company, - is_group: 0 - } - }; - }); - me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) { const row = locals[cdt][cdn]; diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js index 524a671801..0d2b8cbf15 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js @@ -46,14 +46,6 @@ frappe.ui.form.on('Loyalty Program', { }; }); - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company - } - }; - }); - frm.set_value("company", frappe.defaults.get_user_default("Company")); }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index e117471738..ea5487d575 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -88,15 +88,6 @@ frappe.ui.form.on('Payment Entry', { } }); - frm.set_query("cost_center", "deductions", function() { - return { - filters: { - "is_group": 0, - "company": frm.doc.company - } - } - }); - frm.set_query("reference_doctype", "references", function() { if (frm.doc.party_type=="Customer") { var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 1d41d0fa2a..3c07ee75cb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -501,15 +501,6 @@ frappe.ui.form.on("Purchase Invoice", { } } } - - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company, - is_group: 0 - } - }; - }); }, onload: function(frm) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 502e65ed8d..e27bd2fa3a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -571,15 +571,6 @@ frappe.ui.form.on('Sales Invoice', { }; }); - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company, - is_group: 0 - } - }; - }); - frm.custom_make_buttons = { 'Delivery Note': 'Delivery', 'Sales Invoice': 'Sales Return', diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index d0904eec3e..7cfbfed138 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -3,14 +3,6 @@ frappe.ui.form.on('Shipping Rule', { refresh: function(frm) { - frm.set_query("cost_center", function() { - return { - filters: { - company: frm.doc.company - } - } - }) - frm.set_query("account", function() { return { filters: { diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 7ad164a8b9..3af3948fac 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -31,14 +31,6 @@ frappe.ui.form.on('Asset', { } }; }); - - frm.set_query("cost_center", function() { - return { - "filters": { - "company": frm.doc.company, - } - }; - }); }, setup: function(frm) { diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 221300b519..cbafd7d3ac 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -167,15 +167,6 @@ frappe.ui.form.on("Expense Claim", { }; }); - frm.set_query("cost_center", "expenses", function() { - return { - filters: { - "company": frm.doc.company, - "is_group": 0 - } - }; - }); - frm.set_query("payable_account", function() { return { filters: { diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 1abc869c53..96006158b6 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -113,14 +113,7 @@ frappe.ui.form.on('Payroll Entry', { } }; }), - frm.set_query("cost_center", function () { - return { - filters: { - "is_group": 0, - company: frm.doc.company - } - }; - }), + frm.set_query("project", function () { return { filters: { diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 6e97d811fc..45c494e3e5 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -31,15 +31,6 @@ frappe.ui.form.on(cur_frm.doctype, { } } }); - - frm.set_query("cost_center", "taxes", function(doc) { - return { - filters: { - 'company': doc.company, - "is_group": 0 - } - } - }); } }, validate: function(frm) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1358a4bd08..ec6b3dc6df 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -159,16 +159,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }; }); } - if (this.frm.fields_dict["items"].grid.get_field("cost_center")) { - this.frm.set_query("cost_center", "items", function(doc) { - return { - filters: { - "company": doc.company, - "is_group": 0 - } - }; - }); - } if (this.frm.fields_dict["items"].grid.get_field("expense_account")) { this.frm.set_query("expense_account", "items", function(doc) { From 1f9b03345dd6360e5b8fc2df98948cebe1e53a1e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 09:55:35 +0530 Subject: [PATCH 10/73] fix: Remove project filter --- erpnext/accounts/doctype/budget/budget.js | 8 -------- erpnext/payroll/doctype/payroll_entry/payroll_entry.js | 10 +--------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index 48cc493522..1b79398247 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -3,14 +3,6 @@ frappe.ui.form.on('Budget', { onload: function(frm) { - frm.set_query("project", function() { - return { - filters: { - company: frm.doc.company - } - } - }) - frm.set_query("account", "accounts", function() { return { filters: { diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 96006158b6..bc34d6c3b1 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -112,15 +112,7 @@ frappe.ui.form.on('Payroll Entry', { "company": frm.doc.company } }; - }), - - frm.set_query("project", function () { - return { - filters: { - company: frm.doc.company - } - }; - }); + }) }, payroll_frequency: function (frm) { From 9e9ea965824c8562d915d3983641b0df1bafec15 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 12:03:47 +0530 Subject: [PATCH 11/73] fix: linting --- .../accounting_dimension_filter.js | 10 +++++----- .../accounting_dimension_filter.py | 1 - erpnext/payroll/doctype/payroll_entry/payroll_entry.js | 2 +- erpnext/public/js/queries.js | 2 +- erpnext/public/js/utils/dimension_tree_filter.js | 9 ++++----- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 6c254fcfb7..3e880d3ca6 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -5,13 +5,13 @@ frappe.ui.form.on('Accounting Dimension Filter', { onload: function(frm) { frappe.db.get_list('Accounting Dimension', {fields: ['name']}).then((res) => { - let options = ['Cost Center', 'Project']; + let options = ['Cost Center', 'Project']; - res.forEach((dimension) => { - options.push(dimension.name); - }); + res.forEach((dimension) => { + options.push(dimension.name); + }); - frm.set_df_property('accounting_dimension', 'options', options); + frm.set_df_property('accounting_dimension', 'options', options); }); }, diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index ccfafd96ed..0dcf116423 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -44,7 +44,6 @@ def get_dimension_filter_map(): """, as_dict=1) dimension_filter_map = {} - account_filter_map = {} for f in filters: if f.accounting_dimension in ('Cost Center', 'Project'): diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index bc34d6c3b1..d32fdbcaf1 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -112,7 +112,7 @@ frappe.ui.form.on('Payroll Entry', { "company": frm.doc.company } }; - }) + }); }, payroll_frequency: function (frm) { diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 7b7a9df1ac..98f1b504cc 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -134,7 +134,7 @@ $.extend(erpnext.queries, { 'account': account, 'company': company } - } + }; } }); diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 34b563553e..7a42fb5614 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -1,5 +1,4 @@ frappe.provide('frappe.ui.form'); - let default_dimensions = {}; let doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", @@ -34,7 +33,7 @@ doctypes_with_dimensions.forEach((doctype) => { parent_fields.push(df.fieldname); } else if (df.fieldtype === 'Table') { setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); - }; + } setup_account_filters(frm, dimension['fieldname'], parent_fields); }); @@ -90,7 +89,7 @@ let copy_dimension = function(frm, cdt, cdn, fieldname) { let row = frappe.get_doc(cdt, cdn); frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); }); -} +}; let setup_child_filters = function(frm, doctype, parentfield, dimension) { let fields = []; @@ -107,10 +106,10 @@ let setup_child_filters = function(frm, doctype, parentfield, dimension) { return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company); }); }); -} +}; let setup_account_filters = function(frm, dimension, fields) { frm.set_query(dimension, function(doc) { return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); }); -} \ No newline at end of file +}; \ No newline at end of file From d51c953c22a2de6824fbe88e5d9989db19220d1b Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Nov 2020 12:48:40 +0530 Subject: [PATCH 12/73] chore: Fix Tests, use project name instead of project_name field --- .../accounts/doctype/budget/test_budget.py | 15 +++++++--- .../journal_entry/test_journal_entry.py | 23 +++++++++------ .../purchase_invoice/test_purchase_invoice.py | 29 +++++++++++-------- .../sales_invoice/test_sales_invoice.py | 14 ++++----- .../doctype/crop_cycle/test_crop_cycle.py | 2 +- .../test_employee_onboarding.py | 3 +- .../expense_claim/test_expense_claim.py | 17 ++++++----- .../projects/doctype/project/test_project.py | 4 +++ erpnext/projects/doctype/task/test_task.py | 8 +++-- .../doctype/timesheet/test_timesheet.py | 5 ++-- 10 files changed, 73 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 0f115f9cc2..62d17c4e69 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -122,8 +122,10 @@ class TestBudget(unittest.TestCase): frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") + project = frappe.get_value("Project", {"project_name": "_Test Project"}) + jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) + "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -147,8 +149,11 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Project") + project = frappe.get_value("Project", {"project_name": "_Test Project"}) + jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate()) + "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", + project=project, posting_date=nowdate()) self.assertRaises(BudgetError, jv.submit) @@ -184,9 +189,11 @@ class TestBudget(unittest.TestCase): if month > 10: month = 10 + project = frappe.get_value("Project", {"project_name": "_Test Project"}) for i in range(month): jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", - "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") + "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, + project=project) self.assertTrue(frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})) @@ -289,7 +296,7 @@ def make_budget(**args): budget = frappe.new_doc("Budget") if budget_against == "Project": - budget.project = "_Test Project" + budget.project = frappe.get_value("Project", {"project_name": "_Test Project"}) else: budget.cost_center =cost_center or "_Test Cost Center - _TC" diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 53c07583d8..402ea5085e 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -168,7 +168,7 @@ class TestJournalEntry(unittest.TestCase): self.assertFalse(gle) def test_reverse_journal_entry(self): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry + from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry jv = make_journal_entry("_Test Bank USD - _TC", "Sales - _TC", 100, exchange_rate=50, save=False) @@ -307,15 +307,20 @@ class TestJournalEntry(unittest.TestCase): def test_jv_with_project(self): from erpnext.projects.doctype.project.test_project import make_project - project = make_project({ - 'project_name': 'Journal Entry Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2020-01-01' - }) + + if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}): + project = make_project({ + 'project_name': 'Journal Entry Project', + 'project_template_name': 'Test Project Template', + 'start_date': '2020-01-01' + }) + project_name = project.name + else: + project_name = frappe.get_value("Project", {"project_name": "_Test Project"}) jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) for d in jv.accounts: - d.project = project.project_name + d.project = project_name jv.voucher_type = "Bank Entry" jv.multi_currency = 0 jv.cheque_no = "112233" @@ -325,10 +330,10 @@ class TestJournalEntry(unittest.TestCase): expected_values = { "_Test Cash - _TC": { - "project": project.project_name + "project": project_name }, "_Test Bank - _TC": { - "project": project.project_name + "project": project_name } } diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2e5a7142a3..26acad3a4f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -436,26 +436,31 @@ class TestPurchaseInvoice(unittest.TestCase): ) def test_total_purchase_cost_for_project(self): - make_project({'project_name':'_Test Project'}) + if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}): + project = make_project({'project_name':'_Test Project for Purchase'}) + else: + project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"}) existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) - from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""") + from `tabPurchase Invoice Item` + where project = '{0}' + and docstatus=1""".format(project.name)) existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 - pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project") - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + 15000) - pi1 = make_purchase_invoice(qty=10, project="_Test Project") - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + pi1 = make_purchase_invoice(qty=10, project=project.name) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + 15500) pi1.cancel() - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + 15000) pi.cancel() - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost) def test_return_purchase_invoice(self): set_perpetual_inventory() @@ -874,17 +879,17 @@ class TestPurchaseInvoice(unittest.TestCase): }) pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) - pi.items[0].project = item_project.project_name - pi.project = project.project_name + pi.items[0].project = item_project.name + pi.project = project.name pi.submit() expected_values = { "Creditors - _TC": { - "project": project.project_name + "project": project.name }, "_Test Account Cost for Goods Sold - _TC": { - "project": item_project.project_name + "project": item_project.name } } diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9660c9570e..29b1ea2f9f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1571,7 +1571,7 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - + def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project @@ -1587,17 +1587,17 @@ class TestSalesInvoice(unittest.TestCase): }) sales_invoice = create_sales_invoice(do_not_save=1) - sales_invoice.items[0].project = item_project.project_name - sales_invoice.project = project.project_name + sales_invoice.items[0].project = item_project.name + sales_invoice.project = project.name sales_invoice.submit() expected_values = { "Debtors - _TC": { - "project": project.project_name + "project": project.name }, "Sales - _TC": { - "project": item_project.project_name + "project": item_project.name } } @@ -1605,9 +1605,9 @@ class TestSalesInvoice(unittest.TestCase): debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s order by account asc""", sales_invoice.name, as_dict=1) - + self.assertTrue(gl_entries) - + for gle in gl_entries: self.assertEqual(expected_values[gle.account]["project"], gle.project) diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py index 5510d5ac02..763b4036c3 100644 --- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py @@ -71,4 +71,4 @@ def check_task_creation(): def check_project_creation(): - return True if frappe.db.exists('Project', 'Basil from seed 2017') else False + return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index 4e9ee3b143..336e13c9b7 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -38,7 +38,8 @@ class TestEmployeeOnboarding(unittest.TestCase): onboarding.insert() onboarding.submit() - self.assertEqual(onboarding.project, 'Employee Onboarding : Test Researcher - test@researcher.com') + project_name = frappe.db.get_value("Project", onboarding.project, "project_name") + self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com') # don't allow making employee if onboarding is not complete self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 6e97f0513d..d9b472cce7 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -19,35 +19,36 @@ class TestExpenseClaim(unittest.TestCase): frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """) frappe.db.sql("update `tabExpense Claim` set project = '', task = ''") - frappe.get_doc({ + project = frappe.get_doc({ "project_name": "_Test Project 1", "doctype": "Project" - }).save() + }) + project.save() task = frappe.get_doc(dict( doctype = 'Task', subject = '_Test Project Task 1', status = 'Open', - project = '_Test Project 1' + project = project.name )).insert() task_name = task.name payable_account = get_payable_account(company_name) - make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", "_Test Project 1", task_name) + make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) - self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200) - expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4","_Test Project 1", task_name) + expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) - self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700) expense_claim2.cancel() self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) - self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200) def test_expense_claim_status(self): payable_account = get_payable_account(company_name) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 0c4f6f1bdf..f31225e36b 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -47,6 +47,10 @@ def get_project(name): def make_project(args): args = frappe._dict(args) + + if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}): + return frappe.get_doc("Project", {"project_name": args.project_name}) + if args.project_template_name: template = make_project_template(args.project_template_name) else: diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 47a28fd111..6ad8a19532 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -30,14 +30,16 @@ class TestTask(unittest.TestCase): }) def test_reschedule_dependent_task(self): + project = frappe.get_value("Project", {"project_name": "_Test Project"}) + task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10)) task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name) - task2.get("depends_on")[0].project = "_Test Project" + task2.get("depends_on")[0].project = project task2.save() task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name) - task3.get("depends_on")[0].project = "_Test Project" + task3.get("depends_on")[0].project = project task3.save() task1.update({ @@ -104,7 +106,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, sa task.subject = subject task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() - task.project = project or "_Test Project" + task.project = project or frappe.get_value("Project", {"project_name": "_Test Project"}) if save: task.save() else: diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index a5ce44dcf2..4cb38049ff 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -89,10 +89,11 @@ class TestTimesheet(unittest.TestCase): def test_timesheet_billing_based_on_project(self): emp = make_employee("test_employee_6@salary.com") + project = frappe.get_value("Project", {"project_name": "_Test Project"}) - timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company') + timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company') sales_invoice = create_sales_invoice(do_not_save=True) - sales_invoice.project = '_Test Project' + sales_invoice.project = project sales_invoice.submit() ts = frappe.get_doc('Timesheet', timesheet.name) From 0c6319194e7fb498e9639d6efb66cbf1a629df89 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 13:20:19 +0530 Subject: [PATCH 13/73] fix: GL Entry validation for dimensions --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index def9ed6803..1ac607940f 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -13,6 +13,8 @@ from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts +from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map +from six import iteritems exclude_from_linked_with = True class GLEntry(Document): @@ -37,6 +39,7 @@ class GLEntry(Document): def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'): self.validate_account_details(adv_adj) self.validate_dimensions_for_pl_and_bs() + self.validate_allowed_dimensions() validate_frozen_account(self.account, adv_adj) validate_balance_type(self.account, adv_adj) @@ -91,6 +94,21 @@ class GLEntry(Document): frappe.throw(_("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.") .format(dimension.label, self.account)) + def validate_allowed_dimensions(self): + dimension_filter_map = get_dimension_filter_map() + for key, value in iteritems(dimension_filter_map): + dimension = key[0] + account = key[1] + + if self.account == account: + if value['allow_or_restrict'] == 'Allow': + if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: + frappe.throw(_("Invalid value {0} for account {1}").format( + frappe.bold(self.get(dimension)), frappe.bold(self.account))) + else: + if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: + frappe.throw(_("Invalid value {0} for account {1}").format( + frappe.bold(self.get(dimension)), frappe.bold(self.account))) def check_pl_account(self): if self.is_opening=='Yes' and \ From d82c0f3beafe43146f396dce9593edd4a9d69557 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 20:32:16 +0530 Subject: [PATCH 14/73] fix: Add disable and mandatory check for accounting dimension filters --- .../accounting_dimension_filter.js | 37 ++++++++++++++++++- .../accounting_dimension_filter.json | 23 +++++++++++- .../accounting_dimension_filter.py | 8 ++-- .../allowed_dimension/allowed_dimension.json | 3 +- .../applicable_on_account.json | 15 +++++++- erpnext/accounts/doctype/gl_entry/gl_entry.py | 6 ++- 6 files changed, 80 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 3e880d3ca6..c8c32d58bd 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -3,16 +3,48 @@ frappe.ui.form.on('Accounting Dimension Filter', { onload: function(frm) { + frm.set_query('applicable_on_account', 'accounts', function() { + return { + filters : { + 'company': frm.doc.company + } + } + }); + frappe.db.get_list('Accounting Dimension', - {fields: ['name']}).then((res) => { + {fields: ['document_type']}).then((res) => { let options = ['Cost Center', 'Project']; res.forEach((dimension) => { - options.push(dimension.name); + options.push(dimension.document_type); }); frm.set_df_property('accounting_dimension', 'options', options); }); + + frm.trigger('setup_filters'); + }, + + setup_filters: function(frm) { + let filters = {}; + + frappe.model.with_doctype(frm.doc.accounting_dimension, function() { + if (frm.doc.accounting_dimension) { + if (frappe.model.is_tree(frm.doc.accounting_dimension)) { + filters['is_group'] = 0; + } + + if (frappe.meta.has_field(frm.doc.accounting_dimension, 'company')) { + filters['company'] = frm.doc.company; + } + + frm.set_query('dimension_value', 'dimensions', function() { + return { + filters: filters + } + }); + } + }); }, accounting_dimension: function(frm) { @@ -20,6 +52,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { let row = frm.add_child("dimensions"); row.accounting_dimension = frm.doc.accounting_dimension; frm.refresh_field("dimensions"); + frm.trigger('setup_filters'); }, }); diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index e626a09ce0..c1190a395f 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -7,8 +7,10 @@ "engine": "InnoDB", "field_order": [ "accounting_dimension", - "column_break_2", "allow_or_restrict", + "column_break_2", + "company", + "disabled", "section_break_4", "accounts", "column_break_6", @@ -70,11 +72,28 @@ "reqd": 1, "show_days": 1, "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-14 18:02:02.616932", + "modified": "2020-11-16 17:27:40.292860", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 0dcf116423..210b2c8ea8 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -32,12 +32,13 @@ def get_dimension_filter_map(): filters = frappe.db.sql( """ SELECT a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, ad.fieldname + p.allow_or_restrict, ad.fieldname, a.is_mandatory FROM `tabApplicable On Account` a, `tabAllowed Dimension` d, `tabAccounting Dimension Filter` p, `tabAccounting Dimension` ad WHERE p.name = a.parent + AND p.disabled = 0 AND p.name = d.parent AND (p.accounting_dimension = ad.name OR p.accounting_dimension in ('Cost Center', 'Project')) @@ -50,13 +51,14 @@ def get_dimension_filter_map(): f.fieldname = scrub(f.accounting_dimension) build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value, - f.allow_or_restrict) + f.allow_or_restrict, f.is_mandatory) return dimension_filter_map -def build_map(map_object, dimension, account, filter_value, allow_or_restrict): +def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): map_object.setdefault((dimension, account), { 'allowed_dimensions': [], + 'is_mandatory': is_mandatory, 'allow_or_restrict': allow_or_restrict }) map_object[(dimension, account)]['allowed_dimensions'].append(filter_value) diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json index 20024b0322..c2d34b3b7e 100644 --- a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json @@ -22,6 +22,7 @@ "fieldname": "dimension_value", "fieldtype": "Dynamic Link", "in_list_view": 1, + "label": "Applicable Dimension", "options": "accounting_dimension", "show_days": 1, "show_seconds": 1 @@ -30,7 +31,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-14 19:54:03.269016", + "modified": "2020-11-16 17:41:50.422843", "modified_by": "Administrator", "module": "Accounts", "name": "Allowed Dimension", diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json index 8305da2ba0..5c809515c2 100644 --- a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json @@ -5,7 +5,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "applicable_on_account" + "applicable_on_account", + "is_mandatory" ], "fields": [ { @@ -17,12 +18,22 @@ "reqd": 1, "show_days": 1, "show_seconds": 1 + }, + { + "columns": 2, + "default": "0", + "fieldname": "is_mandatory", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Mandatory", + "show_days": 1, + "show_seconds": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-14 16:54:06.756883", + "modified": "2020-11-16 13:36:59.129672", "modified_by": "Administrator", "module": "Accounts", "name": "Applicable On Account", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 1ac607940f..b3caf6a82e 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -77,11 +77,9 @@ class GLEntry(Document): .format(self.voucher_type, self.voucher_no, self.account)) def validate_dimensions_for_pl_and_bs(self): - account_type = frappe.db.get_value("Account", self.account, "report_type") for dimension in get_checks_for_pl_and_bs_accounts(): - if account_type == "Profit and Loss" \ and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled: if not self.get(dimension.fieldname): @@ -101,6 +99,10 @@ class GLEntry(Document): account = key[1] if self.account == account: + if value['is_mandatory'] and not self.get(dimension): + frappe.throw(_("{0} is mandatory for account {1}").format( + frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account))) + if value['allow_or_restrict'] == 'Allow': if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: frappe.throw(_("Invalid value {0} for account {1}").format( From 4bd52b48424c2fbe02d5e00ca5ddd3ce4665eb44 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Nov 2020 23:01:36 +0530 Subject: [PATCH 15/73] fix: Add test case --- .../test_accounting_dimension.py | 64 ++++++++-------- .../accounting_dimension_filter.py | 10 +-- .../test_accounting_dimension_filter.py | 75 ++++++++++++++++++- 3 files changed, 110 insertions(+), 39 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 104880f6f3..b5375e106f 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -11,37 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import d class TestAccountingDimension(unittest.TestCase): def setUp(self): - frappe.set_user("Administrator") - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc({ - "doctype": "Accounting Dimension", - "document_type": "Department", - }).insert() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 0 - dimension1.save() - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): - dimension1 = frappe.get_doc({ - "doctype": "Accounting Dimension", - "document_type": "Location", - }) - - dimension1.append("dimension_defaults", { - "company": "_Test Company", - "reference_document": "Location", - "default_dimension": "Block 1", - "mandatory_for_bs": 1 - }) - - dimension1.insert() - dimension1.save() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Location") - dimension1.disabled = 0 - dimension1.save() + create_dimension() def test_dimension_against_sales_invoice(self): si = create_sales_invoice(do_not_save=1) @@ -101,6 +71,38 @@ class TestAccountingDimension(unittest.TestCase): def tearDown(self): disable_dimension() +def create_dimension(): + frappe.set_user("Administrator") + + if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): + dimension = frappe.get_doc({ + "doctype": "Accounting Dimension", + "document_type": "Department", + }).insert() + else: + dimension1 = frappe.get_doc("Accounting Dimension", "Department") + dimension1.disabled = 0 + dimension1.save() + + if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): + dimension1 = frappe.get_doc({ + "doctype": "Accounting Dimension", + "document_type": "Location", + }) + + dimension1.append("dimension_defaults", { + "company": "_Test Company", + "reference_document": "Location", + "default_dimension": "Block 1", + "mandatory_for_bs": 1 + }) + + dimension1.insert() + dimension1.save() + else: + dimension1 = frappe.get_doc("Accounting Dimension", "Location") + dimension1.disabled = 0 + dimension1.save() def disable_dimension(): dimension1 = frappe.get_doc("Accounting Dimension", "Department") diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 210b2c8ea8..440073b723 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -32,23 +32,21 @@ def get_dimension_filter_map(): filters = frappe.db.sql( """ SELECT a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, ad.fieldname, a.is_mandatory + p.allow_or_restrict, a.is_mandatory FROM `tabApplicable On Account` a, `tabAllowed Dimension` d, - `tabAccounting Dimension Filter` p, `tabAccounting Dimension` ad + `tabAccounting Dimension Filter` p WHERE p.name = a.parent AND p.disabled = 0 AND p.name = d.parent - AND (p.accounting_dimension = ad.name - OR p.accounting_dimension in ('Cost Center', 'Project')) + """, as_dict=1) dimension_filter_map = {} for f in filters: - if f.accounting_dimension in ('Cost Center', 'Project'): - f.fieldname = scrub(f.accounting_dimension) + f.fieldname = scrub(f.accounting_dimension) build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value, f.allow_or_restrict, f.is_mandatory) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index c271a25fbd..feb0af1bd5 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -3,8 +3,79 @@ # See license.txt from __future__ import unicode_literals -# import frappe +import frappe import unittest +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension class TestAccountingDimensionFilter(unittest.TestCase): - pass + def setUp(self): + create_accounting_dimension_filter() + + def test_allowed_dimension_validation(self): + si = create_sales_invoice(do_not_save=1) + si.items[0].cost_center = 'Main - _TC' + si.save() + + self.assertRaises(frappe.ValidationError, si.submit) + + def test_mandatory_dimension_validation(self): + si = create_sales_invoice(do_not_save=1) + si.items[0].location = '' + si.save() + + self.assertRaises(frappe.ValidationError, si.submit) + + def tearDown(self): + disable_dimension_filter() + +def create_accounting_dimension_filter(): + if not frappe.db.get_value('Accounting Dimension Filter', + {'accounting_dimension': 'Cost Center'}): + frappe.get_doc({ + 'doctype': 'Accounting Dimension Filter', + 'accounting_dimension': 'Cost Center', + 'allow_or_restrict': 'Allow', + 'company': '_Test Company', + 'accounts': [{ + 'applicable_on_account': 'Sales - _TC', + }], + 'dimensions': [{ + 'accounting_dimension': 'Cost Center', + 'dimension_value': '_Test Cost Center 3 - _TC' + }] + }).insert() + else: + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) + doc.disabled = 0 + doc.save() + + if not frappe.db.get_value('Accounting Dimension Filter', + {'accounting_dimension': 'Location'}): + frappe.get_doc({ + 'doctype': 'Accounting Dimension Filter', + 'accounting_dimension': 'Location', + 'allow_or_restrict': 'Allow', + 'company': '_Test Company', + 'accounts': [{ + 'applicable_on_account': 'Sales - _TC', + 'is_mandatory': 1 + }], + 'dimensions': [{ + 'accounting_dimension': 'Location', + 'dimension_value': 'Block 1' + }] + }).insert() + else: + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc.disabled = 0 + doc.save() + +def disable_dimension_filter(): + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) + doc.disabled = 0 + doc.save() + + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc.disabled = 0 + doc.save() From 6456c3dc244c0b294748707ed08e558826f3ab65 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Nov 2020 11:10:00 +0530 Subject: [PATCH 16/73] fix: Linting and test cases --- .../accounting_dimension/test_accounting_dimension.py | 8 ++++---- .../accounting_dimension_filter.js | 4 ++-- .../test_accounting_dimension_filter.py | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index b5375e106f..fc1d7e344a 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -75,14 +75,14 @@ def create_dimension(): frappe.set_user("Administrator") if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc({ + frappe.get_doc({ "doctype": "Accounting Dimension", "document_type": "Department", }).insert() else: - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 0 - dimension1.save() + dimension = frappe.get_doc("Accounting Dimension", "Department") + dimension.disabled = 0 + dimension.save() if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): dimension1 = frappe.get_doc({ diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index c8c32d58bd..994ee44354 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -8,7 +8,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { filters : { 'company': frm.doc.company } - } + }; }); frappe.db.get_list('Accounting Dimension', @@ -41,7 +41,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { frm.set_query('dimension_value', 'dimensions', function() { return { filters: filters - } + }; }); } }); diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index feb0af1bd5..02fd75e759 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -10,6 +10,7 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): + create_dimension() create_accounting_dimension_filter() def test_allowed_dimension_validation(self): @@ -42,7 +43,7 @@ def create_accounting_dimension_filter(): }], 'dimensions': [{ 'accounting_dimension': 'Cost Center', - 'dimension_value': '_Test Cost Center 3 - _TC' + 'dimension_value': '_Test Cost Center 2 - _TC' }] }).insert() else: From 8b4d92d4fcc8ffbd92366dd63bd66122719156d2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Nov 2020 13:19:29 +0530 Subject: [PATCH 17/73] fix: Disable filters after test --- .../test_accounting_dimension_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 02fd75e759..801786b6e9 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -74,9 +74,9 @@ def create_accounting_dimension_filter(): def disable_dimension_filter(): doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}) - doc.disabled = 0 + doc.disabled = 1 doc.save() doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) - doc.disabled = 0 + doc.disabled = 1 doc.save() From 350972ece4e80b66d02f7943acdc2cb13f277f6f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Nov 2020 13:51:58 +0530 Subject: [PATCH 18/73] fix: Account filters --- .../accounting_dimension_filter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index 994ee44354..f0362d3140 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -28,8 +28,8 @@ frappe.ui.form.on('Accounting Dimension Filter', { setup_filters: function(frm) { let filters = {}; - frappe.model.with_doctype(frm.doc.accounting_dimension, function() { - if (frm.doc.accounting_dimension) { + if (frm.doc.accounting_dimension) { + frappe.model.with_doctype(frm.doc.accounting_dimension, function() { if (frappe.model.is_tree(frm.doc.accounting_dimension)) { filters['is_group'] = 0; } @@ -43,8 +43,8 @@ frappe.ui.form.on('Accounting Dimension Filter', { filters: filters }; }); - } - }); + }); + } }, accounting_dimension: function(frm) { From 6c17b84caef6300f3872ce5b5db031e0ec7501fd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 13:42:16 +0530 Subject: [PATCH 19/73] fix: Replace raw query with ORM --- erpnext/controllers/queries.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 015807d563..e3aac9aba8 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -499,16 +499,17 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map dimension_filters = get_dimension_filter_map() dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account'))) - group_condition = '' - company_condition = '' + query_filters = [] meta = frappe.get_meta(doctype) - if meta.is_tree: - group_condition = 'and is_group = 0 ' + query_filters.append(['is_group', '=', 0]) if meta.has_field('company'): - company_condition = 'and company = %s ' % (frappe.db.escape(filters.get('company'))) + query_filters.append(['company', '=', filters.get('company')]) + + if txt: + query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt]) if dimension_filters: if dimension_filters['allow_or_restrict'] == 'Allow': @@ -521,25 +522,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) else: dimensions = tuple(dimension_filters['allowed_dimensions']) - result = frappe.db.sql("""SELECT name from `tab{doctype}` where - name {query_selector} {restricted} - {group_condition} {company_condition} - and {key} LIKE %(txt)s""".format( - doctype=doctype, query_selector=query_selector, restricted=dimensions, - group_condition = group_condition, - company_condition = company_condition, - key=searchfield), { - 'txt': '%' + txt + '%' - }) + query_filters.append(['name', query_selector, dimensions]) - return result - else: - return frappe.db.sql(""" - SELECT name from `tab{doctype}` where - {key} LIKE %(txt)s {group_condition} {company_condition}""" - .format(doctype=doctype, key=searchfield, - group_condition=group_condition, company_condition=company_condition), - { 'txt': '%' + txt + '%'}) + output = frappe.get_all(doctype, filters=query_filters) + result = [d.name for d in output] + + return [(d,) for d in set(result)] @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From df065f7044df675d161314992a2eaeec83971839 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 13:44:52 +0530 Subject: [PATCH 20/73] fix: form layout and naming fixes --- .../accounting_dimension.js | 1 - .../accounting_dimension.py | 4 +-- .../accounting_dimension_filter.js | 7 +++++- .../accounting_dimension_filter.json | 10 ++++---- .../accounting_dimension_filter.py | 25 +++++++++---------- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 9a6c389339..65c5ff1cea 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -2,7 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Accounting Dimension', { - refresh: function(frm) { frm.set_query('document_type', () => { let invalid_doctypes = frappe.model.core_doctypes_list; diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index b9d4da289e..52e9ff8b76 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -203,7 +203,7 @@ def get_dimension_with_children(doctype, dimension): return all_dimensions @frappe.whitelist() -def get_dimension_filters(with_costcenter_and_project=False): +def get_dimensions(with_cost_center_and_project=False): dimension_filters = frappe.db.sql(""" SELECT label, fieldname, document_type FROM `tabAccounting Dimension` @@ -214,7 +214,7 @@ def get_dimension_filters(with_costcenter_and_project=False): FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p WHERE c.parent = p.name""", as_dict=1) - if with_costcenter_and_project: + if with_cost_center_and_project: dimension_filters.extend([ { 'fieldname': 'cost_center', diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index f0362d3140..a2526e92c3 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -2,10 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('Accounting Dimension Filter', { + refresh: function(frm, cdt, cdn) { + if (frm.doc.accounting_dimension) { + frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value'); + } + }, onload: function(frm) { frm.set_query('applicable_on_account', 'accounts', function() { return { - filters : { + filters: { 'company': frm.doc.company } }; diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index c1190a395f..7736b2dffb 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -7,10 +7,10 @@ "engine": "InnoDB", "field_order": [ "accounting_dimension", - "allow_or_restrict", + "disabled", "column_break_2", "company", - "disabled", + "allow_or_restrict", "section_break_4", "accounts", "column_break_6", @@ -57,7 +57,7 @@ { "fieldname": "accounts", "fieldtype": "Table", - "label": "Accounts", + "label": "Applicable On Account", "options": "Applicable On Account", "reqd": 1, "show_days": 1, @@ -67,7 +67,7 @@ "depends_on": "eval:doc.accounting_dimension", "fieldname": "dimensions", "fieldtype": "Table", - "label": "Dimensions", + "label": "Applicable Dimension", "options": "Allowed Dimension", "reqd": 1, "show_days": 1, @@ -93,7 +93,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-16 17:27:40.292860", + "modified": "2020-11-24 12:34:42.458713", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 440073b723..6aef9caa74 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -29,19 +29,18 @@ class AccountingDimensionFilter(Document): account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension))) def get_dimension_filter_map(): - filters = frappe.db.sql( - """ SELECT - a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, a.is_mandatory - FROM - `tabApplicable On Account` a, `tabAllowed Dimension` d, - `tabAccounting Dimension Filter` p - WHERE - p.name = a.parent - AND p.disabled = 0 - AND p.name = d.parent - - """, as_dict=1) + filters = frappe.db.sql(""" + SELECT + a.applicable_on_account, d.dimension_value, p.accounting_dimension, + p.allow_or_restrict, a.is_mandatory + FROM + `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabAccounting Dimension Filter` p + WHERE + p.name = a.parent + AND p.disabled = 0 + AND p.name = d.parent + """, as_dict=1) dimension_filter_map = {} From 4e991fe668daafe8488560fddef806cbc540373d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 13:46:07 +0530 Subject: [PATCH 21/73] fix: Define specific exceptions and fix tests --- .../test_accounting_dimension_filter.py | 27 ++++++++++++------- erpnext/accounts/doctype/gl_entry/gl_entry.py | 8 +++--- erpnext/exceptions.py | 2 ++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 801786b6e9..f67e1de404 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -6,7 +6,8 @@ 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.accounting_dimension.test_accounting_dimension import create_dimension +from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension +from erpnext.exceptions import InvalidAccountDimension, MandatoryDimension class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): @@ -16,19 +17,25 @@ class TestAccountingDimensionFilter(unittest.TestCase): def test_allowed_dimension_validation(self): si = create_sales_invoice(do_not_save=1) si.items[0].cost_center = 'Main - _TC' + si.location = 'Block 1' si.save() - self.assertRaises(frappe.ValidationError, si.submit) + self.assertRaises(InvalidAccountDimension, si.submit) def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) - si.items[0].location = '' + si.location = 'Block 1' + + # Test with no department for Sales Account + si.items[0].department = '' + si.items[0].cost_center = '_Test Cost Center 2 - _TC' si.save() - self.assertRaises(frappe.ValidationError, si.submit) + self.assertRaises(MandatoryDimension, si.submit) def tearDown(self): disable_dimension_filter() + disable_dimension() def create_accounting_dimension_filter(): if not frappe.db.get_value('Accounting Dimension Filter', @@ -52,10 +59,10 @@ def create_accounting_dimension_filter(): doc.save() if not frappe.db.get_value('Accounting Dimension Filter', - {'accounting_dimension': 'Location'}): + {'accounting_dimension': 'Department'}): frappe.get_doc({ 'doctype': 'Accounting Dimension Filter', - 'accounting_dimension': 'Location', + 'accounting_dimension': 'Department', 'allow_or_restrict': 'Allow', 'company': '_Test Company', 'accounts': [{ @@ -63,12 +70,12 @@ def create_accounting_dimension_filter(): 'is_mandatory': 1 }], 'dimensions': [{ - 'accounting_dimension': 'Location', - 'dimension_value': 'Block 1' + 'accounting_dimension': 'Department', + 'dimension_value': '_Test Department - _TC' }] }).insert() else: - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) doc.disabled = 0 doc.save() @@ -77,6 +84,6 @@ def disable_dimension_filter(): doc.disabled = 1 doc.save() - doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Location'}) + doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'}) doc.disabled = 1 doc.save() diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index b3caf6a82e..f586de82e3 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -11,7 +11,7 @@ from frappe.model.meta import get_field_precision from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_fiscal_year -from erpnext.exceptions import InvalidAccountCurrency +from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimension, MandatoryDimension from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map from six import iteritems @@ -101,16 +101,16 @@ class GLEntry(Document): if self.account == account: if value['is_mandatory'] and not self.get(dimension): frappe.throw(_("{0} is mandatory for account {1}").format( - frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account))) + frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryDimension) if value['allow_or_restrict'] == 'Allow': if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: frappe.throw(_("Invalid value {0} for account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account))) + frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) else: if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: frappe.throw(_("Invalid value {0} for account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account))) + frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) def check_pl_account(self): if self.is_opening=='Yes' and \ diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py index d92af5d722..dcf3d6bad1 100644 --- a/erpnext/exceptions.py +++ b/erpnext/exceptions.py @@ -6,3 +6,5 @@ class PartyFrozen(frappe.ValidationError): pass class InvalidAccountCurrency(frappe.ValidationError): pass class InvalidCurrency(frappe.ValidationError): pass class PartyDisabled(frappe.ValidationError):pass +class InvalidAccountDimension(frappe.ValidationError): pass +class MandatoryDimension(frappe.ValidationError): pass From 92b3449789689fc71dbd2ea3df7a2f7553cb0dec Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 14:01:57 +0530 Subject: [PATCH 22/73] fix: Label changes --- .../accounts/doctype/allowed_dimension/allowed_dimension.json | 3 +-- .../doctype/applicable_on_account/applicable_on_account.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json index c2d34b3b7e..7fe2a3c647 100644 --- a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json +++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json @@ -22,7 +22,6 @@ "fieldname": "dimension_value", "fieldtype": "Dynamic Link", "in_list_view": 1, - "label": "Applicable Dimension", "options": "accounting_dimension", "show_days": 1, "show_seconds": 1 @@ -31,7 +30,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-16 17:41:50.422843", + "modified": "2020-11-23 09:56:19.744200", "modified_by": "Administrator", "module": "Accounts", "name": "Allowed Dimension", diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json index 5c809515c2..95e98d0b67 100644 --- a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json +++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json @@ -13,7 +13,7 @@ "fieldname": "applicable_on_account", "fieldtype": "Link", "in_list_view": 1, - "label": "Applicable On Account", + "label": "Accounts", "options": "Account", "reqd": 1, "show_days": 1, @@ -33,7 +33,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-16 13:36:59.129672", + "modified": "2020-11-22 19:55:13.324136", "modified_by": "Administrator", "module": "Accounts", "name": "Applicable On Account", From 6b9dda5f3eb7af999456ffbf4cc69c255c2f09b2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 14:49:10 +0530 Subject: [PATCH 23/73] fix: Move filter setup to doctype controllers --- erpnext/accounts/doctype/budget/budget.js | 7 +- .../doctype/journal_entry/journal_entry.js | 5 + .../loyalty_program/loyalty_program.js | 7 + .../opening_invoice_creation_tool.js | 3 + .../doctype/payment_entry/payment_entry.js | 4 + .../doctype/pos_profile/pos_profile.js | 3 + .../purchase_invoice/purchase_invoice.js | 7 + .../doctype/sales_invoice/sales_invoice.js | 8 +- .../doctype/shipping_rule/shipping_rule.js | 10 ++ erpnext/assets/doctype/asset/asset.js | 7 + .../asset_value_adjustment.js | 10 ++ .../doctype/purchase_order/purchase_order.js | 8 +- .../doctype/fee_schedule/fee_schedule.js | 7 + .../doctype/fee_structure/fee_structure.js | 8 + erpnext/education/doctype/fees/fees.js | 7 + .../hr/doctype/expense_claim/expense_claim.js | 20 ++- .../doctype/payroll_entry/payroll_entry.js | 5 + erpnext/public/js/controllers/transaction.js | 4 + .../public/js/utils/dimension_tree_filter.js | 164 ++++++++---------- .../doctype/delivery_note/delivery_note.js | 4 +- .../material_request/material_request.js | 7 + .../purchase_receipt/purchase_receipt.js | 3 + .../stock/doctype/stock_entry/stock_entry.js | 4 + .../stock_reconciliation.js | 7 + 24 files changed, 218 insertions(+), 101 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index 1b79398247..e60bc60475 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -1,5 +1,6 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Budget', { onload: function(frm) { @@ -11,7 +12,7 @@ frappe.ui.form.on('Budget', { is_group: 0 } } - }) + }); frm.set_query("monthly_distribution", function() { return { @@ -19,7 +20,9 @@ frappe.ui.form.on('Budget', { fiscal_year: frm.doc.fiscal_year } } - }) + }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d60a7b76cc..37b03f3f0e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -120,6 +120,8 @@ frappe.ui.form.on("Journal Entry", { } } }); + + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, voucher_type: function(frm){ @@ -197,6 +199,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ this.load_defaults(); this.setup_queries(); this.setup_balance_formatter(); + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, onload_post_render: function() { @@ -397,6 +400,8 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ } } cur_frm.cscript.update_totals(doc); + + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts'); }, }); diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js index 0d2b8cbf15..f90f86728d 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js @@ -1,6 +1,8 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Loyalty Program', { setup: function(frm) { var help_content = @@ -47,11 +49,16 @@ frappe.ui.form.on('Loyalty Program', { }); frm.set_value("company", frappe.defaults.get_user_default("Company")); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) { frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules.")); } + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } }); diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 3ce5701823..c087980798 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -36,6 +36,8 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); frm.page.set_indicator(__('In Progress'), 'orange'); }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -100,6 +102,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { } }) } + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, invoice_type: function(frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ea5487d575..4318aea2bd 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1,6 +1,7 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt {% include "erpnext/public/js/controllers/accounts.js" %} +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Payment Entry', { onload: function(frm) { @@ -8,6 +9,8 @@ frappe.ui.form.on('Payment Entry', { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, setup: function(frm) { @@ -158,6 +161,7 @@ frappe.ui.form.on('Payment Entry', { company: function(frm) { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, contact_person: function(frm) { diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 558e21c13a..668cf016d3 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -48,6 +48,8 @@ frappe.ui.form.on('POS Profile', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -58,6 +60,7 @@ frappe.ui.form.on('POS Profile', { company: function(frm) { frm.trigger("toggle_display_account_head"); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, toggle_display_account_head: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3c07ee75cb..3fa6ee76e6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -16,6 +16,11 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }); } }, + + company: function() { + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + }, + onload: function() { this._super(); @@ -31,6 +36,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger('supplier'); } + + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, refresh: function(doc) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index e27bd2fa3a..b89cecfab5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -5,14 +5,17 @@ cur_frm.pformat.print_heading = 'Invoice'; {% include 'erpnext/selling/sales_common.js' %}; - - frappe.provide("erpnext.accounts"); + + erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({ setup: function(doc) { this.setup_posting_date_time_check(); this._super(doc); }, + company: function() { + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + }, onload: function() { var me = this; this._super(); @@ -33,6 +36,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.refresh_fields(); } erpnext.queries.setup_warehouse_query(this.frm); + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, refresh: function(doc, dt, dn) { diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index 7cfbfed138..8e4b806f02 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -1,7 +1,17 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.provide('erpnext.accounts.dimensions'); + frappe.ui.form.on('Shipping Rule', { + onload: function(frm) { + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + refresh: function(frm) { frm.set_query("account", function() { return { diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 3af3948fac..e9c8aff678 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -2,6 +2,7 @@ // For license information, please see license.txt frappe.provide("erpnext.asset"); +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Asset', { onload: function(frm) { @@ -31,6 +32,12 @@ frappe.ui.form.on('Asset', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, setup: function(frm) { diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js index a6e6974c48..79c8861bcd 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js @@ -1,6 +1,8 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Asset Value Adjustment', { setup: function(frm) { frm.add_fetch('company', 'cost_center', 'cost_center'); @@ -13,11 +15,19 @@ frappe.ui.form.on('Asset Value Adjustment', { } }); }, + onload: function(frm) { if(frm.is_new() && frm.doc.asset) { frm.trigger("set_current_asset_value"); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + asset: function(frm) { frm.trigger("set_current_asset_value"); }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 2f52a9e035..92e2708eab 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -2,7 +2,7 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.buying"); - +frappe.provide("erpnext.accounts.dimensions"); {% include 'erpnext/public/js/controllers/buying.js' %}; frappe.ui.form.on("Purchase Order", { @@ -30,6 +30,10 @@ frappe.ui.form.on("Purchase Order", { }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + onload: function(frm) { set_schedule_date(frm); if (!frm.doc.transaction_date){ @@ -39,6 +43,8 @@ frappe.ui.form.on("Purchase Order", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); } }); diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.js b/erpnext/education/doctype/fee_schedule/fee_schedule.js index 75dd4469e8..65b5fa6cf2 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule.js +++ b/erpnext/education/doctype/fee_schedule/fee_schedule.js @@ -1,6 +1,7 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Fee Schedule', { setup: function(frm) { frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account'); @@ -8,6 +9,10 @@ frappe.ui.form.on('Fee Schedule', { frm.add_fetch('fee_structure', 'cost_center', 'cost_center'); }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + onload: function(frm) { frm.set_query('receivable_account', function(doc) { return { @@ -50,6 +55,8 @@ frappe.ui.form.on('Fee Schedule', { } } }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/education/doctype/fee_structure/fee_structure.js b/erpnext/education/doctype/fee_structure/fee_structure.js index b331c6d3c0..310c4105f4 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.js +++ b/erpnext/education/doctype/fee_structure/fee_structure.js @@ -1,6 +1,8 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Fee Structure', { setup: function(frm) { frm.add_fetch('company', 'default_receivable_account', 'receivable_account'); @@ -8,6 +10,10 @@ frappe.ui.form.on('Fee Structure', { frm.add_fetch('company', 'cost_center', 'cost_center'); }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + onload: function(frm) { frm.set_query('academic_term', function() { return { @@ -35,6 +41,8 @@ frappe.ui.form.on('Fee Structure', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index aaf42b4751..433bd64d2f 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -1,6 +1,7 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Fees", { setup: function(frm) { @@ -9,6 +10,10 @@ frappe.ui.form.on("Fees", { frm.add_fetch("fee_structure", "cost_center", "cost_center"); }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + onload: function(frm){ frm.set_query("academic_term",function(){ return{ @@ -45,6 +50,8 @@ frappe.ui.form.on("Fees", { if (!frm.doc.posting_date) { frm.doc.posting_date = frappe.datetime.get_today(); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index cbafd7d3ac..e399b22f90 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -2,11 +2,21 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.hr"); +frappe.provide("erpnext.accounts.dimensions"); -erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ - expense_type: function(doc, cdt, cdn) { +frappe.ui.form.on('Expense Claim', { + onload: function(frm) { + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, +}); + +frappe.ui.form.on('Expense Claim Detail', { + expense_type: function(frm, cdt, cdn) { var d = locals[cdt][cdn]; - if(!doc.company) { + if(!frm.doc.company) { d.expense_type = ""; frappe.msgprint(__("Please set the Company")); this.frm.refresh_fields(); @@ -20,7 +30,7 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center", args: { "expense_claim_type": d.expense_type, - "company": doc.company + "company": frm.doc.company }, callback: function(r) { if (r.message) { @@ -32,8 +42,6 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ } }); -$.extend(cur_frm.cscript, new erpnext.hr.ExpenseClaimController({frm: cur_frm})); - cur_frm.add_fetch('employee', 'company', 'company'); cur_frm.add_fetch('employee','employee_name','employee_name'); cur_frm.add_fetch('expense_type','description','description'); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index d32fdbcaf1..410840771c 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -3,6 +3,8 @@ var in_progress = false; +frappe.provide("erpnext.accounts.dimensions"); + frappe.ui.form.on('Payroll Entry', { onload: function (frm) { if (!frm.doc.posting_date) { @@ -17,6 +19,8 @@ frappe.ui.form.on('Payroll Entry', { } }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -122,6 +126,7 @@ frappe.ui.form.on('Payroll Entry', { company: function (frm) { frm.events.clear_employee_table(frm); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, department: function (frm) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ec6b3dc6df..3f293782b4 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.provide('erpnext.accounts.dimensions'); + erpnext.TransactionController = erpnext.taxes_and_totals.extend({ setup: function() { this._super(); @@ -106,6 +108,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(!item.warehouse && frm.doc.set_warehouse) { item.warehouse = frm.doc.set_warehouse; } + + erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items'); } }); diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 7a42fb5614..c79736d0e1 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -1,60 +1,79 @@ -frappe.provide('frappe.ui.form'); -let default_dimensions = {}; +frappe.provide('erpnext.accounts'); -let doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", - "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Shipping Rule", "Loyalty Program", - "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", - "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Asset", "Asset Value Adjustment"]; - -let child_docs = ["Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", - "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", - "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan", - "Sales Taxes and Charges", "Purchase Taxes and Charges"]; - -frappe.call({ - method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters", - args: { - 'with_costcenter_and_project': true +erpnext.accounts.dimensions = { + setup_dimension_filters(frm, doctype) { + this.accounting_dimensions = []; + this.default_dimensions = {}; + this.fetch_custom_dimensions(frm, doctype); }, - callback: function(r) { - erpnext.dimension_filters = r.message[0]; - default_dimensions = r.message[1]; - } -}); -doctypes_with_dimensions.forEach((doctype) => { - frappe.ui.form.on(doctype, { - onload: function(frm) { - erpnext.dimension_filters.forEach((dimension) => { - frappe.model.with_doctype(dimension['document_type'], () => { - let parent_fields = []; - frappe.meta.get_docfields(doctype).forEach((df) => { - if (df.fieldtype === 'Link' && df.options === 'Account') { - parent_fields.push(df.fieldname); - } else if (df.fieldtype === 'Table') { - setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); - } + fetch_custom_dimensions(frm, doctype) { + let me = this; + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + args: { + 'with_cost_center_and_project': true + }, + callback: function(r) { + me.accounting_dimensions = r.message[0]; + me.default_dimensions = r.message[1]; + me.setup_filters(frm, doctype); + } + }); + }, - setup_account_filters(frm, dimension['fieldname'], parent_fields); - }); + setup_filters(frm, doctype) { + this.accounting_dimensions.forEach((dimension) => { + frappe.model.with_doctype(dimension['document_type'], () => { + let parent_fields = []; + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + parent_fields.push(df.fieldname); + } else if (df.fieldtype === 'Table') { + this.setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); + } + + if (frappe.meta.has_field(doctype, dimension['fieldname'])) { + this.setup_account_filters(frm, dimension['fieldname'], parent_fields); + } }); }); - }, + }); + }, - company: function(frm) { - if(frm.doc.company && (Object.keys(default_dimensions || {}).length > 0) - && default_dimensions[frm.doc.company]) { - frm.trigger('update_dimension'); - } - }, + setup_child_filters(frm, doctype, parentfield, dimension) { + let fields = []; - update_dimension: function(frm) { - erpnext.dimension_filters.forEach((dimension) => { + if (frappe.meta.has_field(doctype, dimension)) { + frappe.model.with_doctype(doctype, () => { + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + fields.push(df.fieldname); + } + }); + + frm.set_query(dimension, parentfield, function(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company); + }); + }); + } + }, + + setup_account_filters(frm, dimension, fields) { + frm.set_query(dimension, function(doc) { + return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); + }); + }, + + update_dimension(frm, doctype) { + if (this.accounting_dimensions) { + this.accounting_dimensions.forEach((dimension) => { if(frm.is_new()) { - if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0 - && default_dimensions[frm.doc.company]) { + if(frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0 + && this.default_dimensions[frm.doc.company]) { - let default_dimension = default_dimensions[frm.doc.company][dimension['fieldname']]; + let default_dimension = this.default_dimensions[frm.doc.company][dimension['fieldname']]; if(default_dimension) { if (frappe.meta.has_field(doctype, dimension['fieldname'])) { @@ -69,47 +88,14 @@ doctypes_with_dimensions.forEach((doctype) => { } }); } - }); -}); + }, -child_docs.forEach((doctype) => { - frappe.ui.form.on(doctype, { - items_add: function(frm, cdt, cdn) { - copy_dimension(frm, cdt, cdn, "items"); - }, - - accounts_add: function(frm, cdt, cdn) { - copy_dimension(frm, cdt, cdn, "accounts"); + copy_dimension_from_first_row(frm, cdt, cdn, fieldname) { + if (frappe.meta.has_field(frm.doctype, fieldname)) { + this.accounting_dimensions.forEach((dimension) => { + let row = frappe.get_doc(cdt, cdn); + frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); + }); } - }); -}); - -let copy_dimension = function(frm, cdt, cdn, fieldname) { - erpnext.dimension_filters.forEach((dimension) => { - let row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); - }); -}; - -let setup_child_filters = function(frm, doctype, parentfield, dimension) { - let fields = []; - - frappe.model.with_doctype(doctype, () => { - frappe.meta.get_docfields(doctype).forEach((df) => { - if (df.fieldtype === 'Link' && df.options === 'Account') { - fields.push(df.fieldname); - } - }); - - frm.set_query(dimension, parentfield, function(doc, cdt, cdn) { - let row = locals[cdt][cdn]; - return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company); - }); - }); -}; - -let setup_account_filters = function(frm, dimension, fields) { - frm.set_query(dimension, function(doc) { - return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company); - }); -}; \ No newline at end of file + } +} \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 251a26a592..ccc719c26d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -7,6 +7,7 @@ cur_frm.add_fetch('customer', 'tax_id', 'tax_id'); frappe.provide("erpnext.stock"); frappe.provide("erpnext.stock.delivery_note"); +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Delivery Note", { setup: function(frm) { @@ -75,7 +76,7 @@ frappe.ui.form.on("Delivery Note", { } }); - + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, print_without_amount: function(frm) { @@ -305,6 +306,7 @@ frappe.ui.form.on('Delivery Note', { company: function(frm) { frm.trigger("unhide_account_head"); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, unhide_account_head: function(frm) { diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 01edd99e9d..527b0d3ea9 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -2,6 +2,7 @@ // License: GNU General Public License v3. See license.txt // eslint-disable-next-line +frappe.provide("erpnext.accounts.dimensions"); {% include 'erpnext/public/js/controllers/buying.js' %}; frappe.ui.form.on('Material Request', { @@ -66,6 +67,12 @@ frappe.ui.form.on('Material Request', { filters: {'company': doc.company} }; }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, onload_post_render: function(frm) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bc1d81d356..d998729c47 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -46,6 +46,8 @@ frappe.ui.form.on("Purchase Receipt", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { @@ -75,6 +77,7 @@ frappe.ui.form.on("Purchase Receipt", { company: function(frm) { frm.trigger("toggle_display_account_head"); + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, toggle_display_account_head: function(frm) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 91217582ca..80f18a63f5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1,6 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.stock"); +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on('Stock Entry', { setup: function(frm) { @@ -97,6 +98,7 @@ frappe.ui.form.on('Stock Entry', { }); frm.add_fetch("bom_no", "inspection_required", "inspection_required"); + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, setup_quality_inspection: function(frm) { @@ -312,6 +314,8 @@ frappe.ui.form.on('Stock Entry', { frm.set_value("letter_head", company_doc.default_letter_head); } frm.trigger("toggle_display_account_head"); + + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } }, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index e2121fce3f..be9404d9c8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -2,6 +2,7 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.stock"); +frappe.provide("erpnext.accounts.dimensions") frappe.ui.form.on("Stock Reconciliation", { onload: function(frm) { @@ -26,6 +27,12 @@ frappe.ui.form.on("Stock Reconciliation", { if (!frm.doc.expense_account) { frm.trigger("set_expense_account"); } + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, refresh: function(frm) { From ab5053ef9cbd5ce27f9253a3409bd7be24f597e2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Dec 2020 12:34:40 +0530 Subject: [PATCH 24/73] fix: Accounting dimension import in PCV --- .../doctype/period_closing_voucher/period_closing_voucher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 7dd5b01770..a74fa062b6 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -8,7 +8,7 @@ from frappe import _ from erpnext.accounts.utils import get_account_currency from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions, - get_dimension_filters) + get_dimensions) class PeriodClosingVoucher(AccountsController): def validate(self): @@ -58,7 +58,7 @@ class PeriodClosingVoucher(AccountsController): for dimension in accounting_dimensions: dimension_fields.append('t1.{0}'.format(dimension)) - dimension_filters, default_dimensions = get_dimension_filters() + dimension_filters, default_dimensions = get_dimensions() pl_accounts = self.get_pl_balances(dimension_fields) From 59820004b8b1084511664142e29e8d8dbde9e9c6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Dec 2020 12:34:59 +0530 Subject: [PATCH 25/73] fix: Exception naming --- .../test_accounting_dimension_filter.py | 6 +++--- erpnext/accounts/doctype/gl_entry/gl_entry.py | 8 ++++---- erpnext/exceptions.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index f67e1de404..fa700e115c 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -7,7 +7,7 @@ import frappe import unittest from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension -from erpnext.exceptions import InvalidAccountDimension, MandatoryDimension +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): @@ -20,7 +20,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): si.location = 'Block 1' si.save() - self.assertRaises(InvalidAccountDimension, si.submit) + self.assertRaises(InvalidAccountDimensionError, si.submit) def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) @@ -31,7 +31,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): si.items[0].cost_center = '_Test Cost Center 2 - _TC' si.save() - self.assertRaises(MandatoryDimension, si.submit) + self.assertRaises(MandatoryAccountDimensionError, si.submit) def tearDown(self): disable_dimension_filter() diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index f586de82e3..fd6f01e345 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -11,7 +11,7 @@ from frappe.model.meta import get_field_precision from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_fiscal_year -from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimension, MandatoryDimension +from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map from six import iteritems @@ -101,16 +101,16 @@ class GLEntry(Document): if self.account == account: if value['is_mandatory'] and not self.get(dimension): frappe.throw(_("{0} is mandatory for account {1}").format( - frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryDimension) + frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError) if value['allow_or_restrict'] == 'Allow': if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: frappe.throw(_("Invalid value {0} for account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) + frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) else: if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: frappe.throw(_("Invalid value {0} for account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimension) + frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) def check_pl_account(self): if self.is_opening=='Yes' and \ diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py index dcf3d6bad1..04291cd5bd 100644 --- a/erpnext/exceptions.py +++ b/erpnext/exceptions.py @@ -6,5 +6,5 @@ class PartyFrozen(frappe.ValidationError): pass class InvalidAccountCurrency(frappe.ValidationError): pass class InvalidCurrency(frappe.ValidationError): pass class PartyDisabled(frappe.ValidationError):pass -class InvalidAccountDimension(frappe.ValidationError): pass -class MandatoryDimension(frappe.ValidationError): pass +class InvalidAccountDimensionError(frappe.ValidationError): pass +class MandatoryAccountDimensionError(frappe.ValidationError): pass From 7b2d518059f1a131a0ece9e6872c5ed8c4a93d04 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Dec 2020 11:28:26 +0530 Subject: [PATCH 26/73] fix: Dimension filters in accounting reports --- erpnext/public/js/utils.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 891bbe5b59..2635d47f88 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -194,15 +194,21 @@ $.extend(erpnext.utils, { add_dimensions: function(report_name, index) { let filters = frappe.query_reports[report_name].filters; - erpnext.dimension_filters.forEach((dimension) => { - let found = filters.some(el => el.fieldname === dimension['fieldname']); + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + callback: function(r) { + let accounting_dimensions = r.message[0]; + accounting_dimensions.forEach((dimension) => { + let found = filters.some(el => el.fieldname === dimension['fieldname']); - if (!found) { - filters.splice(index, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] + if (!found) { + filters.splice(index, 0 ,{ + "fieldname": dimension["fieldname"], + "label": __(dimension["label"]), + "fieldtype": "Link", + "options": dimension["document_type"] + }); + } }); } }); From 924f99bead32bb4eba656019a15931f797eb04ae Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Dec 2020 15:42:50 +0530 Subject: [PATCH 27/73] fix: Help message --- .../accounting_dimension_filter.js | 12 ++++++++++++ .../accounting_dimension_filter.json | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js index a2526e92c3..74b7b51676 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js @@ -6,6 +6,18 @@ frappe.ui.form.on('Accounting Dimension Filter', { if (frm.doc.accounting_dimension) { frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value'); } + + let help_content = + ` + +
+

+ + {{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}} +

+
`; + + frm.set_df_property('dimension_filter_help', 'options', help_content); }, onload: function(frm) { frm.set_query('applicable_on_account', 'accounts', function() { diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json index 7736b2dffb..c0327ad0ad 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json @@ -14,7 +14,9 @@ "section_break_4", "accounts", "column_break_6", - "dimensions" + "dimensions", + "section_break_10", + "dimension_filter_help" ], "fields": [ { @@ -89,11 +91,24 @@ "reqd": 1, "show_days": 1, "show_seconds": 1 + }, + { + "fieldname": "dimension_filter_help", + "fieldtype": "HTML", + "label": "Dimension Filter Help", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-24 12:34:42.458713", + "modified": "2020-12-16 15:27:23.659285", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting Dimension Filter", From e061004956bbe585fc0874ba2532c5ac83a4ac11 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Dec 2020 17:00:39 +0530 Subject: [PATCH 28/73] fix: Improve validation message --- .../test_accounting_dimension_filter.py | 6 +++--- erpnext/accounts/doctype/gl_entry/gl_entry.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index fa700e115c..e822c0c017 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -17,14 +17,14 @@ class TestAccountingDimensionFilter(unittest.TestCase): def test_allowed_dimension_validation(self): si = create_sales_invoice(do_not_save=1) si.items[0].cost_center = 'Main - _TC' - si.location = 'Block 1' + si.department = 'Accounts - _TC' si.save() self.assertRaises(InvalidAccountDimensionError, si.submit) def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) - si.location = 'Block 1' + si.department = '' # Test with no department for Sales Account si.items[0].department = '' @@ -71,7 +71,7 @@ def create_accounting_dimension_filter(): }], 'dimensions': [{ 'accounting_dimension': 'Department', - 'dimension_value': '_Test Department - _TC' + 'dimension_value': 'Accounts - _TC' }] }).insert() else: diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 329d6e5aa7..e27bf5e2b3 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -107,12 +107,12 @@ class GLEntry(Document): if value['allow_or_restrict'] == 'Allow': if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']: - frappe.throw(_("Invalid value {0} for account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) + frappe.throw(_("Invalid value {0} for {1} against account {2}").format( + frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) else: if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']: - frappe.throw(_("Invalid value {0} for account {1}").format( - frappe.bold(self.get(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) + frappe.throw(_("Invalid value {0} for {1} against account {2}").format( + frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError) def check_pl_account(self): if self.is_opening=='Yes' and \ From 23ab5c5cc0bfac56260acc2be982e594eaaeaffe Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 31 Dec 2020 11:29:06 +0530 Subject: [PATCH 29/73] fix: Multiplle sider issues --- erpnext/accounts/doctype/budget/budget.js | 4 ++-- erpnext/education/doctype/fees/fees.js | 4 ++-- erpnext/hr/doctype/expense_claim/expense_claim.js | 2 +- erpnext/public/js/queries.js | 2 +- erpnext/public/js/utils.js | 2 +- erpnext/public/js/utils/dimension_tree_filter.js | 8 ++++---- .../doctype/stock_reconciliation/stock_reconciliation.js | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index e60bc60475..e162e3222d 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -11,7 +11,7 @@ frappe.ui.form.on('Budget', { report_type: "Profit and Loss", is_group: 0 } - } + }; }); frm.set_query("monthly_distribution", function() { @@ -19,7 +19,7 @@ frappe.ui.form.on('Budget', { filters: { fiscal_year: frm.doc.fiscal_year } - } + }; }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index 433bd64d2f..40f50999ad 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -15,9 +15,9 @@ frappe.ui.form.on("Fees", { }, onload: function(frm){ - frm.set_query("academic_term",function(){ + frm.set_query("academic_term",function() { return{ - "filters":{ + "filters": { "academic_year": (frm.doc.academic_year) } }; diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index e399b22f90..629341ff2a 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -16,7 +16,7 @@ frappe.ui.form.on('Expense Claim', { frappe.ui.form.on('Expense Claim Detail', { expense_type: function(frm, cdt, cdn) { var d = locals[cdt][cdn]; - if(!frm.doc.company) { + if (!frm.doc.company) { d.expense_type = ""; frappe.msgprint(__("Please set the Company")); this.frm.refresh_fields(); diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 98f1b504cc..b635adcd44 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -115,7 +115,7 @@ $.extend(erpnext.queries, { ["Warehouse", "is_group", "=",0] ] - } + }; }, get_filtered_dimensions: function(doc, child_fields, dimension, company) { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 2635d47f88..c39609bd38 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -202,7 +202,7 @@ $.extend(erpnext.utils, { let found = filters.some(el => el.fieldname === dimension['fieldname']); if (!found) { - filters.splice(index, 0 ,{ + filters.splice(index, 0, { "fieldname": dimension["fieldname"], "label": __(dimension["label"]), "fieldtype": "Link", diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index c79736d0e1..319cbd2b5d 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -69,13 +69,13 @@ erpnext.accounts.dimensions = { update_dimension(frm, doctype) { if (this.accounting_dimensions) { this.accounting_dimensions.forEach((dimension) => { - if(frm.is_new()) { - if(frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0 + if (frm.is_new()) { + if (frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0 && this.default_dimensions[frm.doc.company]) { let default_dimension = this.default_dimensions[frm.doc.company][dimension['fieldname']]; - if(default_dimension) { + if (default_dimension) { if (frappe.meta.has_field(doctype, dimension['fieldname'])) { frm.set_value(dimension['fieldname'], default_dimension); } @@ -98,4 +98,4 @@ erpnext.accounts.dimensions = { }); } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index be9404d9c8..ac4ed5e75d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -2,7 +2,7 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.stock"); -frappe.provide("erpnext.accounts.dimensions") +frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Stock Reconciliation", { onload: function(frm) { From a873ae0d9f0157ce69c48a26277097eff710af9a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 4 Jan 2021 14:23:31 +0530 Subject: [PATCH 30/73] fix: Check for custom dimensions --- .../public/js/utils/dimension_tree_filter.js | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 319cbd2b5d..96e181788e 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -23,22 +23,24 @@ erpnext.accounts.dimensions = { }, setup_filters(frm, doctype) { - this.accounting_dimensions.forEach((dimension) => { - frappe.model.with_doctype(dimension['document_type'], () => { - let parent_fields = []; - frappe.meta.get_docfields(doctype).forEach((df) => { - if (df.fieldtype === 'Link' && df.options === 'Account') { - parent_fields.push(df.fieldname); - } else if (df.fieldtype === 'Table') { - this.setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); - } + if (this.accounting_dimensions) { + this.accounting_dimensions.forEach((dimension) => { + frappe.model.with_doctype(dimension['document_type'], () => { + let parent_fields = []; + frappe.meta.get_docfields(doctype).forEach((df) => { + if (df.fieldtype === 'Link' && df.options === 'Account') { + parent_fields.push(df.fieldname); + } else if (df.fieldtype === 'Table') { + this.setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']); + } - if (frappe.meta.has_field(doctype, dimension['fieldname'])) { - this.setup_account_filters(frm, dimension['fieldname'], parent_fields); - } + if (frappe.meta.has_field(doctype, dimension['fieldname'])) { + this.setup_account_filters(frm, dimension['fieldname'], parent_fields); + } + }); }); }); - }); + } }, setup_child_filters(frm, doctype, parentfield, dimension) { @@ -91,7 +93,7 @@ erpnext.accounts.dimensions = { }, copy_dimension_from_first_row(frm, cdt, cdn, fieldname) { - if (frappe.meta.has_field(frm.doctype, fieldname)) { + if (frappe.meta.has_field(frm.doctype, fieldname) && this.accounting_dimensions) { this.accounting_dimensions.forEach((dimension) => { let row = frappe.get_doc(cdt, cdn); frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]); From 0c883853b3acc8c22b2c44cff212b6ba13ac5b25 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Jan 2021 18:48:50 +0530 Subject: [PATCH 31/73] fix: Dont validate warehouse values between MR to Stock Entry - Remove validation thta checks if warehouse in Stock Entry is the same as MR that it was pulled from --- erpnext/stock/doctype/stock_entry/stock_entry.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2fc7da8389..4782a9df0f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1333,9 +1333,6 @@ class StockEntry(StockController): frappe.MappingMismatchError) elif self.purpose == "Material Transfer" and self.add_to_transit: continue - elif mreq_item.warehouse != (item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse): - frappe.throw(_("Warehouse for row {0} does not match Material Request").format(item.idx), - frappe.MappingMismatchError) def validate_batch(self): if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]: From 2ffa4b9ce4614273f06bc74aa62e3c85ca304712 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 8 Jan 2021 20:58:00 +0530 Subject: [PATCH 32/73] fix: Link timesheets with corresponding projects --- erpnext/projects/doctype/timesheet/timesheet.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 4c2edf4f03..b28682184e 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -15,7 +15,7 @@ "column_break_3", "salary_slip", "status", - "project", + "parent_project", "employee_detail", "employee", "employee_name", @@ -261,7 +261,7 @@ "read_only": 1 }, { - "fieldname": "project", + "fieldname": "parent_project", "fieldtype": "Link", "label": "Project", "options": "Project" @@ -271,7 +271,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-29 07:50:35.938231", + "modified": "2021-01-08 20:51:14.590080", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", From 1b208e0695983124bb2ff31a576c5ed5d26a2967 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 10 Jan 2021 12:29:55 +0530 Subject: [PATCH 33/73] fix: Add total loan interest amount field in loans --- .../loan_interest_accrual.json | 9 +++- .../loan_interest_accrual.py | 6 ++- .../test_loan_interest_accrual.py | 43 ++++++++++++++++--- .../doctype/loan_repayment/loan_repayment.py | 2 +- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index f157f0df8f..185bf7a666 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -22,6 +22,7 @@ "paid_principal_amount", "column_break_14", "interest_amount", + "total_pending_interest_amount", "paid_interest_amount", "penalty_amount", "section_break_15", @@ -172,13 +173,19 @@ "hidden": 1, "label": "Last Accrual Date", "read_only": 1 + }, + { + "fieldname": "total_pending_interest_amount", + "fieldtype": "Currency", + "label": "Total Pending Interest Amount", + "options": "Company:company:default_currency" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-07 05:49:25.448875", + "modified": "2021-01-10 00:15:21.544140", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index d17f5af490..7d7992d40a 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -100,6 +100,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) payable_interest = interest_per_day * no_of_days + pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure') + args = frappe._dict({ 'loan': loan.name, 'applicant_type': loan.applicant_type, @@ -108,7 +110,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i 'loan_account': loan.loan_account, 'pending_principal_amount': pending_principal_amount, 'interest_amount': payable_interest, - 'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'], + 'total_pending_interest_amount': pending_amounts['interest_amount'], + 'penalty_amount': pending_amounts['penalty_amount'], 'process_loan_interest': process_loan_interest, 'posting_date': posting_date, 'accrual_type': accrual_type @@ -202,6 +205,7 @@ def make_loan_interest_accrual_entry(args): loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) + loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision) loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision) loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 46a6440553..85e008ac29 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -37,10 +37,8 @@ class TestLoanInterestAccrual(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) - loan.submit() first_date = '2019-10-01' @@ -50,11 +48,46 @@ class TestLoanInterestAccrual(unittest.TestCase): accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0)) + + def test_accumulated_amounts(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + create_pledge(loan_application) + loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, + posting_date=get_first_day(nowdate())) + loan.submit() + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) + loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + + self.assertEquals(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0)) + + next_start_date = '2019-10-31' + next_end_date = '2019-11-29' + + no_of_days = date_diff(next_end_date, next_start_date) + 1 + process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date) + new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0) + + loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name, + 'process_loan_interest_accrual': process}) + self.assertEquals(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 415ba993c7..ac30c91b67 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -377,7 +377,7 @@ def get_amounts(amounts, against_loan, posting_date): amounts["penalty_amount"] = flt(penalty_amount, precision) amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["pending_accrual_entries"] = pending_accrual_entries - amounts["unaccrued_interest"] = unaccrued_interest + amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) if final_due_date: amounts["due_date"] = final_due_date From d3634f6dac7f5611a5336fedb9e13fb68c9974b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 10 Jan 2021 19:26:45 +0530 Subject: [PATCH 34/73] feat: Loan Interest Report --- .../report/loan_interest_report/__init__.py | 0 .../loan_interest_report.js | 9 ++ .../loan_interest_report.json | 29 +++++++ .../loan_interest_report.py | 87 +++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 erpnext/loan_management/report/loan_interest_report/__init__.py create mode 100644 erpnext/loan_management/report/loan_interest_report/loan_interest_report.js create mode 100644 erpnext/loan_management/report/loan_interest_report/loan_interest_report.json create mode 100644 erpnext/loan_management/report/loan_interest_report/loan_interest_report.py diff --git a/erpnext/loan_management/report/loan_interest_report/__init__.py b/erpnext/loan_management/report/loan_interest_report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js new file mode 100644 index 0000000000..852e3ca366 --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -0,0 +1,9 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Interest Report"] = { + "filters": [ + + ] +}; diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json new file mode 100644 index 0000000000..321d6064e3 --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2021-01-10 02:03:26.742693", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-01-10 02:03:26.742693", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Interest Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Interest Accrual", + "report_name": "Loan Interest Report", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py new file mode 100644 index 0000000000..e039fe82d3 --- /dev/null +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -0,0 +1,87 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt, get_first_day, getdate + + +def execute(filters=None): + columns = get_columns(filters) + data = get_active_loan_details(filters) + return columns, data + +def get_columns(filters): + columns = [ + {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, + {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, + {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, + {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100}, + {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "Currency", "width": 120}, + {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "Currency", "width": 120}, + {"label": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Penalty For The Month"), "fieldname": "month_penalty", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Total Outstanding"), "fieldname": "total_payment", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, + {"label": _("Penalty Interest %"), "fieldname": "precentage_percentage", "fieldtype": "Percent", "width": 100}, + ] + + return columns + +def get_active_loan_details(filters): + loan_details = frappe.get_all("Loan", + fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type", + "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid", + "total_interest_payable", "written_off_amount"], + filters={"status": ("!=", "Closed")}) + + loan_list = [d.loan for d in loan_details] + + sanctioned_amount_map = get_sanctioned_amount_map() + payments = get_payments(loan_list) + accrual_map = get_interest_accruals(loan_list) + + for loan in loan_details: + loan.update({ + "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)), + "principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \ + - flt(loan.total_interest_payable) - flt(loan.written_off_amount), + "total_repayment": flt(payments.get(loan.loan)), + "month_interest": flt(accrual_map.get(loan.loan, {}).get("month_interest")), + "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")) + }) + return loan_details + +def get_sanctioned_amount_map(): + return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], + as_list=1)) + +def get_payments(loans): + return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"], + filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1)) + +def get_interest_accruals(loans): + accrual_map = {} + current_month_start = get_first_day(getdate()) + + interest_accruals = frappe.get_all("Loan Interest Accrual", + fields=["loan", "interest_amount", "posting_date", "penalty"], + filters={"loan": ("in", loans)}) + + for entry in interest_accruals: + accrual_map.setdefault(entry.loan, { + 'month_interest': 0.0, + 'accrued_interest': 0.0 + }) + + if getdate(entry.posting_date) < getdate(current_month_start): + accrual_map[entry.loan]['accrued_interest'] += entry.interest_amount + else: + accrual_map[entry.loan]['month_interest'] += entry.interest_amount + + return accrual_map \ No newline at end of file From 7cd7bf7f968ddb24ef2e71a96948f9e1bc3e373b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 13 Jan 2021 00:05:29 +0530 Subject: [PATCH 35/73] fix: incorrect key --- erpnext/controllers/buying_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6edc020701..4dee375e5a 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -336,7 +336,7 @@ class BuyingController(StockController): raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_nos', '') + consumed_serial_nos = raw_material_data.get('serial_no', '') consumed_batch_nos = raw_material_data.get('batch_nos', '') transferred_qty = raw_material.qty From 1354197c72f7e402a38fb0dd8812a155f9b8ac45 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 13 Jan 2021 09:12:50 +0530 Subject: [PATCH 36/73] feat(Healthcare Settings): Do Not Bill Patient Encounters for Inpatients --- .../doctype/healthcare_settings/healthcare_settings.json | 9 ++++++++- erpnext/healthcare/utils.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json index b33c326313..ddf1bce492 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json @@ -19,6 +19,7 @@ "valid_days", "inpatient_settings_section", "allow_discharge_despite_unbilled_services", + "do_not_bill_inpatient_encounters", "healthcare_service_items", "inpatient_visit_charge_item", "op_consulting_charge_item", @@ -315,11 +316,17 @@ "fieldname": "allow_discharge_despite_unbilled_services", "fieldtype": "Check", "label": "Allow Discharge Despite Unbilled Healthcare Services" + }, + { + "default": "0", + "fieldname": "do_not_bill_inpatient_encounters", + "fieldtype": "Check", + "label": "Do Not Bill Patient Encounters for Inpatients" } ], "issingle": 1, "links": [], - "modified": "2021-01-04 10:19:22.329272", + "modified": "2021-01-13 09:04:35.877700", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Settings", diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 96282f50a9..50f6f47622 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -90,6 +90,10 @@ def get_encounters_to_invoice(patient, company): income_account = None service_item = None if encounter.practitioner: + if encounter.inpatient_record and \ + frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'): + continue + service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter) income_account = get_income_account(encounter.practitioner, encounter.company) From 0f05925ff481792a2558f97c4ad1495e76064d38 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 13 Jan 2021 09:46:33 +0530 Subject: [PATCH 37/73] test: Do Not Bill Patient Encounters for Inpatients --- .../inpatient_record/test_inpatient_record.py | 37 +++++++++++++++++-- erpnext/healthcare/utils.py | 4 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index e8a9444fec..10990d412d 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -8,6 +8,8 @@ import unittest from frappe.utils import now_datetime, today from frappe.utils.make_random import get_random from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge +from erpnext.healthcare.doctype.lab_test.test_lab_test import create_patient_encounter +from erpnext.healthcare.utils import get_encounters_to_invoice class TestInpatientRecord(unittest.TestCase): def test_admit_and_discharge(self): @@ -42,7 +44,7 @@ class TestInpatientRecord(unittest.TestCase): def test_allow_discharge_despite_unbilled_services(self): frappe.db.sql("""delete from `tabInpatient Record`""") - setup_inpatient_settings() + setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=1) patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) @@ -64,6 +66,35 @@ class TestInpatientRecord(unittest.TestCase): self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) + setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=0) + + def test_do_not_bill_patient_encounters_for_inpatients(self): + frappe.db.sql("""delete from `tabInpatient Record`""") + setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=1) + patient = create_patient() + # Schedule Admission + ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 + ip_record.save(ignore_permissions = True) + + # Admit + service_unit = get_healthcare_service_unit() + admit_patient(ip_record, service_unit, now_datetime()) + + # Patient Encounter + patient_encounter = create_patient_encounter() + encounters = get_encounters_to_invoice(patient, "_Test Company") + encounter_ids = [entry.reference_name for entry in encounters] + self.assertFalse(patient_encounter.name in encounter_ids) + + # Discharge + schedule_discharge(frappe.as_json({"patient": patient})) + self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) + + ip_record = frappe.get_doc("Inpatient Record", ip_record.name) + mark_invoiced_inpatient_occupancy(ip_record) + discharge_patient(ip_record) + setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0) def test_validate_overlap_admission(self): frappe.db.sql("""delete from `tabInpatient Record`""") @@ -89,9 +120,9 @@ def mark_invoiced_inpatient_occupancy(ip_record): ip_record.save(ignore_permissions = True) -def setup_inpatient_settings(): +def setup_inpatient_settings(key, value): settings = frappe.get_single("Healthcare Settings") - settings.allow_discharge_despite_unbilled_services = 1 + settings.set(key, value) settings.save() diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 50f6f47622..6a499aab7f 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -77,11 +77,13 @@ def get_appointments_to_invoice(patient, company): def get_encounters_to_invoice(patient, company): + if not isinstance(patient, str): + patient = patient.name encounters_to_invoice = [] encounters = frappe.get_list( 'Patient Encounter', fields=['*'], - filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1} + filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1} ) if encounters: for encounter in encounters: From 8aeadc743eaa717bec021eb206182f7b6fc2a361 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 13 Jan 2021 12:56:04 +0530 Subject: [PATCH 38/73] fix: assessment plan error handling for course field (#23961) * fix: assessment plan error handling for course field * fix: message rectification * fix(travis): clean-up tests * fix: travis * fix: tests Co-authored-by: pateljannat Co-authored-by: Rucha Mahabal --- .../program_enrollment/program_enrollment.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index 6fbcd8aa97..886a7d85d8 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -124,21 +124,24 @@ class ProgramEnrollment(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_program_courses(doctype, txt, searchfield, start, page_len, filters): - if filters.get('program'): - return frappe.db.sql("""select course, course_name from `tabProgram Course` - where parent = %(program)s and course like %(txt)s {match_cond} - order by - if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999), - idx desc, - `tabProgram Course`.course asc - limit {start}, {page_len}""".format( - match_cond=get_match_cond(doctype), - start=start, - page_len=page_len), { - "txt": "%{0}%".format(txt), - "_txt": txt.replace('%', ''), - "program": filters['program'] - }) + if not filters.get('program'): + frappe.msgprint(_("Please select a Program first.")) + return [] + + return frappe.db.sql("""select course, course_name from `tabProgram Course` + where parent = %(program)s and course like %(txt)s {match_cond} + order by + if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999), + idx desc, + `tabProgram Course`.course asc + limit {start}, {page_len}""".format( + match_cond=get_match_cond(doctype), + start=start, + page_len=page_len), { + "txt": "%{0}%".format(txt), + "_txt": txt.replace('%', ''), + "program": filters['program'] + }) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs From 1e396dcb2a58fa904f718c6223ad8ee6278a7dc8 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 13 Jan 2021 14:01:57 +0530 Subject: [PATCH 39/73] fix: validated GST state --- erpnext/regional/india/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index f256a66266..0d8263835d 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -48,6 +48,9 @@ def validate_gstin_for_india(doc, method): validate_gstin_check_digit(doc.gstin) set_gst_state_and_state_number(doc) + if not doc.gst_state: + frappe.throw(_("Please Enter GST state")) + if doc.gst_state_number != doc.gstin[:2]: frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") .format(doc.gst_state_number)) From ef5f0c0461953315905d965aeb48ffb82979f0ae Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 13 Jan 2021 19:59:16 +0530 Subject: [PATCH 40/73] feat: Issue Summary Script Report (#23603) * feat: Issue Summary Report * feat: Add Issue Metrics to Issue Summary Report * fix: code clean-up * feat: Added Report Summary * feat: Add SLA status fields * fix: add report link to desk page * fix: sider issues Co-authored-by: Marica Co-authored-by: Nabin Hait --- .../support/desk_page/support/support.json | 4 +- .../support/report/issue_summary/__init__.py | 0 .../report/issue_summary/issue_summary.js | 73 ++++ .../report/issue_summary/issue_summary.json | 26 ++ .../report/issue_summary/issue_summary.py | 353 ++++++++++++++++++ 5 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 erpnext/support/report/issue_summary/__init__.py create mode 100644 erpnext/support/report/issue_summary/issue_summary.js create mode 100644 erpnext/support/report/issue_summary/issue_summary.json create mode 100644 erpnext/support/report/issue_summary/issue_summary.py diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index 28410f3a71..18cf87ab0b 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -28,7 +28,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-08-11 15:49:34.307341", + "modified": "2020-10-12 18:40:22.252915", "modified_by": "Administrator", "module": "Support", "name": "Support", diff --git a/erpnext/support/report/issue_summary/__init__.py b/erpnext/support/report/issue_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js new file mode 100644 index 0000000000..684482ac8d --- /dev/null +++ b/erpnext/support/report/issue_summary/issue_summary.js @@ -0,0 +1,73 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Issue Summary"] = { + "filters": [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"], + default: "Customer", + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "status", + label: __("Status"), + fieldtype: "Select", + options:[ + {label: __('Open'), value: 'Open'}, + {label: __('Replied'), value: 'Replied'}, + {label: __('Resolved'), value: 'Resolved'}, + {label: __('Closed'), value: 'Closed'} + ] + }, + { + fieldname: "priority", + label: __("Issue Priority"), + fieldtype: "Link", + options: "Issue Priority" + }, + { + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer" + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project" + }, + { + fieldname: "assigned_to", + label: __("Assigned To"), + fieldtype: "Link", + options: "User" + } + ] +}; \ No newline at end of file diff --git a/erpnext/support/report/issue_summary/issue_summary.json b/erpnext/support/report/issue_summary/issue_summary.json new file mode 100644 index 0000000000..b8a580ccef --- /dev/null +++ b/erpnext/support/report/issue_summary/issue_summary.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2020-10-12 01:01:55.181777", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2020-10-12 14:54:55.655920", + "modified_by": "Administrator", + "module": "Support", + "name": "Issue Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Issue", + "report_name": "Issue Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Support Team" + } + ] +} \ No newline at end of file diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py new file mode 100644 index 0000000000..3d735314f4 --- /dev/null +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -0,0 +1,353 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from six import iteritems +from frappe import _, scrub +from frappe.utils import flt + +def execute(filters=None): + return IssueSummary(filters).run() + +class IssueSummary(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + + def run(self): + self.get_columns() + self.get_data() + self.get_chart_data() + self.get_report_summary() + + return self.columns, self.data, None, self.chart, self.report_summary + + def get_columns(self): + self.columns = [] + + if self.filters.based_on == 'Customer': + self.columns.append({ + 'label': _('Customer'), + 'options': 'Customer', + 'fieldname': 'customer', + 'fieldtype': 'Link', + 'width': 200 + }) + + elif self.filters.based_on == 'Assigned To': + self.columns.append({ + 'label': _('User'), + 'fieldname': 'user', + 'fieldtype': 'Link', + 'options': 'User', + 'width': 200 + }) + + elif self.filters.based_on == 'Issue Type': + self.columns.append({ + 'label': _('Issue Type'), + 'fieldname': 'issue_type', + 'fieldtype': 'Link', + 'options': 'Issue Type', + 'width': 200 + }) + + elif self.filters.based_on == 'Issue Priority': + self.columns.append({ + 'label': _('Issue Priority'), + 'fieldname': 'priority', + 'fieldtype': 'Link', + 'options': 'Issue Priority', + 'width': 200 + }) + + self.statuses = ['Open', 'Replied', 'Resolved', 'Closed'] + for status in self.statuses: + self.columns.append({ + 'label': _(status), + 'fieldname': scrub(status), + 'fieldtype': 'Int', + 'width': 80 + }) + + self.columns.append({ + 'label': _('Total Issues'), + 'fieldname': 'total_issues', + 'fieldtype': 'Int', + 'width': 100 + }) + + self.sla_status_map = { + 'SLA Failed': 'failed', + 'SLA Fulfilled': 'fulfilled', + 'SLA Ongoing': 'ongoing' + } + + for label, fieldname in self.sla_status_map.items(): + self.columns.append({ + 'label': _(label), + 'fieldname': fieldname, + 'fieldtype': 'Int', + 'width': 100 + }) + + self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time', + 'Avg Resolution Time', 'Avg User Resolution Time'] + + for metric in self.metrics: + self.columns.append({ + 'label': _(metric), + 'fieldname': scrub(metric), + 'fieldtype': 'Duration', + 'width': 170 + }) + + def get_data(self): + self.get_issues() + self.get_rows() + + def get_issues(self): + filters = self.get_common_filters() + self.field_map = { + 'Customer': 'customer', + 'Issue Type': 'issue_type', + 'Issue Priority': 'priority', + 'Assigned To': '_assign' + } + + self.entries = frappe.db.get_all('Issue', + fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time', + 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'], + filters=filters + ) + + def get_common_filters(self): + filters = {} + filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + + if self.filters.get('assigned_to'): + filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%') + + for entry in ['company', 'status', 'priority', 'customer', 'project']: + if self.filters.get(entry): + filters[entry] = self.filters.get(entry) + + return filters + + def get_rows(self): + self.data = [] + self.get_summary_data() + + for entity, data in iteritems(self.issue_summary_data): + if self.filters.based_on == 'Customer': + row = {'customer': entity} + elif self.filters.based_on == 'Assigned To': + row = {'user': entity} + elif self.filters.based_on == 'Issue Type': + row = {'issue_type': entity} + elif self.filters.based_on == 'Issue Priority': + row = {'priority': entity} + + for status in self.statuses: + count = flt(data.get(status, 0.0)) + row[scrub(status)] = count + + row['total_issues'] = data.get('total_issues', 0.0) + + for sla_status in self.sla_status_map.values(): + value = flt(data.get(sla_status), 0.0) + row[sla_status] = value + + for metric in self.metrics: + value = flt(data.get(scrub(metric)), 0.0) + row[scrub(metric)] = value + + self.data.append(row) + + def get_summary_data(self): + self.issue_summary_data = frappe._dict() + + for d in self.entries: + status = d.status + agreement_status = scrub(d.agreement_status) + + if self.filters.based_on == 'Assigned To': + if d._assign: + for entry in json.loads(d._assign): + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data[entry][status] += 1 + self.issue_summary_data[entry][agreement_status] += 1 + self.issue_summary_data[entry]['total_issues'] += 1 + + else: + field = self.field_map.get(self.filters.based_on) + value = d.get(field) + if not value: + value = _('Not Specified') + + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0) + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0) + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data[value][status] += 1 + self.issue_summary_data[value][agreement_status] += 1 + self.issue_summary_data[value]['total_issues'] += 1 + + self.get_metrics_data() + + def get_metrics_data(self): + issues = [] + + metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time', + 'avg_resolution_time', 'avg_user_resolution_time'] + + for entry in self.entries: + issues.append(entry.name) + + field = self.field_map.get(self.filters.based_on) + + if issues: + if self.filters.based_on == 'Assigned To': + assignment_map = frappe._dict() + for d in self.entries: + if d._assign: + for entry in json.loads(d._assign): + for metric in metrics_list: + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0) + + self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0 + self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0 + self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0 + self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0 + self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0 + + if not assignment_map.get(entry): + assignment_map[entry] = 0 + assignment_map[entry] += 1 + + for entry in assignment_map: + for metric in metrics_list: + self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry)) + + else: + data = frappe.db.sql(""" + SELECT + {0}, AVG(first_response_time) as avg_frt, + AVG(avg_response_time) as avg_resp_time, + AVG(total_hold_time) as avg_hold_time, + AVG(resolution_time) as avg_resolution_time, + AVG(user_resolution_time) as avg_user_resolution_time + FROM `tabIssue` + WHERE + name IN %(issues)s + GROUP BY {0} + """.format(field), {'issues': issues}, as_dict=1) + + for entry in data: + value = entry.get(field) + if not value: + value = _('Not Specified') + + for metric in metrics_list: + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0) + + self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0 + self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0 + self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0 + self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0 + self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0 + + def get_chart_data(self): + if not self.data: + return None + + labels = [] + open_issues = [] + replied_issues = [] + resolved_issues = [] + closed_issues = [] + + entity = self.filters.based_on + entity_field = self.field_map.get(entity) + if entity == 'Assigned To': + entity_field = 'user' + + for entry in self.data: + labels.append(entry.get(entity_field)) + open_issues.append(entry.get('open')) + replied_issues.append(entry.get('replied')) + resolved_issues.append(entry.get('resolved')) + closed_issues.append(entry.get('closed')) + + self.chart = { + 'data': { + 'labels': labels[:30], + 'datasets': [ + { + 'name': 'Open', + 'values': open_issues[:30] + }, + { + 'name': 'Replied', + 'values': replied_issues[:30] + }, + { + 'name': 'Resolved', + 'values': resolved_issues[:30] + }, + { + 'name': 'Closed', + 'values': closed_issues[:30] + } + ] + }, + 'type': 'bar', + 'barOptions': { + 'stacked': True + } + } + + def get_report_summary(self): + if not self.data: + return None + + open_issues = 0 + replied = 0 + resolved = 0 + closed = 0 + + for entry in self.data: + open_issues += entry.get('open') + replied += entry.get('replied') + resolved += entry.get('resolved') + closed += entry.get('closed') + + self.report_summary = [ + { + 'value': open_issues, + 'indicator': 'Red', + 'label': _('Open'), + 'datatype': 'Int', + }, + { + 'value': replied, + 'indicator': 'Grey', + 'label': _('Replied'), + 'datatype': 'Int', + }, + { + 'value': resolved, + 'indicator': 'Green', + 'label': _('Resolved'), + 'datatype': 'Int', + }, + { + 'value': closed, + 'indicator': 'Green', + 'label': _('Closed'), + 'datatype': 'Int', + } + ] + From 511434190dca98a987b47ba4278d72da793baa3b Mon Sep 17 00:00:00 2001 From: Kanchan Chauhan Date: Wed, 13 Jan 2021 20:59:43 +0530 Subject: [PATCH 41/73] fix(work order): Actual start and end dates update (#24360) Currently, even when the Work Order (without Operations) is completed and Stock Entries are there, the Actual Start Date and Actual End Date is not updated. For some reason need to db_set, then it updates the Actual Start Date and Actual End Date --- erpnext/manufacturing/doctype/work_order/work_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8e7fac8ce8..ca530bbadd 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -456,10 +456,10 @@ class WorkOrder(Document): if data and len(data): dates = [d.posting_datetime for d in data] - self.actual_start_date = min(dates) + self.db_set('actual_start_date', min(dates)) if self.status == "Completed": - self.actual_end_date = max(dates) + self.db_set('actual_end_date', max(dates)) self.set_lead_time() From 53cb9f9f47fba12046db8a694f169577a34a74d4 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 13 Jan 2021 21:00:44 +0530 Subject: [PATCH 42/73] fix(e-invoicing): ux issues (#24358) * fix: overseas invoice rounding adjustment * fix: overseas shipping address * fix: qrcode for document name having forward slash * feat: sandbox mode toggle * fix: cannot delete sales invoice if linked to e invoice req log --- .../e_invoice_request_log.json | 7 +++--- .../e_invoice_settings.json | 9 ++++++- erpnext/regional/india/e_invoice/utils.py | 25 ++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json index 5c1c79dc04..3034370fea 100644 --- a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -24,9 +24,8 @@ }, { "fieldname": "reference_invoice", - "fieldtype": "Link", - "label": "Reference Invoice", - "options": "Sales Invoice" + "fieldtype": "Data", + "label": "Reference Invoice" }, { "fieldname": "headers", @@ -64,7 +63,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-24 21:09:38.882866", + "modified": "2021-01-13 12:06:57.253111", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Request Log", diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json index 4dcb22a54c..db8bda75bf 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -7,6 +7,7 @@ "field_order": [ "enable", "section_break_2", + "sandbox_mode", "credentials", "auth_token", "token_expiry" @@ -41,12 +42,18 @@ "label": "Credentials", "mandatory_depends_on": "enable", "options": "E Invoice User" + }, + { + "default": "0", + "fieldname": "sandbox_mode", + "fieldtype": "Check", + "label": "Sandbox Mode" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-22 15:34:57.280044", + "modified": "2021-01-13 12:04:49.449199", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Settings", diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index abe15043af..d0cac90e4d 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -218,8 +218,8 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) invoice_value_details.base_total = abs(invoice.base_total) - invoice_value_details.invoice_discount_amt = invoice.discount_amount - invoice_value_details.round_off = invoice.rounding_adjustment + invoice_value_details.invoice_discount_amt = invoice.base_discount_amount + invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) @@ -322,7 +322,10 @@ def make_einvoice(invoice): shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: - shipping_details = get_party_details(invoice.shipping_address_name) + if invoice.gst_category == 'Overseas': + shipping_details = get_overseas_address_details(invoice.shipping_address_name) + else: + shipping_details = get_party_details(invoice.shipping_address_name) if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) @@ -414,15 +417,19 @@ class RequestFailed(Exception): pass class GSPConnector(): def __init__(self, doctype=None, docname=None): self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + sandbox_mode = self.e_invoice_settings.sandbox_mode + self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None self.credentials = self.get_credentials() - self.base_url = 'https://gsp.adaequare.com' - self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token' - self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' - self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' - self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' + # authenticate url is same for sandbox & live + self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' + self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test' + self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' + self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' + self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' + self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi' self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' @@ -758,7 +765,7 @@ class GSPConnector(): _file = frappe.new_doc('File') _file.update({ - 'file_name': f'QRCode_{docname}.png', + 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')), 'attached_to_doctype': doctype, 'attached_to_name': docname, 'content': 'qrcode', From 00ccec7314283617ea7c89da6600531e6e206bac Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:02:15 +0530 Subject: [PATCH 43/73] fix: subscription prepaid date validation (#24356) --- erpnext/accounts/doctype/subscription/subscription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 552a5d476b..e023b47cac 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -446,7 +446,7 @@ class Subscription(Document): if not self.generate_invoice_at_period_start: return False - if self.is_new_subscription(): + if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start): return True # Check invoice dates and make sure it doesn't have outstanding invoices From 0b04e23f6d5e962c2736a308e0c4053426266bfb Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:04:03 +0530 Subject: [PATCH 44/73] fix: BOM Stock Report UoM correction (#24339) --- .../report/bom_stock_report/bom_stock_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 75ebcbc971..1c6758e6f3 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -20,6 +20,7 @@ def get_columns(): _("Item") + ":Link/Item:150", _("Description") + "::300", _("BOM Qty") + ":Float:160", + _("BOM UoM") + "::160", _("Required Qty") + ":Float:120", _("In Stock Qty") + ":Float:120", _("Enough Parts to Build") + ":Float:200", @@ -32,7 +33,7 @@ def get_bom_stock(filters): bom = filters.get("bom") table = "`tabBOM Item`" - qty_field = "qty" + qty_field = "stock_qty" qty_to_produce = filters.get("qty_to_produce", 1) if int(qty_to_produce) <= 0: @@ -40,7 +41,6 @@ def get_bom_stock(filters): if filters.get("show_exploded_view"): table = "`tabBOM Explosion Item`" - qty_field = "stock_qty" if filters.get("warehouse"): warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) @@ -59,6 +59,7 @@ def get_bom_stock(filters): bom_item.item_code, bom_item.description , bom_item.{qty_field}, + bom_item.stock_uom, bom_item.{qty_field} * {qty_to_produce} / bom.quantity, sum(ledger.actual_qty) as actual_qty, sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) From 33fac19bcef75d224728b9e2338c2952187883c1 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:06:04 +0530 Subject: [PATCH 45/73] fix: calculation of remaining_sub_periods if relieving date before month start date (#24319) --- erpnext/payroll/doctype/payroll_period/payroll_period.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py index d7893d0657..46f6cd842c 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt +from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months from frappe.model.document import Document from erpnext.hr.utils import get_holidays_for_employee @@ -88,6 +88,8 @@ def get_period_factor(employee, start_date, end_date, payroll_frequency, payroll period_start = joining_date if relieving_date and getdate(relieving_date) < getdate(period_end): period_end = relieving_date + if month_diff(period_end, start_date) > 1: + start_date = add_months(start_date, - (month_diff(period_end, start_date)+1)) total_sub_periods, remaining_sub_periods = 0.0, 0.0 From 4a649a4fce762ad3c1c0a046da545b287f44e53a Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 13 Jan 2021 21:10:49 +0530 Subject: [PATCH 46/73] fix: removing payment_field from loan repayment closuer (#24291) --- .../loan_repayment_and_closure/loan_repayment_and_closure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py index b63cc8ed5a..c6f6b990cc 100644 --- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py @@ -103,7 +103,7 @@ def get_data(filters): loan_repayments = frappe.get_all("Loan Repayment", filters = query_filters, - fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount", + fields=["posting_date", "applicant", "name", "against_loan", "payable_amount", "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"] ) From e62ce4b1729d64ae30081fdc096bb4da11febd50 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Wed, 13 Jan 2021 21:13:12 +0530 Subject: [PATCH 47/73] fix: Add button PO, PI, SI, DN and, Quotation Dashboard (#24187) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 +- erpnext/buying/doctype/purchase_order/purchase_order.js | 4 ++-- erpnext/selling/doctype/quotation/quotation.js | 2 +- erpnext/stock/doctype/delivery_note/delivery_note.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 7830cfd370..3863768a8b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -498,7 +498,7 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){ frappe.ui.form.on("Purchase Invoice", { setup: function(frm) { frm.custom_make_buttons = { - 'Purchase Invoice': 'Debit Note', + 'Purchase Invoice': 'Return / Debit Note', 'Payment Entry': 'Payment' } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 5efc32e11d..89b716c180 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -592,7 +592,7 @@ frappe.ui.form.on('Sales Invoice', { frm.custom_make_buttons = { 'Delivery Note': 'Delivery', - 'Sales Invoice': 'Sales Return', + 'Sales Invoice': 'Return / Credit Note', 'Payment Request': 'Payment Request', 'Payment Entry': 'Payment' }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 47483c9d1c..38532d18f3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -58,8 +58,8 @@ frappe.ui.form.on("Purchase Order Item", { erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({ setup: function() { this.frm.custom_make_buttons = { - 'Purchase Receipt': 'Receipt', - 'Purchase Invoice': 'Invoice', + 'Purchase Receipt': 'Purchase Receipt', + 'Purchase Invoice': 'Purchase Invoice', 'Stock Entry': 'Material to Supplier', 'Payment Entry': 'Payment', } diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 661e107e1e..5a0d9c9065 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -7,7 +7,7 @@ frappe.ui.form.on('Quotation', { setup: function(frm) { frm.custom_make_buttons = { - 'Sales Order': 'Make Sales Order' + 'Sales Order': 'Sales Order' }, frm.set_query("quotation_to", function() { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 5f2658c102..cb1e31b15b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Delivery Note", { frm.custom_make_buttons = { 'Packing Slip': 'Packing Slip', 'Installation Note': 'Installation Note', - 'Sales Invoice': 'Invoice', + 'Sales Invoice': 'Sales Invoice', 'Stock Entry': 'Return', 'Shipment': 'Shipment' }, From fdad94f98314ee2e4c603f044e4682e3c43dfd69 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 00:40:58 +0530 Subject: [PATCH 48/73] feat(Payroll): compute Year to Date for Salary Slip components --- .../doctype/salary_detail/salary_detail.json | 11 +++- .../doctype/salary_slip/salary_slip.js | 8 +-- .../doctype/salary_slip/salary_slip.py | 53 +++++++++++++++---- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 5c1eb61281..9bc25a6710 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -9,6 +9,7 @@ "abbr", "column_break_3", "amount", + "year_to_date", "section_break_5", "additional_salary", "statistical_component", @@ -226,11 +227,19 @@ { "fieldname": "column_break_24", "fieldtype": "Column Break" + }, + { + "description": "Total amount spent on this salary component from the beginning of the year (payroll or fiscal) to the current payroll date.", + "fieldname": "year_to_date", + "fieldtype": "Currency", + "label": "Year To Date", + "options": "currency", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-11-25 13:12:41.081106", + "modified": "2021-01-13 17:33:19.184195", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 51fb3596e9..945bd45275 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -138,11 +138,11 @@ frappe.ui.form.on("Salary Slip", { }, change_grid_labels: function(frm) { - frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"], frm.doc.currency, "earnings"); + let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit", + "tax_on_additional_salary"]; - frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"], frm.doc.currency, "deductions"); + frm.set_currency_labels(fields, frm.doc.currency, "earnings"); + frm.set_currency_labels(fields, frm.doc.currency, "deductions"); }, refresh: function(frm) { diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 183ad13411..2d3bc57900 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -52,6 +52,7 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() self.compute_year_to_date() self.compute_month_to_date() + self.compute_component_wise_year_to_date() if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") @@ -1138,16 +1139,7 @@ class SalarySlip(TransactionBase): def compute_year_to_date(self): year_to_date = 0 - payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) - - if payroll_period: - period_start_date = payroll_period.start_date - period_end_date = payroll_period.end_date - else: - # get dates based on fiscal year if no payroll period exists - fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1) - period_start_date = fiscal_year.year_start_date - period_end_date = fiscal_year.year_end_date + period_start_date, period_end_date = self.get_year_to_date_period() salary_slip_sum = frappe.get_list('Salary Slip', fields = ['sum(net_pay) as sum'], @@ -1180,6 +1172,47 @@ class SalarySlip(TransactionBase): month_to_date += self.net_pay self.month_to_date = month_to_date + def compute_component_wise_year_to_date(self): + period_start_date, period_end_date = self.get_year_to_date_period() + + for key in ('earnings', 'deductions'): + for component in self.get(key): + year_to_date = 0 + component_sum = frappe.db.sql(""" + SELECT sum(detail.amount) as sum + FROM `tabSalary Detail` as detail + INNER JOIN `tabSalary Slip` as salary_slip + ON detail.parent = salary_slip.name + WHERE + salary_slip.employee_name = %(employee_name)s + AND detail.salary_component = %(component)s + AND salary_slip.start_date >= %(period_start_date)s + AND salary_slip.end_date < %(period_end_date)s + AND salary_slip.name != %(docname)s + AND salary_slip.docstatus = 1""", + {'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date, + 'period_end_date': period_end_date, 'docname': self.name} + ) + + year_to_date = flt(component_sum[0][0]) if component_sum else 0.0 + year_to_date += component.amount + component.year_to_date = year_to_date + + def get_year_to_date_period(self): + payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) + + if payroll_period: + period_start_date = payroll_period.start_date + period_end_date = payroll_period.end_date + else: + # get dates based on fiscal year if no payroll period exists + fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1) + period_start_date = fiscal_year.year_start_date + period_end_date = fiscal_year.year_end_date + + return period_start_date, period_end_date + + def unlink_ref_doc_from_salary_slip(ref_no): linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` where journal_entry=%s and docstatus < 2""", (ref_no)) From 2c315a738eb99e1a04735f257573b21f9de0fc18 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 10:06:24 +0530 Subject: [PATCH 49/73] feat: Salary Slip with Year to Date Print Format --- erpnext/payroll/print_format/__init__.py | 0 .../salary_slip_with_year_to_date/__init__.py | 0 .../salary_slip_with_year_to_date.json | 25 +++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 erpnext/payroll/print_format/__init__.py create mode 100644 erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py create mode 100644 erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json diff --git a/erpnext/payroll/print_format/__init__.py b/erpnext/payroll/print_format/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py b/erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json new file mode 100644 index 0000000000..71ba37f6ed --- /dev/null +++ b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json @@ -0,0 +1,25 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-01-14 09:56:42.393623", + "custom_format": 0, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Salary Slip", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"

{{doc.name}}

\\n
\\n
\\n
\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"employee\", \"print_hide\": 0, \"label\": \"Employee\"}, {\"fieldname\": \"company\", \"print_hide\": 0, \"label\": \"Company\"}, {\"fieldname\": \"employee_name\", \"print_hide\": 0, \"label\": \"Employee Name\"}, {\"fieldname\": \"department\", \"print_hide\": 0, \"label\": \"Department\"}, {\"fieldname\": \"designation\", \"print_hide\": 0, \"label\": \"Designation\"}, {\"fieldname\": \"branch\", \"print_hide\": 0, \"label\": \"Branch\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"start_date\", \"print_hide\": 0, \"label\": \"Start Date\"}, {\"fieldname\": \"end_date\", \"print_hide\": 0, \"label\": \"End Date\"}, {\"fieldname\": \"total_working_days\", \"print_hide\": 0, \"label\": \"Working Days\"}, {\"fieldname\": \"leave_without_pay\", \"print_hide\": 0, \"label\": \"Leave Without Pay\"}, {\"fieldname\": \"payment_days\", \"print_hide\": 0, \"label\": \"Payment Days\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"earnings\", \"print_hide\": 0, \"label\": \"Earnings\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"deductions\", \"print_hide\": 0, \"label\": \"Deductions\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depends_on_payment_days\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"gross_pay\", \"print_hide\": 0, \"label\": \"Gross Pay\"}, {\"fieldname\": \"total_deduction\", \"print_hide\": 0, \"label\": \"Total Deduction\"}, {\"fieldname\": \"net_pay\", \"print_hide\": 0, \"label\": \"Net Pay\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"total_in_words\", \"print_hide\": 0, \"label\": \"Total in words\"}, {\"fieldtype\": \"Section Break\", \"label\": \"net pay info\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"year_to_date\", \"print_hide\": 0, \"label\": \"Year To Date\"}, {\"fieldname\": \"month_to_date\", \"print_hide\": 0, \"label\": \"Month To Date\"}]", + "idx": 0, + "line_breaks": 0, + "modified": "2021-01-14 10:03:45.283725", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Salary Slip with Year to Date", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file From 49702c1487f7b597b78ff7396e652860cee9ad28 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 11:57:24 +0530 Subject: [PATCH 50/73] test: year to date computation for salary slip components --- .../doctype/salary_slip/salary_slip.js | 2 +- .../doctype/salary_slip/test_salary_slip.py | 36 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 945bd45275..b50c774fbe 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -139,7 +139,7 @@ frappe.ui.form.on("Salary Slip", { change_grid_labels: function(frm) { let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit", - "tax_on_additional_salary"]; + "tax_on_additional_salary"]; frm.set_currency_labels(fields, frm.doc.currency, "earnings"); frm.set_currency_labels(fields, frm.doc.currency, "deductions"); diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 4368c03c2a..f58a8e58c2 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -321,6 +321,38 @@ class TestSalarySlip(unittest.TestCase): year_to_date += flt(slip.net_pay) self.assertEqual(slip.year_to_date, year_to_date) + def test_component_wise_year_to_date_computation(self): + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + applicant = make_employee("test_ytd@salary.com", company="_Test Company") + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), + company="_Test Company") + + salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", + "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + + # clear salary slip for this employee + frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") + + create_salary_slips_for_payroll_period(applicant, salary_structure.name, + payroll_period, deduct_random=False, num=3) + + salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name": + "test_ytd@salary.com"}, order_by = "posting_date") + + year_to_date = dict() + for slip in salary_slips: + doc = frappe.get_doc("Salary Slip", slip.name) + for entry in doc.get("earnings"): + if not year_to_date.get(entry.salary_component): + year_to_date[entry.salary_component] = 0 + + year_to_date[entry.salary_component] += entry.amount + self.assertEqual(year_to_date[entry.salary_component], entry.year_to_date) + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -714,10 +746,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = else: return income_tax_slab_name -def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): +def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12): deducted_dates = [] i = 0 - while i < 12: + while i < num: slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee, "salary_structure": salary_structure, "frequency": "Monthly"}) if i == 0: From 814858061f36487641eac763cb53e9982ddcaaf7 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 14 Jan 2021 12:13:08 +0530 Subject: [PATCH 51/73] fix: test for raising MR-SE mismatch error - Make test add mismatched item code instead of warehouse, since warehouse can be different. --- .../stock/doctype/material_request/test_material_request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 0a29fa05e1..72a3a5e67c 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -424,6 +424,7 @@ class TestMaterialRequest(unittest.TestCase): "basic_rate": 1.0 }) se_doc.get("items")[1].update({ + "item_code": "_Test Item Home Desktop 100", "qty": 3.0, "transfer_qty": 3.0, "s_warehouse": "_Test Warehouse 1 - _TC", @@ -534,7 +535,7 @@ class TestMaterialRequest(unittest.TestCase): mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', uom="_Test UOM 1", conversion_factor=12) - + requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') self.assertEqual(requested_qty, existing_requested_qty + 120) From 6c90e269829d35c0e3b0e11b84507d08c76a8f5e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 14 Jan 2021 13:42:40 +0530 Subject: [PATCH 52/73] feat: add descriptions for YTD and MTD fields --- erpnext/payroll/doctype/salary_detail/salary_detail.json | 4 ++-- erpnext/payroll/doctype/salary_slip/salary_slip.json | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 9bc25a6710..393f647cc8 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -229,7 +229,7 @@ "fieldtype": "Column Break" }, { - "description": "Total amount spent on this salary component from the beginning of the year (payroll or fiscal) to the current payroll date.", + "description": "Total salary booked against this component for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.", "fieldname": "year_to_date", "fieldtype": "Currency", "label": "Year To Date", @@ -239,7 +239,7 @@ ], "istable": 1, "links": [], - "modified": "2021-01-13 17:33:19.184195", + "modified": "2021-01-14 13:39:15.847158", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 43deee43aa..9f9691b59d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -584,6 +584,7 @@ "fieldtype": "Column Break" }, { + "description": "Total salary booked for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.", "fieldname": "year_to_date", "fieldtype": "Currency", "label": "Year To Date", @@ -591,6 +592,7 @@ "read_only": 1 }, { + "description": "Total salary booked for this employee from the beginning of the month up to the current salary slip's end date.", "fieldname": "month_to_date", "fieldtype": "Currency", "label": "Month To Date", @@ -616,7 +618,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-21 23:43:44.959840", + "modified": "2021-01-14 13:37:38.180920", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From bc9eaac3428435f336c7e722c3268c8716ba605a Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 14 Jan 2021 13:47:40 +0530 Subject: [PATCH 53/73] fix: Clear merge conflicts - 'get_project_template' was removed a month back - in 'make_project' insert the project directly as a check for it is there in the beginning. --- erpnext/projects/doctype/project/test_project.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 715cdd9285..d85c82612a 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -42,7 +42,7 @@ class TestProject(unittest.TestCase): task2 = task_exists("Test Template Task Child 1") if not task2: task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) - + task3 = task_exists("Test Template Task Child 2") if not task3: task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) @@ -76,7 +76,7 @@ class TestProject(unittest.TestCase): task2 = task_exists("Test Template Task with Dependency") if not task2: task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) - + template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2]) project = get_project(project_name, template) tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc') @@ -108,11 +108,6 @@ def make_project(args): if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}): return frappe.get_doc("Project", {"project_name": args.project_name}) - if args.project_template_name: - template = make_project_template(args.project_template_name) - else: - template = get_project_template() - project = frappe.get_doc(dict( doctype = 'Project', project_name = args.project_name, @@ -124,8 +119,7 @@ def make_project(args): template = make_project_template(args.project_template_name) project.project_template = template.name - if not frappe.db.exists("Project", args.project_name): - project.insert() + project.insert() return project From 3f015f71428c4d455ddf1859625a0585b8e1406c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Jan 2021 18:44:14 +0530 Subject: [PATCH 54/73] fix: Update fieldnames --- erpnext/projects/doctype/timesheet/timesheet.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index b068245a8b..b123af5d18 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -134,7 +134,7 @@ frappe.ui.form.on("Timesheet", { }); }, - project: function(frm) { + parent_project: function(frm) { set_project_in_timelog(frm); }, @@ -168,8 +168,8 @@ frappe.ui.form.on("Timesheet Detail", { }, time_logs_add: function(frm, cdt, cdn) { - if(frm.doc.project) { - frappe.model.set_value(cdt, cdn, 'project', frm.doc.project); + if(frm.doc.parent_project) { + frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project); } var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row'); @@ -308,7 +308,9 @@ const set_employee_and_company = function(frm) { }; function set_project_in_timelog(frm) { - if(frm.doc.project){ - erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "time_logs", "project"); + if(frm.doc.parent_project) { + $.each(frm.doc.time_logs || [], function(i, item) { + frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project); + }); } } \ No newline at end of file From 00981206adc346badc92be97920b86f550a3167e Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Thu, 14 Jan 2021 19:23:18 +0530 Subject: [PATCH 55/73] fix: last purchase rate not updating when voucher cancelled if only one voucher is present (#24322) * fix: last purchase rate not updating * chore: use orm for updating Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- erpnext/buying/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 47b48665b6..a73cb0d62e 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -35,9 +35,10 @@ def update_last_purchase_rate(doc, is_submit): frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx)) # update last purchsae rate - if last_purchase_rate: - frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""", - (flt(last_purchase_rate), d.item_code)) + frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate)) + + + def validate_for_items(doc): items = [] From 4569e52b45cfa3d346b9445cb3840060528fc667 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Thu, 14 Jan 2021 19:24:30 +0530 Subject: [PATCH 56/73] fix: fix for not having fiscal year while creating new company (#24130) * fix: Fiscal year fix while creating new company * fix: for failing Travis * fix: suggested changes Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- erpnext/regional/india/setup.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5321a9a3b5..526198424f 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -7,7 +7,7 @@ import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property from erpnext.regional.india import states -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today def setup(company=None, patch=True): @@ -629,15 +629,20 @@ def set_salary_components(docs): def set_tax_withholding_category(company): accounts = [] + fiscal_year = None abbr = frappe.get_value("Company", company, "abbr") tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') if company and tds_account: accounts = [dict(company=company, account=tds_account)] - fiscal_year = get_fiscal_year(today(), company=company)[0] - docs = get_tds_details(accounts, fiscal_year) + try: + fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0] + except FiscalYearError: + pass + docs = get_tds_details(accounts, fiscal_year) + for d in docs: try: doc = frappe.get_doc(d) @@ -650,11 +655,14 @@ def set_tax_withholding_category(company): if accounts: doc.append("accounts", accounts[0]) - # if fiscal year don't match with any of the already entered data, append rate row - fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] - if not fy_exist: - doc.append("rates", d.get('rates')[0]) - + if fiscal_year: + # if fiscal year don't match with any of the already entered data, append rate row + fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] + if not fy_exist: + doc.append("rates", d.get('rates')[0]) + + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True doc.save() def set_tds_account(docs, company): From 847c706d2f624745e6a2c2dcc37f044d4eea5795 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Jan 2021 19:37:13 +0530 Subject: [PATCH 57/73] fix: Ignore group cost center validation for period closing voucher --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index c441274908..288111b1b6 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -137,9 +137,10 @@ class GLEntry(Document): frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") .format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) - if self.cost_center and _check_is_group(): - frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""") - .format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) + if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \ + and self.cost_center and _check_is_group(): + frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot + be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) def validate_party(self): validate_party_frozen_disabled(self.party_type, self.party) From 5ead7ee6cfa5f564c8e3f1ddd60a13bb82d5c409 Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 14 Jan 2021 20:58:34 +0530 Subject: [PATCH 58/73] fix:Payment Period based on invoice date report fix/refactor --- .../payment_period_based_on_invoice_date.py | 122 +++++++++++++++--- 1 file changed, 105 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 57a1231f5a..98731d32ae 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -59,23 +59,111 @@ def validate_filters(filters): def get_columns(filters): return [ - _("Payment Document") + ":: 100", - _("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140", - _("Party Type") + "::100", - _("Party") + ":Dynamic Link/Party Type:140", - _("Posting Date") + ":Date:100", - _("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"), - _("Invoice Posting Date") + ":Date:130", - _("Payment Due Date") + ":Date:130", - _("Debit") + ":Currency:120", - _("Credit") + ":Currency:120", - _("Remarks") + "::150", - _("Age") +":Int:40", - "0-30:Currency:100", - "30-60:Currency:100", - "60-90:Currency:100", - _("90-Above") + ":Currency:100", - _("Delay in payment (Days)") + "::150" + { + "fieldname": "payment_document", + "label": _("Payment Document Type"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname": "payment_entry", + "label": _("Payment Document"), + "fieldtype": "Dynamic Link", + "options": "payment_document", + "width": 160 + }, + { + "fieldname": "party_type", + "label": _("Party Type"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname": "party", + "label": _("Party"), + "fieldtype": "Dynamic Link", + "options": "party_type", + "width": 160 + }, + { + "fieldname": "posting_date", + "label": _("Posting Date"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "invoice", + "label": _("Invoice"), + "fieldtype": "Link", + "options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice", + "width": 160 + }, + { + "fieldname": "invoice_posting_date", + "label": _("Invoice Posting Date"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "due_date", + "label": _("Payment Due Date"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "debit", + "label": _("Debit"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "credit", + "label": _("Credit"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "remarks", + "label": _("Remarks"), + "fieldtype": "Data", + "width": 200 + }, + { + "fieldname": "age", + "label": _("Age"), + "fieldtype": "Int", + "width": 50 + }, + { + "fieldname": "range1", + "label": _("0-30"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "range2", + "label": _("30-60"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "range3", + "label": _("60-90"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "range4", + "label": _("90 Above"), + "fieldtype": "Currency", + "width": 140 + }, + { + "fieldname": "delay_in_payment", + "label": _("Delay in payment (Days)"), + "fieldtype": "Int", + "width": 100 + } ] def get_conditions(filters): From 47b42e64415b5551c0918cc6f43dd049b3c88bfb Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 14 Jan 2021 21:20:00 +0530 Subject: [PATCH 59/73] fix: Translation fixes --- .../payment_period_based_on_invoice_date.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index 98731d32ae..7195c7e0b8 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -136,19 +136,19 @@ def get_columns(filters): }, { "fieldname": "range1", - "label": _("0-30"), + "label": "0-30", "fieldtype": "Currency", "width": 140 }, { "fieldname": "range2", - "label": _("30-60"), + "label": "30-60", "fieldtype": "Currency", "width": 140 }, { "fieldname": "range3", - "label": _("60-90"), + "label": "60-90", "fieldtype": "Currency", "width": 140 }, From b1d08126b0db8d967d137d64079a8764cc3ca429 Mon Sep 17 00:00:00 2001 From: Rohan Date: Fri, 15 Jan 2021 12:56:30 +0530 Subject: [PATCH 60/73] feat: add "Sync Now" to Plaid Settings (#23602) --- .../doctype/plaid_settings/plaid_settings.js | 16 +++++++++++++ .../doctype/plaid_settings/plaid_settings.py | 23 +++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 22a4004955..f26b130805 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -15,6 +15,22 @@ frappe.ui.form.on('Plaid Settings', { frm.add_custom_button('Link a new bank account', () => { new erpnext.integrations.plaidLink(frm); }); + + frm.add_custom_button(__("Sync Now"), () => { + frappe.call({ + method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization", + freeze: true, + callback: () => { + let bank_transaction_link = 'Bank Transaction'; + + frappe.msgprint({ + title: __("Sync Started"), + message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]), + alert: 1 + }); + } + }); + }).addClass("btn-primary"); } } }); diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e535e81bde..e12d9ee46c 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -166,7 +166,6 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): 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 @@ -228,13 +227,19 @@ def new_bank_transaction(transaction): 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", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) + enqueue_synchronization() - 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 - ) + +@frappe.whitelist() +def enqueue_synchronization(): + plaid_accounts = frappe.get_all("Bank Account", + filters={"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 + ) From d2c71350cd4e358f7c7579e28a50aa139bd12fa2 Mon Sep 17 00:00:00 2001 From: Kanchan Chauhan Date: Fri, 15 Jan 2021 13:56:03 +0530 Subject: [PATCH 61/73] refactor(Job Application): New fields in Job Applicant and webform (#23326) * refactor(Job Application): New fields in Job Applicant and webform * fix: translation syntax Co-authored-by: Nabin Hait Co-authored-by: Rucha Mahabal --- .../doctype/job_applicant/job_applicant.json | 66 +- .../hr/doctype/job_opening/job_opening.json | 600 +++++------------- erpnext/hr/doctype/job_opening/job_opening.py | 8 +- .../templates/job_opening_row.html | 13 +- .../job_application/job_application.json | 266 +++++--- erpnext/templates/generators/job_opening.html | 13 +- 6 files changed, 447 insertions(+), 519 deletions(-) diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json index c13548ab82..1360fd1890 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.json +++ b/erpnext/hr/doctype/job_applicant/job_applicant.json @@ -11,15 +11,24 @@ "field_order": [ "applicant_name", "email_id", + "phone_number", + "country", "status", "column_break_3", "job_title", "source", "source_name", + "applicant_rating", "section_break_6", "notes", "cover_letter", - "resume_attachment" + "resume_attachment", + "resume_link", + "section_break_16", + "currency", + "column_break_18", + "lower_range", + "upper_range" ], "fields": [ { @@ -91,12 +100,65 @@ "fieldtype": "Data", "label": "Notes", "read_only": 1 + }, + { + "fieldname": "phone_number", + "fieldtype": "Data", + "label": "Phone Number", + "options": "Phone" + }, + { + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, + { + "fieldname": "resume_link", + "fieldtype": "Data", + "label": "Resume Link" + }, + { + "fieldname": "applicant_rating", + "fieldtype": "Rating", + "in_list_view": 1, + "label": "Applicant Rating" + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break", + "label": "Salary Expectation" + }, + { + "fieldname": "lower_range", + "fieldtype": "Currency", + "label": "Lower Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "upper_range", + "fieldtype": "Currency", + "label": "Upper Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" } ], "icon": "fa fa-user", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-01-13 16:19:39.113330", + "modified": "2020-09-18 12:39:02.557563", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", diff --git a/erpnext/hr/doctype/job_opening/job_opening.json b/erpnext/hr/doctype/job_opening/job_opening.json index 4437e02fc8..b8f6df6f7a 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.json +++ b/erpnext/hr/doctype/job_opening/job_opening.json @@ -1,456 +1,188 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:route", - "beta": 0, - "creation": "2013-01-15 16:13:36", - "custom": 0, - "description": "Description of a Job Opening", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "autoname": "field:route", + "creation": "2013-01-15 16:13:36", + "description": "Description of a Job Opening", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "job_title", + "company", + "status", + "column_break_5", + "designation", + "department", + "staffing_plan", + "planned_vacancies", + "section_break_6", + "publish", + "route", + "column_break_12", + "job_application_route", + "section_break_14", + "description", + "section_break_16", + "currency", + "lower_range", + "upper_range", + "column_break_20", + "publish_salary_range" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "job_title", - "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": "Job Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "job_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Job Title", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "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": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "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 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "status", - "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": 1, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Open\nClosed", - "permlevel": 0, - "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 - }, + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Open\nClosed" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "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 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "designation", - "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": "Designation", - "length": 0, - "no_copy": 0, - "options": "Designation", - "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 - }, + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "department", - "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": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "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 - }, + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "staffing_plan", - "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": "Staffing Plan", - "length": 0, - "no_copy": 0, - "options": "Staffing Plan", - "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 - }, + "fieldname": "staffing_plan", + "fieldtype": "Link", + "label": "Staffing Plan", + "options": "Staffing Plan", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "staffing_plan", - "fieldname": "planned_vacancies", - "fieldtype": "Int", - "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": "Planned number of Positions", - "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": 0 - }, + "depends_on": "staffing_plan", + "fieldname": "planned_vacancies", + "fieldtype": "Int", + "label": "Planned number of Positions", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "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 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "publish", - "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": "Publish on website", - "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 - }, + "default": "0", + "fieldname": "publish", + "fieldtype": "Check", + "label": "Publish on website" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "publish", - "fieldname": "route", - "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": "Route", - "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, + "depends_on": "publish", + "fieldname": "route", + "fieldtype": "Data", + "label": "Route", "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Job profile, qualifications required etc.", - "fieldname": "description", - "fieldtype": "Text Editor", - "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, - "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 + "description": "Job profile, qualifications required etc.", + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "lower_range", + "fieldtype": "Currency", + "label": "Lower Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "upper_range", + "fieldtype": "Currency", + "label": "Upper Range", + "options": "currency", + "precision": "0" + }, + { + "fieldname": "column_break_20", + "fieldtype": "Column Break" + }, + { + "depends_on": "publish", + "description": "Route to the custom Job Application Webform", + "fieldname": "job_application_route", + "fieldtype": "Data", + "label": "Job Application Route" + }, + { + "default": "0", + "fieldname": "publish_salary_range", + "fieldtype": "Check", + "label": "Publish Salary Range" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-bookmark", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-05-20 15:38:44.705823", - "modified_by": "Administrator", - "module": "HR", - "name": "Job Opening", - "owner": "Administrator", + ], + "icon": "fa fa-bookmark", + "idx": 1, + "links": [], + "modified": "2020-09-18 11:23:29.488923", + "modified_by": "Administrator", + "module": "HR", + "name": "Job Opening", + "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": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Guest", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Guest" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index 00883d75f1..1e89767177 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -43,9 +43,8 @@ class JobOpening(WebsiteGenerator): current_count = designation_counts['employee_count'] + designation_counts['job_openings'] if self.planned_vacancies <= current_count: - frappe.throw(_("Job Openings for designation {0} already open \ - or hiring completed as per Staffing Plan {1}" - .format(self.designation, self.staffing_plan))) + frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format( + self.designation, self.staffing_plan)) def get_context(self, context): context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] @@ -56,7 +55,8 @@ def get_list_context(context): context.get_list = get_job_openings def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None): - fields = ['name', 'status', 'job_title', 'description'] + fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range', + 'lower_range', 'upper_range', 'currency', 'job_application_route'] filters = filters or {} filters.update({ diff --git a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html index 5da8cc82a2..c015101600 100644 --- a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html +++ b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html @@ -1,9 +1,18 @@

{{ doc.job_title }}

{{ doc.description }}

+ {%- if doc.publish_salary_range -%} +

{{_("Salary range per month")}}: {{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}

+ {% endif %}
diff --git a/erpnext/hr/web_form/job_application/job_application.json b/erpnext/hr/web_form/job_application/job_application.json index f630570c4c..512ba5c555 100644 --- a/erpnext/hr/web_form/job_application/job_application.json +++ b/erpnext/hr/web_form/job_application/job_application.json @@ -1,86 +1,200 @@ { - "accept_payment": 0, - "allow_comments": 1, - "allow_delete": 0, - "allow_edit": 1, - "allow_incomplete": 0, - "allow_multiple": 1, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 0, - "creation": "2016-09-10 02:53:16.598314", - "doc_type": "Job Applicant", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "introduction_text": "", - "is_standard": 1, - "login_required": 0, - "max_attachment_size": 0, - "modified": "2016-12-20 00:21:44.081622", - "modified_by": "Administrator", - "module": "HR", - "name": "job-application", - "owner": "Administrator", - "published": 1, - "route": "job_application", - "show_sidebar": 1, - "sidebar_items": [], - "success_message": "Thank you for applying.", - "success_url": "/jobs", - "title": "Job Application", + "accept_payment": 0, + "allow_comments": 1, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 1, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "apply_document_permissions": 0, + "client_script": "frappe.web_form.on('resume_link', (field, value) => {\n if (!frappe.utils.is_url(value)) {\n frappe.msgprint(__('Resume link not valid'));\n }\n});\n", + "creation": "2016-09-10 02:53:16.598314", + "doc_type": "Job Applicant", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "introduction_text": "", + "is_standard": 1, + "login_required": 0, + "max_attachment_size": 0, + "modified": "2020-10-07 19:27:17.143355", + "modified_by": "Administrator", + "module": "HR", + "name": "job-application", + "owner": "Administrator", + "published": 1, + "route": "job_application", + "route_to_success_link": 0, + "show_attachments": 0, + "show_in_grid": 0, + "show_sidebar": 1, + "sidebar_items": [], + "success_message": "Thank you for applying.", + "success_url": "/jobs", + "title": "Job Application", "web_form_fields": [ { - "fieldname": "job_title", - "fieldtype": "Data", - "hidden": 0, - "label": "Job Opening", - "max_length": 0, - "max_value": 0, - "options": "", - "read_only": 1, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "job_title", + "fieldtype": "Data", + "hidden": 0, + "label": "Job Opening", + "max_length": 0, + "max_value": 0, + "options": "", + "read_only": 1, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "applicant_name", - "fieldtype": "Data", - "hidden": 0, - "label": "Applicant Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "applicant_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Applicant Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "email_id", - "fieldtype": "Data", - "hidden": 0, - "label": "Email Address", - "max_length": 0, - "max_value": 0, - "options": "Email", - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "email_id", + "fieldtype": "Data", + "hidden": 0, + "label": "Email Address", + "max_length": 0, + "max_value": 0, + "options": "Email", + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "cover_letter", - "fieldtype": "Text", - "hidden": 0, - "label": "Cover Letter", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "phone_number", + "fieldtype": "Data", + "hidden": 0, + "label": "Phone Number", + "max_length": 0, + "max_value": 0, + "options": "Phone", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "resume_attachment", - "fieldtype": "Attach", - "hidden": 0, - "label": "Resume Attachment", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 + "allow_read_on_all_link_options": 0, + "fieldname": "country", + "fieldtype": "Link", + "hidden": 0, + "label": "Country of Residence", + "max_length": 0, + "max_value": 0, + "options": "Country", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "cover_letter", + "fieldtype": "Text", + "hidden": 0, + "label": "Cover Letter", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "resume_link", + "fieldtype": "Data", + "hidden": 0, + "label": "Resume Link", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Section Break", + "hidden": 0, + "label": "Expected Salary Range per month", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 0, + "label": "Currency", + "max_length": 0, + "max_value": 0, + "options": "Currency", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Column Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "lower_range", + "fieldtype": "Currency", + "hidden": 0, + "label": "Lower Range", + "max_length": 0, + "max_value": 0, + "options": "currency", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "", + "fieldtype": "Column Break", + "hidden": 0, + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "upper_range", + "fieldtype": "Currency", + "hidden": 0, + "label": "Upper Range", + "max_length": 0, + "max_value": 0, + "options": "currency", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 } ] } \ No newline at end of file diff --git a/erpnext/templates/generators/job_opening.html b/erpnext/templates/generators/job_opening.html index f92e72eaa7..c562db3c25 100644 --- a/erpnext/templates/generators/job_opening.html +++ b/erpnext/templates/generators/job_opening.html @@ -13,10 +13,21 @@ {%- if description -%}
{{ description }}
{% endif %} + +{%- if publish_salary_range -%} +
{{_("Salary range per month")}}: {{ frappe.format_value(frappe.utils.flt(lower_range), currency=currency) }} - {{ frappe.format_value(frappe.utils.flt(upper_range), currency=currency) }}
+{% endif %} +

- + {{ _("Apply Now") }} + {% else %} + {{ _("Apply Now") }} + {% endif %}

{% endblock %} From e30b33a3b85c14e6a8c7be53d288c625c9d54eef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 15 Jan 2021 15:47:15 +0530 Subject: [PATCH 62/73] fix: Linting issues --- erpnext/education/doctype/fees/fees.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index 40f50999ad..ac66acd00f 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -14,15 +14,15 @@ frappe.ui.form.on("Fees", { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, - onload: function(frm){ - frm.set_query("academic_term",function() { + onload: function(frm) { + frm.set_query("academic_term", function() { return{ "filters": { "academic_year": (frm.doc.academic_year) } }; }); - frm.set_query("fee_structure",function(){ + frm.set_query("fee_structure", function() { return{ "filters":{ "academic_year": (frm.doc.academic_year) From 0c4d61269a60498fb26c9be5e493fc32103acf33 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 15 Jan 2021 15:57:18 +0530 Subject: [PATCH 63/73] fix: test case --- .../test_accounting_dimension_filter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index e822c0c017..5bb4b6f6b5 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -18,6 +18,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): si = create_sales_invoice(do_not_save=1) si.items[0].cost_center = 'Main - _TC' si.department = 'Accounts - _TC' + si.location = 'Block 1' si.save() self.assertRaises(InvalidAccountDimensionError, si.submit) @@ -25,6 +26,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) si.department = '' + si.location = 'Block 1' # Test with no department for Sales Account si.items[0].department = '' From 1564d6ee1fc8a02dd2c6fbfab492f235cc7dbf47 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 15 Jan 2021 19:51:15 +0530 Subject: [PATCH 64/73] fix: Test Case --- .../test_accounting_dimension_filter.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 5bb4b6f6b5..7877abd026 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -13,6 +13,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): def setUp(self): create_dimension() create_accounting_dimension_filter() + self.invoice_list = [] def test_allowed_dimension_validation(self): si = create_sales_invoice(do_not_save=1) @@ -22,6 +23,7 @@ class TestAccountingDimensionFilter(unittest.TestCase): si.save() self.assertRaises(InvalidAccountDimensionError, si.submit) + self.invoice_list.append(si) def test_mandatory_dimension_validation(self): si = create_sales_invoice(do_not_save=1) @@ -34,11 +36,17 @@ class TestAccountingDimensionFilter(unittest.TestCase): si.save() self.assertRaises(MandatoryAccountDimensionError, si.submit) + self.invoice_list.append(si) def tearDown(self): disable_dimension_filter() disable_dimension() + for si in self.invoice_list: + si.load_from_db() + if si.docstatus == 1: + si.cancel() + def create_accounting_dimension_filter(): if not frappe.db.get_value('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'}): From 7976d12ed0248c821bc299b94278f914fa6e5d99 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 16 Jan 2021 18:10:01 +0530 Subject: [PATCH 65/73] fix: Applicant-Wise Loan Security Exposure report --- .../__init__.py | 0 .../applicant_wise_loan_security_exposure.js | 9 ++ ...applicant_wise_loan_security_exposure.json | 29 +++++ .../applicant_wise_loan_security_exposure.py | 112 ++++++++++++++++++ .../loan_interest_report.py | 5 +- 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json create mode 100644 erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js new file mode 100644 index 0000000000..e954a3942c --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js @@ -0,0 +1,9 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Applicant-Wise Loan Security Exposure"] = { + "filters": [ + + ] +}; diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json new file mode 100644 index 0000000000..a778cd7055 --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-01-15 23:48:38.913514", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-01-15 23:48:38.913514", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Applicant-Wise Loan Security Exposure", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Security", + "report_name": "Applicant-Wise Loan Security Exposure", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py new file mode 100644 index 0000000000..6b7f3ad328 --- /dev/null +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -0,0 +1,112 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import get_datetime, flt +from six import iteritems + +def execute(filters=None): + columns = get_columns(filters) + data = get_data(filters) + return columns, data + + +def get_columns(filters): + columns = [ + {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, + {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, + {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, + {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, + {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, + {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, + {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100} + ] + + return columns + +def get_data(filters): + data = [] + loan_security_details = get_loan_security_details(filters) + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(loan_security_details) + + for key, qty in iteritems(pledge_values): + row = {} + current_value = flt(qty * loan_security_details.get(key[1])['latest_price']) + row.update(loan_security_details.get(key[1])) + row.update({ + 'applicant_type': applicant_type_map.get(key[0]), + 'applicant_name': key[0], + 'total_qty': qty, + 'current_value': current_value, + 'portfolio_percent': current_value * 100 / total_value_map.get(key[0]) + }) + + data.append(row) + + return data + +def get_loan_security_details(filters): + update_time = get_datetime() + security_detail_map = {} + + loan_security_price_map = frappe._dict(frappe.db.sql(""" + SELECT loan_security, loan_security_price + FROM `tabLoan Security Price` t1 + WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2 + WHERE t1.loan_security = t2.loan_security) + """, as_list=1)) + + loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security', + 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type', + 'disabled']) + + for security in loan_security_details: + security.update({'latest_price': flt(loan_security_price_map.get(security.loan_security))}) + security_detail_map.setdefault(security.loan_security, security) + + return security_detail_map + +def get_applicant_wise_total_loan_security_qty(loan_security_details): + current_pledges = {} + total_value_map = {} + applicant_type_map = {} + applicant_wise_unpledges = {} + + unpledges = frappe.db.sql(""" + SELECT up.applicant, u.loan_security, sum(u.qty) as qty + FROM `tabLoan Security Unpledge` up, `tabUnpledge` u + WHERE u.parent = up.name + AND up.status = 'Approved' + GROUP BY up.applicant, u.loan_security + """, as_dict=1) + + for unpledge in unpledges: + applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty) + + pledges = frappe.db.sql(""" + SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty + FROM `tabLoan Security Pledge` lp, `tabPledge`p + WHERE p.parent = lp.name + AND lp.status = 'Pledged' + GROUP BY lp.applicant, p.loan_security + """, as_dict=1) + + for security in pledges: + current_pledges.setdefault((security.applicant, security.loan_security), security.qty) + total_value_map.setdefault(security.applicant, 0.0) + applicant_type_map.setdefault(security.applicant, security.applicant_type) + + current_pledges[(security.applicant, security.loan_security)] -= \ + applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0) + + total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \ + * loan_security_details.get(security.loan_security)['latest_price'] + + return current_pledges, total_value_map, applicant_type_map \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index e039fe82d3..e38770b4ed 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -15,6 +15,7 @@ def execute(filters=None): def get_columns(filters): columns = [ {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100}, @@ -37,7 +38,7 @@ def get_active_loan_details(filters): loan_details = frappe.get_all("Loan", fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type", "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid", - "total_interest_payable", "written_off_amount"], + "total_interest_payable", "written_off_amount", "status"], filters={"status": ("!=", "Closed")}) loan_list = [d.loan for d in loan_details] @@ -70,7 +71,7 @@ def get_interest_accruals(loans): current_month_start = get_first_day(getdate()) interest_accruals = frappe.get_all("Loan Interest Accrual", - fields=["loan", "interest_amount", "posting_date", "penalty"], + fields=["loan", "interest_amount", "posting_date", "penalty_amount"], filters={"loan": ("in", loans)}) for entry in interest_accruals: From addea9697da2fcb3f63e790ecd8172c417dde19f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 11:36:51 +0530 Subject: [PATCH 66/73] feat: Loan Exposure report --- .../report/loan_security_exposure/__init__.py | 0 .../loan_security_exposure.js | 16 ++++ .../loan_security_exposure.json | 29 +++++++ .../loan_security_exposure.py | 77 +++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 erpnext/loan_management/report/loan_security_exposure/__init__.py create mode 100644 erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js create mode 100644 erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json create mode 100644 erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py diff --git a/erpnext/loan_management/report/loan_security_exposure/__init__.py b/erpnext/loan_management/report/loan_security_exposure/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js new file mode 100644 index 0000000000..777f29624a --- /dev/null +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Loan Security Exposure"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + } + ] +}; diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json new file mode 100644 index 0000000000..d4dca08212 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-01-16 08:08:01.694583", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-01-16 08:08:01.694583", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Exposure", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Loan Security", + "report_name": "Loan Security Exposure", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Loan Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py new file mode 100644 index 0000000000..dc880f7266 --- /dev/null +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -0,0 +1,77 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import get_datetime, flt +from six import iteritems +from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \ + import get_loan_security_details, get_applicant_wise_total_loan_security_qty + +def execute(filters=None): + columns = get_columns(filters) + data = get_data(filters) + return columns, data + +def get_columns(filters): + columns = [ + {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, + {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, + {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, + {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, + {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, + {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100}, + ] + + return columns + +def get_data(filters): + data = [] + loan_security_details = get_loan_security_details(filters) + current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details) + + for security, value in iteritems(current_pledges): + row = {} + current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price']) + row.update(loan_security_details.get(security)) + row.update({ + 'total_qty': value['qty'], + 'current_value': current_value, + 'portfolio_percent': current_value * 100 / total_portfolio_value, + 'pledged_applicant_count': value['applicant_count'] + }) + + data.append(row) + + return data + + + +def get_company_wise_loan_security_details(filters, loan_security_details): + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, + loan_security_details) + + total_portfolio_value = 0 + security_wise_map = {} + for key, qty in iteritems(pledge_values): + security_wise_map.setdefault(key[1], { + 'qty': 0.0, + 'applicant_count': 0.0 + }) + + security_wise_map[key[1]]['qty'] += qty + if qty: + security_wise_map[key[1]]['applicant_count'] += 1 + + total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price']) + + return security_wise_map, total_portfolio_value + + + From 914ab7e4b037efd1a662c304add0e06326b3f1bc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 14:40:32 +0530 Subject: [PATCH 67/73] fix: Add filters and currency in reports --- .../process_loan_security_shortfall.json | 6 +- .../applicant_wise_loan_security_exposure.js | 9 ++- .../applicant_wise_loan_security_exposure.py | 30 ++++++--- .../loan_interest_report.js | 9 ++- .../loan_interest_report.py | 62 +++++++++++++------ .../loan_security_exposure.py | 12 ++-- 6 files changed, 93 insertions(+), 35 deletions(-) diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json index ffc3671132..3feb3055a6 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json @@ -30,7 +30,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-02-01 08:14:05.845161", + "modified": "2021-01-17 03:59:14.494557", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Security Shortfall", @@ -45,7 +45,9 @@ "read": 1, "report": 1, "role": "System Manager", + "select": 1, "share": 1, + "submit": 1, "write": 1 }, { @@ -57,7 +59,9 @@ "read": 1, "report": 1, "role": "Loan Manager", + "select": 1, "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js index e954a3942c..73d60c4045 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js @@ -4,6 +4,13 @@ frappe.query_reports["Applicant-Wise Loan Security Exposure"] = { "filters": [ - + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + } ] }; diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index 6b7f3ad328..0aff2e3623 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import erpnext from frappe import _ from frappe.utils import get_datetime, flt from six import iteritems @@ -24,9 +25,10 @@ def get_columns(filters): {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, - {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100} + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, + {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, + {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, ] return columns @@ -34,7 +36,10 @@ def get_columns(filters): def get_data(filters): data = [] loan_security_details = get_loan_security_details(filters) - pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(loan_security_details) + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, + loan_security_details) + + currency = erpnext.get_company_currency(filters.get('company')) for key, qty in iteritems(pledge_values): row = {} @@ -43,9 +48,10 @@ def get_data(filters): row.update({ 'applicant_type': applicant_type_map.get(key[0]), 'applicant_name': key[0], - 'total_qty': qty, + 'total_qty': qty, 'current_value': current_value, - 'portfolio_percent': current_value * 100 / total_value_map.get(key[0]) + 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2), + 'currency': currency }) data.append(row) @@ -73,19 +79,24 @@ def get_loan_security_details(filters): return security_detail_map -def get_applicant_wise_total_loan_security_qty(loan_security_details): +def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): current_pledges = {} total_value_map = {} applicant_type_map = {} applicant_wise_unpledges = {} + conditions = "" + + if filters.get('company'): + conditions = "AND company = %(company)s" unpledges = frappe.db.sql(""" SELECT up.applicant, u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' + {conditions} GROUP BY up.applicant, u.loan_security - """, as_dict=1) + """.format(conditions=conditions), filters, as_dict=1) for unpledge in unpledges: applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty) @@ -95,8 +106,9 @@ def get_applicant_wise_total_loan_security_qty(loan_security_details): FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' + {conditions} GROUP BY lp.applicant, p.loan_security - """, as_dict=1) + """.format(conditions=conditions), filters, as_dict=1) for security in pledges: current_pledges.setdefault((security.applicant, security.loan_security), security.qty) diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js index 852e3ca366..a227b6d797 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js @@ -4,6 +4,13 @@ frappe.query_reports["Loan Interest Report"] = { "filters": [ - + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + } ] }; diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index e38770b4ed..730176f4a9 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import erpnext from frappe import _ from frappe.utils import flt, get_first_day, getdate @@ -19,33 +20,41 @@ def get_columns(filters): {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100}, - {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "Currency", "width": 120}, - {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "Currency", "width": 120}, - {"label": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Penalty For The Month"), "fieldname": "month_penalty", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Total Outstanding"), "fieldname": "total_payment", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Penalty For The Month"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, - {"label": _("Penalty Interest %"), "fieldname": "precentage_percentage", "fieldtype": "Percent", "width": 100}, + {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, ] return columns def get_active_loan_details(filters): + + filter_obj = {"status": ("!=", "Closed")} + if filters.get('company'): + filter_obj.update({'company': filters.get('company')}) + loan_details = frappe.get_all("Loan", fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type", "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid", "total_interest_payable", "written_off_amount", "status"], - filters={"status": ("!=", "Closed")}) + filters=filter_obj) loan_list = [d.loan for d in loan_details] sanctioned_amount_map = get_sanctioned_amount_map() + penal_interest_rate_map = get_penal_interest_rate_map() payments = get_payments(loan_list) accrual_map = get_interest_accruals(loan_list) + currency = erpnext.get_company_currency(filters.get('company')) for loan in loan_details: loan.update({ @@ -54,8 +63,16 @@ def get_active_loan_details(filters): - flt(loan.total_interest_payable) - flt(loan.written_off_amount), "total_repayment": flt(payments.get(loan.loan)), "month_interest": flt(accrual_map.get(loan.loan, {}).get("month_interest")), - "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")) + "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), + "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), + "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), + "penalty_interest": penal_interest_rate_map.get(loan.loan_type), + "currency": currency }) + + loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \ + + loan['penalty'] + return loan_details def get_sanctioned_amount_map(): @@ -71,18 +88,25 @@ def get_interest_accruals(loans): current_month_start = get_first_day(getdate()) interest_accruals = frappe.get_all("Loan Interest Accrual", - fields=["loan", "interest_amount", "posting_date", "penalty_amount"], - filters={"loan": ("in", loans)}) + fields=["loan", "interest_amount", "posting_date", "penalty_amount", + "paid_interest_amount"], filters={"loan": ("in", loans)}, order_by="posting_date") for entry in interest_accruals: accrual_map.setdefault(entry.loan, { - 'month_interest': 0.0, - 'accrued_interest': 0.0 + "month_interest": 0.0, + "accrued_interest": 0.0, + "interest_outstanding": 0.0 }) if getdate(entry.posting_date) < getdate(current_month_start): - accrual_map[entry.loan]['accrued_interest'] += entry.interest_amount + accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount else: - accrual_map[entry.loan]['month_interest'] += entry.interest_amount + accrual_map[entry.loan]["month_interest"] += entry.interest_amount - return accrual_map \ No newline at end of file + accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount + accrual_map[entry.loan]["penalty"] = entry.penalty_amount + + return accrual_map + +def get_penal_interest_rate_map(): + return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1)) \ No newline at end of file diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index dc880f7266..2f4d98066b 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +import erpnext from frappe import _ from frappe.utils import get_datetime, flt from six import iteritems @@ -23,10 +24,11 @@ def get_columns(filters): {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, - {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "Currency", "width": 100}, - {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "Currency", "width": 100}, + {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, + {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, ] return columns @@ -35,6 +37,7 @@ def get_data(filters): data = [] loan_security_details = get_loan_security_details(filters) current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details) + currency = erpnext.get_company_currency(filters.get('company')) for security, value in iteritems(current_pledges): row = {} @@ -43,8 +46,9 @@ def get_data(filters): row.update({ 'total_qty': value['qty'], 'current_value': current_value, - 'portfolio_percent': current_value * 100 / total_portfolio_value, - 'pledged_applicant_count': value['applicant_count'] + 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), + 'pledged_applicant_count': value['applicant_count'], + 'currency': currency }) data.append(row) From ec600631559cfde903ab9af874156e6a46984597 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 17:39:51 +0530 Subject: [PATCH 68/73] fix: Auto Loan Write Off --- erpnext/loan_management/doctype/loan/loan.py | 24 ++++++++++--------- .../doctype/loan_type/loan_type.json | 6 ++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 2e0a4d13ab..e607d4f3cb 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -202,7 +202,9 @@ def request_loan_closure(loan, posting_date=None): # checking greater than 0 as there may be some minor precision error if pending_amount < write_off_limit: - # update status as loan closure requested + # Auto create loan write off and update status as loan closure requested + write_off = make_loan_write_off(loan) + write_off.submit() frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') else: frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) @@ -336,13 +338,13 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a return unpledge_request def validate_employee_currency_with_company_currency(applicant, company): - from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency - if not applicant: - frappe.throw(_("Please select Applicant")) - if not company: - frappe.throw(_("Please select Company")) - employee_currency = get_employee_currency(applicant) - company_currency = erpnext.get_company_currency(company) - if employee_currency != company_currency: - frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") - .format(applicant, employee_currency)) + from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency + if not applicant: + frappe.throw(_("Please select Applicant")) + if not company: + frappe.throw(_("Please select Company")) + employee_currency = get_employee_currency(applicant) + company_currency = erpnext.get_company_currency(company) + if employee_currency != company_currency: + frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") + .format(applicant, employee_currency)) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 18a97315f0..3ef53044c2 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -144,17 +144,17 @@ }, { "allow_on_submit": 1, - "description": "Pending amount that will be automatically ignored on loan closure request ", + "description": "Loan Write Off will be automatically created on loan closure request if pending amount is below this limit", "fieldname": "write_off_amount", "fieldtype": "Currency", - "label": "Write Off Amount ", + "label": "Auto Write Off Amount ", "options": "Company:company:default_currency" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-26 07:13:55.029811", + "modified": "2021-01-17 06:51:26.082879", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", From dd25ecb70deb94bc2257df8e2d843da31ff7006a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 17:52:02 +0530 Subject: [PATCH 69/73] fix: Add reports in Desk Page --- erpnext/loan_management/desk_page/loan/loan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json index fc59c19325..75036bd097 100644 --- a/erpnext/loan_management/desk_page/loan/loan.json +++ b/erpnext/loan_management/desk_page/loan/loan.json @@ -23,7 +23,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Interest Accrual\",\n \"is_query_report\": true,\n \"label\": \"Loan Interest Report\",\n \"name\": \"Loan Interest Report\",\n \"route\": \"#query-report/Loan Interest Report\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Exposure\",\n \"name\": \"Loan Security Exposure\",\n \"route\": \"#query-report/Loan Security Exposure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Applicant-Wise Loan Security Exposure\",\n \"name\": \"Applicant-Wise Loan Security Exposure\",\n \"route\": \"#query-report/Applicant-Wise Loan Security Exposure\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -38,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "Loan", - "modified": "2020-10-17 12:59:50.336085", + "modified": "2021-01-17 07:21:22.092184", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", From 2cb12e6f70f0510c0fa99c6558b4420247403942 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Jan 2021 18:38:21 +0530 Subject: [PATCH 70/73] fix: Loan Security name fetch --- erpnext/loan_management/doctype/loan/test_loan.py | 4 ++-- .../loan_security_price/loan_security_price.json | 11 ++++++++++- erpnext/loan_management/doctype/pledge/pledge.json | 10 +++++++++- .../doctype/proposed_pledge/proposed_pledge.json | 10 +++++++++- .../loan_management/doctype/unpledge/unpledge.json | 10 +++++++++- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 2abd7d84d9..f3c9db6233 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -321,7 +321,7 @@ class TestLoan(unittest.TestCase): self.assertEquals(sum(pledged_qty.values()), 0) amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertTrue(amounts['pending_principal_amount'] < 0) + self.assertEqual(amounts['pending_principal_amount'], 0) self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) @@ -473,7 +473,7 @@ class TestLoan(unittest.TestCase): self.assertEquals(loan.status, "Loan Closure Requested") amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertTrue(amounts['pending_principal_amount'] < 0.0) + self.assertEqual(amounts['pending_principal_amount'], 0.0) def test_partial_unaccrued_interest_payment(self): pledge = [{ diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json index a55b482bd6..b6e8763756 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "loan_security_type", "column_break_2", "uom", @@ -79,10 +80,18 @@ "label": "Loan Security Type", "options": "Loan Security Type", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-06-11 03:41:33.900340", + "modified": "2021-01-17 07:41:49.598086", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Price", diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json index 801e3a3117..c23479c825 100644 --- a/erpnext/loan_management/doctype/pledge/pledge.json +++ b/erpnext/loan_management/doctype/pledge/pledge.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "loan_security_type", "loan_security_code", "uom", @@ -85,11 +86,18 @@ "label": "Post Haircut Amount", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-11-05 10:07:15.424937", + "modified": "2021-01-17 07:41:12.452514", "modified_by": "Administrator", "module": "Loan Management", "name": "Pledge", diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json index 3e7e778a25..a0b3a79b56 100644 --- a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "qty", "loan_security_price", "amount", @@ -56,12 +57,19 @@ "label": "Post Haircut Amount", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-05 10:07:37.542344", + "modified": "2021-01-17 07:29:01.671722", "modified_by": "Administrator", "module": "Loan Management", "name": "Proposed Pledge", diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json index 00356685eb..0091e6c43d 100644 --- a/erpnext/loan_management/doctype/unpledge/unpledge.json +++ b/erpnext/loan_management/doctype/unpledge/unpledge.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "loan_security", + "loan_security_name", "loan_security_type", "loan_security_code", "haircut", @@ -61,12 +62,19 @@ "fieldtype": "Percent", "label": "Haircut", "read_only": 1 + }, + { + "fetch_from": "loan_security.loan_security_name", + "fieldname": "loan_security_name", + "fieldtype": "Data", + "label": "Loan Security Name", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-05 10:07:28.106961", + "modified": "2021-01-17 07:36:20.212342", "modified_by": "Administrator", "module": "Loan Management", "name": "Unpledge", From 52172252b4ab9533866d323a74ca6a2fc3e1140f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 18 Jan 2021 12:07:54 +0530 Subject: [PATCH 71/73] fix: Interest calculations in Loan Interest Report --- .../applicant_wise_loan_security_exposure.py | 1 - .../loan_interest_report.py | 40 ++++++++++++------- .../loan_security_exposure.py | 4 +- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index 0aff2e3623..6d7c3b730d 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -59,7 +59,6 @@ def get_data(filters): return data def get_loan_security_details(filters): - update_time = get_datetime() security_detail_map = {} loan_security_price_map = frappe._dict(frappe.db.sql(""" diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index 730176f4a9..aa0325ef35 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import erpnext from frappe import _ -from frappe.utils import flt, get_first_day, getdate +from frappe.utils import flt, getdate, add_days def execute(filters=None): @@ -22,13 +22,13 @@ def get_columns(filters): {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100}, {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Interest For The Month"), "fieldname": "month_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Penalty For The Month"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, + {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100}, {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, @@ -62,11 +62,11 @@ def get_active_loan_details(filters): "principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \ - flt(loan.total_interest_payable) - flt(loan.written_off_amount), "total_repayment": flt(payments.get(loan.loan)), - "month_interest": flt(accrual_map.get(loan.loan, {}).get("month_interest")), "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), "penalty_interest": penal_interest_rate_map.get(loan.loan_type), + "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")), "currency": currency }) @@ -85,26 +85,38 @@ def get_payments(loans): def get_interest_accruals(loans): accrual_map = {} - current_month_start = get_first_day(getdate()) interest_accruals = frappe.get_all("Loan Interest Accrual", fields=["loan", "interest_amount", "posting_date", "penalty_amount", - "paid_interest_amount"], filters={"loan": ("in", loans)}, order_by="posting_date") + "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc") for entry in interest_accruals: accrual_map.setdefault(entry.loan, { - "month_interest": 0.0, "accrued_interest": 0.0, - "interest_outstanding": 0.0 + "undue_interest": 0.0, + "interest_outstanding": 0.0, + "last_accrual_date": '', + "due_date": '' }) - if getdate(entry.posting_date) < getdate(current_month_start): - accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount - else: - accrual_map[entry.loan]["month_interest"] += entry.interest_amount + if entry.accrual_type == 'Regular': + if not accrual_map[entry.loan]['due_date']: + accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1) + if not accrual_map[entry.loan]['last_accrual_date']: + accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date - accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount - accrual_map[entry.loan]["penalty"] = entry.penalty_amount + due_date = accrual_map[entry.loan]['due_date'] + last_accrual_date = accrual_map[entry.loan]['last_accrual_date'] + + if due_date and getdate(entry.posting_date) < getdate(due_date): + accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount + else: + accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount + + accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount + + if last_accrual_date and getdate(entry.posting_date) == last_accrual_date: + accrual_map[entry.loan]["penalty"] = entry.penalty_amount return accrual_map diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index 2f4d98066b..3ef10c0f41 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -2,10 +2,9 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe import erpnext from frappe import _ -from frappe.utils import get_datetime, flt +from frappe.utils import flt from six import iteritems from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \ import get_loan_security_details, get_applicant_wise_total_loan_security_qty @@ -56,7 +55,6 @@ def get_data(filters): return data - def get_company_wise_loan_security_details(filters, loan_security_details): pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, loan_security_details) From ea19434af4b2b511086d1ea80b952b2ce264e1fc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 18 Jan 2021 13:53:52 +0530 Subject: [PATCH 72/73] feat: Issue Analytics Script Report (#23604) * feat: Issue Analytics Report * feat: add more filters, code clean-up * fix: add report link to desk page * test: Issue Analytics Report * fix: sider issues * fix: test * debug: travis * debug: travis * fix: travis * fix: travis Co-authored-by: Marica Co-authored-by: Nabin Hait --- .../support/desk_page/support/support.json | 4 +- erpnext/support/doctype/issue/issue.py | 2 +- erpnext/support/doctype/issue/test_issue.py | 8 +- .../report/issue_analytics/__init__.py | 0 .../report/issue_analytics/issue_analytics.js | 141 +++++++++++ .../issue_analytics/issue_analytics.json | 26 ++ .../report/issue_analytics/issue_analytics.py | 222 ++++++++++++++++++ .../issue_analytics/test_issue_analytics.py | 211 +++++++++++++++++ 8 files changed, 609 insertions(+), 5 deletions(-) create mode 100644 erpnext/support/report/issue_analytics/__init__.py create mode 100644 erpnext/support/report/issue_analytics/issue_analytics.js create mode 100644 erpnext/support/report/issue_analytics/issue_analytics.json create mode 100644 erpnext/support/report/issue_analytics/issue_analytics.py create mode 100644 erpnext/support/report/issue_analytics/test_issue_analytics.py diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index 18cf87ab0b..dba2b1463f 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -28,7 +28,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Analytics\",\n \"name\": \"Issue Analytics\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Issue Summary\",\n \"name\": \"Issue Summary\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-10-12 18:40:22.252915", + "modified": "2021-01-13 20:15:03.064256", "modified_by": "Administrator", "module": "Support", "name": "Support", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 62b39cced5..02d10a4dda 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -214,7 +214,7 @@ class Issue(Document): def before_insert(self): if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - self.set_response_and_resolution_time() + self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) def set_response_and_resolution_time(self, priority=None, service_level_agreement=None): service_level_agreement = get_active_service_level_agreement_for(priority=priority, diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index c962dc6b31..483bb155db 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -135,15 +135,19 @@ class TestIssue(unittest.TestCase): self.assertEqual(flt(issue.total_hold_time, 2), 2700) -def make_issue(creation=None, customer=None, index=0): +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), "customer": customer, "raised_by": "test@example.com", "description": "Service Level Agreement Issue", + "issue_type": issue_type, + "priority": priority, "creation": creation, - "service_level_agreement_creation": creation + "opening_date": creation, + "service_level_agreement_creation": creation, + "company": "_Test Company" }).insert(ignore_permissions=True) return issue diff --git a/erpnext/support/report/issue_analytics/__init__.py b/erpnext/support/report/issue_analytics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/support/report/issue_analytics/issue_analytics.js b/erpnext/support/report/issue_analytics/issue_analytics.js new file mode 100644 index 0000000000..f87b2c2ddd --- /dev/null +++ b/erpnext/support/report/issue_analytics/issue_analytics.js @@ -0,0 +1,141 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Issue Analytics"] = { + "filters": [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"], + default: "Customer", + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "range", + label: __("Range"), + fieldtype: "Select", + options: [ + { "value": "Weekly", "label": __("Weekly") }, + { "value": "Monthly", "label": __("Monthly") }, + { "value": "Quarterly", "label": __("Quarterly") }, + { "value": "Yearly", "label": __("Yearly") } + ], + default: "Monthly", + reqd: 1 + }, + { + fieldname: "status", + label: __("Status"), + fieldtype: "Select", + options:[ + {label: __('Open'), value: 'Open'}, + {label: __('Replied'), value: 'Replied'}, + {label: __('Resolved'), value: 'Resolved'}, + {label: __('Closed'), value: 'Closed'} + ] + }, + { + fieldname: "priority", + label: __("Issue Priority"), + fieldtype: "Link", + options: "Issue Priority" + }, + { + fieldname: "customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer" + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project" + }, + { + fieldname: "assigned_to", + label: __("Assigned To"), + fieldtype: "Link", + options: "User" + } + ], + after_datatable_render: function(datatable_obj) { + $(datatable_obj.wrapper).find(".dt-row-0").find('input[type=checkbox]').click(); + }, + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + events: { + onCheckRow: function(data) { + if (data && data.length) { + row_name = data[2].content; + row_values = data.slice(3).map(function(column) { + return column.content; + }) + entry = { + 'name': row_name, + 'values': row_values + } + + let raw_data = frappe.query_report.chart.data; + let new_datasets = raw_data.datasets; + + var found = false; + + for(var i=0; i < new_datasets.length; i++){ + if (new_datasets[i].name == row_name){ + found = true; + new_datasets.splice(i,1); + break; + } + } + + if (!found){ + new_datasets.push(entry); + } + + let new_data = { + labels: raw_data.labels, + datasets: new_datasets + } + + setTimeout(() => { + frappe.query_report.chart.update(new_data) + },500) + + + setTimeout(() => { + frappe.query_report.chart.draw(true); + }, 1000) + + frappe.query_report.raw_chart_data = new_data; + } + }, + } + }); + } +}; \ No newline at end of file diff --git a/erpnext/support/report/issue_analytics/issue_analytics.json b/erpnext/support/report/issue_analytics/issue_analytics.json new file mode 100644 index 0000000000..dd18498d1d --- /dev/null +++ b/erpnext/support/report/issue_analytics/issue_analytics.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2020-10-09 19:52:10.227317", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2020-10-11 19:43:19.358625", + "modified_by": "Administrator", + "module": "Support", + "name": "Issue Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Issue", + "report_name": "Issue Analytics", + "report_type": "Script Report", + "roles": [ + { + "role": "Support Team" + } + ] +} \ No newline at end of file diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py new file mode 100644 index 0000000000..0b629151a6 --- /dev/null +++ b/erpnext/support/report/issue_analytics/issue_analytics.py @@ -0,0 +1,222 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from six import iteritems +from frappe import _, scrub +from frappe.utils import getdate, flt, add_to_date, add_days +from erpnext.accounts.utils import get_fiscal_year + +def execute(filters=None): + return IssueAnalytics(filters).run() + +class IssueAnalytics(object): + def __init__(self, filters=None): + """Issue Analytics Report""" + self.filters = frappe._dict(filters or {}) + self.get_period_date_ranges() + + def run(self): + self.get_columns() + self.get_data() + self.get_chart_data() + + return self.columns, self.data, None, self.chart + + def get_columns(self): + self.columns = [] + + if self.filters.based_on == 'Customer': + self.columns.append({ + 'label': _('Customer'), + 'options': 'Customer', + 'fieldname': 'customer', + 'fieldtype': 'Link', + 'width': 200 + }) + + elif self.filters.based_on == 'Assigned To': + self.columns.append({ + 'label': _('User'), + 'fieldname': 'user', + 'fieldtype': 'Link', + 'options': 'User', + 'width': 200 + }) + + elif self.filters.based_on == 'Issue Type': + self.columns.append({ + 'label': _('Issue Type'), + 'fieldname': 'issue_type', + 'fieldtype': 'Link', + 'options': 'Issue Type', + 'width': 200 + }) + + elif self.filters.based_on == 'Issue Priority': + self.columns.append({ + 'label': _('Issue Priority'), + 'fieldname': 'priority', + 'fieldtype': 'Link', + 'options': 'Issue Priority', + 'width': 200 + }) + + for end_date in self.periodic_daterange: + period = self.get_period(end_date) + self.columns.append({ + 'label': _(period), + 'fieldname': scrub(period), + 'fieldtype': 'Int', + 'width': 120 + }) + + self.columns.append({ + 'label': _('Total'), + 'fieldname': 'total', + 'fieldtype': 'Int', + 'width': 120 + }) + + def get_data(self): + self.get_issues() + self.get_rows() + + def get_period(self, date): + months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + if self.filters.range == 'Weekly': + period = 'Week ' + str(date.isocalendar()[1]) + elif self.filters.range == 'Monthly': + period = str(months[date.month - 1]) + elif self.filters.range == 'Quarterly': + period = 'Quarter ' + str(((date.month - 1) // 3) + 1) + else: + year = get_fiscal_year(date, self.filters.company) + period = str(year[0]) + + if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year and self.filters.range != 'Yearly': + period += ' ' + str(date.year) + + return period + + def get_period_date_ranges(self): + from dateutil.relativedelta import relativedelta, MO + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) + + increment = { + 'Monthly': 1, + 'Quarterly': 3, + 'Half-Yearly': 6, + 'Yearly': 12 + }.get(self.filters.range, 1) + + if self.filters.range in ['Monthly', 'Quarterly']: + from_date = from_date.replace(day=1) + elif self.filters.range == 'Yearly': + from_date = get_fiscal_year(from_date)[1] + else: + from_date = from_date + relativedelta(from_date, weekday=MO(-1)) + + self.periodic_daterange = [] + for dummy in range(1, 53): + if self.filters.range == 'Weekly': + period_end_date = add_days(from_date, 6) + else: + period_end_date = add_to_date(from_date, months=increment, days=-1) + + if period_end_date > to_date: + period_end_date = to_date + + self.periodic_daterange.append(period_end_date) + + from_date = add_days(period_end_date, 1) + if period_end_date == to_date: + break + + def get_issues(self): + filters = self.get_common_filters() + self.field_map = { + 'Customer': 'customer', + 'Issue Type': 'issue_type', + 'Issue Priority': 'priority', + 'Assigned To': '_assign' + } + + self.entries = frappe.db.get_all('Issue', + fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'], + filters=filters, + debug=1 + ) + + def get_common_filters(self): + filters = {} + filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + + if self.filters.get('assigned_to'): + filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%') + + for entry in ['company', 'status', 'priority', 'customer', 'project']: + if self.filters.get(entry): + filters[entry] = self.filters.get(entry) + + return filters + + def get_rows(self): + self.data = [] + self.get_periodic_data() + + for entity, period_data in iteritems(self.issue_periodic_data): + if self.filters.based_on == 'Customer': + row = {'customer': entity} + elif self.filters.based_on == 'Assigned To': + row = {'user': entity} + elif self.filters.based_on == 'Issue Type': + row = {'issue_type': entity} + elif self.filters.based_on == 'Issue Priority': + row = {'priority': entity} + + total = 0 + for end_date in self.periodic_daterange: + period = self.get_period(end_date) + amount = flt(period_data.get(period, 0.0)) + row[scrub(period)] = amount + total += amount + + row['total'] = total + + self.data.append(row) + + def get_periodic_data(self): + self.issue_periodic_data = frappe._dict() + + for d in self.entries: + period = self.get_period(d.get('opening_date')) + + if self.filters.based_on == 'Assigned To': + if d._assign: + for entry in json.loads(d._assign): + self.issue_periodic_data.setdefault(entry, frappe._dict()).setdefault(period, 0.0) + self.issue_periodic_data[entry][period] += 1 + + else: + field = self.field_map.get(self.filters.based_on) + value = d.get(field) + if not value: + value = _('Not Specified') + + self.issue_periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0.0) + self.issue_periodic_data[value][period] += 1 + + def get_chart_data(self): + length = len(self.columns) + labels = [d.get('label') for d in self.columns[1:length-1]] + self.chart = { + 'data': { + 'labels': labels, + 'datasets': [] + }, + 'type': 'line' + } \ No newline at end of file diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py new file mode 100644 index 0000000000..432906db9b --- /dev/null +++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py @@ -0,0 +1,211 @@ +from __future__ import unicode_literals +import unittest +import frappe +from frappe.utils import getdate, add_months +from erpnext.support.report.issue_analytics.issue_analytics import execute +from erpnext.support.doctype.issue.test_issue import make_issue, create_customer +from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues +from frappe.desk.form.assign_to import add as add_assignment + +months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +class TestIssueAnalytics(unittest.TestCase): + @classmethod + def setUpClass(self): + frappe.db.sql("delete from `tabIssue` where company='_Test Company'") + frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) + + current_month_date = getdate() + last_month_date = add_months(current_month_date, -1) + self.current_month = str(months[current_month_date.month - 1]).lower() + '_' + str(current_month_date.year) + self.last_month = str(months[last_month_date.month - 1]).lower() + '_' + str(last_month_date.year) + + def test_issue_analytics(self): + create_service_level_agreements_for_issues() + create_issue_types() + create_records() + + self.compare_result_for_customer() + self.compare_result_for_issue_type() + self.compare_result_for_issue_priority() + self.compare_result_for_assignment() + + def compare_result_for_customer(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Customer', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'customer': '__Test Customer 2', + self.last_month: 1.0, + self.current_month: 0.0, + 'total': 1.0 + }, + { + 'customer': '__Test Customer 1', + self.last_month: 0.0, + self.current_month: 1.0, + 'total': 1.0 + }, + { + 'customer': '__Test Customer', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + def compare_result_for_issue_type(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Issue Type', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'issue_type': 'Discomfort', + self.last_month: 1.0, + self.current_month: 0.0, + 'total': 1.0 + }, + { + 'issue_type': 'Service Request', + self.last_month: 0.0, + self.current_month: 1.0, + 'total': 1.0 + }, + { + 'issue_type': 'Bug', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + def compare_result_for_issue_priority(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Issue Priority', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'priority': 'Medium', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + }, + { + 'priority': 'Low', + self.last_month: 1.0, + self.current_month: 0.0, + 'total': 1.0 + }, + { + 'priority': 'High', + self.last_month: 0.0, + self.current_month: 1.0, + 'total': 1.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + def compare_result_for_assignment(self): + filters = { + 'company': '_Test Company', + 'based_on': 'Assigned To', + 'from_date': add_months(getdate(), -1), + 'to_date': getdate(), + 'range': 'Monthly' + } + + report = execute(filters) + + expected_data = [ + { + 'user': 'test@example.com', + self.last_month: 1.0, + self.current_month: 1.0, + 'total': 2.0 + }, + { + 'user': 'test1@example.com', + self.last_month: 2.0, + self.current_month: 1.0, + 'total': 3.0 + } + ] + + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols + + +def create_issue_types(): + for entry in ['Bug', 'Service Request', 'Discomfort']: + if not frappe.db.exists('Issue Type', entry): + frappe.get_doc({ + 'doctype': 'Issue Type', + '__newname': entry + }).insert() + + +def create_records(): + create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory") + create_customer("__Test Customer 1", "_Test SLA Customer Group", "__Test SLA Territory") + create_customer("__Test Customer 2", "_Test SLA Customer Group", "__Test SLA Territory") + + current_month_date = getdate() + last_month_date = add_months(current_month_date, -1) + + issue = make_issue(current_month_date, "__Test Customer", 2, "High", "Bug") + add_assignment({ + "assign_to": ["test@example.com"], + "doctype": "Issue", + "name": issue.name + }) + + issue = make_issue(last_month_date, "__Test Customer", 2, "Low", "Bug") + add_assignment({ + "assign_to": ["test1@example.com"], + "doctype": "Issue", + "name": issue.name + }) + + issue = make_issue(current_month_date, "__Test Customer 1", 2, "Medium", "Service Request") + add_assignment({ + "assign_to": ["test1@example.com"], + "doctype": "Issue", + "name": issue.name + }) + + issue = make_issue(last_month_date, "__Test Customer 2", 2, "Medium", "Discomfort") + add_assignment({ + "assign_to": ["test@example.com", "test1@example.com"], + "doctype": "Issue", + "name": issue.name + }) \ No newline at end of file From 7b8eac958ec0a60229620425c3dbaa35a8add7ff Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 18 Jan 2021 16:44:41 +0530 Subject: [PATCH 73/73] feat: re-linking bank accounts with plaid (#24392) --- erpnext/accounts/doctype/bank/bank.js | 85 ++++++++++++++++++- .../doctype/plaid_settings/plaid_connector.py | 23 +++-- .../doctype/plaid_settings/plaid_settings.js | 18 ++-- .../doctype/plaid_settings/plaid_settings.py | 6 +- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js index de9498e075..49b2b186c4 100644 --- a/erpnext/accounts/doctype/bank/bank.js +++ b/erpnext/accounts/doctype/bank/bank.js @@ -1,5 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.provide('erpnext.integrations'); frappe.ui.form.on('Bank', { onload: function(frm) { @@ -20,7 +21,12 @@ frappe.ui.form.on('Bank', { frm.set_df_property('address_and_contact', 'hidden', 0); frappe.contacts.render_address_and_contact(frm); } - }, + if (frm.doc.plaid_access_token) { + frm.add_custom_button(__('Refresh Plaid Link'), () => { + new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token); + }); + } + } }); @@ -40,4 +46,79 @@ let add_fields_to_mapping_table = function (frm) { frm.doc.name).options = options; frm.fields_dict.bank_transaction_mapping.grid.refresh(); -}; \ No newline at end of file +}; + +erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { + constructor(access_token) { + this.access_token = access_token; + this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; + this.init_config(); + } + + async init_config() { + this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env'); + this.token = await this.get_link_token_for_update(); + this.init_plaid(); + } + + async get_link_token_for_update() { + const token = frappe.xcall( + 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update', + { access_token: this.access_token } + ) + if (!token) { + frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information')); + } + return token; + } + + 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 = Plaid.create({ + env: me.plaid_env, + token: me.token, + onSuccess: me.plaid_success + }); + } + + onScriptError(error) { + frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + console.log(error); + } + + plaid_success(token, response) { + frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); + } +}; diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 8d4b510490..66d0e5f77d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -29,14 +29,11 @@ class PlaidConnector(): response = self.client.Item.public_token.exchange(public_token) access_token = response["access_token"] return access_token - - def get_link_token(self): + + def get_token_request(self, update_mode=False): country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"] - token_request = { + args = { "client_name": self.client_name, - "client_id": self.settings.plaid_client_id, - "secret": self.settings.plaid_secret, - "products": self.products, # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", "country_codes": country_codes, @@ -45,6 +42,20 @@ class PlaidConnector(): } } + if update_mode: + args["access_token"] = self.access_token + else: + args.update({ + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + }) + + return args + + def get_link_token(self, update_mode=False): + token_request = self.get_token_request(update_mode) + try: response = self.client.LinkToken.create(token_request) except InvalidRequestError: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index f26b130805..bbc2ca8846 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -12,7 +12,7 @@ frappe.ui.form.on('Plaid Settings', { refresh: function (frm) { if (frm.doc.enabled) { - frm.add_custom_button('Link a new bank account', () => { + frm.add_custom_button(__('Link a new bank account'), () => { new erpnext.integrations.plaidLink(frm); }); @@ -46,10 +46,18 @@ erpnext.integrations.plaidLink = class plaidLink { this.product = ["auth", "transactions"]; this.plaid_env = this.frm.doc.plaid_env; this.client_name = frappe.boot.sitename; - this.token = await this.frm.call("get_link_token").then(resp => resp.message); + this.token = await this.get_link_token(); this.init_plaid(); } + async get_link_token() { + const token = await this.frm.call("get_link_token").then(resp => resp.message); + if (!token) { + frappe.throw(__('Cannot retrieve link token. Check Error Log for more information')); + } + return token; + } + init_plaid() { const me = this; me.loadScript(me.plaidUrl) @@ -94,8 +102,8 @@ erpnext.integrations.plaidLink = class plaidLink { } onScriptError(error) { - frappe.msgprint("There was an issue connecting to Plaid's authentication server"); - frappe.msgprint(error); + frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information")); + console.log(error); } plaid_success(token, response) { @@ -123,4 +131,4 @@ erpnext.integrations.plaidLink = class plaidLink { }); }, __("Select a company"), __("Continue")); } -}; +}; \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index e12d9ee46c..70c7f3fe5d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -230,7 +230,6 @@ def automatic_synchronization(): if settings.enabled == 1 and settings.automatic_sync == 1: enqueue_synchronization() - @frappe.whitelist() def enqueue_synchronization(): plaid_accounts = frappe.get_all("Bank Account", @@ -243,3 +242,8 @@ def enqueue_synchronization(): bank=plaid_account.bank, bank_account=plaid_account.name ) + +@frappe.whitelist() +def get_link_token_for_update(access_token): + plaid = PlaidConnector(access_token) + return plaid.get_link_token(update_mode=True)