From 9d829532420689bb2d4692b77180f3c47c54e3b5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 6 Aug 2020 23:58:56 +0530 Subject: [PATCH 001/203] fix: Opportunity Status fix --- erpnext/selling/doctype/quotation/quotation.py | 17 ++++++++--------- .../selling/doctype/sales_order/sales_order.py | 1 - 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 449a968a4f9..01479a16540 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -25,7 +25,6 @@ class Quotation(SellingController): def validate(self): super(Quotation, self).validate() self.set_status() - self.update_opportunity() self.validate_uom_is_integer("stock_uom", "qty") self.validate_valid_till() self.set_customer_name() @@ -50,20 +49,20 @@ class Quotation(SellingController): lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) self.customer_name = company_name or lead_name - def update_opportunity(self): + def update_opportunity(self, status): for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])): if opportunity: - self.update_opportunity_status(opportunity) + self.update_opportunity_status(status, opportunity) if self.opportunity: - self.update_opportunity_status() + self.update_opportunity_status(status) - def update_opportunity_status(self, opportunity=None): + def update_opportunity_status(self, status, opportunity=None): if not opportunity: opportunity = self.opportunity opp = frappe.get_doc("Opportunity", opportunity) - opp.status = None + opp.status = status opp.set_status(update=True) def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): @@ -82,7 +81,7 @@ class Quotation(SellingController): else: frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason')))) - self.update_opportunity() + self.update_opportunity('Lost') self.update_lead() self.save() @@ -95,7 +94,7 @@ class Quotation(SellingController): self.company, self.base_grand_total, self) #update enquiry status - self.update_opportunity() + self.update_opportunity('Quotation') self.update_lead() def on_cancel(self): @@ -105,7 +104,7 @@ class Quotation(SellingController): #update enquiry status self.set_status(update=True) - self.update_opportunity() + self.update_opportunity('Open') self.update_lead() def print_other_charges(self,docname): diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ffb66354fa0..f17af69e5be 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -159,7 +159,6 @@ class SalesOrder(SellingController): frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) - doc.update_opportunity() def validate_drop_ship(self): for d in self.get('items'): From 288ced24dbf771988b1c601d2e6d5f0fde07bd12 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 30 Aug 2020 19:33:27 +0530 Subject: [PATCH 002/203] 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 f3cecd9059b..122a1a96f4e 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 003/203] 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 122a1a96f4e..c91b01eaa9f 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 004/203] 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 cae150c428a..afbd9b4e6e0 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 005/203] 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 c91b01eaa9f..8ed68888541 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 006/203] 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 8ed68888541..3cdfcb212f5 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 73d944da21045d1f6387b5fc583e37e37850c30d Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 13 Oct 2020 18:11:05 +0530 Subject: [PATCH 007/203] fix: review changes --- erpnext/selling/doctype/quotation/quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7c55d7742f8..3157982d528 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -62,7 +62,7 @@ class Quotation(SellingController): opportunity = self.opportunity opp = frappe.get_doc("Opportunity", opportunity) - opp.status = status + opp.set_status(status=status) opp.set_status(update=True) def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): From 8b6370bb45232d0e39508a1329258388c4d47439 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Nov 2020 22:41:36 +0530 Subject: [PATCH 008/203] 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 00000000000..e69de29bb2d 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 00000000000..6c254fcfb73 --- /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 00000000000..e626a09ce0c --- /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 00000000000..ccfafd96ed3 --- /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 00000000000..c271a25fbd8 --- /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 00000000000..e69de29bb2d 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 00000000000..20024b03226 --- /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 00000000000..c2afc1a2621 --- /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 00000000000..e69de29bb2d 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 00000000000..8305da2ba08 --- /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 00000000000..0fccaf302fb --- /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 009/203] 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 f888d9e038a..b9d4da289ee 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 8fe3816c24a..015807d5639 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 b6720c05cb2..34b563553e1 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 010/203] 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 560a5617da5..7b7a9df1ac0 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 011/203] 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 cadf1e7e0ca..48cc493522a 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 ff12967155f..d60a7b76cc4 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 524a671801b..0d2b8cbf151 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 e1174717382..ea5487d5754 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 1d41d0fa2a9..3c07ee75cbb 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 502e65ed8d0..e27bd2fa3ac 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 d0904eec3e3..7cfbfed1388 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 7ad164a8b9b..3af3948fac6 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 221300b519a..cbafd7d3ac0 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 1abc869c539..96006158b68 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 6e97d811fc1..45c494e3e56 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 1358a4bd088..ec6b3dc6df1 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 012/203] 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 48cc493522a..1b793982472 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 96006158b68..bc34d6c3b15 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 013/203] 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 6c254fcfb73..3e880d3ca68 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 ccfafd96ed3..0dcf1164238 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 bc34d6c3b15..d32fdbcaf16 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 7b7a9df1ac0..98f1b504ccc 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 34b563553e1..7a42fb56148 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 014/203] 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 0f115f9cc20..62d17c4e697 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 53c07583d8e..402ea5085e5 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 2e5a7142a33..26acad3a4fb 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 9660c9570e2..29b1ea2f9f0 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 5510d5ac020..763b4036c3a 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 4e9ee3b143a..336e13c9b77 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 6e97f0513d6..d9b472cce7f 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 0c4f6f1bdfe..f31225e36b6 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 47a28fd1114..6ad8a19532b 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 a5ce44dcf24..4cb38049ff4 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 015/203] 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 def9ed6803e..1ac607940fc 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 016/203] 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 3e880d3ca68..c8c32d58bd3 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 e626a09ce0c..c1190a395fe 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 0dcf1164238..210b2c8ea89 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 20024b03226..c2d34b3b7e6 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 8305da2ba08..5c809515c29 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 1ac607940fc..b3caf6a82e8 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 017/203] 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 104880f6f34..b5375e106fe 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 210b2c8ea89..440073b7230 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 c271a25fbd8..feb0af1bd59 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 018/203] 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 b5375e106fe..fc1d7e344af 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 c8c32d58bd3..994ee44354d 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 feb0af1bd59..02fd75e7595 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 019/203] 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 02fd75e7595..801786b6e96 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 020/203] 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 994ee44354d..f0362d31403 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 8aeb340dc8bd87651a73bbfdef9e3d2c681de878 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 19 Nov 2020 19:18:48 +0530 Subject: [PATCH 021/203] fix: add remarks to sales invoice --- .../doctype/sales_invoice/sales_invoice.py | 9 ++++-- erpnext/patches.txt | 1 + .../v12_0/update_sales_invoice_remarks.py | 32 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v12_0/update_sales_invoice_remarks.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index af6c6968dc1..0530aa2d234 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults -from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form +from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date from frappe.model.mapper import get_mapped_doc @@ -535,7 +535,12 @@ class SalesInvoice(SellingController): self.against_income_account = ','.join(against_acc) def add_remarks(self): - if not self.remarks: self.remarks = 'No Remarks' + if not self.remarks: + if self.po_no and self.po_date: + self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, + formatdate(self.po_date)) + else: + self.remarks = _("No Remarks") def validate_auto_set_posting_time(self): # Don't auto set the posting date and time if invoice is amended diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 25be8841174..4a38cb3ab80 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v12_0.update_sales_invoice_remarks \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_sales_invoice_remarks.py b/erpnext/patches/v12_0/update_sales_invoice_remarks.py new file mode 100644 index 00000000000..7e8feaaca6c --- /dev/null +++ b/erpnext/patches/v12_0/update_sales_invoice_remarks.py @@ -0,0 +1,32 @@ +from __future__ import unicode_literals +import frappe + +from frappe import _ +from frappe.utils import formatdate + +def execute(): + si_list = frappe.db.get_all('Sales Invoice', filters = { + 'docstatus': 1, + 'remarks': 'No Remarks', + 'po_no' : ['!=', ''], + 'po_date' : ['!=', ''] + }, + fields = ['name', 'po_no', 'po_date'] + ) + + for doc in si_list: + remarks = _("Against Customer Order {0} dated {1}").format(doc.po_no, + formatdate(doc.po_date)) + + frappe.db.set_value('Sales Invoice', doc.name, 'remarks', remarks) + + gl_entry_list = frappe.db.get_all('GL Entry', filters = { + 'voucher_type': 'Sales Invoice', + 'remarks': 'No Remarks', + 'voucher_no' : doc.name + }, + fields = ['name'] + ) + + for entry in gl_entry_list: + frappe.db.set_value('GL Entry', entry.name, 'remarks', remarks) \ No newline at end of file From 6c17b84caef6300f3872ce5b5db031e0ec7501fd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 Nov 2020 13:42:16 +0530 Subject: [PATCH 022/203] 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 015807d5639..e3aac9aba85 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 023/203] 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 9a6c3893393..65c5ff1ceaf 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 b9d4da289ee..52e9ff8b764 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 f0362d31403..a2526e92c36 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 c1190a395fe..7736b2dffb2 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 440073b7230..6aef9caa747 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 024/203] 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 801786b6e96..f67e1de4044 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 b3caf6a82e8..f586de82e35 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 d92af5d7227..dcf3d6bad1a 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 025/203] 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 c2d34b3b7e6..7fe2a3c647e 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 5c809515c29..95e98d0b673 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 026/203] 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 1b793982472..e60bc60475e 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 d60a7b76cc4..37b03f3f0e0 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 0d2b8cbf151..f90f86728de 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 3ce5701823e..c087980798c 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 ea5487d5754..4318aea2bda 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 558e21c13aa..668cf016d35 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 3c07ee75cbb..3fa6ee76e61 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 e27bd2fa3ac..b89cecfab5e 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 7cfbfed1388..8e4b806f02d 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 3af3948fac6..e9c8aff678d 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 a6e6974c48d..79c8861bcdc 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 2f52a9e0355..92e2708eab6 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 75dd4469e84..65b5fa6cf23 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 b331c6d3c0e..310c4105f47 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 aaf42b47517..433bd64d2fb 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 cbafd7d3ac0..e399b22f90f 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 d32fdbcaf16..410840771cf 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 ec6b3dc6df1..3f293782b41 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 7a42fb56148..c79736d0e19 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 251a26a592e..ccc719c26d0 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 01edd99e9d2..527b0d3ea93 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 bc1d81d3565..d998729c479 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 91217582ca4..80f18a63f56 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 e2121fce3f2..be9404d9c8c 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 027/203] 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 7dd5b017703..a74fa062b6a 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 028/203] 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 f67e1de4044..fa700e115ce 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 f586de82e35..fd6f01e345f 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 dcf3d6bad1a..04291cd5bd1 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 029/203] 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 891bbe5b598..2635d47f886 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 7fef622b136c4e7da72651a03c120878e2a6d386 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 4 Dec 2020 19:18:36 +0530 Subject: [PATCH 030/203] fix: drop ship partial order fixed --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ae227e0110e..3e1c82f9616 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -831,7 +831,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar for supplier in suppliers: po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) - if len(po) == 0: + if len(po) == 0 or any( item.get("delivered_by_supplier") == 1 for item in selected_items): doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { "doctype": "Purchase Order", From c32ac223ed181d3a1a122775b93d6e4672d2e38f Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 14:53:15 +0530 Subject: [PATCH 031/203] feat: adding task field in project template --- .../project_template_task.json | 195 ++---------------- erpnext/projects/doctype/task/task.json | 21 +- 2 files changed, 32 insertions(+), 184 deletions(-) diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json index 8644d897bb0..80c510db1b0 100644 --- a/erpnext/projects/doctype/project_template_task/project_template_task.json +++ b/erpnext/projects/doctype/project_template_task/project_template_task.json @@ -1,203 +1,32 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2019-02-18 17:24:41.830096", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "task" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "subject", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, + "fieldname": "task", + "fieldtype": "Link", "in_list_view": 1, - "in_standard_filter": 0, - "label": "Subject", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start", - "fieldtype": "Int", - "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": "Begin On (Days)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "duration", - "fieldtype": "Int", - "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": "Duration (Days)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "task_weight", - "fieldtype": "Float", - "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": "Task Weight", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "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, - "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 + "label": "Task", + "options": "Task", + "reqd": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-02-18 18:30:22.688966", + "links": [], + "modified": "2020-12-07 13:28:40.961810", "modified_by": "Administrator", "module": "Projects", "name": "Project Template Task", - "name_case": "", "owner": "Administrator", "permissions": [], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index 27f1a71a528..a9e3d9bc0fe 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -12,6 +12,7 @@ "issue", "type", "is_group", + "is_template", "column_break0", "status", "priority", @@ -22,9 +23,11 @@ "sb_timeline", "exp_start_date", "expected_time", + "start", "column_break_11", "exp_end_date", "progress", + "duration", "is_milestone", "sb_details", "description", @@ -360,6 +363,22 @@ "label": "Completed By", "no_copy": 1, "options": "User" + }, + { + "default": "0", + "fieldname": "is_template", + "fieldtype": "Check", + "label": "Is Template" + }, + { + "fieldname": "start", + "fieldtype": "Int", + "label": "Begin On (Days)" + }, + { + "fieldname": "duration", + "fieldtype": "Int", + "label": "Duration (Days)" } ], "icon": "fa fa-check", @@ -367,7 +386,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2020-07-03 12:36:04.960457", + "modified": "2020-12-07 13:26:53.614689", "modified_by": "Administrator", "module": "Projects", "name": "Task", From 5597e386b437ce7fda30960005d64d5fcc00d6a9 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 15:53:45 +0530 Subject: [PATCH 032/203] fix: added test for drop ship partial order --- .../doctype/sales_order/test_sales_order.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a33d401b572..1d11ad6a7e4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -772,6 +772,56 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() so.cancel() + def test_drop_shipping_partial_order(self): + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \ + update_status as so_update_status + + # make items + po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1}) + po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1}) + + so_items = [ + { + "item_code": po_item1.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": po_item2.item_code, + "warehouse": "_Test Warehouse - _TC", + "qty": 2, + "rate": 300, + "conversion_factor": 1.0 + } + ] + + # create so and po + so = make_sales_order(item_list=so_items, do_not_submit=True) + so.submit() + + # create po for only one item + po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) + po.submit() + + self.assertEqual(so.customer, po.customer) + self.assertEqual(po.items[0].sales_order, so.name) + self.assertEqual(po.items[0].item_code, po_item.item_code) + #test po_item length + self.assertEqual(len(po.items), 1) + + # create po for remaining item + po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]]) + po.submit() + + # teardown + so_update_status("Draft", so.name) + po.cancel() + so.load_from_db() + so.cancel() + def test_reserved_qty_for_closing_so(self): bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"]) From 89624ad6226cdd09a413589b1b87a73607cf9d6d Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 16:50:03 +0530 Subject: [PATCH 033/203] fix: sider issue fixed --- erpnext/selling/doctype/sales_order/test_sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 1d11ad6a7e4..a0af49982b8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -808,7 +808,7 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.customer, po.customer) self.assertEqual(po.items[0].sales_order, so.name) - self.assertEqual(po.items[0].item_code, po_item.item_code) + self.assertEqual(po.items[0].item_code, po_item1.item_code) #test po_item length self.assertEqual(len(po.items), 1) From 32e77d73cf992551fc6bf77a7c2607deed1aa7a2 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 17:39:37 +0530 Subject: [PATCH 034/203] fix: added supplier to second item in test --- erpnext/selling/doctype/sales_order/test_sales_order.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a0af49982b8..0ca58121f41 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -791,10 +791,11 @@ class TestSalesOrder(unittest.TestCase): }, { "item_code": po_item2.item_code, - "warehouse": "_Test Warehouse - _TC", + "warehouse": "", "qty": 2, - "rate": 300, - "conversion_factor": 1.0 + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' } ] From 71b1a0ca7dd3e6cd4cbc91d50e6ffe4b62737ef0 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 7 Dec 2020 19:09:29 +0530 Subject: [PATCH 035/203] fix: cancelling both test po created --- .../doctype/sales_order/test_sales_order.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 0ca58121f41..5954602bea6 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -804,22 +804,24 @@ class TestSalesOrder(unittest.TestCase): so.submit() # create po for only one item - po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) - po.submit() + po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) + po1.submit() - self.assertEqual(so.customer, po.customer) - self.assertEqual(po.items[0].sales_order, so.name) - self.assertEqual(po.items[0].item_code, po_item1.item_code) - #test po_item length - self.assertEqual(len(po.items), 1) + self.assertEqual(so.customer, po1.customer) + self.assertEqual(po1.items[0].sales_order, so.name) + self.assertEqual(po1.items[0].item_code, po_item1.item_code) + #test po item length + self.assertEqual(len(po1.items), 1) # create po for remaining item - po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]]) - po.submit() + po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]]) + po2.submit() # teardown so_update_status("Draft", so.name) - po.cancel() + + po1.cancel() + po2.cancel() so.load_from_db() so.cancel() From c82b52855cf43774e78d54a7fcd41c62884ac47a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 8 Dec 2020 11:07:59 +0530 Subject: [PATCH 036/203] fix: template filter for tasks --- .../doctype/project_template/project_template.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index d7a876dfbd3..acc6849f2bb 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -5,4 +5,13 @@ frappe.ui.form.on('Project Template', { // refresh: function(frm) { // } + setup: function (frm) { + me.frm.set_query("task", "tasks", function (doc, cdt, cdn) { + return { + filters: { + "is_template": 1 + } + } + }); + } }); From 91e1136d2c5d368256ac0934dca3b83ef3a71ff6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 8 Dec 2020 22:53:03 +0530 Subject: [PATCH 037/203] feat: Batch wise item pricing --- erpnext/public/js/controllers/transaction.js | 5 +++++ erpnext/stock/doctype/item_price/item_price.py | 5 +++-- erpnext/stock/get_item_details.py | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7f08cd1359f..3dcec892e42 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1103,6 +1103,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + batch_no: function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + this.apply_pricing_rule(item, true); + }, + toggle_conversion_factor: function(item) { // toggle read only property for conversion factor field if the uom and stock uom are same if(this.frm.get_field('items').grid.fields_map.conversion_factor) { diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bed5ea9ab66..e82a19b0dc0 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -54,7 +54,8 @@ class ItemPrice(Document): "valid_upto", "packing_unit", "customer", - "supplier",]: + "supplier", + "batch_no"]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: @@ -68,7 +69,7 @@ class ItemPrice(Document): self.as_dict(),) if price_list_rate: - frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) + frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) def before_save(self): if self.selling: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 08f7a83b893..a5f8e5f0a05 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -680,6 +680,9 @@ def get_item_price(args, item_code, ignore_party=False): else: conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" + if args.get('batch_no'): + conditions += "and batch_no = %(batch_no)s" + if args.get('transaction_date'): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" @@ -709,6 +712,7 @@ def get_price_list_rate_for(args, item_code): "uom": args.get('uom'), "transaction_date": args.get('transaction_date'), "posting_date": args.get('posting_date'), + "batch_no": args.get('batch_no') } item_price_data = 0 From bc0a2859e44feecec1611fab0e4b6f4867ba1a4c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Dec 2020 12:40:09 +0530 Subject: [PATCH 038/203] fix: Rate for items with no batch --- erpnext/public/js/controllers/buying.js | 4 + erpnext/public/js/controllers/transaction.js | 3 +- erpnext/selling/sales_common.js | 4 + .../stock/doctype/item_price/item_price.json | 116 ++++++++++++++---- erpnext/stock/get_item_details.py | 7 +- 5 files changed, 102 insertions(+), 32 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index e5be4997c1d..a537b8633d7 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -194,6 +194,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this._super(doc, cdt, cdn); }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + received_qty: function(doc, cdt, cdn) { this.calculate_accepted_qty(doc, cdt, cdn) }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3dcec892e42..4a9ecd7784f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1105,7 +1105,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ batch_no: function(doc, cdt, cdn) { let item = frappe.get_doc(cdt, cdn); - this.apply_pricing_rule(item, true); + this.apply_price_list(item, true); }, toggle_conversion_factor: function(item) { @@ -1412,6 +1412,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ "pricing_rules": d.pricing_rules, "warehouse": d.warehouse, "serial_no": d.serial_no, + "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, "conversion_factor": d.conversion_factor || 1.0 }); diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 7f00fca8f05..ce084646e15 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -399,6 +399,10 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ } }, + batch_no: function(doc, cdt, cdn) { + this._super(doc, cdt, cdn); + }, + qty: function(doc, cdt, cdn) { this._super(doc, cdt, cdn); diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 5f62381f8b3..83177b372ad 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -18,6 +18,7 @@ "price_list", "customer", "supplier", + "batch_no", "column_break_3", "buying", "selling", @@ -47,31 +48,41 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM" + "options": "UOM", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit" + "label": "Packing Unit", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "item_code.brand", @@ -79,19 +90,25 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Brand", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags" + "options": "fa fa-tags", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list", @@ -100,7 +117,9 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -108,37 +127,49 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer" + "options": "Customer", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier" + "options": "Supplier", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag" + "options": "fa fa-tag", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -146,11 +177,15 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_rate", @@ -162,53 +197,80 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From" + "label": "Valid From", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days" + "label": "Lead Time in days", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_18", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Upto", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_24", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note" + "label": "Note", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference" + "label": "Reference", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-flag", "idx": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-06 22:31:32.943475", + "modified": "2020-12-08 18:12:15.395772", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index a5f8e5f0a05..2d2abd71aa1 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -672,6 +672,8 @@ def get_item_price(args, item_code, ignore_party=False): and price_list=%(price_list)s and ifnull(uom, '') in ('', %(uom)s)""" + conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" + if not ignore_party: if args.get("customer"): conditions += " and customer=%(customer)s" @@ -680,9 +682,6 @@ def get_item_price(args, item_code, ignore_party=False): else: conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" - if args.get('batch_no'): - conditions += "and batch_no = %(batch_no)s" - if args.get('transaction_date'): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" @@ -693,7 +692,7 @@ def get_item_price(args, item_code, ignore_party=False): return frappe.db.sql(""" select name, price_list_rate, uom from `tabItem Price` {conditions} - order by valid_from desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args) def get_price_list_rate_for(args, item_code): """ From 811629a9616f411eb40b0d0adb301dc5f0ff8cf3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 9 Dec 2020 15:52:05 +0530 Subject: [PATCH 039/203] feat: tasks in project template and tests --- erpnext/projects/doctype/project/project.py | 38 ++++++-- .../projects/doctype/project/test_project.py | 92 +++++++++++++++---- .../project_template/test_project_template.py | 49 +++++----- erpnext/projects/doctype/task/test_task.py | 6 +- 4 files changed, 126 insertions(+), 59 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5bbd29c4c42..04a0fb6c4f0 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -55,16 +55,34 @@ class Project(Document): # create tasks from template for task in template.tasks: - frappe.get_doc(dict( - doctype = 'Task', - subject = task.subject, - project = self.name, - status = 'Open', - exp_start_date = add_days(self.expected_start_date, task.start), - exp_end_date = add_days(self.expected_start_date, task.start + task.duration), - description = task.description, - task_weight = task.task_weight - )).insert() + template_task_details = frappe.get_doc("Task", task.task) + project_task = self.create_task_from_template(template_task_details) + + if template_task_details.depends_on: + for child_task in template_task_details.depends_on: + child_task_details = frappe.get_doc("Task",child_task.task) + self.create_task_from_template(child_task_details, project_task) + + def create_task_from_template(self, task_details, project_task=None): + doc = frappe.get_doc(dict( + doctype = 'Task', + subject = task_details.subject, + project = self.name, + status = 'Open', + exp_start_date = add_days(self.expected_start_date, task_details.start), + exp_end_date = add_days(self.expected_start_date, task_details.start + task_details.duration), + description = task_details.description, + task_weight = task_details.task_weight, + type = task_details.type, + issue = task_details.issue, + is_group = task_details.is_group + )) + if task_details.parent_task and project_task: + doc.parent_task = project_task.name + if not task_details.is_group: + doc.depends_on = task_details.depends_on + doc.insert() + return doc def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 0c4f6f1bdfe..ce95b056141 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -7,33 +7,87 @@ import frappe, unittest test_records = frappe.get_test_records('Project') test_ignore = ["Sales Order"] -from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template +from erpnext.projects.doctype.project_template.test_project_template import make_project_template from erpnext.projects.doctype.project.project import set_project_status - -from frappe.utils import getdate +from erpnext.projects.doctype.task.test_task import create_task +from frappe.utils import getdate, nowdate, add_days class TestProject(unittest.TestCase): - def test_project_with_template(self): - frappe.db.sql('delete from tabTask where project = "Test Project with Template"') - frappe.delete_doc('Project', 'Test Project with Template') + def test_project_with_template_having_no_parent_and_depend_tasks(self): + """ + Test Action: Basic Test of a Project created from template. The template has a single task. + """ + frappe.db.sql('delete from tabTask where project = "Test Project with Templ - no parent and dependend tasks"') + frappe.delete_doc('Project', 'Test Project with Templ - no parent and dependend tasks') - project = get_project('Test Project with Template') + if not frappe.db.exists("Task", "Test Temp Task with no parent and dependency"): + task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=0, duration=3) + template = make_project_template("Test Project Template - no parent and dependend tasks", [task1]) + project = get_project("Test Project with Templ - no parent and dependend tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - task1 = tasks[0] - self.assertEqual(task1.subject, 'Task 1') - self.assertEqual(task1.description, 'Task 1 description') - self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01')) - self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04')) + self.assertEqual(task[0].subject, 'Test Temp Task with no parent and dependency') + self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate() + 0 + 3)) + self.assertEqual(len(tasks), 1) - self.assertEqual(len(tasks), 4) - task4 = tasks[3] - self.assertEqual(task4.subject, 'Task 4') - self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06')) + def test_project_template_having_parent_child_tasks(self): -def get_project(name): - template = get_project_template() + frappe.db.sql('delete from tabTask where project = "Test Project with Templ - tasks with parent-child"') + frappe.delete_doc('Project', 'Test Project with Templ - tasks with parent-child') + + if not frappe.db.exists("Task", "Test Temp Task parent"): + task1 = create_task(subject="Test Temp Task parent", is_group=1, is_template=1, begin=1, duration=1) + + if not frappe.db.exists("Task", "Test Temp Task child 1"): + task2 = create_task(subject="Test Temp Task child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) + + if not frappe.db.exists("Task", "Test Temp Task child 2"): + task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) + + template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) + project = get_project("Test Project with Templ - tasks with parent-child", template) + tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + + self.assertEqual(task[0].subject, 'Test Temp Task parent') + self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 1 + 1)) + print(task[0].depends_on) + + self.assertEqual(task[1].subject, 'Test Temp Task child 1') + self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 1 + 3)) + self.assertEqual(task[1].parent_task, task[0].name) + + self.assertEqual(task[2].subject, 'Test Temp Task child 2') + self.assertEqual(getdate(task[2].exp_end_date), add_days(nowdate()+ 2 + 3)) + self.assertEqual(task[2].parent_task, task[0].name) + + self.assertEqual(len(tasks), 3) + + def test_project_template_having_dependent_tasks(self): + + frappe.db.sql('delete from tabTask where project = "Test Project with Templ - dependent tasks"') + frappe.delete_doc('Project', 'Test Project with Templ - dependent tasks') + + if not frappe.db.exists("Task", "Test Temp Task for dependency"): + task1 = create_task(subject="Test Temp Task for dependency", is_template=1, begin=3, duration=1) + + if not frappe.db.exists("Task", "Test Temp Task with dependency"): + task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) + + template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2]) + project = get_project("Test Project with Templ - tasks with parent-child", template) + tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + + self.assertEqual(task[0].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 3 + 1)) + + self.assertEqual(task[1].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 2 + 2)) + self.assertEqual(task[1].depends_on, task[0].name) + + self.assertEqual(len(tasks), 2) + +def get_project(name, template): project = frappe.get_doc(dict( doctype = 'Project', @@ -49,8 +103,6 @@ def make_project(args): args = frappe._dict(args) 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', diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 2c5831a5dc9..dd98d02c021 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -5,44 +5,37 @@ from __future__ import unicode_literals import frappe import unittest +from erpnext.projects.doctype.task.test_task import create_task class TestProjectTemplate(unittest.TestCase): pass -def get_project_template(): - if not frappe.db.exists('Project Template', 'Test Project Template'): - frappe.get_doc(dict( - doctype = 'Project Template', - name = 'Test Project Template', - tasks = [ - dict(subject='Task 1', description='Task 1 description', - start=0, duration=3), - dict(subject='Task 2', description='Task 2 description', - start=0, duration=2), - dict(subject='Task 3', description='Task 3 description', - start=2, duration=4), - dict(subject='Task 4', description='Task 4 description', - start=3, duration=2), - ] - )).insert() - - return frappe.get_doc('Project Template', 'Test Project Template') - -def make_project_template(project_template_name, project_tasks=[]): +def get_project_template(project_template_name="Test Project Template", project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): frappe.get_doc(dict( doctype = 'Project Template', name = project_template_name, tasks = project_tasks or [ - dict(subject='Task 1', description='Task 1 description', - start=0, duration=3), - dict(subject='Task 2', description='Task 2 description', - start=0, duration=2), - dict(subject='Task 3', description='Task 3 description', - start=2, duration=4), - dict(subject='Task 4', description='Task 4 description', - start=3, duration=2), + create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), + create_task(subject="_Test Template Task 3", is_template=1, begin=2, duration=4), + create_task(subject="_Test Template Task 4", is_template=1, begin=3, duration=2), ] )).insert() + return frappe.get_doc('Project Template', project_template_name) + +def make_project_template(project_template_name, project_tasks=[]): + if not frappe.db.exists('Project Template', project_template_name): + doc = frappe.get_doc(dict( + doctype = 'Project Template', + name = project_template_name, + tasks = project_tasks or [ + create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), + ] + )) + print("doc",doc.tasks) + doc.insert() + return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 47a28fd1114..181a2dc3162 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -97,7 +97,7 @@ class TestTask(unittest.TestCase): self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") -def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True): +def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True): if not frappe.db.exists("Task", subject): task = frappe.new_doc('Task') task.status = "Open" @@ -105,6 +105,10 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, sa task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() task.project = project or "_Test Project" + task.is_template = is_template, + task.start = begin + task.duration = duration, + task.is_group = is_group if save: task.save() else: From c41fb7556b37d80f41db48643bd1e3bffec7ee03 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Dec 2020 15:55:39 +0530 Subject: [PATCH 040/203] fix: Add test case for batch pricing --- erpnext/stock/doctype/batch/test_batch.py | 70 ++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index c2a3d3c151f..b0d30a765da 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -9,6 +9,8 @@ import unittest from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no from frappe.utils import cint, flt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.stock.get_item_details import get_item_details class TestBatch(unittest.TestCase): @@ -187,7 +189,7 @@ class TestBatch(unittest.TestCase): stock_entry.cancel() current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty) - + @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): '''Make a new stock entry for given target warehouse and batch name of item''' @@ -257,6 +259,72 @@ class TestBatch(unittest.TestCase): return batch + def test_batch_wise_item_price(self): + if not frappe.db.get_value('Item', '_Test Batch Price Item'): + doc = frappe.get_doc({ + 'doctype': 'Item', + 'is_stock_item': 1, + 'item_code': '_Test Batch Price Item', + 'item_group': 'Products', + 'has_batch_no': 1, + 'create_new_batch': 1 + }).insert(ignore_permissions=True) + + batch1 = create_batch('_Test Batch Price Item', 200, 1) + batch2 = create_batch('_Test Batch Price Item', 300, 1) + batch3 = create_batch('_Test Batch Price Item', 400, 0) + + args = frappe._dict({ + "item_code": "_Test Batch Price Item", + "company": "_Test Company with perpetual inventory", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + "name": None + }) + + #test price for batch1 + args.update({'batch_no': batch1}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 200) + + #test price for batch2 + args.update({'batch_no': batch2}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 300) + + #test price for batch3 + args.update({'batch_no': batch3}) + details = get_item_details(args) + self.assertEqual(details.get('price_list_rate'), 400) + +def create_batch(item_code, rate, create_item_price_for_batch): + pi = make_purchase_invoice(company="_Test Company with perpetual inventory", + warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, + expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) + + batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) + + if not create_item_price_for_batch: + create_price_list_for_batch(item_code, None, rate) + else: + create_price_list_for_batch(item_code, batch, rate) + + return batch + +def create_price_list_for_batch(item_code, batch, rate): + frappe.get_doc({ + 'doctype': 'Item Price', + 'item_code': '_Test Batch Price Item', + 'price_list': '_Test Price List', + 'batch_no': batch, + 'price_list_rate': rate + }).insert() + def make_new_batch(**args): args = frappe._dict(args) From f936e8a334bb8abab78d9938d6808d9e8688a6ee Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 9 Dec 2020 17:09:22 +0530 Subject: [PATCH 041/203] fix: sider issues fixed --- .../projects/doctype/project/test_project.py | 33 +++++++++---------- .../project_template/project_template.js | 4 +-- .../project_template/test_project_template.py | 6 ++-- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index ce95b056141..52f877b8b75 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -27,8 +27,8 @@ class TestProject(unittest.TestCase): project = get_project("Test Project with Templ - no parent and dependend tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(task[0].subject, 'Test Temp Task with no parent and dependency') - self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate() + 0 + 3)) + self.assertEqual(tasks[0].subject, 'Test Temp Task with no parent and dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate() + 0 + 3)) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): @@ -48,18 +48,17 @@ class TestProject(unittest.TestCase): template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + print(tasks, type(tasks), len(tasks)) + self.assertEqual(tasks[0].subject, 'Test Temp Task parent') + self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 1 + 1)) - self.assertEqual(task[0].subject, 'Test Temp Task parent') - self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 1 + 1)) - print(task[0].depends_on) + self.assertEqual(tasks[1].subject, 'Test Temp Task child 1') + self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 1 + 3)) + self.assertEqual(tasks[1].parent_task, tasks[0].name) - self.assertEqual(task[1].subject, 'Test Temp Task child 1') - self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 1 + 3)) - self.assertEqual(task[1].parent_task, task[0].name) - - self.assertEqual(task[2].subject, 'Test Temp Task child 2') - self.assertEqual(getdate(task[2].exp_end_date), add_days(nowdate()+ 2 + 3)) - self.assertEqual(task[2].parent_task, task[0].name) + self.assertEqual(tasks[2].subject, 'Test Temp Task child 2') + self.assertEqual(getdate(tasks[2].exp_end_date), add_days(nowdate()+ 2 + 3)) + self.assertEqual(tasks[2].parent_task, tasks[0].name) self.assertEqual(len(tasks), 3) @@ -78,12 +77,12 @@ class TestProject(unittest.TestCase): project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(task[0].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(task[0].exp_end_date), add_days(nowdate()+ 3 + 1)) + self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 3 + 1)) - self.assertEqual(task[1].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(task[1].exp_end_date), add_days(nowdate()+ 2 + 2)) - self.assertEqual(task[1].depends_on, task[0].name) + self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 2 + 2)) + self.assertEqual(tasks[1].depends_on, tasks[0].name) self.assertEqual(len(tasks), 2) diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index acc6849f2bb..7668df3e139 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -6,12 +6,12 @@ frappe.ui.form.on('Project Template', { // } setup: function (frm) { - me.frm.set_query("task", "tasks", function (doc, cdt, cdn) { + frm.set_query("task", "tasks", function () { return { filters: { "is_template": 1 } - } + }; }); } }); diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index dd98d02c021..379365f9998 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -27,15 +27,13 @@ def get_project_template(project_template_name="Test Project Template", project_ def make_project_template(project_template_name, project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): - doc = frappe.get_doc(dict( + frappe.get_doc(dict( doctype = 'Project Template', name = project_template_name, tasks = project_tasks or [ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), ] - )) - print("doc",doc.tasks) - doc.insert() + )).insert() return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file From 249595a2d8c4e314e079fe7d775124028982aaf6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Dec 2020 18:13:23 +0530 Subject: [PATCH 042/203] fix: Sider Issues --- erpnext/stock/doctype/batch/test_batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index b0d30a765da..81c443579c6 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -261,7 +261,7 @@ class TestBatch(unittest.TestCase): def test_batch_wise_item_price(self): if not frappe.db.get_value('Item', '_Test Batch Price Item'): - doc = frappe.get_doc({ + frappe.get_doc({ 'doctype': 'Item', 'is_stock_item': 1, 'item_code': '_Test Batch Price Item', From e15ef1e19f124436d8705d633a02388a1107056f Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 10 Dec 2020 20:55:25 +0530 Subject: [PATCH 043/203] fix: corrected tests --- erpnext/projects/doctype/project/project.py | 35 +++++++----- .../projects/doctype/project/test_project.py | 57 ++++++++++++------- .../project_template/test_project_template.py | 19 ++++--- erpnext/projects/doctype/task/test_task.py | 8 +-- 4 files changed, 71 insertions(+), 48 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 04a0fb6c4f0..dfb54a2f77f 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -54,17 +54,15 @@ class Project(Document): self.project_type = template.project_type # create tasks from template + project_tasks = [] for task in template.tasks: template_task_details = frappe.get_doc("Task", task.task) - project_task = self.create_task_from_template(template_task_details) + project_tasks.append(self.create_task_from_template(template_task_details)) - if template_task_details.depends_on: - for child_task in template_task_details.depends_on: - child_task_details = frappe.get_doc("Task",child_task.task) - self.create_task_from_template(child_task_details, project_task) + #self.dependency_mapping(template.tasks, project_tasks) - def create_task_from_template(self, task_details, project_task=None): - doc = frappe.get_doc(dict( + def create_task_from_template(self, task_details): + return frappe.get_doc(dict( doctype = 'Task', subject = task_details.subject, project = self.name, @@ -75,14 +73,21 @@ class Project(Document): task_weight = task_details.task_weight, type = task_details.type, issue = task_details.issue, - is_group = task_details.is_group - )) - if task_details.parent_task and project_task: - doc.parent_task = project_task.name - if not task_details.is_group: - doc.depends_on = task_details.depends_on - doc.insert() - return doc + is_group = task_details.is_group, + start = task_details.start, + duration = task_details.duration + )).insert() + + """ def dependency_mapping(self, template_tasks, project_tasks): + for tmp_task in template_tasks: + for prj_task in project_tasks: + if tmp_task.subject == prj_task.subject: + if tmp_task.depends_on and not prj_task.depends_on: + for child_task in tmp_task.depends_on: + child_task_detai + prj_task.depends_on = tmp_task.depends_on + """ + def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 52f877b8b75..f9bb1b3ac4d 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -20,15 +20,16 @@ class TestProject(unittest.TestCase): frappe.db.sql('delete from tabTask where project = "Test Project with Templ - no parent and dependend tasks"') frappe.delete_doc('Project', 'Test Project with Templ - no parent and dependend tasks') - if not frappe.db.exists("Task", "Test Temp Task with no parent and dependency"): - task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=0, duration=3) + task1 = task_exists("Test Temp Task with no parent and dependency") + if not task1: + task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=5, duration=3) template = make_project_template("Test Project Template - no parent and dependend tasks", [task1]) project = get_project("Test Project with Templ - no parent and dependend tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') self.assertEqual(tasks[0].subject, 'Test Temp Task with no parent and dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate() + 0 + 3)) + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): @@ -36,28 +37,31 @@ class TestProject(unittest.TestCase): frappe.db.sql('delete from tabTask where project = "Test Project with Templ - tasks with parent-child"') frappe.delete_doc('Project', 'Test Project with Templ - tasks with parent-child') - if not frappe.db.exists("Task", "Test Temp Task parent"): + task1 = task_exists("Test Temp Task parent") + if not task1: task1 = create_task(subject="Test Temp Task parent", is_group=1, is_template=1, begin=1, duration=1) - if not frappe.db.exists("Task", "Test Temp Task child 1"): + task2 = task_exists("Test Temp Task child 1") + if not task2: task2 = create_task(subject="Test Temp Task child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) - if not frappe.db.exists("Task", "Test Temp Task child 2"): + task3 = task_exists("Test Temp Task child 2") + if not task3: task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) - template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) + template = make_project_template("Test Project Template - tasks with parent-child", [task1]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - print(tasks, type(tasks), len(tasks)) + print(tasks[0].duration) self.assertEqual(tasks[0].subject, 'Test Temp Task parent') - self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 1 + 1)) + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) self.assertEqual(tasks[1].subject, 'Test Temp Task child 1') - self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 1 + 3)) + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) self.assertEqual(tasks[1].parent_task, tasks[0].name) self.assertEqual(tasks[2].subject, 'Test Temp Task child 2') - self.assertEqual(getdate(tasks[2].exp_end_date), add_days(nowdate()+ 2 + 3)) + self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, tasks[2])) self.assertEqual(tasks[2].parent_task, tasks[0].name) self.assertEqual(len(tasks), 3) @@ -67,22 +71,24 @@ class TestProject(unittest.TestCase): frappe.db.sql('delete from tabTask where project = "Test Project with Templ - dependent tasks"') frappe.delete_doc('Project', 'Test Project with Templ - dependent tasks') - if not frappe.db.exists("Task", "Test Temp Task for dependency"): + task1 = task_exists("Test Temp Task for dependency") + if not task1: task1 = create_task(subject="Test Temp Task for dependency", is_template=1, begin=3, duration=1) - if not frappe.db.exists("Task", "Test Temp Task with dependency"): + task2 = task_exists("Test Temp Task with dependency") + if not task2: task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) - template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2]) + template = make_project_template("Test Project with Templ - dependent tasks", [task2]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), add_days(nowdate()+ 3 + 1)) + self.assertEqual(tasks[0].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) + self.assertEqual(tasks[0].depends_on, tasks[1].name) - self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(tasks[1].exp_end_date), add_days(nowdate()+ 2 + 2)) - self.assertEqual(tasks[1].depends_on, tasks[0].name) + self.assertEqual(tasks[1].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]) ) self.assertEqual(len(tasks), 2) @@ -93,7 +99,7 @@ def get_project(name, template): project_name = name, status = 'Open', project_template = template.name, - expected_start_date = '2019-01-01' + expected_start_date = nowdate() )).insert() return project @@ -114,4 +120,13 @@ def make_project(args): if not frappe.db.exists("Project", args.project_name): project.insert() - return project \ No newline at end of file + return project + +def task_exists(subject): + result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"]) + if not len(result): + return False + return frappe.get_doc("Task", result[0].name) + +def calculate_end_date(project, task): + return getdate(add_days(project.expected_start_date, task.start + task.duration)) \ No newline at end of file diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 379365f9998..6c6b78368ed 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -17,9 +17,7 @@ def get_project_template(project_template_name="Test Project Template", project_ name = project_template_name, tasks = project_tasks or [ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), - create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), - create_task(subject="_Test Template Task 3", is_template=1, begin=2, duration=4), - create_task(subject="_Test Template Task 4", is_template=1, begin=3, duration=2), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2) ] )).insert() @@ -27,13 +25,18 @@ def get_project_template(project_template_name="Test Project Template", project_ def make_project_template(project_template_name, project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): - frappe.get_doc(dict( - doctype = 'Project Template', - name = project_template_name, - tasks = project_tasks or [ + project_tasks = project_tasks or [ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), ] - )).insert() + doc = frappe.get_doc(dict( + doctype = 'Project Template', + name = project_template_name + )) + for task in project_tasks: + doc.append("tasks",{ + "task": task.name + }) + doc.insert() return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 181a2dc3162..d43d132e80e 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -104,11 +104,12 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa task.subject = subject task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() - task.project = project or "_Test Project" - task.is_template = is_template, + task.project = project + task.is_template = is_template task.start = begin - task.duration = duration, + task.duration = duration task.is_group = is_group + task.parent_task = parent_task if save: task.save() else: @@ -120,5 +121,4 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa }) if save: task.save() - return task From fa72671929581c318cd0b828b07477987199a003 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 11:16:54 +0530 Subject: [PATCH 044/203] fix: partial order for drop ship --- .../doctype/sales_order/sales_order.py | 77 +++++++++---------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 3e1c82f9616..2379a304bbc 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -830,52 +830,45 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) - if len(po) == 0 or any( item.get("delivered_by_supplier") == 1 for item in selected_items): - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address" - ], - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Order Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_item"], - ["parent", "sales_order"], - ["stock_uom", "stock_uom"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["delivery_date", "schedule_date"] - ], - "field_no_map": [ - "rate", - "price_list_rate", - "item_tax_template" - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map + doc = get_mapped_doc("Sales Order", source_name, { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address" + ], + "validation": { + "docstatus": ["=", 1] } - }, target_doc, set_missing_values) + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"] + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template" + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map + } + }, target_doc, set_missing_values) - doc.insert() - else: - suppliers =[] - if suppliers: + doc.insert() frappe.db.commit() return doc - else: - frappe.msgprint(_("Purchase Order already created for all Sales Order items")) @frappe.whitelist() def make_purchase_order(source_name, selected_items=[], target_doc=None): From 06961a261e8560c591e2828e1733a90720fc4872 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 11:46:43 +0530 Subject: [PATCH 045/203] fix: conflicts --- erpnext/selling/doctype/sales_order/sales_order.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 2379a304bbc..5d341b746a6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -840,7 +840,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar "contact_email", "contact_person", "taxes_and_charges", - "shipping_address" + "shipping_address", + "terms" ], "validation": { "docstatus": ["=", 1] @@ -859,7 +860,10 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=[], tar "field_no_map": [ "rate", "price_list_rate", - "item_tax_template" + "item_tax_template", + "discount_percentage", + "discount_amount", + "pricing_rules" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map From f9751f1f95e9f69701da999bcb342c6b620bcf95 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 14 Dec 2020 16:20:02 +0530 Subject: [PATCH 046/203] feat: project template having dependent tasks --- erpnext/projects/doctype/project/project.py | 33 +- .../project_template/project_template.py | 23 +- erpnext/projects/doctype/task/task.py | 451 +++++++++--------- 3 files changed, 282 insertions(+), 225 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index dfb54a2f77f..2d3339773a1 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -55,11 +55,13 @@ class Project(Document): # create tasks from template project_tasks = [] + tmp_task_details = [] for task in template.tasks: template_task_details = frappe.get_doc("Task", task.task) + tmp_task_details.append(template_task_details) project_tasks.append(self.create_task_from_template(template_task_details)) - #self.dependency_mapping(template.tasks, project_tasks) + self.dependency_mapping(tmp_task_details, project_tasks) def create_task_from_template(self, task_details): return frappe.get_doc(dict( @@ -78,16 +80,33 @@ class Project(Document): duration = task_details.duration )).insert() - """ def dependency_mapping(self, template_tasks, project_tasks): + def dependency_mapping(self, template_tasks, project_tasks): for tmp_task in template_tasks: for prj_task in project_tasks: if tmp_task.subject == prj_task.subject: - if tmp_task.depends_on and not prj_task.depends_on: - for child_task in tmp_task.depends_on: - child_task_detai - prj_task.depends_on = tmp_task.depends_on - """ + self.check_depends_on_value(tmp_task, prj_task, project_tasks) + self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) + def check_depends_on_value(self, tmp_task, prj_task, project_tasks): + if tmp_task.depends_on and not prj_task.depends_on: + for child_task in tmp_task.depends_on: + child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") + corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + if len(corresponding_prj_task): + prj_task.append("depends_on",{ + "task": corresponding_prj_task[0].name + }) + prj_task.save() + + def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): + if tmp_task.parent_task and not prj_task.parent_task: + parent_task_subject = frappe.db.get_value("Task", tmp_task.parent_task, "subject") + corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + if len(corresponding_prj_task): + prj_task.parent_task = corresponding_prj_task[0].name + print(prj_task.name, prj_task.parent_task, corresponding_prj_task[0].name) + prj_task.save() + print(prj_task.name, corresponding_prj_task[0].name) def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py index ac78135fc42..1beebf7a258 100644 --- a/erpnext/projects/doctype/project_template/project_template.py +++ b/erpnext/projects/doctype/project_template/project_template.py @@ -3,8 +3,27 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document +from frappe import _ class ProjectTemplate(Document): - pass + + def validate(self): + self.validate_dependencies() + + def validate_dependencies(self): + for task in self.tasks: + task_details = frappe.get_doc("Task", task.task) + if task_details.depends_on: + for dependency_task in task_details.depends_on: + if not self.check_dependent_task_presence(dependency_task.task): + task_details_format = """{0}""".format(task_details.name) + dependency_task_format = """{0}""".format(dependency_task.task) + frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format))) + + def check_dependent_task_presence(self, task): + for task_details in self.tasks: + if task_details.task == task: + return True + return False diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index fb84094ffe6..072a848f263 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -17,291 +17,310 @@ class CircularReferenceError(frappe.ValidationError): pass class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass class Task(NestedSet): - nsm_parent_field = 'parent_task' + nsm_parent_field = 'parent_task' - def get_feed(self): - return '{0}: {1}'.format(_(self.status), self.subject) + def get_feed(self): + return '{0}: {1}'.format(_(self.status), self.subject) - def get_customer_details(self): - cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) - if cust: - ret = {'customer_name': cust and cust[0][0] or ''} - return ret + def get_customer_details(self): + cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) + if cust: + ret = {'customer_name': cust and cust[0][0] or ''} + return ret - def validate(self): - self.validate_dates() - self.validate_parent_project_dates() - self.validate_progress() - self.validate_status() - self.update_depends_on() + def validate(self): + self.validate_dates() + self.validate_parent_project_dates() + self.validate_progress() + self.validate_status() + self.update_depends_on() + self.validate_dependencies_for_template_task() - def validate_dates(self): - if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ - frappe.bold("Expected End Date"))) + def validate_dates(self): + if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): + frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ + frappe.bold("Expected End Date"))) - if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ - frappe.bold("Actual End Date"))) + if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): + frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ + frappe.bold("Actual End Date"))) - def validate_parent_project_dates(self): - if not self.project or frappe.flags.in_test: - return + def validate_parent_project_dates(self): + if not self.project or frappe.flags.in_test: + return - expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") + expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") - if expected_end_date: - validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") - validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") + if expected_end_date: + validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") + validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") - def validate_status(self): - if self.status!=self.get_db_value("status") and self.status == "Completed": - for d in self.depends_on: - if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): - frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task))) + def validate_status(self): + if self.status!=self.get_db_value("status") and self.status == "Completed": + for d in self.depends_on: + if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): + frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task))) - close_all_assignments(self.doctype, self.name) + close_all_assignments(self.doctype, self.name) - def validate_progress(self): - if flt(self.progress or 0) > 100: - frappe.throw(_("Progress % for a task cannot be more than 100.")) + def validate_progress(self): + if flt(self.progress or 0) > 100: + frappe.throw(_("Progress % for a task cannot be more than 100.")) - if flt(self.progress) == 100: - self.status = 'Completed' + if flt(self.progress) == 100: + self.status = 'Completed' - if self.status == 'Completed': - self.progress = 100 + if self.status == 'Completed': + self.progress = 100 - def update_depends_on(self): - depends_on_tasks = self.depends_on_tasks or "" - for d in self.depends_on: - if d.task and not d.task in depends_on_tasks: - depends_on_tasks += d.task + "," - self.depends_on_tasks = depends_on_tasks + def validate_dependencies_for_template_task(self): + if self.is_template: + self.validate_parent_template_task() + self.validate_depends_on_tasks() + + def validate_parent_template_task(self): + if self.parent_task: + if not frappe.db.get_value("Task", self.parent_task, "is_template"): + parent_task_format = """{0}""".format(self.parent_task) + frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format)) + + def validate_depends_on_tasks(self): + if self.depends_on: + for task in self.depends_on: + if not frappe.db.get_value("Task", task.task, "is_template"): + dependent_task_format = """{0}""".format(task.task) + frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format)) - def update_nsm_model(self): - frappe.utils.nestedset.update_nsm(self) + def update_depends_on(self): + depends_on_tasks = self.depends_on_tasks or "" + for d in self.depends_on: + if d.task and not d.task in depends_on_tasks: + depends_on_tasks += d.task + "," + self.depends_on_tasks = depends_on_tasks - def on_update(self): - self.update_nsm_model() - self.check_recursion() - self.reschedule_dependent_tasks() - self.update_project() - self.unassign_todo() - self.populate_depends_on() + def update_nsm_model(self): + frappe.utils.nestedset.update_nsm(self) - def unassign_todo(self): - if self.status == "Completed": - close_all_assignments(self.doctype, self.name) - if self.status == "Cancelled": - clear(self.doctype, self.name) + def on_update(self): + self.update_nsm_model() + self.check_recursion() + self.reschedule_dependent_tasks() + self.update_project() + self.unassign_todo() + self.populate_depends_on() - def update_total_expense_claim(self): - self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` - where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0] + def unassign_todo(self): + if self.status == "Completed": + close_all_assignments(self.doctype, self.name) + if self.status == "Cancelled": + clear(self.doctype, self.name) - def update_time_and_costing(self): - tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, - sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, - sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""" - ,self.name, as_dict=1)[0] - if self.status == "Open": - self.status = "Working" - self.total_costing_amount= tl.total_costing_amount - self.total_billing_amount= tl.total_billing_amount - self.actual_time= tl.time - self.act_start_date= tl.start_date - self.act_end_date= tl.end_date + def update_total_expense_claim(self): + self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` + where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0] - def update_project(self): - if self.project and not self.flags.from_project: - frappe.get_cached_doc("Project", self.project).update_project() + def update_time_and_costing(self): + tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, + sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, + sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""" + ,self.name, as_dict=1)[0] + if self.status == "Open": + self.status = "Working" + self.total_costing_amount= tl.total_costing_amount + self.total_billing_amount= tl.total_billing_amount + self.actual_time= tl.time + self.act_start_date= tl.start_date + self.act_end_date= tl.end_date - def check_recursion(self): - if self.flags.ignore_recursion_check: return - check_list = [['task', 'parent'], ['parent', 'task']] - for d in check_list: - task_list, count = [self.name], 0 - while (len(task_list) > count ): - tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " % - (d[0], d[1], '%s'), cstr(task_list[count])) - count = count + 1 - for b in tasks: - if b[0] == self.name: - frappe.throw(_("Circular Reference Error"), CircularReferenceError) - if b[0]: - task_list.append(b[0]) + def update_project(self): + if self.project and not self.flags.from_project: + frappe.get_cached_doc("Project", self.project).update_project() - if count == 15: - break + def check_recursion(self): + if self.flags.ignore_recursion_check: return + check_list = [['task', 'parent'], ['parent', 'task']] + for d in check_list: + task_list, count = [self.name], 0 + while (len(task_list) > count ): + tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " % + (d[0], d[1], '%s'), cstr(task_list[count])) + count = count + 1 + for b in tasks: + if b[0] == self.name: + frappe.throw(_("Circular Reference Error"), CircularReferenceError) + if b[0]: + task_list.append(b[0]) - def reschedule_dependent_tasks(self): - end_date = self.exp_end_date or self.act_end_date - if end_date: - for task_name in frappe.db.sql(""" - select name from `tabTask` as parent - where parent.project = %(project)s - and parent.name in ( - select parent from `tabTask Depends On` as child - where child.task = %(task)s and child.project = %(project)s) - """, {'project': self.project, 'task':self.name }, as_dict=1): - task = frappe.get_doc("Task", task_name.name) - if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open": - task_duration = date_diff(task.exp_end_date, task.exp_start_date) - task.exp_start_date = add_days(end_date, 1) - task.exp_end_date = add_days(task.exp_start_date, task_duration) - task.flags.ignore_recursion_check = True - task.save() + if count == 15: + break - def has_webform_permission(self): - project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user") - if project_user: - return True + def reschedule_dependent_tasks(self): + end_date = self.exp_end_date or self.act_end_date + if end_date: + for task_name in frappe.db.sql(""" + select name from `tabTask` as parent + where parent.project = %(project)s + and parent.name in ( + select parent from `tabTask Depends On` as child + where child.task = %(task)s and child.project = %(project)s) + """, {'project': self.project, 'task':self.name }, as_dict=1): + task = frappe.get_doc("Task", task_name.name) + if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open": + task_duration = date_diff(task.exp_end_date, task.exp_start_date) + task.exp_start_date = add_days(end_date, 1) + task.exp_end_date = add_days(task.exp_start_date, task_duration) + task.flags.ignore_recursion_check = True + task.save() - def populate_depends_on(self): - if self.parent_task: - parent = frappe.get_doc('Task', self.parent_task) - if not self.name in [row.task for row in parent.depends_on]: - parent.append("depends_on", { - "doctype": "Task Depends On", - "task": self.name, - "subject": self.subject - }) - parent.save() + def has_webform_permission(self): + project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user") + if project_user: + return True - def on_trash(self): - if check_if_child_exists(self.name): - throw(_("Child Task exists for this Task. You can not delete this Task.")) + def populate_depends_on(self): + if self.parent_task: + parent = frappe.get_doc('Task', self.parent_task) + if not self.name in [row.task for row in parent.depends_on]: + parent.append("depends_on", { + "doctype": "Task Depends On", + "task": self.name, + "subject": self.subject + }) + parent.save() - self.update_nsm_model() + def on_trash(self): + if check_if_child_exists(self.name): + throw(_("Child Task exists for this Task. You can not delete this Task.")) - def after_delete(self): - self.update_project() + self.update_nsm_model() - def update_status(self): - if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: - from datetime import datetime - if self.exp_end_date < datetime.now().date(): - self.db_set('status', 'Overdue', update_modified=False) - self.update_project() + def after_delete(self): + self.update_project() + + def update_status(self): + if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: + from datetime import datetime + if self.exp_end_date < datetime.now().date(): + self.db_set('status', 'Overdue', update_modified=False) + self.update_project() @frappe.whitelist() def check_if_child_exists(name): - child_tasks = frappe.get_all("Task", filters={"parent_task": name}) - child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks] - return child_tasks + child_tasks = frappe.get_all("Task", filters={"parent_task": name}) + child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks] + return child_tasks @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_project(doctype, txt, searchfield, start, page_len, filters): - from erpnext.controllers.queries import get_match_cond - return frappe.db.sql(""" select name from `tabProject` - where %(key)s like %(txt)s - %(mcond)s - order by name - limit %(start)s, %(page_len)s""" % { - 'key': searchfield, - 'txt': frappe.db.escape('%' + txt + '%'), - 'mcond':get_match_cond(doctype), - 'start': start, - 'page_len': page_len - }) + from erpnext.controllers.queries import get_match_cond + return frappe.db.sql(""" select name from `tabProject` + where %(key)s like %(txt)s + %(mcond)s + order by name + limit %(start)s, %(page_len)s""" % { + 'key': searchfield, + 'txt': frappe.db.escape('%' + txt + '%'), + 'mcond':get_match_cond(doctype), + 'start': start, + 'page_len': page_len + }) @frappe.whitelist() def set_multiple_status(names, status): - names = json.loads(names) - for name in names: - task = frappe.get_doc("Task", name) - task.status = status - task.save() + names = json.loads(names) + for name in names: + task = frappe.get_doc("Task", name) + task.status = status + task.save() def set_tasks_as_overdue(): - tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) - for task in tasks: - if task.status == "Pending Review": - if getdate(task.review_date) > getdate(today()): - continue - frappe.get_doc("Task", task.name).update_status() + tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) + for task in tasks: + if task.status == "Pending Review": + if getdate(task.review_date) > getdate(today()): + continue + frappe.get_doc("Task", task.name).update_status() @frappe.whitelist() def make_timesheet(source_name, target_doc=None, ignore_permissions=False): - def set_missing_values(source, target): - target.append("time_logs", { - "hours": source.actual_time, - "completed": source.status == "Completed", - "project": source.project, - "task": source.name - }) + def set_missing_values(source, target): + target.append("time_logs", { + "hours": source.actual_time, + "completed": source.status == "Completed", + "project": source.project, + "task": source.name + }) - doclist = get_mapped_doc("Task", source_name, { - "Task": { - "doctype": "Timesheet" - } - }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) + doclist = get_mapped_doc("Task", source_name, { + "Task": { + "doctype": "Timesheet" + } + }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) - return doclist + return doclist @frappe.whitelist() def get_children(doctype, parent, task=None, project=None, is_root=False): - filters = [['docstatus', '<', '2']] + filters = [['docstatus', '<', '2']] - if task: - filters.append(['parent_task', '=', task]) - elif parent and not is_root: - # via expand child - filters.append(['parent_task', '=', parent]) - else: - filters.append(['ifnull(`parent_task`, "")', '=', '']) + if task: + filters.append(['parent_task', '=', task]) + elif parent and not is_root: + # via expand child + filters.append(['parent_task', '=', parent]) + else: + filters.append(['ifnull(`parent_task`, "")', '=', '']) - if project: - filters.append(['project', '=', project]) + if project: + filters.append(['project', '=', project]) - tasks = frappe.get_list(doctype, fields=[ - 'name as value', - 'subject as title', - 'is_group as expandable' - ], filters=filters, order_by='name') + tasks = frappe.get_list(doctype, fields=[ + 'name as value', + 'subject as title', + 'is_group as expandable' + ], filters=filters, order_by='name') - # return tasks - return tasks + # return tasks + return tasks @frappe.whitelist() def add_node(): - from frappe.desk.treeview import make_tree_args - args = frappe.form_dict - args.update({ - "name_field": "subject" - }) - args = make_tree_args(**args) + from frappe.desk.treeview import make_tree_args + args = frappe.form_dict + args.update({ + "name_field": "subject" + }) + args = make_tree_args(**args) - if args.parent_task == 'All Tasks' or args.parent_task == args.project: - args.parent_task = None + if args.parent_task == 'All Tasks' or args.parent_task == args.project: + args.parent_task = None - frappe.get_doc(args).insert() + frappe.get_doc(args).insert() @frappe.whitelist() def add_multiple_tasks(data, parent): - data = json.loads(data) - new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""} - new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or "" + data = json.loads(data) + new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""} + new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or "" - for d in data: - if not d.get("subject"): continue - new_doc['subject'] = d.get("subject") - new_task = frappe.get_doc(new_doc) - new_task.insert() + for d in data: + if not d.get("subject"): continue + new_doc['subject'] = d.get("subject") + new_task = frappe.get_doc(new_doc) + new_task.insert() def on_doctype_update(): - frappe.db.add_index("Task", ["lft", "rgt"]) + frappe.db.add_index("Task", ["lft", "rgt"]) def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date): - if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: - frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) + if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: + frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) - if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: - frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) + if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: + frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) From 23f0debf8807eeac894cfb5d628fddb80bf0fd72 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 15 Dec 2020 10:00:21 +0530 Subject: [PATCH 047/203] fix: tests --- erpnext/projects/doctype/project/test_project.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index f9bb1b3ac4d..ea54774d52d 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -79,16 +79,16 @@ class TestProject(unittest.TestCase): if not task2: task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) - template = make_project_template("Test Project with Templ - dependent tasks", [task2]) - project = get_project("Test Project with Templ - tasks with parent-child", template) + template = make_project_template("Test Project with Templ - dependent tasks", [task1, task2]) + project = get_project("Test Project with Templ - dependent tasks", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - self.assertEqual(tasks[0].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) - self.assertEqual(tasks[0].depends_on, tasks[1].name) + self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) + self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 ) - self.assertEqual(tasks[1].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1]) ) + self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]) ) self.assertEqual(len(tasks), 2) From caf67e608f871adca275e001dddc96c96af4ea77 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 15 Dec 2020 10:00:31 +0530 Subject: [PATCH 048/203] fix: tests --- erpnext/projects/doctype/project/project.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 2d3339773a1..5a9375a0e6c 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -82,31 +82,29 @@ class Project(Document): def dependency_mapping(self, template_tasks, project_tasks): for tmp_task in template_tasks: - for prj_task in project_tasks: - if tmp_task.subject == prj_task.subject: - self.check_depends_on_value(tmp_task, prj_task, project_tasks) - self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) + prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0] + self.check_depends_on_value(tmp_task, prj_task, project_tasks) + self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) def check_depends_on_value(self, tmp_task, prj_task, project_tasks): - if tmp_task.depends_on and not prj_task.depends_on: - for child_task in tmp_task.depends_on: + if tmp_task.get("depends_on") and not prj_task.get("depends_on"): + for child_task in tmp_task.get("depends_on"): child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) if len(corresponding_prj_task): prj_task.append("depends_on",{ "task": corresponding_prj_task[0].name }) + print(prj_task.name) prj_task.save() def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): - if tmp_task.parent_task and not prj_task.parent_task: - parent_task_subject = frappe.db.get_value("Task", tmp_task.parent_task, "subject") + if tmp_task.get("parent_task") and not prj_task.get("parent_task"): + parent_task_subject = frappe.db.get_value("Task", tmp_task.get("parent_task"), "subject") corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) if len(corresponding_prj_task): prj_task.parent_task = corresponding_prj_task[0].name - print(prj_task.name, prj_task.parent_task, corresponding_prj_task[0].name) prj_task.save() - print(prj_task.name, corresponding_prj_task[0].name) def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True From a6fef7ae6bbdba8a4f922ebdfbf337033c41ac4d Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 15 Dec 2020 11:50:18 +0530 Subject: [PATCH 049/203] feat: parent-child relation tasks --- erpnext/projects/doctype/project/project.py | 2 +- erpnext/projects/doctype/project/test_project.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5a9375a0e6c..13e72fec8a2 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -83,6 +83,7 @@ class Project(Document): def dependency_mapping(self, template_tasks, project_tasks): for tmp_task in template_tasks: prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0] + prj_task = frappe.get_doc("Task", prj_task.name) self.check_depends_on_value(tmp_task, prj_task, project_tasks) self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) @@ -95,7 +96,6 @@ class Project(Document): prj_task.append("depends_on",{ "task": corresponding_prj_task[0].name }) - print(prj_task.name) prj_task.save() def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index ea54774d52d..c3f56b8e860 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -49,10 +49,10 @@ class TestProject(unittest.TestCase): if not task3: task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) - template = make_project_template("Test Project Template - tasks with parent-child", [task1]) + template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) project = get_project("Test Project with Templ - tasks with parent-child", template) tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') - print(tasks[0].duration) + self.assertEqual(tasks[0].subject, 'Test Temp Task parent') self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) From 87b477a31126e478c2bcc77861975e015474bc6a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 16 Dec 2020 13:37:21 +0530 Subject: [PATCH 050/203] feat: patch for project template tasks --- erpnext/patches.txt | 1 + .../v13_0/update_project_template_tasks.py | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 erpnext/patches/v13_0/update_project_template_tasks.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 86ac613ae5b..435511210bc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -741,3 +741,4 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.update_returned_qty_in_pr_dn +erpnext.patches.v13_0.update_project_template_tasks diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py new file mode 100644 index 00000000000..55f0ff45057 --- /dev/null +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -0,0 +1,32 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + templates = frappe.get_list("Project Template", fields = ["name"]) + for template_name in templates: + template = frappe.get_doc("Project Template", template_name) + replace_tasks = False + new_tasks = [] + for task in template.tasks: + if task.subject: + replace_tasks = True + new_task = frappe.get_doc(dict( + doctype = "Task", + subject = task.subject, + start = task.start, + duration = task.duration, + task_weight = task.task_weight, + description = task.description, + is_template = 1 + )).insert() + new_tasks.append(new_task.name) + if replace_tasks: + template.tasks = [] + for tsk in new_tasks: + template.append("tasks", { + "task": tsk + }) + template.save() \ No newline at end of file From 9962ba86d0db913d53bf87736e1fdd2194436f09 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Wed, 16 Dec 2020 14:41:04 +0530 Subject: [PATCH 051/203] fix: charts not displaying when tree_type changed --- .../purchase_analytics/purchase_analytics.js | 72 +++++++++-------- .../report/sales_analytics/sales_analytics.js | 79 ++++++++++--------- 2 files changed, 81 insertions(+), 70 deletions(-) diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js index e17973c337b..7ee9f2c372a 100644 --- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js @@ -75,62 +75,66 @@ frappe.query_reports["Purchase Analytics"] = { return Object.assign(options, { checkboxColumn: true, events: { - onCheckRow: function(data) { + onCheckRow: function (data) { + if (!data) return; + + const data_doctype = $( + data[2].html + )[0].attributes.getNamedItem("data-doctype").value; + const tree_type = frappe.query_report.filters[0].value; + if (data_doctype != tree_type) return; + row_name = data[2].content; length = data.length; - var tree_type = frappe.query_report.filters[0].value; - - if(tree_type == "Supplier" || tree_type == "Item") { - row_values = data.slice(4,length-1).map(function (column) { - return column.content; - }) - } - else { - row_values = data.slice(3,length-1).map(function (column) { - return column.content; - }) + if (tree_type == "Supplier" || tree_type == "Item") { + row_values = data + .slice(4, length - 1) + .map(function (column) { + return column.content; + }); + } else { + row_values = data + .slice(3, length - 1) + .map(function (column) { + return column.content; + }); } - entry = { - 'name':row_name, - 'values':row_values - } + entry = { + name: row_name, + values: row_values, + }; let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - var found = false; + let found = false; - for(var i=0; i < new_datasets.length;i++){ - if(new_datasets[i].name == row_name){ + for (let i = 0; i < new_datasets.length; i++) { + if (new_datasets[i].name == row_name) { found = true; - new_datasets.splice(i,1); + new_datasets.splice(i, 1); break; } } - if(!found){ + 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) + datasets: new_datasets, + }; + chart_options = { + data: new_data, + type: "line", + }; + frappe.query_report.render_chart(chart_options); frappe.query_report.raw_chart_data = new_data; }, - } + }, }); } } diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index 0e565a3fb6f..aad6bfd5ef1 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -74,67 +74,74 @@ frappe.query_reports["Sales Analytics"] = { return Object.assign(options, { checkboxColumn: true, events: { - onCheckRow: function(data) { + onCheckRow: function (data) { + if (!data) return; + + const data_doctype = $( + data[2].html + )[0].attributes.getNamedItem("data-doctype").value; + const tree_type = frappe.query_report.filters[0].value; + if (data_doctype != tree_type) return; + row_name = data[2].content; length = data.length; - var tree_type = frappe.query_report.filters[0].value; - - if(tree_type == "Customer") { - row_values = data.slice(4,length-1).map(function (column) { - return column.content; - }) + if (tree_type == "Customer") { + row_values = data + .slice(4, length - 1) + .map(function (column) { + return column.content; + }); } else if (tree_type == "Item") { - row_values = data.slice(5,length-1).map(function (column) { - return column.content; - }) - } - else { - row_values = data.slice(3,length-1).map(function (column) { - return column.content; - }) + row_values = data + .slice(5, length - 1) + .map(function (column) { + return column.content; + }); + } else { + row_values = data + .slice(3, length - 1) + .map(function (column) { + return column.content; + }); } entry = { - 'name':row_name, - 'values':row_values - } + name: row_name, + values: row_values, + }; let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - var found = false; + let found = false; - for(var i=0; i < new_datasets.length;i++){ - if(new_datasets[i].name == row_name){ + for (let i = 0; i < new_datasets.length; i++) { + if (new_datasets[i].name == row_name) { found = true; - new_datasets.splice(i,1); + new_datasets.splice(i, 1); break; } } - if(!found){ + 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) + datasets: new_datasets, + }; + chart_options = { + data: new_data, + type: "line", + }; + frappe.query_report.render_chart(chart_options); frappe.query_report.raw_chart_data = new_data; }, - } - }) + }, + }); }, } From 924f99bead32bb4eba656019a15931f797eb04ae Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Dec 2020 15:42:50 +0530 Subject: [PATCH 052/203] 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 a2526e92c36..74b7b516763 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 7736b2dffb2..c0327ad0ad8 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 5a8a52b9c6739f25bbd3b128402cb5a19b40afc0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 15:54:06 +0530 Subject: [PATCH 053/203] fix: Therapy Type and Therapy Plan field visibility in Patient Appointment --- .../patient_appointment/patient_appointment.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index ac35acc21ac..35600e48092 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -23,9 +23,9 @@ "procedure_template", "get_procedure_from_encounter", "procedure_prescription", + "therapy_plan", "therapy_type", "get_prescribed_therapies", - "therapy_plan", "practitioner", "practitioner_name", "department", @@ -284,7 +284,7 @@ "report_hide": 1 }, { - "depends_on": "eval:doc.patient;", + "depends_on": "eval:doc.patient && doc.therapy_plan;", "fieldname": "therapy_type", "fieldtype": "Link", "label": "Therapy", @@ -292,17 +292,16 @@ "set_only_once": 1 }, { - "depends_on": "eval:doc.patient && doc.__islocal;", + "depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;", "fieldname": "get_prescribed_therapies", "fieldtype": "Button", "label": "Get Prescribed Therapies" }, { - "depends_on": "eval: doc.patient && doc.therapy_type", + "depends_on": "eval: doc.patient;", "fieldname": "therapy_plan", "fieldtype": "Link", "label": "Therapy Plan", - "mandatory_depends_on": "eval: doc.patient && doc.therapy_type", "options": "Therapy Plan" }, { @@ -348,7 +347,7 @@ } ], "links": [], - "modified": "2020-05-21 03:04:21.400893", + "modified": "2020-12-16 13:16:58.578503", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", From f2a431d86615a5f3c509605dfb58c62adb24b366 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 16:09:58 +0530 Subject: [PATCH 054/203] fix: filter Therapy Types and Therapy Plan in Patient Appointment --- .../patient_appointment.js | 30 +++++++++++++++++++ .../patient_appointment.py | 13 +++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 2d6b64532b1..79e1775b9db 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -22,6 +22,7 @@ frappe.ui.form.on('Patient Appointment', { filters: {'status': 'Active'} }; }); + frm.set_query('practitioner', function() { return { filters: { @@ -29,6 +30,7 @@ frappe.ui.form.on('Patient Appointment', { } }; }); + frm.set_query('service_unit', function(){ return { filters: { @@ -39,6 +41,16 @@ frappe.ui.form.on('Patient Appointment', { }; }); + frm.set_query('therapy_plan', function() { + return { + filters: { + 'patient': frm.doc.patient + } + }; + }); + + frm.trigger('set_therapy_type_filter'); + if (frm.is_new()) { frm.page.set_primary_action(__('Check Availability'), function() { if (!frm.doc.patient) { @@ -136,6 +148,24 @@ frappe.ui.form.on('Patient Appointment', { } }, + therapy_plan: function(frm) { + frm.trigger('set_therapy_type_filter'); + }, + + set_therapy_type_filter: function(frm) { + if (frm.doc.therapy_plan) { + frm.call('get_therapy_types').then(r => { + frm.set_query('therapy_type', function() { + return { + filters: { + 'name': ['in', r.message] + } + }; + }); + }); + } + }, + therapy_type: function(frm) { if (frm.doc.therapy_type) { frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => { diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index e685b20a8c8..dc820cb464e 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -91,6 +91,17 @@ class PatientAppointment(Document): if fee_validity: frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) + def get_therapy_types(self): + if not self.therapy_plan: + return + + therapy_types = [] + doc = frappe.get_doc('Therapy Plan', self.therapy_plan) + for entry in doc.therapy_plan_details: + therapy_types.append(entry.therapy_type) + + return therapy_types + @frappe.whitelist() def check_payment_fields_reqd(patient): @@ -145,7 +156,7 @@ def invoice_appointment(appointment_doc): sales_invoice.flags.ignore_mandatory = True sales_invoice.save(ignore_permissions=True) sales_invoice.submit() - frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True) + frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) From d44f45c57be854c1c6c625ffccf86b56203c3dd7 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 16 Dec 2020 16:28:09 +0530 Subject: [PATCH 055/203] fix: sider issues --- erpnext/projects/doctype/project/test_project.py | 7 ++++--- erpnext/projects/doctype/task/task.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index c3f56b8e860..ce56a50b4e2 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -106,17 +106,18 @@ def get_project(name, template): def make_project(args): args = frappe._dict(args) - if args.project_template_name: - template = make_project_template(args.project_template_name) project = frappe.get_doc(dict( doctype = 'Project', project_name = args.project_name, status = 'Open', - project_template = template.name, expected_start_date = args.start_date )) + if args.project_template_name: + template = make_project_template(args.project_template_name) + project.project_template = template.name + if not frappe.db.exists("Project", args.project_name): project.insert() diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 072a848f263..80b764ba4f0 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -94,7 +94,7 @@ class Task(NestedSet): def update_depends_on(self): depends_on_tasks = self.depends_on_tasks or "" for d in self.depends_on: - if d.task and not d.task in depends_on_tasks: + if d.task and d.task not in depends_on_tasks: depends_on_tasks += d.task + "," self.depends_on_tasks = depends_on_tasks @@ -180,7 +180,7 @@ class Task(NestedSet): def populate_depends_on(self): if self.parent_task: parent = frappe.get_doc('Task', self.parent_task) - if not self.name in [row.task for row in parent.depends_on]: + if self.name not in [row.task for row in parent.depends_on]: parent.append("depends_on", { "doctype": "Task Depends On", "task": self.name, From 2528d5ee15a5ce9d5d9634eec016946b1416154d Mon Sep 17 00:00:00 2001 From: pateljannat Date: Wed, 16 Dec 2020 18:29:49 +0530 Subject: [PATCH 056/203] fix: tests --- erpnext/projects/doctype/task/test_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index d43d132e80e..aded78b8574 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -104,7 +104,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa task.subject = subject task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() - task.project = project + task.project = project or "_Test Project" task.is_template = is_template task.start = begin task.duration = duration From c9f63accddd6a7b24d6c3ff257d8fba3395c8c94 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 17 Dec 2020 00:12:17 +0530 Subject: [PATCH 057/203] fix: do not manufacture same serial no multiple times --- erpnext/stock/doctype/serial_no/serial_no.py | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 295149e2387..25ce2d59695 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -6,7 +6,7 @@ import frappe import json from frappe.model.naming import make_autoname -from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate +from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate, get_link_to_form from erpnext.stock.get_item_details import get_reserved_qty_for_so from frappe import _, ValidationError @@ -244,7 +244,7 @@ def validate_serial_no(sle, item_det): for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order", - "delivery_document_no", "delivery_document_type", "warehouse", + "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type", "purchase_document_no", "company"], as_dict=1) if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse: @@ -256,9 +256,10 @@ def validate_serial_no(sle, item_det): frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code), SerialNoItemError) - if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle): - frappe.throw(_("Serial No {0} has already been received").format(serial_no), - SerialNoDuplicateError) + if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle): + doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)) + frappe.throw(_("Serial No {0} has already been received in the {1} #{2}") + .format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError) if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation'] and sle.voucher_type == sr.delivery_document_type): @@ -349,7 +350,7 @@ def validate_so_serial_no(sr, sales_order): frappe.throw(_("""{0} Serial No {1} cannot be delivered""") .format(msg, sr.name)) -def has_duplicate_serial_no(sn, sle): +def has_serial_no_exists(sn, sle): if (sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != 'Stock Reconciliation'): return True @@ -359,12 +360,13 @@ def has_duplicate_serial_no(sn, sle): status = False if sn.purchase_document_no: - if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \ - sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]: + if (sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and + sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]): status = True - if status and sle.voucher_type == 'Stock Entry' and \ - frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') != 'Material Receipt': + # If status is receipt then system will allow to in-ward the delivered serial no + if (status and sle.voucher_type == 'Stock Entry' and + frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') == 'Material Receipt'): status = False return status @@ -420,7 +422,7 @@ def auto_make_serial_nos(args): if is_new: created_numbers.append(sr.name) - form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers)) + form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers)) # Setting up tranlated title field for all cases singular_title = _("Serial Number Created") From f2bff8e220a26b1ed9a662e010b7e15fe91df73e Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 11:54:59 +0530 Subject: [PATCH 058/203] fix: patch relaod doctype --- erpnext/patches/v13_0/update_project_template_tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 55f0ff45057..df1886f616c 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe def execute(): + frappe.reload_doctype("Project Template") templates = frappe.get_list("Project Template", fields = ["name"]) for template_name in templates: - template = frappe.get_doc("Project Template", template_name) + template = frappe.get_doc("Project Template", template_name.name) replace_tasks = False new_tasks = [] for task in template.tasks: From 2dbb1d6bc72b28542eec44878c28f1eed069bcca Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 15:49:52 +0530 Subject: [PATCH 059/203] fix: indentation --- erpnext/patches/v13_0/update_project_template_tasks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index df1886f616c..8dd0181eceb 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,10 +5,9 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doctype("Project Template") - templates = frappe.get_list("Project Template", fields = ["name"]) - for template_name in templates: + for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1): template = frappe.get_doc("Project Template", template_name.name) + print(template.tasks) replace_tasks = False new_tasks = [] for task in template.tasks: From 611b42733b82a3ab737fde001f7ca9246f1e6870 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 17 Dec 2020 14:49:48 +0530 Subject: [PATCH 060/203] fix: leave policy dashboard fix and roles --- .../doctype/leave_policy/leave_policy_dashboard.py | 14 +------------- .../leave_policy_assignment.json | 5 ++++- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py index ff5dc2ff3e0..e0ec4be2dce 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py @@ -4,22 +4,10 @@ from frappe import _ def get_data(): return { 'fieldname': 'leave_policy', - 'non_standard_fieldnames': { - 'Employee Grade': 'default_leave_policy' - }, 'transactions': [ - { - 'label': _('Employees'), - 'items': ['Employee', 'Employee Grade'] - }, { 'label': _('Leaves'), 'items': ['Leave Allocation'] }, ] - } - - - - - \ No newline at end of file + } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index ecebb3b7d6c..bbb42227154 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -111,7 +111,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-15 15:18:15.227848", + "modified": "2020-12-17 16:27:20.311060", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", @@ -127,6 +127,7 @@ "report": 1, "role": "HR Manager", "share": 1, + "submit": 1, "write": 1 }, { @@ -139,6 +140,7 @@ "report": 1, "role": "HR User", "share": 1, + "submit": 1, "write": 1 }, { @@ -151,6 +153,7 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 } ], From 09f0e9111d6fb79868c58f55ead0f18351c6d216 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 17:20:21 +0530 Subject: [PATCH 061/203] fix: patch --- erpnext/patches/v13_0/update_project_template_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 8dd0181eceb..0bcd1d3f3a5 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,9 +5,9 @@ from __future__ import unicode_literals import frappe def execute(): + frappe.reload_doc("projects", "doctype", "project_template”) for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1): template = frappe.get_doc("Project Template", template_name.name) - print(template.tasks) replace_tasks = False new_tasks = [] for task in template.tasks: From 5a06908bbc247607e61d18f8fd848e1bceaa6e11 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 17:38:53 +0530 Subject: [PATCH 062/203] fix: Add breadccrumbs to item group page --- erpnext/templates/generators/item_group.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 40a064fc768..74b2ae3c515 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -1,5 +1,9 @@ {% extends "templates/web.html" %} +{% block breadcrumbs %} + {% include "templates/includes/breadcrumbs.html" %} +{% endblock %} + {% block header %}

{{ name }}

{% endblock %} {% block page_content %} From 79b71462cbdec8fabbb20f80f7e258bb55a65620 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 18:21:34 +0530 Subject: [PATCH 063/203] fix: patch --- erpnext/patches/v13_0/update_project_template_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 0bcd1d3f3a5..1303efd93fb 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc("projects", "doctype", "project_template”) + frappe.reload_doc("projects", "doctype", "project_template") for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1): template = frappe.get_doc("Project Template", template_name.name) replace_tasks = False From b8e656512e8ab34601149c3f3ca0f9831441545a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 17 Dec 2020 20:22:06 +0530 Subject: [PATCH 064/203] fix: test cleanup --- .../project_template/test_project_template.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 6c6b78368ed..95663cdcbbb 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -10,19 +10,6 @@ from erpnext.projects.doctype.task.test_task import create_task class TestProjectTemplate(unittest.TestCase): pass -def get_project_template(project_template_name="Test Project Template", project_tasks=[]): - if not frappe.db.exists('Project Template', project_template_name): - frappe.get_doc(dict( - doctype = 'Project Template', - name = project_template_name, - tasks = project_tasks or [ - create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), - create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2) - ] - )).insert() - - return frappe.get_doc('Project Template', project_template_name) - def make_project_template(project_template_name, project_tasks=[]): if not frappe.db.exists('Project Template', project_template_name): project_tasks = project_tasks or [ From 04f48a011d343cad2dbe667b68408db6d523982e Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 01:31:00 +0530 Subject: [PATCH 065/203] feat: Add year_to_date field --- erpnext/payroll/doctype/salary_slip/salary_slip.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 386618cf083..b64e5a08fe6 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -69,6 +69,7 @@ "net_pay_info", "net_pay", "base_net_pay", + "year_to_date", "column_break_53", "rounded_total", "base_rounded_total", @@ -578,13 +579,18 @@ { "fieldname": "column_break_69", "fieldtype": "Column Break" + }, + { + "fieldname": "year_to_date", + "fieldtype": "Currency", + "label": "Year To Date" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-10-21 23:02:59.400249", + "modified": "2020-12-17 21:51:19.612940", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 9f1e018e4f868220a985af6784c1940a67d47b82 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 01:35:27 +0530 Subject: [PATCH 066/203] feat: Compute year_to_date --- .../doctype/salary_slip/salary_slip.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 20365b191d0..27de46acc30 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -35,6 +35,9 @@ class SalarySlip(TransactionBase): def autoname(self): self.name = make_autoname(self.series) + def before_save(self): + self.compute_year_to_date() + def validate(self): self.status = self.get_status() self.validate_dates() @@ -1125,6 +1128,26 @@ class SalarySlip(TransactionBase): self.gross_pay += self.earnings[i].amount self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) + def compute_year_to_date(self): + year_to_date = 0 + fiscal_year = frappe.get_list('Fiscal Year', + fields = ['year','year_start_date','year_end_date'], + filters= {'year_start_date' : ['<=', self.start_date], + 'year_end_date' : ['>=', self.end_date] + })[0] + salary_slips_from_current_fiscal_year = frappe.get_list('Salary Slip', + fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', fiscal_year.year_start_date], + 'end_date' : ['<=', fiscal_year.year_end_date] + }) + + for salary_slip in salary_slips_from_current_fiscal_year: + year_to_date += salary_slip.net_pay + + year_to_date += self.net_pay + self.year_to_date = year_to_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)) @@ -1135,4 +1158,4 @@ def unlink_ref_doc_from_salary_slip(ref_no): def generate_password_for_pdf(policy_template, employee): employee = frappe.get_doc("Employee", employee) - return policy_template.format(**employee.as_dict()) + return policy_template.format(**employee.as_dict()) \ No newline at end of file From a81519f5571f25d15c47e20eefce257c18c3f1ff Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 18 Dec 2020 11:16:01 +0530 Subject: [PATCH 067/203] fix: error popup for submitted doc --- .../payroll/doctype/salary_slip/salary_slip.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index f7e22c63879..abe873d8393 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -214,14 +214,16 @@ frappe.ui.form.on('Salary Slip Timesheet', { }); var calculate_totals = function(frm) { - if (frm.doc.earnings || frm.doc.deductions) { - frappe.call({ - method: "set_totals", - doc: frm.doc, - callback: function() { - frm.refresh_fields(); - } - }); + if (frm.doc.docstatus === 0) { + if (frm.doc.earnings || frm.doc.deductions) { + frappe.call({ + method: "set_totals", + doc: frm.doc, + callback: function() { + frm.refresh_fields(); + } + }); + } } }; From 89a02d7d3f705742bc95e654e2909a821e883e45 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 14:59:20 +0530 Subject: [PATCH 068/203] feat: Changed Fiscal Year to Payroll Period --- .../doctype/salary_slip/salary_slip.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 27de46acc30..0ea0684a8ff 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -35,9 +35,6 @@ class SalarySlip(TransactionBase): def autoname(self): self.name = make_autoname(self.series) - def before_save(self): - self.compute_year_to_date() - def validate(self): self.status = self.get_status() self.validate_dates() @@ -52,6 +49,7 @@ class SalarySlip(TransactionBase): self.get_working_days_details(lwp = self.leave_without_pay) self.calculate_net_pay() + self.compute_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") @@ -1130,19 +1128,21 @@ class SalarySlip(TransactionBase): def compute_year_to_date(self): year_to_date = 0 - fiscal_year = frappe.get_list('Fiscal Year', - fields = ['year','year_start_date','year_end_date'], - filters= {'year_start_date' : ['<=', self.start_date], - 'year_end_date' : ['>=', self.end_date] + payroll_period = frappe.get_list('Payroll Period', + fields = ['start_date','end_date','company'], + filters= {'start_date' : ['<=', self.start_date], + 'end_date' : ['>=', self.end_date], + 'company' : self.company })[0] - salary_slips_from_current_fiscal_year = frappe.get_list('Salary Slip', + salary_slips_from_current_payroll_period = frappe.get_list('Salary Slip', fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], filters = {'employee_name' : self.employee_name, - 'start_date' : ['>=', fiscal_year.year_start_date], - 'end_date' : ['<=', fiscal_year.year_end_date] + 'start_date' : ['>=', payroll_period.start_date], + 'end_date' : ['<=', payroll_period.end_date], + 'name' : ['!=', self.name] }) - for salary_slip in salary_slips_from_current_fiscal_year: + for salary_slip in salary_slips_from_current_payroll_period: year_to_date += salary_slip.net_pay year_to_date += self.net_pay From d6277cdc7f08f14081b7e425f8a901472c4a73cb Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 18 Dec 2020 21:37:19 +0530 Subject: [PATCH 069/203] feat: Value Based and Numeric Quality Inspection - Acceptance Formula is optional - Choose between Value based and Numeric QI - If numeric, select single or multiple readings - Added Min, Max and Mean Values for numeric inspection to avoid formula usage - Deprecated code cleanup in js file --- .../item_quality_inspection_parameter.json | 54 +++++++++- .../quality_inspection/quality_inspection.js | 102 +++++++++--------- .../quality_inspection.json | 4 +- .../quality_inspection/quality_inspection.py | 98 +++++++++++++---- .../quality_inspection_reading.json | 93 ++++++++++++++-- .../quality_inspection_template.py | 4 +- 6 files changed, 268 insertions(+), 87 deletions(-) diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index 888bc2de474..f4501281579 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -8,8 +8,14 @@ "field_order": [ "specification", "value", + "value_based", + "single_reading", "column_break_3", - "acceptance_formula" + "formula_based_criteria", + "acceptance_formula", + "min_value", + "max_value", + "mean_value" ], "fields": [ { @@ -24,10 +30,11 @@ "width": "200px" }, { + "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, - "label": "Acceptance Criteria", + "label": "Acceptance Criteria Value", "oldfieldname": "value", "oldfieldtype": "Data" }, @@ -36,17 +43,56 @@ "fieldtype": "Column Break" }, { - "description": "Simple Python formula based on numeric Readings.
Example 1: reading_1 > 0.2 and reading_1 < 0.5
\nExample 2: (reading_1 + reading_2) / 2 < 10", + "depends_on": "formula_based_criteria", + "description": "Simple Python formula applied on Reading fields.
Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", "in_list_view": 1, "label": "Acceptance Criteria Formula" + }, + { + "default": "0", + "fieldname": "formula_based_criteria", + "fieldtype": "Check", + "label": "Formula Based Criteria" + }, + { + "default": "0", + "depends_on": "eval:!doc.value_based", + "fieldname": "single_reading", + "fieldtype": "Check", + "label": "Single Reading" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", + "fieldname": "mean_value", + "fieldtype": "Float", + "label": "Mean Value" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "fieldname": "min_value", + "fieldtype": "Float", + "label": "Minimum Value" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "fieldname": "max_value", + "fieldtype": "Float", + "label": "Maximum Value" + }, + { + "default": "0", + "description": "Non-numeric Inspection.", + "fieldname": "value_based", + "fieldtype": "Check", + "label": "Value Based" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-11-16 16:33:42.421842", + "modified": "2020-12-18 21:03:29.828723", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 376848afaa4..f0bf9aed802 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -4,6 +4,54 @@ cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; frappe.ui.form.on("Quality Inspection", { + setup: function(frm) { + frm.set_query("batch_no", function() { + return { + filters: { + "item": frm.doc.item_code + } + } + }); + + // Serial No based on item_code + frm.set_query("item_serial_no", function() { + var filters = {}; + if (frm.doc.item_code) { + filters = { + 'item_code': frm.doc.item_code + } + } + return { filters: filters } + }); + + // item code based on GRN/DN + frm.set_query("item_code", function(doc) { + let doctype = doc.reference_type; + + if (doc.reference_type !== "Job Card") { + doctype = (doc.reference_type == "Stock Entry") ? + "Stock Entry Detail" : doc.reference_type + " Item"; + } + + if (doc.reference_type && doc.reference_name) { + let filters = { + "from": doctype, + "inspection_type": doc.inspection_type + }; + + if (doc.reference_type == doctype) + filters["reference_name"] = doc.reference_name; + else + filters["parent"] = doc.reference_name; + + return { + query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query", + filters: filters + }; + } + }); + }, + item_code: function(frm) { if (frm.doc.item_code) { return frm.call({ @@ -26,55 +74,5 @@ frappe.ui.form.on("Quality Inspection", { } }); } - } -}) - -// item code based on GRN/DN -cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) { - let doctype = doc.reference_type; - - if (doc.reference_type !== "Job Card") { - doctype = (doc.reference_type == "Stock Entry") ? - "Stock Entry Detail" : doc.reference_type + " Item"; - } - - if (doc.reference_type && doc.reference_name) { - let filters = { - "from": doctype, - "inspection_type": doc.inspection_type - }; - - if (doc.reference_type == doctype) - filters["reference_name"] = doc.reference_name; - else - filters["parent"] = doc.reference_name; - - return { - query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query", - filters: filters - }; - } -}, - -// Serial No based on item_code -cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) { - var filters = {}; - if (doc.item_code) { - filters = { - 'item_code': doc.item_code - } - } - return { filters: filters } -} - -cur_frm.set_query("batch_no", function(doc) { - return { - filters: { - "item": doc.item_code - } - } -}) - -cur_frm.add_fetch('item_code', 'item_name', 'item_name'); -cur_frm.add_fetch('item_code', 'description', 'description'); - + }, +}) \ No newline at end of file diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index f6d76194d94..edfe7e98b2e 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -136,6 +136,7 @@ "width": "50%" }, { + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -143,6 +144,7 @@ "read_only": 1 }, { + "fetch_from": "item_code.description", "fieldname": "description", "fieldtype": "Small Text", "label": "Description", @@ -236,7 +238,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-19 17:06:05.409963", + "modified": "2020-12-18 19:59:55.710300", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index ae4eb9b9956..a7a023bcbf3 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -6,7 +6,7 @@ import frappe from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, cint from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \ import get_template_details @@ -16,7 +16,7 @@ class QualityInspection(Document): self.get_item_specification_details() if self.readings: - self.set_status_based_on_acceptance_formula() + self.inspect_and_set_status() def get_item_specification_details(self): if not self.quality_inspection_template: @@ -29,9 +29,7 @@ class QualityInspection(Document): parameters = get_template_details(self.quality_inspection_template) for d in parameters: child = self.append('readings', {}) - child.specification = d.specification - child.value = d.value - child.acceptance_formula = d.acceptance_formula + child.update(d) child.status = "Accepted" def get_quality_inspection_template(self): @@ -76,28 +74,84 @@ class QualityInspection(Document): """.format(parent_doc=self.reference_type, child_doc=doctype), (quality_inspection, self.modified, self.reference_name, self.item_code)) - def set_status_based_on_acceptance_formula(self): + def inspect_and_set_status(self): for reading in self.readings: - if not reading.acceptance_formula: continue + if reading.formula_based_criteria: + self.set_status_based_on_acceptance_formula(reading) + else: + self.set_status_based_on_acceptance_values(reading) + + def set_status_based_on_acceptance_values(self, reading): + if cint(reading.value_based): + result = reading.get("reading_value") == reading.get("value") + else: + # numeric readings + if cint(reading.single_reading): + reading_1 = flt(reading.get("reading_1")) + result = flt(reading.get("min_value")) <= reading_1 <= flt(reading.get("max_value")) + else: + result = self.min_max_criteria_passed(reading) and self.mean_criteria_passed(reading) + + reading.status = "Accepted" if result else "Rejected" + + def min_max_criteria_passed(self, reading): + """Determine whether all readings fall in the acceptable range.""" + for i in range(1, 11): + reading_field = reading.get("reading_" + str(i)) + if reading_field is not None: + result = flt(reading.get("min_value")) <= flt(reading_field) <= flt(reading.get("max_value")) + if not result: return False + return True + + def mean_criteria_passed(self, reading): + """Determine whether mean of all readings is acceptable.""" + if reading.get("mean_value"): + from statistics import mean + readings_list = [] - condition = reading.acceptance_formula - data = {} for i in range(1, 11): - field = "reading_" + str(i) - data[field] = flt(reading.get(field)) or 0 + reading_value = reading.get("reading_" + str(i)) + if reading_value is not None: + readings_list.append(flt(reading_value)) - try: - result = frappe.safe_eval(condition, None, data) - reading.status = "Accepted" if result else "Rejected" - except SyntaxError: - frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), - title=_("Invalid Formula")) - except NameError as e: - field = frappe.bold(e.args[0].split()[1]) - frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.") - .format(reading.idx, field), - title=_("Invalid Formula")) + actual_mean = mean(readings_list) if readings_list else 0 + return True if actual_mean == reading.get("mean_value") else False + return True # no mean value, nothing to check + + def set_status_based_on_acceptance_formula(self, reading): + if not reading.acceptance_formula: + frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), + title=_("Missing Formula")) + + condition = reading.acceptance_formula + data = self.get_formula_evaluation_data(reading) + + try: + result = frappe.safe_eval(condition, None, data) + reading.status = "Accepted" if result else "Rejected" + except NameError as e: + field = frappe.bold(e.args[0].split()[1]) + frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.") + .format(reading.idx, field), + title=_("Invalid Formula")) + except Exception: + frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), + title=_("Invalid Formula")) + + def get_formula_evaluation_data(self, reading): + data = {} + if cint(reading.value_based): + data = {"reading_value": reading.get("reading_value")} + else: + # numeric readings + data = {"reading_1": flt(reading.get("reading_1"))} + if not cint(reading.single_reading): + # if multiple numeric readings add all readings to data + for i in range(2, 11): + field = "reading_" + str(i) + data[field] = flt(reading.get(field)) + return data @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index c1976dd1fb5..db95fabee0b 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -7,21 +7,30 @@ "engine": "InnoDB", "field_order": [ "specification", - "value", "status", + "value", + "value_based", "column_break_4", + "formula_based_criteria", "acceptance_formula", + "min_value", + "max_value", + "mean_value", "section_break_3", + "reading_value", + "section_break_14", + "single_reading", + "section_break_12", "reading_1", "reading_2", "reading_3", - "column_break_10", "reading_4", + "column_break_10", "reading_5", "reading_6", - "column_break_14", "reading_7", "reading_8", + "column_break_14", "reading_9", "reading_10" ], @@ -38,10 +47,11 @@ }, { "columns": 2, + "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, - "label": "Acceptance Criteria", + "label": "Acceptance Criteria Value", "oldfieldname": "value", "oldfieldtype": "Data" }, @@ -56,6 +66,7 @@ }, { "columns": 1, + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_2", "fieldtype": "Data", "in_list_view": 1, @@ -65,6 +76,7 @@ }, { "columns": 1, + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_3", "fieldtype": "Data", "in_list_view": 1, @@ -73,6 +85,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_4", "fieldtype": "Data", "label": "Reading 4", @@ -80,6 +93,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_5", "fieldtype": "Data", "label": "Reading 5", @@ -87,6 +101,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_6", "fieldtype": "Data", "label": "Reading 6", @@ -94,6 +109,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_7", "fieldtype": "Data", "label": "Reading 7", @@ -101,6 +117,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_8", "fieldtype": "Data", "label": "Reading 8", @@ -108,6 +125,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_9", "fieldtype": "Data", "label": "Reading 9", @@ -115,6 +133,7 @@ "oldfieldtype": "Data" }, { + "depends_on": "eval:!doc.single_reading", "fieldname": "reading_10", "fieldtype": "Data", "label": "Reading 10", @@ -133,15 +152,18 @@ "options": "Accepted\nRejected" }, { + "depends_on": "value_based", "fieldname": "section_break_3", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Value Based Inspection" }, { "fieldname": "column_break_4", "fieldtype": "Column Break" }, { - "description": "Simple Python formula based on numeric Readings.
Example 1: reading_1 > 0.2 and reading_1 < 0.5
\nExample 2: (reading_1 + reading_2) / 2 < 10", + "depends_on": "formula_based_criteria", + "description": "Simple Python formula applied on Reading fields.
Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -153,12 +175,69 @@ { "fieldname": "column_break_14", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "formula_based_criteria", + "fieldtype": "Check", + "label": "Formula Based Criteria" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", + "fieldname": "mean_value", + "fieldtype": "Float", + "label": "Mean Value" + }, + { + "default": "0", + "fieldname": "single_reading", + "fieldtype": "Check", + "label": "Single Reading" + }, + { + "depends_on": "eval:!doc.value_based", + "fieldname": "section_break_12", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "description": "Applied on each reading.", + "fieldname": "min_value", + "fieldtype": "Float", + "label": "Minimum Value" + }, + { + "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "description": "Applied on each reading.", + "fieldname": "max_value", + "fieldtype": "Float", + "label": "Maximum Value" + }, + { + "default": "0", + "description": "Non-numeric Inspection.", + "fieldname": "value_based", + "fieldtype": "Check", + "label": "Value Based" + }, + { + "depends_on": "value_based", + "fieldname": "reading_value", + "fieldtype": "Data", + "label": "Reading Value" + }, + { + "depends_on": "eval:!doc.value_based", + "fieldname": "section_break_14", + "fieldtype": "Section Break", + "label": "Numeric Inspection" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-11-16 16:34:29.947856", + "modified": "2020-12-18 21:02:04.865777", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index e2848469b88..7dd0febc203 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -13,6 +13,8 @@ def get_template_details(template): if not template: return [] return frappe.get_all('Item Quality Inspection Parameter', - fields=["specification", "value", "acceptance_formula"], + fields=["specification", "value", "acceptance_formula", + "value_based", "formula_based_criteria", "single_reading", + "min_value", "max_value", "mean_value"], filters={'parenttype': 'Quality Inspection Template', 'parent': template}, order_by="idx") \ No newline at end of file From 1b1df6b6bcabedcf27ebc5c1cc4f2c0fac1e73b8 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 23:51:05 +0530 Subject: [PATCH 070/203] feat: Add month_to_date field --- erpnext/payroll/doctype/salary_slip/salary_slip.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index b64e5a08fe6..5141868adb7 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -73,6 +73,7 @@ "column_break_53", "rounded_total", "base_rounded_total", + "month_to_date", "section_break_55", "total_in_words", "column_break_69", @@ -583,14 +584,21 @@ { "fieldname": "year_to_date", "fieldtype": "Currency", - "label": "Year To Date" + "label": "Year To Date(Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "month_to_date", + "fieldtype": "Currency", + "label": "Month To Date" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-17 21:51:19.612940", + "modified": "2020-12-18 23:23:10.484574", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 59fbf702dad1c4231d19fe8cfb4970862e4aaad0 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 23:52:11 +0530 Subject: [PATCH 071/203] feat: Compute month_to_date --- .../doctype/salary_slip/salary_slip.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 0ea0684a8ff..e86a7fc3158 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -50,6 +50,7 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() self.compute_year_to_date() + self.compute_month_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,8 +1139,7 @@ class SalarySlip(TransactionBase): fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', payroll_period.start_date], - 'end_date' : ['<=', payroll_period.end_date], - 'name' : ['!=', self.name] + 'end_date' : ['<', self.start_date] }) for salary_slip in salary_slips_from_current_payroll_period: @@ -1148,6 +1148,22 @@ class SalarySlip(TransactionBase): year_to_date += self.net_pay self.year_to_date = year_to_date + def compute_month_to_date(self): + month_to_date = 0 + date = datetime.datetime.strptime(self.start_date,"%Y-%m-%d") + first_day_of_the_month = "1-" + str(date.month) + "-" + str(date.year) + salary_slips_from_this_month = frappe.get_list('Salary Slip', + fields = ['employee_name', 'start_date', 'net_pay'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', first_day_of_the_month], + 'end_date' : ['<', self.start_date] + }) + for salary_slip in salary_slips_from_this_month: + month_to_date += salary_slip.net_pay + + month_to_date += self.net_pay + self.month_to_date = month_to_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 ddd9fe49fca4f05f31bc97e462a478aa52477e2d Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Dec 2020 23:58:05 +0530 Subject: [PATCH 072/203] feat: Add month_to_date field --- erpnext/payroll/doctype/salary_slip/salary_slip.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 5141868adb7..d981a39953d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -591,14 +591,16 @@ { "fieldname": "month_to_date", "fieldtype": "Currency", - "label": "Month To Date" + "label": "Month To Date(Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-18 23:23:10.484574", + "modified": "2020-12-18 23:57:41.042954", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 0c4f97368d05f2277ccca54b583be0a104acf7a9 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Dec 2020 11:44:48 +0530 Subject: [PATCH 073/203] chore: UX improvement - Removed 'single reading' checkbox, unnecessary - Removed 'Mean' field and added computed mean to formula data - Changed 'Value Based' to 'Non-Numeric' - Re-arranged fields --- .../item_quality_inspection_parameter.json | 43 +++++------- .../quality_inspection/quality_inspection.py | 57 +++++++--------- .../quality_inspection_reading.json | 65 +++++-------------- .../quality_inspection_template.py | 3 +- 4 files changed, 58 insertions(+), 110 deletions(-) diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index f4501281579..9b980a1e013 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -8,14 +8,12 @@ "field_order": [ "specification", "value", - "value_based", - "single_reading", + "non_numeric", "column_break_3", - "formula_based_criteria", - "acceptance_formula", "min_value", "max_value", - "mean_value" + "formula_based_criteria", + "acceptance_formula" ], "fields": [ { @@ -27,10 +25,10 @@ "oldfieldtype": "Data", "print_width": "200px", "reqd": 1, - "width": "200px" + "width": "100px" }, { - "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, @@ -44,10 +42,9 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
\nNumeric eg. 2: mean > 3.5 (mean of populated fields)
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", - "in_list_view": 1, "label": "Acceptance Criteria Formula" }, { @@ -57,42 +54,32 @@ "label": "Formula Based Criteria" }, { - "default": "0", - "depends_on": "eval:!doc.value_based", - "fieldname": "single_reading", - "fieldtype": "Check", - "label": "Single Reading" - }, - { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", - "fieldname": "mean_value", - "fieldtype": "Float", - "label": "Mean Value" - }, - { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "fieldname": "min_value", "fieldtype": "Float", + "in_list_view": 1, "label": "Minimum Value" }, { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "fieldname": "max_value", "fieldtype": "Float", + "in_list_view": 1, "label": "Maximum Value" }, { "default": "0", - "description": "Non-numeric Inspection.", - "fieldname": "value_based", + "fieldname": "non_numeric", "fieldtype": "Check", - "label": "Value Based" + "in_list_view": 1, + "label": "Non-Numeric", + "width": "80px" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-18 21:03:29.828723", + "modified": "2020-12-21 11:37:55.387677", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index a7a023bcbf3..f582658d871 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -79,46 +79,27 @@ class QualityInspection(Document): if reading.formula_based_criteria: self.set_status_based_on_acceptance_formula(reading) else: + # if not formula based check acceptance values set self.set_status_based_on_acceptance_values(reading) def set_status_based_on_acceptance_values(self, reading): - if cint(reading.value_based): + if cint(reading.non_numeric): result = reading.get("reading_value") == reading.get("value") else: # numeric readings - if cint(reading.single_reading): - reading_1 = flt(reading.get("reading_1")) - result = flt(reading.get("min_value")) <= reading_1 <= flt(reading.get("max_value")) - else: - result = self.min_max_criteria_passed(reading) and self.mean_criteria_passed(reading) + result = self.min_max_criteria_passed(reading) reading.status = "Accepted" if result else "Rejected" def min_max_criteria_passed(self, reading): """Determine whether all readings fall in the acceptable range.""" for i in range(1, 11): - reading_field = reading.get("reading_" + str(i)) - if reading_field is not None: - result = flt(reading.get("min_value")) <= flt(reading_field) <= flt(reading.get("max_value")) + reading_value = reading.get("reading_" + str(i)) + if reading_value is not None and reading_value.strip(): + result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value")) if not result: return False return True - def mean_criteria_passed(self, reading): - """Determine whether mean of all readings is acceptable.""" - if reading.get("mean_value"): - from statistics import mean - readings_list = [] - - for i in range(1, 11): - reading_value = reading.get("reading_" + str(i)) - if reading_value is not None: - readings_list.append(flt(reading_value)) - - actual_mean = mean(readings_list) if readings_list else 0 - return True if actual_mean == reading.get("mean_value") else False - - return True # no mean value, nothing to check - def set_status_based_on_acceptance_formula(self, reading): if not reading.acceptance_formula: frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), @@ -141,18 +122,30 @@ class QualityInspection(Document): def get_formula_evaluation_data(self, reading): data = {} - if cint(reading.value_based): + if cint(reading.non_numeric): data = {"reading_value": reading.get("reading_value")} else: # numeric readings - data = {"reading_1": flt(reading.get("reading_1"))} - if not cint(reading.single_reading): - # if multiple numeric readings add all readings to data - for i in range(2, 11): - field = "reading_" + str(i) - data[field] = flt(reading.get(field)) + for i in range(1, 11): + field = "reading_" + str(i) + data[field] = flt(reading.get(field)) + data["mean"] = self.calculate_mean(reading) + return data + def calculate_mean(self, reading): + """Calculate mean of all non-empty readings.""" + from statistics import mean + readings_list = [] + + for i in range(1, 11): + reading_value = reading.get("reading_" + str(i)) + if reading_value is not None and reading_value.strip(): + readings_list.append(flt(reading_value)) + + actual_mean = mean(readings_list) if readings_list else 0 + return actual_mean + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index db95fabee0b..0792f26d2ab 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -9,18 +9,15 @@ "specification", "status", "value", - "value_based", + "non_numeric", "column_break_4", - "formula_based_criteria", - "acceptance_formula", "min_value", "max_value", - "mean_value", + "formula_based_criteria", + "acceptance_formula", "section_break_3", "reading_value", "section_break_14", - "single_reading", - "section_break_12", "reading_1", "reading_2", "reading_3", @@ -47,7 +44,7 @@ }, { "columns": 2, - "depends_on": "eval:(!doc.formula_based_criteria && doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)", "fieldname": "value", "fieldtype": "Data", "in_list_view": 1, @@ -66,7 +63,6 @@ }, { "columns": 1, - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_2", "fieldtype": "Data", "in_list_view": 1, @@ -76,7 +72,6 @@ }, { "columns": 1, - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_3", "fieldtype": "Data", "in_list_view": 1, @@ -85,7 +80,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_4", "fieldtype": "Data", "label": "Reading 4", @@ -93,7 +87,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_5", "fieldtype": "Data", "label": "Reading 5", @@ -101,7 +94,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_6", "fieldtype": "Data", "label": "Reading 6", @@ -109,7 +101,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_7", "fieldtype": "Data", "label": "Reading 7", @@ -117,7 +108,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_8", "fieldtype": "Data", "label": "Reading 8", @@ -125,7 +115,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_9", "fieldtype": "Data", "label": "Reading 9", @@ -133,7 +122,6 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.single_reading", "fieldname": "reading_10", "fieldtype": "Data", "label": "Reading 10", @@ -152,7 +140,7 @@ "options": "Accepted\nRejected" }, { - "depends_on": "value_based", + "depends_on": "non_numeric", "fieldname": "section_break_3", "fieldtype": "Section Break", "label": "Value Based Inspection" @@ -163,7 +151,7 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
Numeric eg.: reading_1 > 0.2 and reading_1 < 0.5
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
\nNumeric eg. 2: mean > 3.5 (mean of populated fields)
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -183,61 +171,42 @@ "label": "Formula Based Criteria" }, { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.single_reading && !doc.value_based)", - "fieldname": "mean_value", - "fieldtype": "Float", - "label": "Mean Value" - }, - { - "default": "0", - "fieldname": "single_reading", - "fieldtype": "Check", - "label": "Single Reading" - }, - { - "depends_on": "eval:!doc.value_based", - "fieldname": "section_break_12", - "fieldtype": "Section Break", - "hide_border": 1 - }, - { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "description": "Applied on each reading.", "fieldname": "min_value", "fieldtype": "Float", "label": "Minimum Value" }, { - "depends_on": "eval:(!doc.formula_based_criteria && !doc.value_based)", + "depends_on": "eval:(!doc.formula_based_criteria && !doc.non_numeric)", "description": "Applied on each reading.", "fieldname": "max_value", "fieldtype": "Float", "label": "Maximum Value" }, { - "default": "0", - "description": "Non-numeric Inspection.", - "fieldname": "value_based", - "fieldtype": "Check", - "label": "Value Based" - }, - { - "depends_on": "value_based", + "depends_on": "non_numeric", "fieldname": "reading_value", "fieldtype": "Data", "label": "Reading Value" }, { - "depends_on": "eval:!doc.value_based", + "depends_on": "eval:!doc.non_numeric", "fieldname": "section_break_14", "fieldtype": "Section Break", "label": "Numeric Inspection" + }, + { + "default": "0", + "fieldname": "non_numeric", + "fieldtype": "Check", + "label": "Non-Numeric" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-18 21:02:04.865777", + "modified": "2020-12-21 11:36:24.885019", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index 7dd0febc203..c5a7974a732 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -14,7 +14,6 @@ def get_template_details(template): return frappe.get_all('Item Quality Inspection Parameter', fields=["specification", "value", "acceptance_formula", - "value_based", "formula_based_criteria", "single_reading", - "min_value", "max_value", "mean_value"], + "non_numeric", "formula_based_criteria", "min_value", "max_value"], filters={'parenttype': 'Quality Inspection Template', 'parent': template}, order_by="idx") \ No newline at end of file From 68f91c96400226254875016d0e0b95bdc3816580 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Dec 2020 12:24:45 +0530 Subject: [PATCH 074/203] chore: Added tests for new ux - Test for value based inspection - tweaks in test for formula based inspection - tweaks in create_quality_inspection as status in child row is auto set now --- .../test_quality_inspection.py | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 2c40009426e..d0bfb466e05 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -44,24 +44,61 @@ class TestQualityInspection(unittest.TestCase): qa.delete() dn.delete() + def test_value_based_qi_readings(self): + # Test QI based on acceptance values (Non formula) + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + readings = [{ + "specification": "Iron Content", # numeric reading + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + }, + { + "specification": "Particle Inspection Needed", # non-numeric reading + "non_numeric": 1, + "value": "Yes", + "reading_value": "Yes" + }] + + qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, + readings=readings, do_not_save=True) + qa.save() + + # status must be auto set as per formula + self.assertEqual(qa.readings[0].status, "Accepted") + self.assertEqual(qa.readings[1].status, "Accepted") + + qa.delete() + dn.delete() + def test_formula_based_qi_readings(self): dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) readings = [{ - "specification": "Iron Content", + "specification": "Iron Content", # numeric reading + "formula_based_criteria": 1, "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", - "reading_1": 0.4 + "reading_1": "0.4" }, { - "specification": "Calcium Content", + "specification": "Calcium Content", # numeric reading + "formula_based_criteria": 1, "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", - "reading_1": 0.7 + "reading_1": "0.7" }, { - "specification": "Mg Content", - "acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9", - "reading_1": 0.5, - "reading_2": 0.7, + "specification": "Mg Content", # numeric reading + "formula_based_criteria": 1, + "acceptance_formula": "mean < 0.9", + "reading_1": "0.5", + "reading_2": "0.7", "reading_3": "random text" # check if random string input causes issues + }, + { + "specification": "Calcium Content", # non-numeric reading + "formula_based_criteria": 1, + "non_numeric": 1, + "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", + "reading_value": "Grade B" }] qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, @@ -72,6 +109,7 @@ class TestQualityInspection(unittest.TestCase): self.assertEqual(qa.readings[0].status, "Accepted") self.assertEqual(qa.readings[1].status, "Rejected") self.assertEqual(qa.readings[2].status, "Accepted") + self.assertEqual(qa.readings[3].status, "Accepted") qa.delete() dn.delete() @@ -86,8 +124,11 @@ def create_quality_inspection(**args): qa.item_code = args.item_code or "_Test Item with QA" qa.sample_size = 1 qa.inspected_by = frappe.session.user + qa.status = args.status or "Accepted" - readings = args.readings or {"specification": "Size", "status": args.status} + readings = args.readings or {"specification": "Size", "min_value": 0, "max_value": 10} + if args.status == "Rejected": + readings["reading_1"] = "12" # status is auto set in child on save if isinstance(readings, list): for entry in readings: From 0e222173ea431b46344f7b73866390c15e80106e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 21 Dec 2020 13:44:03 +0530 Subject: [PATCH 075/203] fix: don't set primary action if workflow is set --- erpnext/payroll/doctype/payroll_entry/payroll_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index cb48abbc363..31abaf40bf2 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -39,7 +39,7 @@ frappe.ui.form.on('Payroll Entry', { } ).toggleClass('btn-primary', !(frm.doc.employees || []).length); } - if ((frm.doc.employees || []).length) { + if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) { frm.page.clear_primary_action(); frm.page.set_primary_action(__('Create Salary Slips'), () => { frm.save('Submit').then(()=>{ From eae31f02cc1a5254292b7c621513e70b91d10b22 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Dec 2020 13:58:44 +0530 Subject: [PATCH 076/203] fix: Sider (missing semi-colons) --- .../doctype/quality_inspection/quality_inspection.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index f0bf9aed802..2ec8a070052 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -10,18 +10,18 @@ frappe.ui.form.on("Quality Inspection", { filters: { "item": frm.doc.item_code } - } + }; }); // Serial No based on item_code frm.set_query("item_serial_no", function() { - var filters = {}; + let filters = {}; if (frm.doc.item_code) { filters = { 'item_code': frm.doc.item_code - } + }; } - return { filters: filters } + return { filters: filters }; }); // item code based on GRN/DN @@ -75,4 +75,4 @@ frappe.ui.form.on("Quality Inspection", { }); } }, -}) \ No newline at end of file +}); \ No newline at end of file From 9466e42e7095f7f4ff32230ab7dace6642455ba9 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 21 Dec 2020 20:52:20 +0530 Subject: [PATCH 077/203] fix: change request modifications --- .../v13_0/update_project_template_tasks.py | 8 +- erpnext/projects/doctype/project/project.py | 51 +++++------ .../projects/doctype/project/test_project.py | 87 ++++++++++--------- .../project_template/project_template.py | 5 +- erpnext/projects/doctype/task/task.json | 4 +- erpnext/projects/doctype/task/test_task.py | 2 +- 6 files changed, 82 insertions(+), 75 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 1303efd93fb..26c42592816 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -6,7 +6,13 @@ import frappe def execute(): frappe.reload_doc("projects", "doctype", "project_template") - for template_name in frappe.db.sql(""" select name from `tabProject Template` """, as_dict=1): + for template_name in frappe.db.sql(""" + select + name + from + `tabProject Template` """, + as_dict=1): + template = frappe.get_doc("Project Template", template_name.name) replace_tasks = False new_tasks = [] diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 13e72fec8a2..2cdfb7af444 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -59,8 +59,8 @@ class Project(Document): for task in template.tasks: template_task_details = frappe.get_doc("Task", task.task) tmp_task_details.append(template_task_details) - project_tasks.append(self.create_task_from_template(template_task_details)) - + task = self.create_task_from_template(template_task_details) + project_tasks.append(task) self.dependency_mapping(tmp_task_details, project_tasks) def create_task_from_template(self, task_details): @@ -75,36 +75,33 @@ class Project(Document): task_weight = task_details.task_weight, type = task_details.type, issue = task_details.issue, - is_group = task_details.is_group, - start = task_details.start, - duration = task_details.duration + is_group = task_details.is_group )).insert() def dependency_mapping(self, template_tasks, project_tasks): - for tmp_task in template_tasks: - prj_task = list(filter(lambda x: x.subject == tmp_task.subject, project_tasks))[0] - prj_task = frappe.get_doc("Task", prj_task.name) - self.check_depends_on_value(tmp_task, prj_task, project_tasks) - self.check_for_parent_tasks(tmp_task, prj_task, project_tasks) + for template_task in template_tasks: + project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] + if template_task.get("depends_on") and not project_task.get("depends_on"): + self.check_depends_on_value(template_task, project_task, project_tasks) + if template_task.get("parent_task") and not project_task.get("parent_task"): + self.check_for_parent_tasks(template_task, project_task, project_tasks) - def check_depends_on_value(self, tmp_task, prj_task, project_tasks): - if tmp_task.get("depends_on") and not prj_task.get("depends_on"): - for child_task in tmp_task.get("depends_on"): - child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") - corresponding_prj_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) - if len(corresponding_prj_task): - prj_task.append("depends_on",{ - "task": corresponding_prj_task[0].name - }) - prj_task.save() + def check_depends_on_value(self, template_task, project_task, project_tasks): + for child_task in template_task.get("depends_on"): + child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") + corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.append("depends_on",{ + "task": corresponding_project_task[0].name + }) + project_task.save() - def check_for_parent_tasks(self, tmp_task, prj_task, project_tasks): - if tmp_task.get("parent_task") and not prj_task.get("parent_task"): - parent_task_subject = frappe.db.get_value("Task", tmp_task.get("parent_task"), "subject") - corresponding_prj_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) - if len(corresponding_prj_task): - prj_task.parent_task = corresponding_prj_task[0].name - prj_task.save() + def check_for_parent_tasks(self, template_task, project_task, project_tasks): + parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") + corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.parent_task = corresponding_project_task[0].name + project_task.save() def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index ce56a50b4e2..1d2980ce461 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -17,78 +17,79 @@ class TestProject(unittest.TestCase): """ Test Action: Basic Test of a Project created from template. The template has a single task. """ - frappe.db.sql('delete from tabTask where project = "Test Project with Templ - no parent and dependend tasks"') - frappe.delete_doc('Project', 'Test Project with Templ - no parent and dependend tasks') + project_name = "Test Project with Template - No Parent and Dependend Tasks" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc('Project', project_name) - task1 = task_exists("Test Temp Task with no parent and dependency") + task1 = task_exists("Test Template Task with No Parent and Dependency") if not task1: - task1 = create_task(subject="Test Temp Task with no parent and dependency", is_template=1, begin=5, duration=3) + task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3) - template = make_project_template("Test Project Template - no parent and dependend tasks", [task1]) - project = get_project("Test Project with Templ - no parent and dependend tasks", template) - tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1]) + project = get_project(project_name, template) + tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc') - self.assertEqual(tasks[0].subject, 'Test Temp Task with no parent and dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) + self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3)) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): + project_name = "Test Project with Template - Tasks with Parent-Child Relation" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc('Project', project_name) - frappe.db.sql('delete from tabTask where project = "Test Project with Templ - tasks with parent-child"') - frappe.delete_doc('Project', 'Test Project with Templ - tasks with parent-child') - - task1 = task_exists("Test Temp Task parent") + task1 = task_exists("Test Template Task Parent") if not task1: - task1 = create_task(subject="Test Temp Task parent", is_group=1, is_template=1, begin=1, duration=1) + task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1) - task2 = task_exists("Test Temp Task child 1") + task2 = task_exists("Test Template Task Child 1") if not task2: - task2 = create_task(subject="Test Temp Task child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) + task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) - task3 = task_exists("Test Temp Task child 2") + task3 = task_exists("Test Template Task Child 2") if not task3: - task3 = create_task(subject="Test Temp Task child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) + task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) - template = make_project_template("Test Project Template - tasks with parent-child", [task1, task2, task3]) - project = get_project("Test Project with Templ - tasks with parent-child", template) - tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3]) + 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') - self.assertEqual(tasks[0].subject, 'Test Temp Task parent') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0])) + self.assertEqual(tasks[0].subject, 'Test Template Task Parent') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1)) - self.assertEqual(tasks[1].subject, 'Test Temp Task child 1') - self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) + self.assertEqual(tasks[1].subject, 'Test Template Task Child 1') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3)) self.assertEqual(tasks[1].parent_task, tasks[0].name) - self.assertEqual(tasks[2].subject, 'Test Temp Task child 2') - self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, tasks[2])) + self.assertEqual(tasks[2].subject, 'Test Template Task Child 2') + self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3)) self.assertEqual(tasks[2].parent_task, tasks[0].name) self.assertEqual(len(tasks), 3) def test_project_template_having_dependent_tasks(self): + project_name = "Test Project with Template - Dependent Tasks" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc('Project', project_name) - frappe.db.sql('delete from tabTask where project = "Test Project with Templ - dependent tasks"') - frappe.delete_doc('Project', 'Test Project with Templ - dependent tasks') - - task1 = task_exists("Test Temp Task for dependency") + task1 = task_exists("Test Template Task for Dependency") if not task1: - task1 = create_task(subject="Test Temp Task for dependency", is_template=1, begin=3, duration=1) + task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1) - task2 = task_exists("Test Temp Task with dependency") + task2 = task_exists("Test Template Task with Dependency") if not task2: - task2 = create_task(subject="Test Temp Task with dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) + 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 Templ - dependent tasks", [task1, task2]) - project = get_project("Test Project with Templ - dependent tasks", template) - tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc') + 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') - self.assertEqual(tasks[1].subject, 'Test Temp Task with dependency') - self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, tasks[1])) + self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency') + self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2)) self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 ) - self.assertEqual(tasks[0].subject, 'Test Temp Task for dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, tasks[0]) ) + self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency') + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) ) self.assertEqual(len(tasks), 2) @@ -129,5 +130,5 @@ def task_exists(subject): return False return frappe.get_doc("Task", result[0].name) -def calculate_end_date(project, task): - return getdate(add_days(project.expected_start_date, task.start + task.duration)) \ No newline at end of file +def calculate_end_date(project, start, duration): + return getdate(add_days(project.expected_start_date, start + duration)) \ No newline at end of file diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py index 1beebf7a258..aace40240c4 100644 --- a/erpnext/projects/doctype/project_template/project_template.py +++ b/erpnext/projects/doctype/project_template/project_template.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ +from frappe.utils import get_link_to_form class ProjectTemplate(Document): @@ -18,8 +19,8 @@ class ProjectTemplate(Document): if task_details.depends_on: for dependency_task in task_details.depends_on: if not self.check_dependent_task_presence(dependency_task.task): - task_details_format = """{0}""".format(task_details.name) - dependency_task_format = """{0}""".format(dependency_task.task) + task_details_format = get_link_to_form("Task",task_details.name) + dependency_task_format = get_link_to_form("Task", dependency_task.task) frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format))) def check_dependent_task_presence(self, task): diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index a9e3d9bc0fe..bb55256f7d9 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -371,11 +371,13 @@ "label": "Is Template" }, { + "depends_on": "is_template", "fieldname": "start", "fieldtype": "Int", "label": "Begin On (Days)" }, { + "depends_on": "is_template", "fieldname": "duration", "fieldtype": "Int", "label": "Duration (Days)" @@ -386,7 +388,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2020-12-07 13:26:53.614689", + "modified": "2020-12-21 11:59:24.196834", "modified_by": "Administrator", "module": "Projects", "name": "Task", diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index aded78b8574..25714f8cde3 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -104,7 +104,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa 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 None if is_template else "_Test Project" task.is_template = is_template task.start = begin task.duration = duration From c36cab81f229376cbdde96cf7cfe4ccbd33b6f36 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 21 Dec 2020 23:46:02 +0530 Subject: [PATCH 078/203] fix: Update year_to_date and month_to_date field labels to show company currency --- .../doctype/salary_slip/salary_slip.js | 6 +++--- .../doctype/salary_slip/salary_slip.json | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index f7e22c63879..56948717628 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -125,15 +125,15 @@ frappe.ui.form.on("Salary Slip", { change_form_labels: function(frm, company_currency) { frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", - "base_net_pay", "base_rounded_total", "base_total_in_words"], + "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"], company_currency); - frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], + frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"], frm.doc.currency); // toggle fields frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", - "base_net_pay", "base_rounded_total", "base_total_in_words"], + "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"], frm.doc.currency != company_currency); }, diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index d981a39953d..43deee43aac 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -70,10 +70,12 @@ "net_pay", "base_net_pay", "year_to_date", + "base_year_to_date", "column_break_53", "rounded_total", "base_rounded_total", "month_to_date", + "base_month_to_date", "section_break_55", "total_in_words", "column_break_69", @@ -584,12 +586,26 @@ { "fieldname": "year_to_date", "fieldtype": "Currency", + "label": "Year To Date", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "month_to_date", + "fieldtype": "Currency", + "label": "Month To Date", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "base_year_to_date", + "fieldtype": "Currency", "label": "Year To Date(Company Currency)", "options": "Company:company:default_currency", "read_only": 1 }, { - "fieldname": "month_to_date", + "fieldname": "base_month_to_date", "fieldtype": "Currency", "label": "Month To Date(Company Currency)", "options": "Company:company:default_currency", @@ -600,7 +616,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-12-18 23:57:41.042954", + "modified": "2020-12-21 23:43:44.959840", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", From 090783804bdc59f22b8a7afee43cb3ddabcd37b2 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Mon, 21 Dec 2020 23:52:05 +0530 Subject: [PATCH 079/203] fix: Improve month_to_date computation --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index e86a7fc3158..02e5f2d1d12 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -1150,8 +1150,7 @@ class SalarySlip(TransactionBase): def compute_month_to_date(self): month_to_date = 0 - date = datetime.datetime.strptime(self.start_date,"%Y-%m-%d") - first_day_of_the_month = "1-" + str(date.month) + "-" + str(date.year) + first_day_of_the_month = get_first_day(self.start_date) salary_slips_from_this_month = frappe.get_list('Salary Slip', fields = ['employee_name', 'start_date', 'net_pay'], filters = {'employee_name' : self.employee_name, From 3a26f26671e19df3acd7ef690300810e1d5026d3 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 11:56:59 +0530 Subject: [PATCH 080/203] fix: get_doc to avoid modified error --- erpnext/projects/doctype/project/project.py | 35 ++++++++++--------- .../projects/doctype/project/test_project.py | 2 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 2cdfb7af444..97134602f86 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -81,27 +81,28 @@ class Project(Document): def dependency_mapping(self, template_tasks, project_tasks): for template_task in template_tasks: project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] - if template_task.get("depends_on") and not project_task.get("depends_on"): - self.check_depends_on_value(template_task, project_task, project_tasks) - if template_task.get("parent_task") and not project_task.get("parent_task"): - self.check_for_parent_tasks(template_task, project_task, project_tasks) + project_task = frappe.get_doc("Task", project_task.name) + self.check_depends_on_value(template_task, project_task, project_tasks) + self.check_for_parent_tasks(template_task, project_task, project_tasks) def check_depends_on_value(self, template_task, project_task, project_tasks): - for child_task in template_task.get("depends_on"): - child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") - corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) - if len(corresponding_project_task): - project_task.append("depends_on",{ - "task": corresponding_project_task[0].name - }) - project_task.save() + if template_task.get("depends_on") and not project_task.get("depends_on"): + for child_task in template_task.get("depends_on"): + child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") + corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.append("depends_on",{ + "task": corresponding_project_task[0].name + }) + project_task.save() def check_for_parent_tasks(self, template_task, project_task, project_tasks): - parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") - corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) - if len(corresponding_project_task): - project_task.parent_task = corresponding_project_task[0].name - project_task.save() + if template_task.get("parent_task") and not project_task.get("parent_task"): + parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") + corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + if len(corresponding_project_task): + project_task.parent_task = corresponding_project_task[0].name + project_task.save() def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: return True diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 1d2980ce461..d77b14ce33c 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -52,7 +52,7 @@ class TestProject(unittest.TestCase): template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3]) 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') + tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc') self.assertEqual(tasks[0].subject, 'Test Template Task Parent') self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1)) From 468f67a4de5e267d7518105541a4dddbdfdcf610 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Dec 2020 12:44:09 +0530 Subject: [PATCH 081/203] fix: Add parent for all-products page --- erpnext/www/all-products/index.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index 0394e4b2cc5..7d7793ac49b 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -15,6 +15,9 @@ def get_context(context): context.items = get_products_for_website(field_filters, attribute_filters, search) + # Add homepage as parent + context.parents = [{"name": frappe._("Home"), "route":"/"}] + product_settings = get_product_settings() context.field_filters = get_field_filter_data() \ if product_settings.enable_field_filters else [] From 6900a79421b141e9d86d7e111ba9eac06e7cf75d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:37:13 +0100 Subject: [PATCH 082/203] fix: fail silently --- erpnext/regional/germany/accounts_controller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index 5b2b31f2043..0ab027b4d6e 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -37,7 +37,14 @@ def validate_regional(doc): for field in required_fields: condition = field.get("condition") - if condition and not frappe.safe_eval(condition, doc.as_dict()): + condition_true = True + try: + condition_true = frappe.safe_eval(condition, doc.as_dict()) + except: + # invalid condition should not result in an error + pass + + if condition and not condition_true: continue field_name = field.get("field_name") From 5adbe49ca65b9230531341e0d2d906670e39002e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:37:43 +0100 Subject: [PATCH 083/203] refactor: translation syntax --- erpnext/regional/germany/accounts_controller.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index 0ab027b4d6e..63da96bddab 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -55,9 +55,6 @@ def validate_regional(doc): def missing(field_label, regulation): """Notify the user that a required field is missing.""" - context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.' - msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format( - field_label=frappe.bold(_(field_label)), - regulation=regulation - ) - ) + translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') + formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation) + msgprint(formatted_msg) From a69021018aea2b2e51f4cccb999dad97bcdc5752 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:38:09 +0100 Subject: [PATCH 084/203] test: add test for accounts controller --- erpnext/regional/germany/test_accounts_controller.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 erpnext/regional/germany/test_accounts_controller.py diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py new file mode 100644 index 00000000000..63bb843d307 --- /dev/null +++ b/erpnext/regional/germany/test_accounts_controller.py @@ -0,0 +1,12 @@ +import frappe +import unittest +from erpnext.regional.germany.accounts_controller import validate_regional + + +class TestAccountsController(unittest.TestCase): + + def setUp(self): + self.sales_invoice = frappe.get_last_doc('Sales Invoice') + + def test_validate_regional(self): + validate_regional(self.sales_invoice) From 511be6466df429acde392aa458c9215cbde48238 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 11:43:33 +0100 Subject: [PATCH 085/203] Revert "fix: fail silently" This reverts commit 6900a79421b141e9d86d7e111ba9eac06e7cf75d. --- erpnext/regional/germany/accounts_controller.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index 63da96bddab..b789960ca01 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -37,14 +37,7 @@ def validate_regional(doc): for field in required_fields: condition = field.get("condition") - condition_true = True - try: - condition_true = frappe.safe_eval(condition, doc.as_dict()) - except: - # invalid condition should not result in an error - pass - - if condition and not condition_true: + if condition and not frappe.safe_eval(condition, doc.as_dict()): continue field_name = field.get("field_name") From 4ebee5014eebcf49669ccabda45c971f3822c814 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 18:14:46 +0530 Subject: [PATCH 086/203] feat: aholiday check before setting start and end date in task --- erpnext/projects/doctype/project/project.py | 20 +++++++++++++++++-- .../projects/doctype/project/test_project.py | 3 --- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 97134602f86..f6bb6e9e745 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -13,6 +13,7 @@ from frappe.desk.reportview import get_match_cond from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from frappe.model.document import Document +from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list class Project(Document): def get_feed(self): @@ -69,8 +70,8 @@ class Project(Document): subject = task_details.subject, project = self.name, status = 'Open', - exp_start_date = add_days(self.expected_start_date, task_details.start), - exp_end_date = add_days(self.expected_start_date, task_details.start + task_details.duration), + exp_start_date = self.calculate_start_date(task_details), + exp_end_date = self.calculate_end_date(task_details), description = task_details.description, task_weight = task_details.task_weight, type = task_details.type, @@ -78,6 +79,21 @@ class Project(Document): is_group = task_details.is_group )).insert() + def calculate_start_date(self, task_details): + self.start_date = add_days(self.expected_start_date, task_details.start) + self.start_date = self.update_if_holiday(self.start_date) + return self.start_date + + def calculate_end_date(self, task_details): + self.end_date = add_days(self.start_date, task_details.duration) + return self.update_if_holiday(self.end_date) + + def update_if_holiday(self, date): + holiday_list = self.holiday_list or get_holiday_list() + while is_holiday(holiday_list, date): + date = add_days(date, 1) + return date + def dependency_mapping(self, template_tasks, project_tasks): for template_task in template_tasks: project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index d77b14ce33c..0faf97670d8 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -14,9 +14,6 @@ from frappe.utils import getdate, nowdate, add_days class TestProject(unittest.TestCase): def test_project_with_template_having_no_parent_and_depend_tasks(self): - """ - Test Action: Basic Test of a Project created from template. The template has a single task. - """ project_name = "Test Project with Template - No Parent and Dependend Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) frappe.delete_doc('Project', project_name) From 6cf018c762ee4d67bfc83b9f6fc3814b51462734 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 19:40:41 +0530 Subject: [PATCH 087/203] fix: holiday update in tests --- erpnext/projects/doctype/project/project.py | 16 ++++++++-------- erpnext/projects/doctype/project/test_project.py | 8 ++++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index f6bb6e9e745..60f85b0e7a6 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -81,18 +81,12 @@ class Project(Document): def calculate_start_date(self, task_details): self.start_date = add_days(self.expected_start_date, task_details.start) - self.start_date = self.update_if_holiday(self.start_date) + self.start_date = update_if_holiday(self.holiday_list, self.start_date) return self.start_date def calculate_end_date(self, task_details): self.end_date = add_days(self.start_date, task_details.duration) - return self.update_if_holiday(self.end_date) - - def update_if_holiday(self, date): - holiday_list = self.holiday_list or get_holiday_list() - while is_holiday(holiday_list, date): - date = add_days(date, 1) - return date + return update_if_holiday(self.holiday_list, self.end_date) def dependency_mapping(self, template_tasks, project_tasks): for template_task in template_tasks: @@ -547,3 +541,9 @@ def set_project_status(project, status): project.status = status project.save() + +def update_if_holiday(holiday_list, date): + holiday_list = holiday_list or get_holiday_list() + while is_holiday(holiday_list, date): + date = add_days(date, 1) + return date diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 0faf97670d8..af978e85fd5 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -8,7 +8,7 @@ test_records = frappe.get_test_records('Project') test_ignore = ["Sales Order"] from erpnext.projects.doctype.project_template.test_project_template import make_project_template -from erpnext.projects.doctype.project.project import set_project_status +from erpnext.projects.doctype.project.project import set_project_status, update_if_holiday from erpnext.projects.doctype.task.test_task import create_task from frappe.utils import getdate, nowdate, add_days @@ -128,4 +128,8 @@ def task_exists(subject): return frappe.get_doc("Task", result[0].name) def calculate_end_date(project, start, duration): - return getdate(add_days(project.expected_start_date, start + duration)) \ No newline at end of file + start = add_days(project.expected_start_date, start) + start = update_if_holiday(project.holiday_list, start) + end = add_days(start, duration) + end = update_if_holiday(project.holiday_list, end) + return getdate(end) \ No newline at end of file From 8dec1c142f96bb171c0e23c92ef9d3b1100cf6b6 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 22 Dec 2020 19:55:31 +0530 Subject: [PATCH 088/203] fix: removed unused imports --- erpnext/projects/doctype/project/test_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index af978e85fd5..97b67b38eb3 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -8,7 +8,7 @@ test_records = frappe.get_test_records('Project') test_ignore = ["Sales Order"] from erpnext.projects.doctype.project_template.test_project_template import make_project_template -from erpnext.projects.doctype.project.project import set_project_status, update_if_holiday +from erpnext.projects.doctype.project.project import update_if_holiday from erpnext.projects.doctype.task.test_task import create_task from frappe.utils import getdate, nowdate, add_days From 2acd8cbc02aca5904e35ece8dfb4b1608e23891e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 17:34:22 +0100 Subject: [PATCH 089/203] fix: sider --- erpnext/regional/germany/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py index b789960ca01..7f76493608e 100644 --- a/erpnext/regional/germany/accounts_controller.py +++ b/erpnext/regional/germany/accounts_controller.py @@ -48,6 +48,6 @@ def validate_regional(doc): def missing(field_label, regulation): """Notify the user that a required field is missing.""" - translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') + translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501 formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation) msgprint(formatted_msg) From df8ea194064d5d7abcdf1a696324af55d679baa3 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 17:34:31 +0100 Subject: [PATCH 090/203] fix: whitespace --- erpnext/regional/germany/test_accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/germany/test_accounts_controller.py b/erpnext/regional/germany/test_accounts_controller.py index 63bb843d307..8bd378c971f 100644 --- a/erpnext/regional/germany/test_accounts_controller.py +++ b/erpnext/regional/germany/test_accounts_controller.py @@ -7,6 +7,6 @@ class TestAccountsController(unittest.TestCase): def setUp(self): self.sales_invoice = frappe.get_last_doc('Sales Invoice') - + def test_validate_regional(self): validate_regional(self.sales_invoice) From 1fb412e3f6b0099082601b6539b0ce62f0345438 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 23 Dec 2020 11:39:37 +1100 Subject: [PATCH 091/203] docs: fix simple typo, udpate -> update There is a small typo in erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py. Should read `update` rather than `udpate`. --- erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py index ad043dd99d3..97e217aa054 100644 --- a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py +++ b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals import frappe def execute(): - # udpate sales cycle + # update sales cycle for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']: frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d) - # udpate purchase cycle + # update purchase cycle for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']: frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d) From 09c6842199f90a11702b40a72d6bf7ccec22c08b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Dec 2020 11:29:26 +0530 Subject: [PATCH 092/203] fix: accounting entries of asset when submitting purchase receipt --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 97e0fa738cd..878dd588779 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -323,7 +323,7 @@ class PurchaseReceipt(BuyingController): elif d.warehouse not in warehouse_with_no_account or \ d.rejected_warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(d.warehouse) - elif d.item_code not in stock_items and flt(d.qty) and auto_accounting_for_non_stock_items: + elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items: service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") credit_currency = get_account_currency(service_received_but_not_billed_account) From 0411a43c6e4c8b5940e7068be0893bea8f0b872b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Dec 2020 12:14:41 +0530 Subject: [PATCH 093/203] fix: set finished good item rate based on qty --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index afdb54ceaa2..579b8c5fe1d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -511,7 +511,7 @@ class StockEntry(StockController): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) - return flt(outgoing_items_cost - scrap_items_cost) + return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty) def distribute_additional_costs(self): # If no incoming items, set additional costs blank From d556847fca7ac82e7f11fc5d30de30ebb45ad63e Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 17 Dec 2020 15:17:25 +0530 Subject: [PATCH 094/203] fix: allow addition and removal of employee in payroll Entry --- .../payroll_employee_detail.json | 5 +-- .../doctype/payroll_entry/payroll_entry.js | 38 ++++++++++--------- .../doctype/payroll_entry/payroll_entry.json | 5 +-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json index 8a55224dca7..09c7eb9a456 100644 --- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json +++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json @@ -17,8 +17,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Employee", - "options": "Employee", - "read_only": 1 + "options": "Employee" }, { "fetch_from": "employee.employee_name", @@ -52,7 +51,7 @@ ], "istable": 1, "links": [], - "modified": "2020-09-30 12:40:07.999878", + "modified": "2020-12-17 15:43:29.542977", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Employee Detail", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index cb48abbc363..28236c04994 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -31,7 +31,7 @@ frappe.ui.form.on('Payroll Entry', { refresh: function(frm) { if (frm.doc.docstatus == 0) { - if(!frm.is_new()) { + if (!frm.is_new()) { frm.page.clear_primary_action(); frm.add_custom_button(__("Get Employees"), function() { @@ -61,33 +61,33 @@ frappe.ui.form.on('Payroll Entry', { doc: frm.doc, method: 'fill_employee_details', }).then(r => { - if (r.docs && r.docs[0].employees){ + if (r.docs && r.docs[0].employees) { frm.employees = r.docs[0].employees; frm.dirty(); frm.save(); frm.refresh(); - if(r.docs[0].validate_attendance){ + if (r.docs[0].validate_attendance) { render_employee_attendance(frm, r.message); } } - }) + }); }, create_salary_slips: function(frm) { frm.call({ doc: frm.doc, method: "create_salary_slips", - callback: function(r) { + callback: function() { frm.refresh(); frm.toolbar.refresh(); } - }) + }); }, add_context_buttons: function(frm) { - if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { + if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { frm.events.add_bank_entry_button(frm); - } else if(frm.doc.salary_slips_created) { + } else if (frm.doc.salary_slips_created) { frm.add_custom_button(__("Submit Salary Slip"), function() { submit_salary_slip(frm); }).addClass("btn-primary"); @@ -192,9 +192,9 @@ frappe.ui.form.on('Payroll Entry', { }, start_date: function (frm) { - if(!in_progress && frm.doc.start_date){ + if (!in_progress && frm.doc.start_date) { frm.trigger("set_end_date"); - }else{ + } else { // reset flag in_progress = false; } @@ -228,7 +228,7 @@ frappe.ui.form.on('Payroll Entry', { } }, - set_end_date: function(frm){ + set_end_date: function(frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -243,9 +243,9 @@ frappe.ui.form.on('Payroll Entry', { }); }, - validate_attendance: function(frm){ - if(frm.doc.validate_attendance && frm.doc.employees){ - frappe.call({ + validate_attendance: function(frm) { + if (frm.doc.validate_attendance && frm.doc.employees) { + frappe.call ({ method: 'validate_employee_attendance', args: {}, callback: function(r) { @@ -255,7 +255,7 @@ frappe.ui.form.on('Payroll Entry', { freeze: true, freeze_message: __('Validating Employee Attendance...') }); - }else{ + } else { frm.fields_dict.attendance_detail_html.html(""); } }, @@ -274,14 +274,16 @@ const submit_salary_slip = function (frm) { frappe.call({ method: 'submit_salary_slips', args: {}, - callback: function() {frm.events.refresh(frm);}, + callback: function() { + frm.events.refresh(frm); + }, doc: frm.doc, freeze: true, freeze_message: __('Submitting Salary Slips and creating Journal Entry...') }); }, function() { - if(frappe.dom.freeze_count) { + if (frappe.dom.freeze_count) { frappe.dom.unfreeze(); frm.events.refresh(frm); } @@ -316,4 +318,4 @@ let render_employee_attendance = function(frm, data) { data: data }) ); -} +}; diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 7a48dd14758..0444134aa4d 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -129,8 +129,7 @@ "fieldname": "employees", "fieldtype": "Table", "label": "Employee Details", - "options": "Payroll Employee Detail", - "read_only": 1 + "options": "Payroll Employee Detail" }, { "fieldname": "section_break_13", @@ -290,7 +289,7 @@ "icon": "fa fa-cog", "is_submittable": 1, "links": [], - "modified": "2020-10-23 13:00:33.753228", + "modified": "2020-12-17 15:13:17.766210", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Entry", From 1ab4f09ee9b597c7775d5d2a0fb2ad6732660686 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 23 Dec 2020 15:18:41 +0530 Subject: [PATCH 095/203] fix: use file_url to save file and not file name --- erpnext/hr/doctype/employee/employee.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index dfc600ca3c5..0fde3a12ac8 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -135,7 +135,7 @@ class Employee(NestedSet): try: frappe.get_doc({ "doctype": "File", - "file_name": self.image, + "file_url": self.image, "attached_to_doctype": "User", "attached_to_name": self.user_id }).insert() From d2f91e8c1189185c49113e2db72ab0352bcac5d0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 24 Dec 2020 11:03:36 +0530 Subject: [PATCH 096/203] fix: Do not cancel reference document on Quality Inspection cancellation (#24197) --- .../stock/doctype/quality_inspection/quality_inspection.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 376848afaa4..03e3de115b7 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -4,6 +4,11 @@ cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; frappe.ui.form.on("Quality Inspection", { + refresh: function(frm) { + // Ignore cancellation of reference doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type]; + }, + item_code: function(frm) { if (frm.doc.item_code) { return frm.call({ From 527a156512f773c092b13465f567e06258e708cf Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 23 Dec 2020 17:22:51 +0530 Subject: [PATCH 097/203] feat: validated employees whose salary has been already precessed --- .../doctype/payroll_entry/payroll_entry.js | 104 +++++++++++++----- .../doctype/payroll_entry/payroll_entry.py | 25 ++++- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 28236c04994..2288a277917 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -10,15 +10,22 @@ frappe.ui.form.on('Payroll Entry', { } frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet); - frm.set_query("department", function() { + frm.events.department_filters(frm); + frm.events.payroll_payable_account_filters(frm); + }, + + department_filters: function (frm) { + frm.set_query("department", function () { return { "filters": { "company": frm.doc.company, } }; }); + }, - frm.set_query("payroll_payable_account", function() { + payroll_payable_account_filters: function (frm) { + frm.set_query("payroll_payable_account", function () { return { filters: { "company": frm.doc.company, @@ -29,12 +36,12 @@ frappe.ui.form.on('Payroll Entry', { }); }, - refresh: function(frm) { + refresh: function (frm) { if (frm.doc.docstatus == 0) { if (!frm.is_new()) { frm.page.clear_primary_action(); frm.add_custom_button(__("Get Employees"), - function() { + function () { frm.events.get_employee_details(frm); } ).toggleClass('btn-primary', !(frm.doc.employees || []).length); @@ -42,7 +49,7 @@ frappe.ui.form.on('Payroll Entry', { if ((frm.doc.employees || []).length) { frm.page.clear_primary_action(); frm.page.set_primary_action(__('Create Salary Slips'), () => { - frm.save('Submit').then(()=>{ + frm.save('Submit').then(() => { frm.page.clear_primary_action(); frm.refresh(); frm.events.refresh(frm); @@ -73,36 +80,36 @@ frappe.ui.form.on('Payroll Entry', { }); }, - create_salary_slips: function(frm) { + create_salary_slips: function (frm) { frm.call({ doc: frm.doc, method: "create_salary_slips", - callback: function() { + callback: function () { frm.refresh(); frm.toolbar.refresh(); } }); }, - add_context_buttons: function(frm) { + add_context_buttons: function (frm) { if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { frm.events.add_bank_entry_button(frm); } else if (frm.doc.salary_slips_created) { - frm.add_custom_button(__("Submit Salary Slip"), function() { + frm.add_custom_button(__("Submit Salary Slip"), function () { submit_salary_slip(frm); }).addClass("btn-primary"); } }, - add_bank_entry_button: function(frm) { + add_bank_entry_button: function (frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries', args: { 'name': frm.doc.name }, - callback: function(r) { + callback: function (r) { if (r.message && !r.message.submitted) { - frm.add_custom_button("Make Bank Entry", function() { + frm.add_custom_button("Make Bank Entry", function () { make_bank_entry(frm); }).addClass("btn-primary"); } @@ -141,8 +148,37 @@ frappe.ui.form.on('Payroll Entry', { }, payroll_frequency: function (frm) { - frm.trigger("set_start_end_dates"); - frm.events.clear_employee_table(frm); + frm.trigger("set_start_end_dates").then( ()=> { + frm.events.clear_employee_table(frm); + frm.events.get_employee_with_salary_slip_and_set_query(frm); + }); + }, + + employee_filters: function (frm, emp_list) { + frm.set_query('employee', 'employees', () => { + return { + filters: { + name: ["not in", emp_list] + } + }; + }); + }, + + get_employee_with_salary_slip_and_set_query: function (frm) { + frappe.db.get_list('Salary Slip', { + filters: { + start_date: frm.doc.start_date, + end_date: frm.doc.end_date, + docstatus: 1, + }, + fields: ['employee'] + }).then((emp) => { + var emp_list = []; + emp.forEach((employee_data) => { + emp_list.push(Object.values(employee_data)[0]); + }); + frm.events.employee_filters(frm, emp_list); + }); }, company: function (frm) { @@ -164,17 +200,17 @@ frappe.ui.form.on('Payroll Entry', { from_currency: frm.doc.currency, to_currency: company_currency, }, - callback: function(r) { + callback: function (r) { frm.set_value("exchange_rate", flt(r.message)); frm.set_df_property('exchange_rate', 'hidden', 0); - frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency - + " = [?] " + company_currency); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); } }); } else { frm.set_value("exchange_rate", 1.0); frm.set_df_property('exchange_rate', 'hidden', 1); - frm.set_df_property("exchange_rate", "description", "" ); + frm.set_df_property("exchange_rate", "description", ""); } } }, @@ -228,7 +264,7 @@ frappe.ui.form.on('Payroll Entry', { } }, - set_end_date: function(frm) { + set_end_date: function (frm) { frappe.call({ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { @@ -243,12 +279,12 @@ frappe.ui.form.on('Payroll Entry', { }); }, - validate_attendance: function(frm) { + validate_attendance: function (frm) { if (frm.doc.validate_attendance && frm.doc.employees) { - frappe.call ({ + frappe.call({ method: 'validate_employee_attendance', args: {}, - callback: function(r) { + callback: function (r) { render_employee_attendance(frm, r.message); }, doc: frm.doc, @@ -270,11 +306,11 @@ frappe.ui.form.on('Payroll Entry', { const submit_salary_slip = function (frm) { frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'), - function() { + function () { frappe.call({ method: 'submit_salary_slips', args: {}, - callback: function() { + callback: function () { frm.events.refresh(frm); }, doc: frm.doc, @@ -282,7 +318,7 @@ const submit_salary_slip = function (frm) { freeze_message: __('Submitting Salary Slips and creating Journal Entry...') }); }, - function() { + function () { if (frappe.dom.freeze_count) { frappe.dom.unfreeze(); frm.events.refresh(frm); @@ -297,9 +333,11 @@ let make_bank_entry = function (frm) { return frappe.call({ doc: cur_frm.doc, method: "make_payment_entry", - callback: function() { + callback: function () { frappe.set_route( - 'List', 'Journal Entry', {"Journal Entry Account.reference_name": frm.doc.name} + 'List', 'Journal Entry', { + "Journal Entry Account.reference_name": frm.doc.name + } ); }, freeze: true, @@ -311,11 +349,19 @@ let make_bank_entry = function (frm) { } }; - -let render_employee_attendance = function(frm, data) { +let render_employee_attendance = function (frm, data) { frm.fields_dict.attendance_detail_html.html( frappe.render_template('employees_to_mark_attendance', { data: data }) ); }; + +frappe.ui.form.on('Payroll Employee Detail', { + employee: function(frm) { + frm.events.clear_employee_table(frm); + if (!frm.doc.payroll_frequency) { + frappe.throw(__("Please set a Payroll Frequency")); + } + } +}); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 8c2d9740ece..a25a6e7a32c 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.model.document import Document from dateutil.relativedelta import relativedelta -from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff +from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, date_diff, comma_and from frappe import _ from erpnext.accounts.utils import get_fiscal_year from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee @@ -19,16 +19,26 @@ class PayrollEntry(Document): # check if salary slips were manually submitted entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) if cint(entries) == len(self.employees): - self.set_onload("submitted_ss", True) + self.set_onload("submitted_ss", True) def on_submit(self): self.create_salary_slips() def before_submit(self): + self.validate_employee_details() if self.validate_attendance: if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) + def validate_employee_details(self): + emp_with_sal_slip = [] + for employee_details in self.employees: + if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + emp_with_sal_slip.append(employee_details.employee) + + if len(emp_with_sal_slip): + frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip))) + def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` where payroll_entry=%s """, (self.name))) @@ -71,8 +81,17 @@ class PayrollEntry(Document): and t2.docstatus = 1 %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True) + + emp_list = self.remove_payrolled_employees(emp_list) return emp_list + def remove_payrolled_employees(self, emp_list): + for employee_details in emp_list: + if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + emp_list.remove(employee_details) + + return emp_list + def fill_employee_details(self): self.set('employees', []) employees = self.get_emp_list() @@ -542,7 +561,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): title = _("Creating Salary Slips...")) else: salary_slip_name = frappe.db.sql( - '''SELECT + '''SELECT name FROM `tabSalary Slip` WHERE company=%s From ed208194323a85b49bead2f593076c44dc0117f4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Dec 2020 16:24:10 +0530 Subject: [PATCH 098/203] fix: travis --- .../stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 +- erpnext/stock/doctype/serial_no/serial_no.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 9b8eeed1a12..5921651feca 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -527,7 +527,7 @@ class TestPurchaseReceipt(unittest.TestCase): se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, serial_no=serial_no, basic_rate=100, do_not_submit=True) - self.assertRaises(SerialNoDuplicateError, se.submit) + se.submit() def test_auto_asset_creation(self): asset_item = "Test Asset Item" diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 25ce2d59695..86f3c1f5616 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -365,8 +365,8 @@ def has_serial_no_exists(sn, sle): status = True # If status is receipt then system will allow to in-ward the delivered serial no - if (status and sle.voucher_type == 'Stock Entry' and - frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') == 'Material Receipt'): + if (status and sle.voucher_type == "Stock Entry" and frappe.db.get_value("Stock Entry", + sle.voucher_no, "purpose") in ("Material Receipt", "Material Transfer")): status = False return status From 304100db3b01992a8c87c6e32838456dae383f54 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 30 Nov 2020 13:05:28 +0530 Subject: [PATCH 099/203] fix: don't cancel job card if manufacturing entry has made --- .../doctype/job_card/job_card.py | 55 ++++++++++++------- .../doctype/work_order/test_work_order.py | 50 +++++++++++++---- 2 files changed, 75 insertions(+), 30 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d15d81ed93d..ec28eb7795c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -17,6 +17,7 @@ class OverlapError(frappe.ValidationError): pass class OperationMismatchError(frappe.ValidationError): pass class OperationSequenceError(frappe.ValidationError): pass +class JobCardCancelError(frappe.ValidationError): pass class JobCard(Document): def validate(self): @@ -217,33 +218,49 @@ class JobCard(Document): field = "operation_id" data = self.get_current_operation_data() if data and len(data) > 0: - for_quantity = data[0].completed_qty - time_in_mins = data[0].time_in_mins + for_quantity = flt(data[0].completed_qty) + time_in_mins = flt(data[0].time_in_mins) - if self.get(field): - time_data = frappe.db.sql(""" + wo = frappe.get_doc('Work Order', self.work_order) + if self.operation_id: + self.validate_produced_quantity(for_quantity, wo) + self.update_work_order_data(for_quantity, time_in_mins, wo) + + def validate_produced_quantity(self, for_quantity, wo): + if self.docstatus < 2: return + + if wo.produced_qty > for_quantity: + first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.") + .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))) + + second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.") + .format(frappe.bold(get_link_to_form("Work Order", self.work_order)))) + + frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg), + JobCardCancelError, title = _("Error")) + + def update_work_order_data(self, for_quantity, time_in_mins, wo): + time_data = frappe.db.sql(""" SELECT min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.{0} = %s and jc.docstatus = 1 - """.format(field), (self.work_order, self.get(field)), as_dict=1) + and jc.operation_id = %s and jc.docstatus = 1 + """, (self.work_order, self.operation_id), as_dict=1) - wo = frappe.get_doc('Work Order', self.work_order) + for data in wo.operations: + if data.get("name") == self.operation_id: + data.completed_qty = for_quantity + data.actual_operation_time = time_in_mins + data.actual_start_time = time_data[0].start_time if time_data else None + data.actual_end_time = time_data[0].end_time if time_data else None - for data in wo.operations: - if data.get("name") == self.get(field): - data.completed_qty = for_quantity - data.actual_operation_time = time_in_mins - data.actual_start_time = time_data[0].start_time if time_data else None - data.actual_end_time = time_data[0].end_time if time_data else None - - wo.flags.ignore_validate_update_after_submit = True - wo.update_operation_status() - wo.calculate_operating_cost() - wo.set_actual_dates() - wo.save() + wo.flags.ignore_validate_update_after_submit = True + wo.update_operation_status() + wo.calculate_operating_cost() + wo.set_actual_dates() + wo.save() def get_current_operation_data(self): return frappe.get_all('Job Card', diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ce9699e1b3c..a77bd159afe 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today +from frappe.utils import flt, now, add_months, cint, today, add_to_date from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry @@ -14,6 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.stock.doctype.item.test_item import make_item from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError class TestWorkOrder(unittest.TestCase): def setUp(self): @@ -369,21 +370,49 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(ste.total_additional_costs, 1000) def test_job_card(self): + stock_entries = [] data = frappe.get_cached_value('BOM', {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) - if data: - frappe.db.set_value("Manufacturing Settings", - None, "disable_capacity_planning", 0) + bom, bom_item = data - bom, bom_item = data + bom_doc = frappe.get_doc('BOM', bom) + work_order = make_wo_order_test_record(item=bom_item, qty=1, + bom_no=bom, source_warehouse="_Test Warehouse - _TC") - bom_doc = frappe.get_doc('BOM', bom) - work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) - self.assertTrue(work_order.planned_end_date) + for row in work_order.required_items: + stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code, + target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100) + stock_entries.append(stock_entry_doc) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) - self.assertEqual(len(job_cards), len(bom_doc.operations)) + ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) + ste.submit() + stock_entries.append(ste) + + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + self.assertEqual(len(job_cards), len(bom_doc.operations)) + + for i, job_card in enumerate(job_cards): + doc = frappe.get_doc("Job Card", job_card) + doc.append("time_logs", { + "from_time": now(), + "hours": i, + "to_time": add_to_date(now(), i), + "completed_qty": doc.for_quantity + }) + doc.submit() + + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + ste1.submit() + stock_entries.append(ste1) + + for job_card in job_cards: + doc = frappe.get_doc("Job Card", job_card) + self.assertRaises(JobCardCancelError, doc.cancel) + + stock_entries.reverse() + for stock_entry in stock_entries: + stock_entry.cancel() def test_capcity_planning(self): frappe.db.set_value("Manufacturing Settings", None, { @@ -509,7 +538,6 @@ class TestWorkOrder(unittest.TestCase): ste1.submit() ste_cancel_list.append(ste1) - print(wo_order.name) ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2)) self.assertEquals(ste3.fg_completed_qty, 2) From aea62da544b21025023c22317be39ec38e77b772 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Dec 2020 22:44:31 +0530 Subject: [PATCH 100/203] fix: multiple pricing rule with margin type not working --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 6 +++++- erpnext/accounts/doctype/pricing_rule/utils.py | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 55a5b0e5139..05652642eb0 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -345,9 +345,13 @@ def apply_price_discount_rule(pricing_rule, item_details, args): if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency) or (pricing_rule.margin_type == 'Percentage')): item_details.margin_type = pricing_rule.margin_type - item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount item_details.has_margin = True + if pricing_rule.apply_multiple_pricing_rules and item_details.margin_rate_or_amount is not None: + item_details.margin_rate_or_amount += pricing_rule.margin_rate_or_amount + else: + item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount + if pricing_rule.rate_or_discount == 'Rate': pricing_rule_rate = 0.0 if pricing_rule.currency == args.currency: diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 2c7cd14451d..fb1fbe484ed 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -164,7 +164,15 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): frappe.throw(_("Invalid {0}").format(args.get(field))) parent_groups = frappe.db.sql_list("""select name from `tab%s` - where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + + if parenttype in ["Customer Group", "Item Group", "Territory"]: + parent_field = "parent_{0}".format(frappe.scrub(parenttype)) + root_name = frappe.db.get_list(parenttype, + {"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1) + + if root_name and root_name[0][0]: + parent_groups.append(root_name[0][0]) if parent_groups: if allow_blank: parent_groups.append('') From 6d74f5b59bd08ccaea79e714b3db657ef78352ed Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 25 Dec 2020 10:26:43 +0530 Subject: [PATCH 101/203] feat: GST E Invoicing (#23455) * feat: init e-invoice settings * feat: read public key file * feat: rsa encryption with public key * feat: save token and sek from auth request * chore: handle error response * feat: AES decryption of SEK with appkey * feat: decrypt json data with SEK * feat: make e invoice from erpnext sales invoice * feat: generate IRN * feat: decode signed json and QR code * chore: validations * feat: cancel IRN * feat: complete e-invoice schema * chore: move e-invoice settings to regional * chore: split einvoice settings and operations * chore: rename schema to template & js cleanup * feat: make IRN field on regional setup * feat: Generate & Cancel IRN from Sales Invoice * chore: minor fixes * fix: item discount * chore: show irn cancelled check after cancellation * fix: hide cancel irn dialog on error * fix: public key is required on validate * fix: cannot find attached key file * fix: validation if e invoicing is disabled * fix: do not show generate irn for invalid supply type * fix: update irn_cancelled after cancelling irn * chore: show irn field for proper gst_category * feat: e-way bill details in e-invoice * fix: save e-way bill no on irn generation * chore: no copy on e invoice custom fields * feat: cancel e-way bill before cancelling IRN * feat: manual download / upload json * chore: group e-invoicing actions * fix: fn name * chore: save signed invoice and qrcode after uplaoding irn * fix: fetch token if not valid * chore: move einvoicing stuff to seperate folder * feat: QRCode Image and E-Invoice Print Format * fix: bug * fix: invalid syntax * chore: code cleanup * chore: clean up e invoice actions * fix: download & upload e-invoice * fix: print format * fix: validations * fix: add permissions on regional setup * feat: add patch * fix: validate document name * fix: return date * fix: credit note einvoice * fix: validations * fix: error logging * fix: e_invoice module not found * fix: add missing package * fix: rename e_invoice_utils.py * fix: einvoice field validation * fix: patch * fix: invoice totals calculation * fix: other charges calculation * chore: improve document name validation message * fix: qr code image string * feat: initialize GSP connector * chore: remove unwanted fields * fix: qr code generation * feat: fetch and cache GSTIN details * feat: generate & cancel IRN * feat: cancel eway bill * chore: remove unwanted fuctions * chore: clean up einvoice actions * fix: attach qrcode on irn generation * fix: generate & cancel IRN * fix: show/hide eway bill fields * fix: valiations * feat: generate eway bill from IRN * chore: remove unwanted imports * chore: error logging * feat: header & footer in GST E Invoice * chore: remove test pincode * fix: invalid syntax * feat: cess non advolem on einvoice item * chore: remove fetch token from e invocie settings * fix: imports * fix: error handling * feat: update timeline on einvoice actions * fix: qrcode image size * fix: exclude intra company transactions * fix: eway bill test * fix: ewaybill mandatory conditions * chore: add tests * fix: returning condition * feat: log e-invocing requests * chore: add ack date and ack no field for print formats * fix: sider issues * feat: show e-invoice preview before IRN generation * fix: use as_list for error message * fix: minor ux issues * fix: dialog is undefined * fix: error handling * feat: add docs link to e invoice settings * feat: multiple gstins for e invoicing * fix: uncomment test condition * fix: remove test pincode * fix: cannot cancel irn without submitting sales invoice * chore: code cleanup * fix: sider issues * fix: e invoice request log permissions Co-authored-by: Nabin Hait --- .../doctype/sales_invoice/regional/india.js | 2 + .../doctype/sales_invoice/sales_invoice.py | 2 +- .../sales_invoice/test_sales_invoice.py | 269 +++-- .../print_format/gst_e_invoice/__init__.py | 0 .../gst_e_invoice/gst_e_invoice.html | 162 +++ .../gst_e_invoice/gst_e_invoice.json | 24 + erpnext/controllers/accounts_controller.py | 10 + erpnext/hooks.py | 3 +- erpnext/patches.txt | 1 + .../patches/v12_0/setup_einvoice_fields.py | 55 + .../doctype/e_invoice_request_log/__init__.py | 0 .../e_invoice_request_log.js | 8 + .../e_invoice_request_log.json | 103 ++ .../e_invoice_request_log.py | 10 + .../test_e_invoice_request_log.py | 10 + .../doctype/e_invoice_settings/__init__.py | 0 .../e_invoice_settings/e_invoice_settings.js | 11 + .../e_invoice_settings.json | 58 ++ .../e_invoice_settings/e_invoice_settings.py | 14 + .../test_e_invoice_settings.py | 10 + .../doctype/e_invoice_user/__init__.py | 0 .../e_invoice_user/e_invoice_user.json | 48 + .../doctype/e_invoice_user/e_invoice_user.py | 10 + erpnext/regional/india/e_invoice/__init__.py | 0 .../india/e_invoice/einv_item_template.json | 31 + .../india/e_invoice/einv_template.json | 110 ++ .../india/e_invoice/einv_validation.json | 956 ++++++++++++++++++ erpnext/regional/india/e_invoice/einvoice.js | 305 ++++++ erpnext/regional/india/e_invoice/utils.py | 772 ++++++++++++++ erpnext/regional/india/setup.py | 31 +- requirements.txt | 1 + 31 files changed, 2922 insertions(+), 94 deletions(-) create mode 100644 erpnext/accounts/print_format/gst_e_invoice/__init__.py create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json create mode 100644 erpnext/patches/v12_0/setup_einvoice_fields.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py create mode 100644 erpnext/regional/doctype/e_invoice_user/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.json create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.py create mode 100644 erpnext/regional/india/e_invoice/__init__.py create mode 100644 erpnext/regional/india/e_invoice/einv_item_template.json create mode 100644 erpnext/regional/india/e_invoice/einv_template.json create mode 100644 erpnext/regional/india/e_invoice/einv_validation.json create mode 100644 erpnext/regional/india/e_invoice/einvoice.js create mode 100644 erpnext/regional/india/e_invoice/utils.py diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index 6336db16ebc..f54bce8aac7 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,6 +1,8 @@ {% include "erpnext/regional/india/taxes.js" %} +{% include "erpnext/regional/india/e_invoice/einvoice.js" %} erpnext.setup_auto_gst_taxation('Sales Invoice'); +erpnext.setup_einvoice_actions('Sales Invoice') frappe.ui.form.on("Sales Invoice", { setup: function(frm) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 50734c865cd..40009ac69d0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -232,9 +232,9 @@ class SalesInvoice(SellingController): frappe.throw(_("At least one mode of payment is required for POS invoice.")) def before_cancel(self): + super(SalesInvoice, self).before_cancel() self.update_time_sheet(None) - def on_cancel(self): super(SalesInvoice, self).on_cancel() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ceb79079893..3c681eeecf2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1825,93 +1825,7 @@ class TestSalesInvoice(unittest.TestCase): # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) def test_eway_bill_json(self): - if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Address for Eway bill", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "27AAECE4835E1ZR", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "401108" - }).insert() - - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company" - }) - - address.save() - - if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Customer-Address for Eway bill", - "address_type": "Shipping", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "410038" - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Customer" - }) - - address.save() - - gst_settings = frappe.get_doc("GST Settings") - - gst_account = frappe.get_all( - "GST Account", - fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"}) - - if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company", - "cgst_account": "CGST - _TC", - "sgst_account": "SGST - _TC", - "igst_account": "IGST - _TC", - }) - - gst_settings.save() - - si = create_sales_invoice(do_not_save =1, rate = '60000') - - si.distance = 2000 - si.company_address = "_Test Address for Eway bill-Billing" - si.customer_address = "_Test Customer-Address for Eway bill-Shipping" - si.vehicle_no = "KA12KA1234" - si.gst_category = "Registered Regular" - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "CGST - _TC", - "cost_center": "Main - _TC", - "description": "CGST @ 9.0", - "rate": 9 - }) - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "SGST - _TC", - "cost_center": "Main - _TC", - "description": "SGST @ 9.0", - "rate": 9 - }) + si = make_sales_invoice_for_ewaybill() si.submit() @@ -1927,6 +1841,187 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) + + def test_einvoice_submission_without_irn(self): + # init + frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1) + country = frappe.flags.country + frappe.flags.country = 'India' + + si = make_sales_invoice_for_ewaybill() + self.assertRaises(frappe.ValidationError, si.submit) + + si.irn = 'test_irn' + si.submit() + + # reset + frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0) + frappe.flags.country = country + + def test_einvoice_json(self): + from erpnext.regional.india.e_invoice.utils import make_einvoice + + customer_gstin = '27AACCM7806M1Z3' + customer_gstin_dtls = { + 'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City', + 'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg', + 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' + } + company_gstin = '27AAECE4835E1ZR' + company_gstin_dtls = { + 'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City', + 'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg', + 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' + } + # set cache gstin details to avoid fetching details which will require connection to GSP servers + frappe.local.gstin_cache = {} + frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls + frappe.local.gstin_cache[company_gstin] = company_gstin_dtls + + si = make_sales_invoice_for_ewaybill() + si.naming_series = 'INV-2020-.#####' + si.items = [] + si.append("items", { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2, + "rate": 100, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + si.append("items", { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 4, + "rate": 150, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + si.save() + + einvoice = make_einvoice(si) + + total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']]) + total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']]) + total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']]) + total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']]) + total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']]) + + self.assertEqual(einvoice['Version'], '1.1') + self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value) + self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value) + self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value) + self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value) + self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value) + self.assertTrue(einvoice['EwbDtls']) + +def make_sales_invoice_for_ewaybill(): + if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Address for Eway bill", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AAECE4835E1ZR", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "401108" + }).insert() + + address.append("links", { + "link_doctype": "Company", + "link_name": "_Test Company" + }) + + address.save() + + if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Customer-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AACCM7806M1Z3", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "410038" + }).insert() + + address.append("links", { + "link_doctype": "Customer", + "link_name": "_Test Customer" + }) + + address.save() + + if not frappe.db.exists('Supplier', '_Test Transporter'): + frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": "_Test Transporter", + "country": "India", + "supplier_group": "_Test Supplier Group", + "supplier_type": "Company", + "is_transporter": 1 + }).insert() + + gst_settings = frappe.get_doc("GST Settings") + + gst_account = frappe.get_all( + "GST Account", + fields=["cgst_account", "sgst_account", "igst_account"], + filters = {"company": "_Test Company"}) + + if not gst_account: + gst_settings.append("gst_accounts", { + "company": "_Test Company", + "cgst_account": "CGST - _TC", + "sgst_account": "SGST - _TC", + "igst_account": "IGST - _TC", + }) + + gst_settings.save() + + si = create_sales_invoice(do_not_save =1, rate = '60000') + + si.distance = 2000 + si.company_address = "_Test Address for Eway bill-Billing" + si.customer_address = "_Test Customer-Address for Eway bill-Shipping" + si.vehicle_no = "KA12KA1234" + si.gst_category = "Registered Regular" + si.mode_of_transport = 'Road' + si.transporter = '_Test Transporter' + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "CGST - _TC", + "cost_center": "Main - _TC", + "description": "CGST @ 9.0", + "rate": 9 + }) + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "SGST - _TC", + "cost_center": "Main - _TC", + "description": "SGST @ 9.0", + "rate": 9 + }) + + return si def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql("""select account, debit, credit, posting_date diff --git a/erpnext/accounts/print_format/gst_e_invoice/__init__.py b/erpnext/accounts/print_format/gst_e_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html new file mode 100644 index 00000000000..9827e00b71b --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -0,0 +1,162 @@ +{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} +{%- set einvoice = json.loads(doc.signed_einvoice) -%} + +
+
+ {% if letter_head and not no_letterhead %} +
{{ letter_head }}
+ {% endif %} + +
+ {% if print_settings.repeat_header_footer %} + + {% endif %} +
+
1. Transaction Details
+
+
+
+
{{ einvoice.Irn }}
+
+
+
+
{{ einvoice.AckNo }}
+
+
+
+
{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}
+
+
+
+
{{ einvoice.TranDtls.SupTyp }}
+
+
+
+
{{ einvoice.DocDtls.Typ }}
+
+
+
+
{{ einvoice.DocDtls.No }}
+
+
+
+ +
+
+
+
2. Party Details
+ {%- set seller = einvoice.SellerDtls -%} +
+
Seller
+

{{ seller.Gstin }}

+

{{ seller.LglNm }}

+

{{ seller.Addr1 }}

+ {%- if seller.Addr2 -%}

{{ seller.Addr2 }}

{% endif %} +

{{ seller.Loc }}

+

{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}

+ + {%- if einvoice.ShipDtls -%} + {%- set shipping = einvoice.ShipDtls -%} +
Shipping
+

{{ shipping.Gstin }}

+

{{ shipping.LglNm }}

+

{{ shipping.Addr1 }}

+ {%- if shipping.Addr2 -%}

{{ shipping.Addr2 }}

{% endif %} +

{{ shipping.Loc }}

+

{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}

+ {% endif %} +
+ {%- set buyer = einvoice.BuyerDtls -%} +
+
Buyer
+

{{ buyer.Gstin }}

+

{{ buyer.LglNm }}

+

{{ buyer.Addr1 }}

+ {%- if buyer.Addr2 -%}

{{ buyer.Addr2 }}

{% endif %} +

{{ buyer.Loc }}

+

{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}

+
+
+
+
3. Item Details
+ + + + + + + + + + + + + + + + + + {% for item in einvoice.ItemList %} + + + + + + + + + + + + + + {% endfor %} + +
Sr. No.ItemHSN CodeQtyUOMRateDiscountTaxable AmountTax RateOther ChargesTotal
{{ item.SlNo }}{{ item.PrdDesc }}{{ item.HsnCd }}{{ item.Qty }}{{ item.Unit }}{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}{{ item.GstRt + item.CesRt }} %{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}
+
+
+
4. Value Details
+ + + + + + + + + + + + + + + + + {%- set value_details = einvoice.ValDtls -%} + + + + + + + + + + + + + +
Taxable AmountCGSTSGSTIGSTCESSState CESSDiscountOther ChargesRound OffTotal Value
{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}
+
+
\ No newline at end of file diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json new file mode 100644 index 00000000000..1001199a092 --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json @@ -0,0 +1,24 @@ +{ + "align_labels_right": 1, + "creation": "2020-10-10 18:01:21.032914", + "custom_format": 0, + "default_print_language": "en-US", + "disabled": 1, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "", + "idx": 0, + "line_breaks": 1, + "modified": "2020-10-23 19:54:40.634936", + "modified_by": "Administrator", + "module": "Accounts", + "name": "GST E-Invoice", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 1, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 32c5d3a3b14..0f1aa23064c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -110,8 +110,14 @@ class AccountsController(TransactionBase): self.set_inter_company_account() validate_regional(self) + + validate_einvoice_fields(self) + if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) + + def before_cancel(self): + validate_einvoice_fields(self) def validate_deferred_start_and_end_date(self): for d in self.items: @@ -1518,3 +1524,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil @erpnext.allow_regional def validate_regional(doc): pass + +@erpnext.allow_regional +def validate_einvoice_fields(doc): + pass diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 1e3bb6a5cfb..a2d9d861bb8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -397,7 +397,8 @@ regional_overrides = { 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', - 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries' + 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', + 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields' }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9e33014c38e..d69dabf15cd 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -732,6 +732,7 @@ erpnext.patches.v13_0.set_youtube_video_id erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail +erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py new file mode 100644 index 00000000000..d0782765dee --- /dev/null +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -0,0 +1,55 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from erpnext.regional.india.setup import add_permissions, add_print_formats + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + frappe.reload_doc("regional", "doctype", "e_invoice_settings") + custom_fields = { + 'Sales Invoice': [ + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + ] + } + create_custom_fields(custom_fields, update=True) + add_permissions() + add_print_formats() + + einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + t = { + 'mode_of_transport': [{'default': None}], + 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}], + 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], + 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], + 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], + 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], + 'ewaybill': [ + {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'}, + {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'} + ] + } + + for field, conditions in t.items(): + for c in conditions: + [(prop, value)] = c.items() + frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value) diff --git a/erpnext/regional/doctype/e_invoice_request_log/__init__.py b/erpnext/regional/doctype/e_invoice_request_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js new file mode 100644 index 00000000000..7b7ba964e5e --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Request Log', { + // refresh: function(frm) { + + // } +}); 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 new file mode 100644 index 00000000000..5c1c79dc047 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -0,0 +1,103 @@ +{ + "actions": [], + "autoname": "EINV-REQ-.#####", + "creation": "2020-12-08 12:54:08.175992", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "url", + "headers", + "response", + "column_break_7", + "timestamp", + "reference_invoice", + "data" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, + { + "fieldname": "reference_invoice", + "fieldtype": "Link", + "label": "Reference Invoice", + "options": "Sales Invoice" + }, + { + "fieldname": "headers", + "fieldtype": "Code", + "label": "Headers", + "options": "JSON" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "options": "JSON" + }, + { + "default": "Now", + "fieldname": "timestamp", + "fieldtype": "Datetime", + "label": "Timestamp" + }, + { + "fieldname": "response", + "fieldtype": "Code", + "label": "Response", + "options": "JSON" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-24 21:09:38.882866", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Request Log", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py new file mode 100644 index 00000000000..9150bdd9260 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.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 EInvoiceRequestLog(Document): + pass diff --git a/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py new file mode 100644 index 00000000000..c84e9a249bd --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.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 TestEInvoiceRequestLog(unittest.TestCase): + pass diff --git a/erpnext/regional/doctype/e_invoice_settings/__init__.py b/erpnext/regional/doctype/e_invoice_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js new file mode 100644 index 00000000000..cc2d9f06d2d --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -0,0 +1,11 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Settings', { + refresh(frm) { + const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing'; + frm.dashboard.set_headline( + __("Read {0} for more information on E Invoicing features.", [`documentation`]) + ); + } +}); diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json new file mode 100644 index 00000000000..4dcb22a54c7 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "creation": "2020-09-24 16:23:16.235722", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable", + "section_break_2", + "credentials", + "auth_token", + "token_expiry" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "depends_on": "enable", + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "auth_token", + "fieldtype": "Data", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "token_expiry", + "fieldtype": "Datetime", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "credentials", + "fieldtype": "Table", + "label": "Credentials", + "mandatory_depends_on": "enable", + "options": "E Invoice User" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-12-22 15:34:57.280044", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Settings", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py new file mode 100644 index 00000000000..c24ad886ea1 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -0,0 +1,14 @@ +# -*- 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 _ +from frappe.model.document import Document + +class EInvoiceSettings(Document): + def validate(self): + if self.enable and not self.credentials: + frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.')) + diff --git a/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py new file mode 100644 index 00000000000..a11ce63ee6c --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.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 TestEInvoiceSettings(unittest.TestCase): + pass diff --git a/erpnext/regional/doctype/e_invoice_user/__init__.py b/erpnext/regional/doctype/e_invoice_user/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json new file mode 100644 index 00000000000..dd9d99773a3 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "creation": "2020-12-22 15:02:46.229474", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gstin", + "username", + "password" + ], + "fields": [ + { + "fieldname": "gstin", + "fieldtype": "Data", + "in_list_view": 1, + "label": "GSTIN", + "reqd": 1 + }, + { + "fieldname": "username", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Username", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Password", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-22 15:10:53.466205", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice User", + "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/regional/doctype/e_invoice_user/e_invoice_user.py b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py new file mode 100644 index 00000000000..056c54f069d --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.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 EInvoiceUser(Document): + pass diff --git a/erpnext/regional/india/e_invoice/__init__.py b/erpnext/regional/india/e_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json new file mode 100644 index 00000000000..78e56518dff --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -0,0 +1,31 @@ +{{ + "SlNo": "{item.sr_no}", + "PrdDesc": "{item.description}", + "IsServc": "{item.is_service_item}", + "HsnCd": "{item.gst_hsn_code}", + "Barcde": "{item.barcode}", + "Unit": "{item.uom}", + "Qty": "{item.qty}", + "FreeQty": "{item.free_qty}", + "UnitPrice": "{item.unit_rate}", + "TotAmt": "{item.gross_amount}", + "Discount": "{item.discount_amount}", + "AssAmt": "{item.taxable_value}", + "PrdSlNo": "{item.serial_no}", + "GstRt": "{item.tax_rate}", + "IgstAmt": "{item.igst_amount}", + "CgstAmt": "{item.cgst_amount}", + "SgstAmt": "{item.sgst_amount}", + "CesRt": "{item.cess_rate}", + "CesAmt": "{item.cess_amount}", + "CesNonAdvlAmt": "{item.cess_nadv_amount}", + "StateCesRt": "{item.state_cess_rate}", + "StateCesAmt": "{item.state_cess_amount}", + "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}", + "OthChrg": "{item.other_charges}", + "TotItemVal": "{item.total_value}", + "BchDtls": {{ + "Nm": "{item.batch_no}", + "ExpDt": "{item.batch_expiry_date}" + }} +}} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json new file mode 100644 index 00000000000..e5751da5612 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -0,0 +1,110 @@ +{{ + "Version": "1.1", + "TranDtls": {{ + "TaxSch": "{transaction_details.tax_scheme}", + "SupTyp": "{transaction_details.supply_type}", + "RegRev": "{transaction_details.reverse_charge}", + "EcmGstin": "{transaction_details.ecom_gstin}", + "IgstOnIntra": "{transaction_details.igst_on_intra}" + }}, + "DocDtls": {{ + "Typ": "{doc_details.invoice_type}", + "No": "{doc_details.invoice_name}", + "Dt": "{doc_details.invoice_date}" + }}, + "SellerDtls": {{ + "Gstin": "{seller_details.gstin}", + "LglNm": "{seller_details.legal_name}", + "TrdNm": "{seller_details.trade_name}", + "Loc": "{seller_details.location}", + "Pin": "{seller_details.pincode}", + "Stcd": "{seller_details.state_code}", + "Addr1": "{seller_details.address_line1}", + "Addr2": "{seller_details.address_line2}", + "Ph": "{seller_details.phone}", + "Em": "{seller_details.email}" + }}, + "BuyerDtls": {{ + "Gstin": "{buyer_details.gstin}", + "LglNm": "{buyer_details.legal_name}", + "TrdNm": "{buyer_details.trade_name}", + "Addr1": "{buyer_details.address_line1}", + "Addr2": "{buyer_details.address_line2}", + "Loc": "{buyer_details.location}", + "Pin": "{buyer_details.pincode}", + "Stcd": "{buyer_details.state_code}", + "Ph": "{buyer_details.phone}", + "Em": "{buyer_details.email}", + "Pos": "{buyer_details.place_of_supply}" + }}, + "DispDtls": {{ + "Nm": "{dispatch_details.company_name}", + "Addr1": "{dispatch_details.address_line1}", + "Addr2": "{dispatch_details.address_line2}", + "Loc": "{dispatch_details.location}", + "Pin": "{dispatch_details.pincode}", + "Stcd": "{dispatch_details.state_code}" + }}, + "ShipDtls": {{ + "Gstin": "{shipping_details.gstin}", + "LglNm": "{shipping_details.legal_name}", + "TrdNm": "{shipping_details.trader_name}", + "Addr1": "{shipping_details.address_line1}", + "Addr2": "{shipping_details.address_line2}", + "Loc": "{shipping_details.location}", + "Pin": "{shipping_details.pincode}", + "Stcd": "{shipping_details.state_code}" + }}, + "ItemList": [ + {item_list} + ], + "ValDtls": {{ + "AssVal": "{invoice_value_details.base_net_total}", + "CgstVal": "{invoice_value_details.total_cgst_amt}", + "SgstVal": "{invoice_value_details.total_sgst_amt}", + "IgstVal": "{invoice_value_details.total_igst_amt}", + "CesVal": "{invoice_value_details.total_cess_amt}", + "Discount": "{invoice_value_details.invoice_discount_amt}", + "RndOffAmt": "{invoice_value_details.round_off}", + "OthChrg": "{invoice_value_details.total_other_charges}", + "TotInvVal": "{invoice_value_details.base_grand_total}", + "TotInvValFc": "{invoice_value_details.grand_total}" + }}, + "PayDtls": {{ + "Nm": "{payment_details.payee_name}", + "AccDet": "{payment_details.account_no}", + "Mode": "{payment_details.mode_of_payment}", + "FinInsBr": "{payment_details.ifsc_code}", + "PayTerm": "{payment_details.terms}", + "PaidAmt": "{payment_details.paid_amount}", + "PaymtDue": "{payment_details.outstanding_amount}" + }}, + "RefDtls": {{ + "DocPerdDtls": {{ + "InvStDt": "{period_details.start_date}", + "InvEndDt": "{period_details.end_date}" + }}, + "PrecDocDtls": [{{ + "InvNo": "{prev_doc_details.invoice_name}", + "InvDt": "{prev_doc_details.invoice_date}" + }}] + }}, + "ExpDtls": {{ + "ShipBNo": "{export_details.bill_no}", + "ShipBDt": "{export_details.bill_date}", + "Port": "{export_details.port}", + "ForCur": "{export_details.foreign_curr_code}", + "CntCode": "{export_details.country_code}", + "ExpDuty": "{export_details.export_duty}" + }}, + "EwbDtls": {{ + "TransId": "{eway_bill_details.gstin}", + "TransName": "{eway_bill_details.name}", + "TransMode": "{eway_bill_details.mode_of_transport}", + "Distance": "{eway_bill_details.distance}", + "TransDocNo": "{eway_bill_details.document_name}", + "TransDocDt": "{eway_bill_details.document_date}", + "VehNo": "{eway_bill_details.vehicle_no}", + "VehType": "{eway_bill_details.vehicle_type}" + }} +}} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json new file mode 100644 index 00000000000..86290cfe524 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -0,0 +1,956 @@ +{ + "Version": { + "type": "string", + "minLength": 1, + "maxLength": 6, + "description": "Version of the schema" + }, + "Irn": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "Invoice Reference Number" + }, + "TranDtls": { + "type": "object", + "properties": { + "TaxSch": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["GST"], + "description": "GST- Goods and Services Tax Scheme" + }, + "SupTyp": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"], + "description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export" + }, + "RegRev": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Y- whether the tax liability is payable under reverse charge" + }, + "EcmGstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "description": "E-Commerce GSTIN", + "validationMsg": "E-Commerce GSTIN is invalid" + }, + "IgstOnIntra": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Y- indicates the supply is intra state but chargeable to IGST" + } + }, + "required": ["TaxSch", "SupTyp"] + }, + "DocDtls": { + "type": "object", + "properties": { + "Typ": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "enum": ["INV", "CRN", "DBN"], + "description": "Document Type" + }, + "No": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", + "description": "Document Number", + "validationMsg": "Document Number should not be starting with 0, / and -" + }, + "Dt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Document Date" + } + }, + "required": ["Typ", "No", "Dt"] + }, + "SellerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "description": "Supplier GSTIN", + "validationMsg": "Company GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Tradename" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Supplier State Code" + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12, + "description": "Phone" + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100, + "description": "Email-Id" + } + }, + "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "BuyerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "description": "Buyer GSTIN", + "validationMsg": "Customer GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Trade Name" + }, + "Pos": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Place of Supply State code" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "Buyer State Code" + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12, + "description": "Phone" + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100, + "description": "Email-Id" + } + }, + "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"] + }, + "DispDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Dispatch Address Name" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "State Code" + } + }, + "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ShipDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "maxLength": 15, + "minLength": 3, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "description": "Shipping Address GSTIN", + "validationMsg": "Shipping Address GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Legal Name" + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Trade Name" + }, + "Addr1": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Address Line 2" + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Location" + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999, + "description": "Pincode" + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2, + "description": "State Code" + } + }, + "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ItemList": { + "type": "Array", + "properties": { + "SlNo": { + "type": "string", + "minLength": 1, + "maxLength": 6, + "description": "Serial No. of Item" + }, + "PrdDesc": { + "type": "string", + "minLength": 3, + "maxLength": 300, + "description": "Item Name" + }, + "IsServc": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"], + "description": "Is Service Item" + }, + "HsnCd": { + "type": "string", + "minLength": 4, + "maxLength": 8, + "description": "HSN Code" + }, + "Barcde": { + "type": "string", + "minLength": 3, + "maxLength": 30, + "description": "Barcode" + }, + "Qty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999, + "description": "Quantity" + }, + "FreeQty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999, + "description": "Free Quantity" + }, + "Unit": { + "type": "string", + "minLength": 3, + "maxLength": 8, + "description": "UOM" + }, + "UnitPrice": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.999, + "description": "Rate" + }, + "TotAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Gross Amount" + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Discount" + }, + "PreTaxVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Pre tax value" + }, + "AssAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Taxable Value" + }, + "GstRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "GST Rate" + }, + "IgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "IGST Amount" + }, + "CgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "CGST Amount" + }, + "SgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "SGST Amount" + }, + "CesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "Cess Rate" + }, + "CesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Cess Amount (Advalorem)" + }, + "CesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Cess Amount (Non-Advalorem)" + }, + "StateCesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999, + "description": "State CESS Rate" + }, + "StateCesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "State CESS Amount" + }, + "StateCesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "State CESS Amount (Non Advalorem)" + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Other Charges" + }, + "TotItemVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Total Item Value" + }, + "OrdLineRef": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Order line reference" + }, + "OrgCntry": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Origin Country" + }, + "PrdSlNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Serial number" + }, + "BchDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 20, + "description": "Batch number" + }, + "ExpDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Batch Expiry Date" + }, + "WrDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Warranty Date" + } + }, + "required": ["Nm"] + }, + "AttribDtls": { + "type": "Array", + "Attribute": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Attribute name of the item" + }, + "Val": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Attribute value of the item" + } + } + } + } + }, + "required": [ + "SlNo", + "IsServc", + "HsnCd", + "UnitPrice", + "TotAmt", + "AssAmt", + "GstRt", + "TotItemVal" + ] + }, + "ValDtls": { + "type": "object", + "properties": { + "AssVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total Assessable value of all items" + }, + "CgstVal": { + "type": "number", + "maximum": 99999999999999.99, + "minimum": 0, + "description": "Total CGST value of all items" + }, + "SgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total SGST value of all items" + }, + "IgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total IGST value of all items" + }, + "CesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total CESS value of all items" + }, + "StCesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Total State CESS value of all items" + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Invoice Discount" + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Other Charges" + }, + "RndOffAmt": { + "type": "number", + "minimum": -99.99, + "maximum": 99.99, + "description": "Rounded off Amount" + }, + "TotInvVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Final Invoice Value " + }, + "TotInvValFc": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Final Invoice value in Foreign Currency" + } + }, + "required": ["AssVal", "TotInvVal"] + }, + "PayDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Payee Name" + }, + "AccDet": { + "type": "string", + "minLength": 1, + "maxLength": 18, + "description": "Bank Account Number of Payee" + }, + "Mode": { + "type": "string", + "minLength": 1, + "maxLength": 18, + "description": "Mode of Payment" + }, + "FinInsBr": { + "type": "string", + "minLength": 1, + "maxLength": 11, + "description": "Branch or IFSC code" + }, + "PayTerm": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Terms of Payment" + }, + "PayInstr": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Payment Instruction" + }, + "CrTrn": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Credit Transfer" + }, + "DirDr": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Direct Debit" + }, + "CrDay": { + "type": "number", + "minimum": 0, + "maximum": 9999, + "description": "Credit Days" + }, + "PaidAmt": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Advance Amount" + }, + "PaymtDue": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99, + "description": "Outstanding Amount" + } + } + }, + "RefDtls": { + "type": "object", + "properties": { + "InvRm": { + "type": "string", + "maxLength": 100, + "minLength": 3, + "pattern": "^[0-9A-Za-z/-]{3,100}$", + "description": "Remarks/Note" + }, + "DocPerdDtls": { + "type": "object", + "properties": { + "InvStDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period Start Date" + }, + "InvEndDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period End Date" + } + }, + "required": ["InvStDt ", "InvEndDt "] + }, + "PrecDocDtls": { + "type": "object", + "properties": { + "InvNo": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$", + "description": "Reference of Original Invoice" + }, + "InvDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of Orginal Invoice" + }, + "OthRefNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Other Reference" + } + } + }, + "required": ["InvNo", "InvDt"], + "ContrDtls": { + "type": "object", + "properties": { + "RecAdvRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Receipt Advice No." + }, + "RecAdvDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of receipt advice" + }, + "TendRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Lot/Batch Reference No." + }, + "ContrRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Contract Reference Number" + }, + "ExtRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Any other reference" + }, + "ProjRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Project Reference Number" + }, + "PORefr": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([0-9A-Za-z/-]){1,16}$", + "description": "PO Reference Number" + }, + "PORefDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "PO Reference date" + } + } + } + } + }, + "AddlDocDtls": { + "type": "Array", + "properties": { + "Url": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Supporting document URL" + }, + "Docs": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Supporting document in Base64 Format" + }, + "Info": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Any additional information" + } + } + }, + + "ExpDtls": { + "type": "object", + "properties": { + "ShipBNo": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "Shipping Bill No." + }, + "ShipBDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Shipping Bill Date" + }, + "Port": { + "type": "string", + "minLength": 2, + "maxLength": 10, + "pattern": "^[0-9A-Za-z]{2,10}$", + "description": "Port Code. Refer the master" + }, + "RefClm": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "description": "Claiming Refund. Y/N" + }, + "ForCur": { + "type": "string", + "minLength": 3, + "maxLength": 16, + "description": "Additional Currency Code. Refer the master" + }, + "CntCode": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Country Code. Refer the master" + }, + "ExpDuty": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99, + "description": "Export Duty" + } + } + }, + "EwbDtls": { + "type": "object", + "properties": { + "TransId": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "description": "Transporter GSTIN" + }, + "TransName": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Transporter Name" + }, + "TransMode": { + "type": "string", + "maxLength": 1, + "minLength": 1, + "enum": ["1", "2", "3", "4"], + "description": "Mode of Transport" + }, + "Distance": { + "type": "number", + "minimum": 1, + "maximum": 9999, + "description": "Distance" + }, + "TransDocNo": { + "type": "string", + "minLength": 1, + "maxLength": 15, + "pattern": "^([0-9A-Z/-]){1,15}$", + "description": "Tranport Document Number" + }, + "TransDocDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Transport Document Date" + }, + "VehNo": { + "type": "string", + "minLength": 4, + "maxLength": 20, + "description": "Vehicle Number" + }, + "VehType": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["O", "R"], + "description": "Vehicle Type" + } + }, + "required": ["Distance"] + }, + "required": [ + "Version", + "TranDtls", + "DocDtls", + "SellerDtls", + "BuyerDtls", + "ItemList", + "ValDtls" + ] +} diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js new file mode 100644 index 00000000000..9c86cc89f55 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -0,0 +1,305 @@ +erpnext.setup_einvoice_actions = (doctype) => { + frappe.ui.form.on(doctype, { + refresh(frm) { + const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); + const supply_type = frm.doc.gst_category; + const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); + const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; + + if (!einvoicing_enabled || !valid_supply_type || company_transaction) return; + + const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + + const add_custom_button = (label, action) => { + if (!frm.custom_buttons[label]) { + frm.add_custom_button(label, action, __('E Invoicing')); + } + }; + + if (!irn && !__unsaved) { + const action = () => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.get_einvoice', + args: { doctype, docname: name }, + freeze: true, + callback: (res) => { + const einvoice = res.message; + show_einvoice_preview(frm, einvoice); + } + }); + }; + + add_custom_button(__("Generate IRN"), action); + } + + if (irn && !irn_cancelled && !ewaybill) { + const fields = [ + { + "label": "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const action = () => { + const d = new frappe.ui.Dialog({ + title: __("Cancel IRN"), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', + args: { + doctype, + docname: name, + irn: irn, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + add_custom_button(__("Cancel IRN"), action); + } + + if (irn && !irn_cancelled && !ewaybill) { + const action = () => { + const d = new frappe.ui.Dialog({ + title: __('Generate E-Way Bill'), + wide: 1, + fields: get_ewaybill_fields(frm), + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill', + args: { + doctype, + docname: name, + irn, + ...data + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + + add_custom_button(__("Generate E-Way Bill"), action); + } + + if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { + const fields = [ + { + "label": "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const action = () => { + const d = new frappe.ui.Dialog({ + title: __('Cancel E-Way Bill'), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', + args: { + doctype, + docname: name, + eway_bill: ewaybill, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + add_custom_button(__("Cancel E-Way Bill"), action); + } + } + }); +}; + +const get_ewaybill_fields = (frm) => { + return [ + { + 'fieldname': 'transporter', + 'label': 'Transporter', + 'fieldtype': 'Link', + 'options': 'Supplier', + 'default': frm.doc.transporter + }, + { + 'fieldname': 'gst_transporter_id', + 'label': 'GST Transporter ID', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.gst_transporter_id', + 'default': frm.doc.gst_transporter_id + }, + { + 'fieldname': 'driver', + 'label': 'Driver', + 'fieldtype': 'Link', + 'options': 'Driver', + 'default': frm.doc.driver + }, + { + 'fieldname': 'lr_no', + 'label': 'Transport Receipt No', + 'fieldtype': 'Data', + 'default': frm.doc.lr_no + }, + { + 'fieldname': 'vehicle_no', + 'label': 'Vehicle No', + 'fieldtype': 'Data', + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.vehicle_no + }, + { + 'fieldname': 'distance', + 'label': 'Distance (in km)', + 'fieldtype': 'Float', + 'default': frm.doc.distance + }, + { + 'fieldname': 'transporter_col_break', + 'fieldtype': 'Column Break', + }, + { + 'fieldname': 'transporter_name', + 'label': 'Transporter Name', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.name', + 'read_only': 1, + 'default': frm.doc.transporter_name + }, + { + 'fieldname': 'mode_of_transport', + 'label': 'Mode of Transport', + 'fieldtype': 'Select', + 'options': `\nRoad\nAir\nRail\nShip`, + 'default': frm.doc.mode_of_transport + }, + { + 'fieldname': 'driver_name', + 'label': 'Driver Name', + 'fieldtype': 'Data', + 'fetch_from': 'driver.full_name', + 'read_only': 1, + 'default': frm.doc.driver_name + }, + { + 'fieldname': 'lr_date', + 'label': 'Transport Receipt Date', + 'fieldtype': 'Date', + 'default': frm.doc.lr_date + }, + { + 'fieldname': 'gst_vehicle_type', + 'label': 'GST Vehicle Type', + 'fieldtype': 'Select', + 'options': `Regular\nOver Dimensional Cargo (ODC)`, + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.gst_vehicle_type + } + ]; +}; + +const request_irn_generation = (frm) => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_irn', + args: { doctype: frm.doc.doctype, docname: frm.doc.name }, + freeze: true, + callback: () => frm.reload_doc() + }); +}; + +const get_preview_dialog = (frm, action) => { + const dialog = new frappe.ui.Dialog({ + title: __("Preview"), + wide: 1, + fields: [ + { + "label": "Preview", + "fieldname": "preview_html", + "fieldtype": "HTML" + } + ], + primary_action: () => action(frm) || dialog.hide(), + primary_action_label: __('Generate IRN') + }); + return dialog; +}; + +const show_einvoice_preview = (frm, einvoice) => { + const preview_dialog = get_preview_dialog(frm, request_irn_generation); + + // initialize e-invoice fields + einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate(); + frm.doc.signed_einvoice = JSON.stringify(einvoice); + + // initialize preview wrapper + const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper; + $preview_wrapper.html( + `
+ +
+
` + ); + + frappe.call({ + method: "frappe.www.printview.get_html_and_style", + args: { + doc: frm.doc, + print_format: "GST E-Invoice", + no_letterhead: 1 + }, + callback: function (r) { + if (!r.exc) { + $preview_wrapper.find(".print-format").html(r.message.html); + const style = ` + .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; } + .print-preview { min-height: 0px; } + .modal-dialog { width: 720px; }`; + + frappe.dom.set_style(style, "custom-print-style"); + preview_dialog.show(); + } + } + }); +}; \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py new file mode 100644 index 00000000000..cb92c42464e --- /dev/null +++ b/erpnext/regional/india/e_invoice/utils.py @@ -0,0 +1,772 @@ +# -*- 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 os +import re +import jwt +import sys +import json +import base64 +import frappe +import traceback +from frappe import _, bold +from pyqrcode import create as qrcreate +from frappe.integrations.utils import make_post_request, make_get_request +from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply +from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date + +def validate_einvoice_fields(doc): + einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) + invalid_doctype = doc.doctype not in ['Sales Invoice'] + invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] + company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') + + if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return + + if doc.docstatus == 0 and doc._action == 'save': + if doc.irn: + frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + if len(doc.name) > 16: + raise_document_name_too_long_error() + + elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: + frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) + + elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: + frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) + +def raise_document_name_too_long_error(): + title = _('Document ID Too Long') + msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice, ') + msg += _('document id {} exceed 16 letters. ').format(bold(_('should not'))) + msg += '

' + msg += _('You must {} your {} in order to have document id of {} length 16. ').format( + bold(_('modify')), bold(_('naming series')), bold(_('maximum')) + ) + msg += _('Please account for ammended documents too. ') + frappe.throw(msg, title=title) + +def read_json(name): + file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) + with open(file_path, 'r') as f: + return cstr(f.read()) + +def get_transaction_details(invoice): + supply_type = '' + if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' + elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' + elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' + elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + + if not supply_type: + rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') + frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), + title=_('Invalid Supply Type')) + + return frappe._dict(dict( + tax_scheme='GST', + supply_type=supply_type, + reverse_charge=invoice.reverse_charge + )) + +def get_doc_details(invoice): + invoice_type = 'CRN' if invoice.is_return else 'INV' + + invoice_name = invoice.name + invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') + + return frappe._dict(dict( + invoice_type=invoice_type, + invoice_name=invoice_name, + invoice_date=invoice_date + )) + +def get_party_details(address_name): + address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] + gstin = address.get('gstin') + + gstin_details = get_gstin_details(gstin) + legal_name = gstin_details.get('LegalName') + location = gstin_details.get('AddrLoc') or address.get('city') + state_code = gstin_details.get('StateCode') + pincode = gstin_details.get('AddrPncd') + address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) + address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) + email_id = address.get('email_id') + phone = address.get('phone') + # get last 10 digit + phone = phone.replace(" ", "")[-10:] if phone else '' + + if state_code == 97: + # according to einvoice standard + pincode = 999999 + + return frappe._dict(dict( + gstin=gstin, legal_name=legal_name, location=location, + pincode=pincode, state_code=state_code, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone + )) + +def get_gstin_details(gstin): + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} + + key = gstin + details = frappe.local.gstin_cache.get(key) + if details: + return details + + details = frappe.cache().hget('gstin_cache', key) + if details: + frappe.local.gstin_cache[key] = details + return details + + if not details: + return GSPConnector.get_gstin_details(gstin) + +def get_overseas_address_details(address_name): + address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( + 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] + ) + + return frappe._dict(dict( + gstin='URP', legal_name=address_title, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone, + pincode=999999, state_code=96, place_of_supply=96, location=city + )) + +def get_item_list(invoice): + item_list = [] + + for d in invoice.items: + einvoice_item_schema = read_json('einv_item_template') + item = frappe._dict({}) + item.update(d.as_dict()) + + item.sr_no = d.idx + item.qty = abs(item.qty) + item.description = d.item_name + item.taxable_value = abs(item.base_net_amount) + item.discount_amount = abs(item.discount_amount * item.qty) + item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) + item.gross_amount = abs(item.unit_rate * item.qty) + + item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None + item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None + item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' + + item = update_item_taxes(invoice, item) + + item.total_value = abs( + item.taxable_value + item.igst_amount + item.sgst_amount + + item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges + ) + einv_item = einvoice_item_schema.format(item=item) + item_list.append(einv_item) + + return ', '.join(item_list) + +def update_item_taxes(invoice, item): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + for attr in [ + 'tax_rate', 'cess_rate', 'cess_nadv_amount', + 'cgst_amount', 'sgst_amount', 'igst_amount', + 'cess_amount', 'cess_nadv_amount', 'other_charges' + ]: + item[attr] = 0 + + for t in invoice.taxes: + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + if t.charge_type == 'On Item Quantity': + item.cess_nadv_amount += abs(item_tax_detail[1]) + else: + item.cess_rate += item_tax_detail[0] + item.cess_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.igst_account: + item.tax_rate += item_tax_detail[0] + item.igst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.sgst_account: + item.tax_rate += item_tax_detail[0] + item.sgst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.cgst_account: + item.tax_rate += item_tax_detail[0] + item.cgst_amount += abs(item_tax_detail[1]) + + return item + +def get_invoice_value_details(invoice): + invoice_value_details = frappe._dict(dict()) + invoice_value_details.base_net_total = abs(invoice.base_net_total) + invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 + # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off + invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) + disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') + invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) + invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) + + invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) + + return invoice_value_details + +def update_invoice_taxes(invoice, invoice_value_details): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + invoice_value_details.total_cgst_amt = 0 + invoice_value_details.total_sgst_amt = 0 + invoice_value_details.total_igst_amt = 0 + invoice_value_details.total_cess_amt = 0 + invoice_value_details.total_other_charges = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.igst_account: + invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.sgst_account: + invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.cgst_account: + invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + else: + invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) + + return invoice_value_details + +def get_payment_details(invoice): + payee_name = invoice.company + mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) + paid_amount = invoice.base_paid_amount + outstanding_amount = invoice.outstanding_amount + + return frappe._dict(dict( + payee_name=payee_name, mode_of_payment=mode_of_payment, + paid_amount=paid_amount, outstanding_amount=outstanding_amount + )) + +def get_return_doc_reference(invoice): + invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') + return frappe._dict(dict( + invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') + )) + +def get_eway_bill_details(invoice): + if invoice.is_return: + frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed')) + + mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } + vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } + + return frappe._dict(dict( + gstin=invoice.gst_transporter_id, + name=invoice.transporter_name, + mode_of_transport=mode_of_transport[invoice.mode_of_transport], + distance=invoice.distance or 0, + document_name=invoice.lr_no, + document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), + vehicle_no=invoice.vehicle_no, + vehicle_type=vehicle_type[invoice.gst_vehicle_type] + )) + +def make_einvoice(invoice): + schema = read_json('einv_template') + + transaction_details = get_transaction_details(invoice) + item_list = get_item_list(invoice) + doc_details = get_doc_details(invoice) + invoice_value_details = get_invoice_value_details(invoice) + seller_details = get_party_details(invoice.company_address) + + if invoice.gst_category == 'Overseas': + buyer_details = get_overseas_address_details(invoice.customer_address) + else: + buyer_details = get_party_details(invoice.customer_address) + place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin + place_of_supply = place_of_supply[:2] + buyer_details.update(dict(place_of_supply=place_of_supply)) + + 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.is_pos and invoice.base_paid_amount: + payment_details = get_payment_details(invoice) + + if invoice.is_return and invoice.return_against: + prev_doc_details = get_return_doc_reference(invoice) + + if invoice.transporter: + eway_bill_details = get_eway_bill_details(invoice) + + # not yet implemented + dispatch_details = period_details = export_details = frappe._dict({}) + + einvoice = schema.format( + transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details, + seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, + item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details, + period_details=period_details, prev_doc_details=prev_doc_details, + export_details=export_details, eway_bill_details=eway_bill_details + ) + einvoice = json.loads(einvoice) + + validations = json.loads(read_json('einv_validation')) + errors = validate_einvoice(validations, einvoice) + if errors: + message = "\n".join([ + "E Invoice: ", json.dumps(einvoice, indent=4), + "-" * 50, + "Errors: ", json.dumps(errors, indent=4) + ]) + frappe.log_error(title="E Invoice Validation Failed", message=message) + frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) + + return einvoice + +def validate_einvoice(validations, einvoice, errors=[]): + for fieldname, field_validation in validations.items(): + value = einvoice.get(fieldname, None) + if not value or value == "None": + # remove keys with empty values + einvoice.pop(fieldname, None) + continue + + value_type = field_validation.get("type").lower() + if value_type in ['object', 'array']: + child_validations = field_validation.get('properties') + + if isinstance(value, list): + for d in value: + validate_einvoice(child_validations, d, errors) + if not d: + # remove empty dicts + einvoice.pop(fieldname, None) + else: + validate_einvoice(child_validations, value, errors) + if not value: + # remove empty dicts + einvoice.pop(fieldname, None) + continue + + # convert to int or str + if value_type == 'string': + einvoice[fieldname] = str(value) + elif value_type == 'number': + is_integer = '.' not in str(field_validation.get('maximum')) + einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) + value = einvoice[fieldname] + + max_length = field_validation.get('maxLength') + minimum = flt(field_validation.get('minimum')) + maximum = flt(field_validation.get('maximum')) + pattern_str = field_validation.get('pattern') + pattern = re.compile(pattern_str or '') + + label = field_validation.get('description') or fieldname + + if value_type == 'string' and len(value) > max_length: + errors.append(_('{} should not exceed {} characters').format(label, max_length)) + if value_type == 'number' and (value > maximum or value < minimum): + errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) + if pattern_str and not pattern.match(value): + errors.append(field_validation.get('validationMsg')) + + return errors + +class RequestFailed(Exception): pass + +class GSPConnector(): + def __init__(self, doctype=None, docname=None): + self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + 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 + 'test/enriched/ei/api/master/gstin' + self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' + self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn' + self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' + self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' + self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' + + def get_credentials(self): + if self.invoice: + gstin = self.get_seller_gstin() + credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin) + else: + credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + return credentials + + def get_seller_gstin(self): + gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') + if not gstin: + frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) + return gstin + + def get_auth_token(self): + if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0: + self.fetch_auth_token() + + return self.e_invoice_settings.auth_token + + def make_request(self, request_type, url, headers=None, data=None): + if request_type == 'post': + res = make_post_request(url, headers=headers, data=data) + else: + res = make_get_request(url, headers=headers, data=data) + + self.log_request(url, headers, data, res) + return res + + def log_request(self, url, headers, data, res): + headers.update({ 'password': self.credentials.password }) + request_log = frappe.get_doc({ + "doctype": "E Invoice Request Log", + "user": frappe.session.user, + "reference_invoice": self.invoice.name if self.invoice else None, + "url": url, + "headers": json.dumps(headers, indent=4) if headers else None, + "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, + "response": json.dumps(res, indent=4) if res else None + }) + request_log.insert(ignore_permissions=True) + frappe.db.commit() + + def fetch_auth_token(self): + headers = { + 'gspappid': frappe.conf.einvoice_client_id, + 'gspappsecret': frappe.conf.einvoice_client_secret + } + res = {} + try: + res = self.make_request('post', self.authenticate_url, headers) + self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) + self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) + self.e_invoice_settings.save() + + except Exception: + self.log_error(res) + self.raise_error(True) + + def get_headers(self): + return { + 'content-type': 'application/json', + 'user_name': self.credentials.username, + 'password': self.credentials.get_password(), + 'gstin': self.credentials.gstin, + 'authorization': self.get_auth_token(), + 'requestid': str(base64.b64encode(os.urandom(18))), + } + + def fetch_gstin_details(self, gstin): + headers = self.get_headers() + + try: + params = '?gstin={gstin}'.format(gstin=gstin) + res = self.make_request('get', self.gstin_details_url + params, headers) + if res.get('success'): + return res.get('result') + else: + self.log_error(res) + raise RequestFailed + + except RequestFailed: + self.raise_error() + + except Exception: + self.log_error() + self.raise_error(True) + + @staticmethod + def get_gstin_details(gstin): + '''fetch and cache GSTIN details''' + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} + + key = gstin + gsp_connector = GSPConnector() + details = gsp_connector.fetch_gstin_details(gstin) + + frappe.local.gstin_cache[key] = details + frappe.cache().hset('gstin_cache', key, details) + return details + + def generate_irn(self): + headers = self.get_headers() + einvoice = make_einvoice(self.invoice) + data = json.dumps(einvoice, indent=4) + + try: + res = self.make_request('post', self.generate_irn_url, headers, data) + if res.get('success'): + self.set_einvoice_data(res.get('result')) + + elif '2150' in res.get('message'): + # IRN already generated but not updated in invoice + # Extract the IRN from the response description and fetch irn details + irn = res.get('result')[0].get('Desc').get('Irn') + irn_details = self.get_irn_details(irn) + if irn_details: + self.set_einvoice_data(irn_details) + else: + raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \ + Contact ERPNext support to resolve the issue.') + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def get_irn_details(self, irn): + headers = self.get_headers() + + try: + params = '?irn={irn}'.format(irn=irn) + res = self.make_request('get', self.irn_details_url + params, headers) + if res.get('success'): + return res.get('result') + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error() + self.raise_error(True) + + def cancel_irn(self, irn, reason, remark): + headers = self.get_headers() + data = json.dumps({ + 'Irn': irn, + 'Cnlrsn': reason, + 'Cnlrem': remark + }, indent=4) + + try: + res = self.make_request('post', self.cancel_irn_url, headers, data) + if res.get('success'): + self.invoice.irn_cancelled = 1 + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('IRN Cancelled - {}').format(remark) + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def generate_eway_bill(self, **kwargs): + args = frappe._dict(kwargs) + + headers = self.get_headers() + eway_bill_details = get_eway_bill_details(args) + data = json.dumps({ + 'Irn': args.irn, + 'Distance': cint(eway_bill_details.distance), + 'TransMode': eway_bill_details.mode_of_transport, + 'TransId': eway_bill_details.gstin, + 'TransName': eway_bill_details.transporter, + 'TrnDocDt': eway_bill_details.document_date, + 'TrnDocNo': eway_bill_details.document_name, + 'VehNo': eway_bill_details.vehicle_no, + 'VehType': eway_bill_details.vehicle_type + }, indent=4) + + try: + res = self.make_request('post', self.generate_ewaybill_url, headers, data) + if res.get('success'): + self.invoice.ewaybill = res.get('result').get('EwbNo') + self.invoice.eway_bill_cancelled = 0 + self.invoice.update(args) + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Generated') + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def cancel_eway_bill(self, eway_bill, reason, remark): + headers = self.get_headers() + data = json.dumps({ + 'ewbNo': eway_bill, + 'cancelRsnCode': reason, + 'cancelRmrk': remark + }, indent=4) + + try: + res = self.make_request('post', self.cancel_ewaybill_url, headers, data) + if res.get('success'): + self.invoice.ewaybill = '' + self.invoice.eway_bill_cancelled = 1 + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Cancelled - {}').format(remark) + } + self.update_invoice() + + else: + raise RequestFailed + + except RequestFailed: + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) + + except Exception: + self.log_error(data) + self.raise_error(True) + + def sanitize_error_message(self, message): + ''' + On validation errors, response message looks something like this: + message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, + 3095 : Supplier GSTIN is inactive' + we search for string between ':' to extract the error messages + errors = [ + ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', + ': Test' + ] + then we trim down the message by looping over errors + ''' + errors = re.findall(': [^:]+', message) + for idx, e in enumerate(errors): + # remove colons + errors[idx] = errors[idx].replace(':', '').strip() + # if not last + if idx != len(errors) - 1: + # remove last 7 chars eg: ', 3095 ' + errors[idx] = errors[idx][:-6] + + return errors + + def log_error(self, data={}): + if not isinstance(data, dict): + data = json.loads(data) + + seperator = "--" * 50 + err_tb = traceback.format_exc() + err_msg = str(sys.exc_info()[1]) + data = json.dumps(data, indent=4) + + message = "\n".join([ + "Error", err_msg, seperator, + "Data:", data, seperator, + "Exception:", err_tb + ]) + frappe.log_error(title=_('E Invoice Request Failed'), message=message) + + def raise_error(self, raise_exception=False, errors=[]): + title = _('E Invoice Request Failed') + if errors: + frappe.throw(errors, title=title, as_list=1) + else: + link_to_error_list = 'Error Log' + frappe.msgprint( + _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), + title=title, + raise_exception=raise_exception, + indicator='red' + ) + + def set_einvoice_data(self, res): + enc_signed_invoice = res.get('SignedInvoice') + dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] + + self.invoice.irn = res.get('Irn') + self.invoice.ewaybill = res.get('EwbNo') + self.invoice.signed_einvoice = dec_signed_invoice + self.invoice.signed_qr_code = res.get('SignedQRCode') + + self.attach_qrcode_image() + + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('IRN Generated') + } + self.update_invoice() + + def attach_qrcode_image(self): + qrcode = self.invoice.signed_qr_code + doctype = self.invoice.doctype + docname = self.invoice.name + + _file = frappe.new_doc('File') + _file.update({ + 'file_name': f'QRCode_{docname}.png', + 'attached_to_doctype': doctype, + 'attached_to_name': docname, + 'content': 'qrcode', + 'is_private': 1 + }) + _file.insert() + frappe.db.commit() + url = qrcreate(qrcode, error='L') + abs_file_path = os.path.abspath(_file.get_full_path()) + url.png(abs_file_path, scale=2, quiet_zone=1) + + self.invoice.qrcode_image = _file.file_url + + def update_invoice(self): + self.invoice.flags.ignore_validate_update_after_submit = True + self.invoice.flags.ignore_validate = True + self.invoice.save() + +@frappe.whitelist() +def get_einvoice(doctype, docname): + invoice = frappe.get_doc(doctype, docname) + return make_einvoice(invoice) + +@frappe.whitelist() +def generate_irn(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_irn() + +@frappe.whitelist() +def cancel_irn(doctype, docname, irn, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_irn(irn, reason, remark) + +@frappe.whitelist() +def generate_eway_bill(doctype, docname, **kwargs): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_eway_bill(**kwargs) + +@frappe.whitelist() +def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_eway_bill(eway_bill, reason, remark) \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index cbcd6e3203a..5321a9a3b5a 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -87,7 +87,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) @@ -103,9 +103,10 @@ def add_permissions(): def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") + frappe.reload_doc("accounts", "print_format", "GST E-Invoice") frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('GST POS Invoice', 'GST Tax Invoice') """) + name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', @@ -351,7 +352,6 @@ def make_custom_fields(update=True): 'label': 'Mode of Transport', 'fieldtype': 'Select', 'options': '\nRoad\nAir\nRail\nShip', - 'default': 'Road', 'insert_after': 'transporter_name', 'print_hide': 1, 'translatable': 0 @@ -388,13 +388,34 @@ def make_custom_fields(update=True): 'fieldname': 'ewaybill', 'label': 'E-Way Bill No.', 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', + 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', 'allow_on_submit': 1, 'insert_after': 'tax_id', 'translatable': 0 } ] + si_einvoice_fields = [ + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + ] + custom_fields = { 'Address': [ dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', @@ -407,7 +428,7 @@ def make_custom_fields(update=True): 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, + 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Sales Order': sales_invoice_gst_fields, 'Tax Category': inter_state_gst_field, diff --git a/requirements.txt b/requirements.txt index 678cf74fef0..4511aa54d8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ taxjar==1.9.0 tweepy==3.8.0 Unidecode==1.1.1 WooCommerce==2.1.1 +pycryptodome==3.9.8 \ No newline at end of file From 5bcc6c6b15a6dc722361edbb4dae31f462e4196f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 25 Dec 2020 14:27:38 +0530 Subject: [PATCH 102/203] fix: added shipment link in delivery note dashboard --- .../stock/doctype/delivery_note/delivery_note.js | 1 + .../stock/doctype/delivery_note/delivery_note.py | 14 +++++++++++--- .../delivery_note/delivery_note_dashboard.py | 2 +- erpnext/stock/doctype/shipment/shipment.json | 5 +++-- erpnext/stock/doctype/shipment/shipment.py | 7 ++++++- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 03921c554e3..5f2658c1028 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -15,6 +15,7 @@ frappe.ui.form.on("Delivery Note", { 'Installation Note': 'Installation Note', 'Sales Invoice': 'Invoice', 'Stock Entry': 'Return', + 'Shipment': 'Shipment' }, frm.set_indicator_formatter('item_code', function(doc) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1a6a5550927..a30cadf0a04 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -598,6 +598,9 @@ def make_shipment(source_name, target_doc=None): pickup_contact_display += '
' + user.mobile_no target.pickup_contact = pickup_contact_display + # As we are using session user details in the pickup_contact then pickup_contact_person will be session user + target.pickup_contact_person = frappe.session.user + contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) delivery_contact_display = '{}'.format(source.contact_display) if contact: @@ -609,6 +612,13 @@ def make_shipment(source_name, target_doc=None): delivery_contact_display += '
' + contact.mobile_no target.delivery_contact = delivery_contact_display + if source.shipping_address_name: + target.delivery_address_name = source.shipping_address_name + target.delivery_address = source.shipping_address + elif source.customer_address: + target.delivery_address_name = source.customer_address + target.delivery_address = source.address_display + doclist = get_mapped_doc("Delivery Note", source_name, { "Delivery Note": { "doctype": "Shipment", @@ -617,9 +627,7 @@ def make_shipment(source_name, target_doc=None): "company": "pickup_company", "company_address": "pickup_address_name", "company_address_display": "pickup_address", - "address_display": "delivery_address", "customer": "delivery_customer", - "shipping_address_name": "delivery_address_name", "contact_person": "delivery_contact_name", "contact_email": "delivery_contact_email" }, @@ -637,7 +645,7 @@ def make_shipment(source_name, target_doc=None): } } }, target_doc, postprocess) - + return doclist @frappe.whitelist() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index beeb9ebb05d..47684d5c6ec 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -19,7 +19,7 @@ def get_data(): }, { 'label': _('Reference'), - 'items': ['Sales Order', 'Quality Inspection'] + 'items': ['Sales Order', 'Shipment', 'Quality Inspection'] }, { 'label': _('Returns'), diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json index 37a9cc6c02c..76c331c5c25 100644 --- a/erpnext/stock/doctype/shipment/shipment.json +++ b/erpnext/stock/doctype/shipment/shipment.json @@ -345,7 +345,8 @@ "label": "Status", "no_copy": 1, "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "tracking_url", @@ -430,7 +431,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-12-02 15:43:44.607039", + "modified": "2020-12-25 15:02:34.891976", "modified_by": "Administrator", "module": "Stock", "name": "Shipment", diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index de0c243b057..9167bfcd2f5 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, get_time, to_timedelta from frappe.model.document import Document from erpnext.accounts.party import get_party_shipping_address from frappe.contacts.doctype.contact.contact import get_default_contact @@ -13,6 +13,7 @@ from frappe.contacts.doctype.contact.contact import get_default_contact class Shipment(Document): def validate(self): self.validate_weight() + self.validate_pickup_time() self.set_value_of_goods() if self.docstatus == 0: self.status = 'Draft' @@ -32,6 +33,10 @@ class Shipment(Document): if flt(parcel.weight) <= 0: frappe.throw(_('Parcel weight cannot be 0')) + def validate_pickup_time(self): + if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from): + frappe.throw(_("Pickup To time should be greater than Pickup From time")) + def set_value_of_goods(self): value_of_goods = 0 for entry in self.get("shipment_delivery_note"): From de10d7dcf2079c37bc5a7f6afa7be74299825ba6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 25 Dec 2020 15:15:55 +0530 Subject: [PATCH 103/203] Update shipment.py --- erpnext/stock/doctype/shipment/shipment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 9167bfcd2f5..4697a7b3235 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, get_time, to_timedelta +from frappe.utils import flt, get_time from frappe.model.document import Document from erpnext.accounts.party import get_party_shipping_address from frappe.contacts.doctype.contact.contact import get_default_contact From dab1ab990d158cde0d12192018abadf069311272 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 25 Dec 2020 13:23:12 +0530 Subject: [PATCH 104/203] fix: partial serial no return issue --- .../controllers/sales_and_purchase_return.py | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 8f65c31f3d1..79792262c0a 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -204,21 +204,25 @@ def get_already_returned_items(doc): return items def get_returned_qty_map_for_row(row_name, doctype): + if doctype == "POS Invoice": return {} + child_doctype = doctype + " Item" - reference_field = frappe.scrub(child_doctype) if doctype == "Purchase Receipt" else "dn_detail" + reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) fields = [ "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype) ] - if doctype == "Purchase Receipt": + if doctype in ("Purchase Receipt", "Purchase Invoice"): fields += [ "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), - "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), - "sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype) + "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype) ] + if doctype == "Purchase Receipt": + fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] + data = frappe.db.get_list(doctype, fields = fields, filters = [ @@ -231,6 +235,7 @@ def get_returned_qty_map_for_row(row_name, doctype): def make_return_doc(doctype, source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos company = frappe.db.get_value("Delivery Note", source_name, "company") default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return") @@ -290,6 +295,12 @@ def make_return_doc(doctype, source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty = -1 * source_doc.qty + if source_doc.serial_no: + returned_serial_nos = get_returned_serial_nos(source_doc, source_parent) + serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos)) + if serial_nos: + target_doc.serial_no = '\n'.join(serial_nos) + if doctype == "Purchase Receipt": returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) @@ -305,10 +316,12 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": - target_doc.received_qty = -1 * source_doc.received_qty - target_doc.rejected_qty = -1 * source_doc.rejected_qty - target_doc.qty = -1* source_doc.qty - target_doc.stock_qty = -1 * source_doc.stock_qty + returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) + target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0)) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_receipt = source_doc.purchase_receipt target_doc.rejected_warehouse = source_doc.rejected_warehouse @@ -330,6 +343,10 @@ def make_return_doc(doctype, source_name, target_doc=None): if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice" or doctype == "POS Invoice": + returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) + target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0)) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0)) + target_doc.sales_order = source_doc.sales_order target_doc.delivery_note = source_doc.delivery_note target_doc.so_detail = source_doc.so_detail @@ -406,4 +423,22 @@ def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, ite if reference_voucher_detail_no: filters["voucher_detail_no"] = reference_voucher_detail_no - return filters \ No newline at end of file + return filters + +def get_returned_serial_nos(child_doc, parent_doc): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + return_ref_field = frappe.scrub(child_doc.doctype) + if child_doc.doctype == "Delivery Note Item": + return_ref_field = "dn_detail" + + serial_nos = [] + + fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] + + filters = [[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "is_return", "=", 1], + [child_doc.doctype, return_ref_field, "=", child_doc.name], [parent_doc.doctype, "docstatus", "=", 1]] + + for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): + serial_nos.extend(get_serial_nos(row.serial_no)) + + return serial_nos \ No newline at end of file From 46d5f4c7f14f9cdbf046f2afc06ce93ff751852d Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Fri, 25 Dec 2020 16:34:43 +0530 Subject: [PATCH 105/203] refactor(analytics report): linting --- .../purchase_analytics/purchase_analytics.js | 24 +++++++++++-------- .../report/sales_analytics/sales_analytics.js | 17 ++++++------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js index 7ee9f2c372a..ba8535a3ae4 100644 --- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js +++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js @@ -87,12 +87,18 @@ frappe.query_reports["Purchase Analytics"] = { row_name = data[2].content; length = data.length; - if (tree_type == "Supplier" || tree_type == "Item") { + if (tree_type == "Supplier") { row_values = data .slice(4, length - 1) .map(function (column) { return column.content; }); + } else if (tree_type == "Item") { + row_values = data + .slice(5, length - 1) + .map(function (column) { + return column.content; + }); } else { row_values = data .slice(3, length - 1) @@ -109,17 +115,15 @@ frappe.query_reports["Purchase Analytics"] = { let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - let found = false; - - for (let i = 0; i < new_datasets.length; i++) { - if (new_datasets[i].name == row_name) { - found = true; - new_datasets.splice(i, 1); - break; + let element_found = new_datasets.some((element, index, array)=>{ + if(element.name == row_name){ + array.splice(index, 1) + return true } - } + return false + }) - if (!found) { + if (!element_found) { new_datasets.push(entry); } let new_data = { diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index aad6bfd5ef1..9089b53fb04 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -76,7 +76,6 @@ frappe.query_reports["Sales Analytics"] = { events: { onCheckRow: function (data) { if (!data) return; - const data_doctype = $( data[2].html )[0].attributes.getNamedItem("data-doctype").value; @@ -114,17 +113,15 @@ frappe.query_reports["Sales Analytics"] = { let raw_data = frappe.query_report.chart.data; let new_datasets = raw_data.datasets; - let found = false; - - for (let i = 0; i < new_datasets.length; i++) { - if (new_datasets[i].name == row_name) { - found = true; - new_datasets.splice(i, 1); - break; + let element_found = new_datasets.some((element, index, array)=>{ + if(element.name == row_name){ + array.splice(index, 1) + return true } - } + return false + }) - if (!found) { + if (!element_found) { new_datasets.push(entry); } From b99c77b753827f46ccfcc85388a00679276ad7c5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 25 Dec 2020 18:12:35 +0530 Subject: [PATCH 106/203] Reposting fixes (#24202) * fix: finished item validation and rate * fix: Check if stock and account balance in sync after reposting * fix: validate stock accounts in journal entry * fix: validate expense against budget --- .../doctype/journal_entry/journal_entry.py | 21 +++- .../journal_entry/test_journal_entry.py | 18 ++- erpnext/accounts/general_ledger.py | 62 ---------- erpnext/accounts/utils.py | 115 ++++++++++++++---- erpnext/controllers/buying_controller.py | 2 +- erpnext/controllers/stock_controller.py | 10 +- .../purchase_receipt/purchase_receipt.py | 2 +- .../repost_item_valuation.py | 35 +++++- .../stock/doctype/stock_entry/stock_entry.py | 9 +- .../doctype/stock_entry/test_stock_entry.py | 78 ++++++------ .../stock_entry_detail.json | 2 +- .../stock_and_account_value_comparison.py | 3 +- 12 files changed, 205 insertions(+), 152 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index cd712738aa6..cb90f8036e2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -6,14 +6,18 @@ import frappe, erpnext, json from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form from frappe import msgprint, _, scrub from erpnext.controllers.accounts_controller import AccountsController -from erpnext.accounts.utils import get_balance_on, get_account_currency +from erpnext.accounts.utils import get_balance_on, get_stock_accounts, get_stock_and_account_balance, \ + get_account_currency, check_if_stock_and_account_balance_synced from erpnext.accounts.party import get_party_account from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount -from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting +from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \ + import get_party_account_based_on_invoice_discounting from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts from six import string_types, iteritems +class StockAccountInvalidTransaction(frappe.ValidationError): pass + class JournalEntry(AccountsController): def __init__(self, *args, **kwargs): super(JournalEntry, self).__init__(*args, **kwargs) @@ -46,6 +50,7 @@ class JournalEntry(AccountsController): self.validate_empty_accounts_table() self.set_account_and_party_balance() self.validate_inter_company_accounts() + self.validate_stock_accounts() if not self.title: self.title = self.get_title() @@ -57,6 +62,8 @@ class JournalEntry(AccountsController): self.update_expense_claim() self.update_inter_company_jv() self.update_invoice_discounting() + check_if_stock_and_account_balance_synced(self.posting_date, + self.company, self.doctype, self.name) def on_cancel(self): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries @@ -95,6 +102,16 @@ class JournalEntry(AccountsController): if account_currency == previous_account_currency: if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) + + def validate_stock_accounts(self): + stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) + for account in stock_accounts: + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, + self.posting_date, self.company) + + if account_bal == stock_bal: + frappe.throw(_("Account: {0} can only be updated via Stock Transactions") + .format(account), StockAccountInvalidTransaction) def update_inter_company_jv(self): if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 1d2eacdb80c..b56f8e5fe2f 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -6,7 +6,7 @@ import unittest, frappe from frappe.utils import flt, nowdate from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.exceptions import InvalidAccountCurrency -from erpnext.accounts.general_ledger import StockAccountInvalidTransaction +from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction class TestJournalEntry(unittest.TestCase): def test_journal_entry_with_against_jv(self): @@ -84,25 +84,31 @@ class TestJournalEntry(unittest.TestCase): company = "_Test Company with perpetual inventory" stock_account = get_inventory_account(company) + from erpnext.accounts.utils import get_stock_and_account_balance + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company) + diff = flt(account_bal) - flt(stock_bal) + + if not diff: + diff = 100 + jv = frappe.new_doc("Journal Entry") jv.company = company jv.posting_date = nowdate() jv.append("accounts", { "account": stock_account, "cost_center": "Main - TCP1", - "debit_in_account_currency": 100 + "debit_in_account_currency": 0 if diff > 0 else abs(diff), + "credit_in_account_currency": diff if diff > 0 else 0 }) jv.append("accounts", { "account": "Stock Adjustment - TCP1", - "credit_in_account_currency": 100, "cost_center": "Main - TCP1", + "debit_in_account_currency": diff if diff > 0 else 0, + "credit_in_account_currency": 0 if diff > 0 else abs(diff) }) jv.insert() - from erpnext.accounts.utils import get_stock_and_account_balance - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company) - if account_bal == stock_bal: self.assertRaises(StockAccountInvalidTransaction, jv.submit) frappe.db.rollback() diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index c7f0c8781c0..287c79f13fe 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -5,15 +5,11 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now from frappe import _ -from erpnext.accounts.utils import get_stock_and_account_balance from frappe.model.meta import get_field_precision from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions - class ClosedAccountingPeriod(frappe.ValidationError): pass -class StockAccountInvalidTransaction(frappe.ValidationError): pass -class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): if gl_map: @@ -131,10 +127,6 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): for entry in gl_map: make_entry(entry, adv_adj, update_outstanding, from_repost) - if not from_repost: - validate_account_for_perpetual_inventory(gl_map) - - def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle = frappe.new_doc("GL Entry") gle.update(args) @@ -144,63 +136,9 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) gle.submit() - # check against budget if not from_repost: validate_expense_against_budget(args) -def validate_account_for_perpetual_inventory(gl_map): - if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)): - account_list = [gl_entries.account for gl_entries in gl_map] - - aii_accounts = [d.name for d in frappe.get_all("Account", - filters={'account_type': 'Stock', 'is_group': 0, 'company': gl_map[0].company})] - - for account in account_list: - if account not in aii_accounts: - continue - - # Always use current date to get stock and account balance as there can future entries for - # other items - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, - gl_map[0].posting_date, gl_map[0].company) - - if gl_map[0].voucher_type=="Journal Entry": - # In case of Journal Entry, there are no corresponding SL entries, - # hence deducting currency amount - account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit) - if account_bal == stock_bal: - frappe.throw(_("Account: {0} can only be updated via Stock Transactions") - .format(account), StockAccountInvalidTransaction) - - elif abs(account_bal - stock_bal) > 0.1: - precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), - currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) - - diff = flt(stock_bal - account_bal, precision) - error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses on {3}.").format( - stock_bal, account_bal, frappe.bold(account), gl_map[0].posting_date) - error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) - stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") - - db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency') - db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') - - journal_entry_args = { - 'accounts':[ - {'account': account, db_or_cr_warehouse_account : abs(diff)}, - {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff)} - ] - } - - frappe.msgprint(msg="""{0}

{1}

""".format(error_reason, error_resolution), - raise_exception=StockValueAndAccountBalanceOutOfSync, - title=_('Values Out Of Sync'), - primary_action={ - 'label': _('Make Journal Entry'), - 'client_action': 'erpnext.route_to_adjustment_jv', - 'args': journal_entry_args - }) - def validate_cwip_accounts(gl_map): cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 540ac841823..67c7fd2d22a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -12,11 +12,12 @@ from frappe.utils import formatdate, get_number_format_info from six import iteritems # imported to enable erpnext.accounts.utils.get_account_currency from erpnext.accounts.doctype.account.account import get_account_currency +from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_stock_value_on from erpnext.stock import get_warehouse_account_map - +class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass class FiscalYearError(frappe.ValidationError): pass @frappe.whitelist() @@ -585,24 +586,6 @@ def fix_total_debit_credit(): (dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr), (d.diff, d.voucher_type, d.voucher_no)) -def get_stock_and_account_balance(account=None, posting_date=None, company=None): - if not posting_date: posting_date = nowdate() - - warehouse_account = get_warehouse_account_map(company) - - account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True) - - related_warehouses = [wh for wh, wh_details in warehouse_account.items() - if wh_details.account == account and not wh_details.is_group] - - total_stock_value = 0.0 - for warehouse in related_warehouses: - value = get_stock_value_on(warehouse, posting_date) - total_stock_value += value - - precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") - return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses - def get_currency_precision(): precision = cint(frappe.db.get_default("currency_precision")) if not precision: @@ -903,12 +886,6 @@ def get_coa(doctype, parent, is_root, chart=None): return accounts -def get_stock_accounts(company): - return frappe.get_all("Account", filters = { - "account_type": "Stock", - "company": company - }) - def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, warehouse_account=None, company=None): def _delete_gl_entries(voucher_type, voucher_no): @@ -983,4 +960,90 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle): if not account_existed: matched = False break - return matched \ No newline at end of file + return matched + +def check_if_stock_and_account_balance_synced(posting_date, company, voucher_type=None, voucher_no=None): + if not cint(erpnext.is_perpetual_inventory_enabled(company)): + return + + accounts = get_stock_accounts(company, voucher_type, voucher_no) + stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account") + + for account in accounts: + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account, + posting_date, company) + + if abs(account_bal - stock_bal) > 0.1: + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value('Company', company, "default_currency")) + + diff = flt(stock_bal - account_bal, precision) + + error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format( + stock_bal, account_bal, frappe.bold(account), posting_date) + error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\ + .format(frappe.bold(diff), frappe.bold(posting_date)) + + frappe.msgprint( + msg="""{0}

{1}

""".format(error_reason, error_resolution), + raise_exception=StockValueAndAccountBalanceOutOfSync, + title=_('Values Out Of Sync'), + primary_action={ + 'label': _('Make Journal Entry'), + 'client_action': 'erpnext.route_to_adjustment_jv', + 'args': get_journal_entry(account, stock_adjustment_account, diff) + }) + +def get_stock_accounts(company, voucher_type=None, voucher_no=None): + stock_accounts = [d.name for d in frappe.db.get_all("Account", { + "account_type": "Stock", + "company": company, + "is_group": 0 + })] + if voucher_type and voucher_no: + if voucher_type == "Journal Entry": + stock_accounts = [d.account for d in frappe.db.get_all("Journal Entry Account", { + "parent": voucher_no, + "account": ["in", stock_accounts] + }, "account")] + + else: + stock_accounts = [d.account for d in frappe.db.get_all("GL Entry", { + "voucher_type": voucher_type, + "voucher_no": voucher_no, + "account": ["in", stock_accounts] + }, "account")] + + return stock_accounts + +def get_stock_and_account_balance(account=None, posting_date=None, company=None): + if not posting_date: posting_date = nowdate() + + warehouse_account = get_warehouse_account_map(company) + + account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True) + + related_warehouses = [wh for wh, wh_details in warehouse_account.items() + if wh_details.account == account and not wh_details.is_group] + + total_stock_value = 0.0 + for warehouse in related_warehouses: + value = get_stock_value_on(warehouse, posting_date) + total_stock_value += value + + precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") + return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses + +def get_journal_entry(account, stock_adjustment_account, amount): + db_or_cr_warehouse_account =('credit_in_account_currency' if amount < 0 else 'debit_in_account_currency') + db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if amount < 0 else 'credit_in_account_currency') + + return { + 'accounts':[{ + 'account': account, + db_or_cr_warehouse_account: abs(amount) + }, { + 'account': stock_adjustment_account, + db_or_cr_stock_adjustment_account : abs(amount) + }] + } diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index dc61870df30..6edc020701d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -241,7 +241,7 @@ class BuyingController(StockController): if rate > 0: d.rate = rate - d.amount = flt(d.consumed_qty) * flt(d.rate) + d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) supplied_items_cost += flt(d.amount) return supplied_items_cost diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 51c063c2c0b..439997616c7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,7 +6,7 @@ import frappe, erpnext from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock.stock_ledger import get_valuation_rate @@ -402,6 +402,14 @@ class StockController(AccountsController): if check_if_future_sle_exists(args): create_repost_item_valuation_entry(args) + elif not is_reposting_pending(): + check_if_stock_and_account_balance_synced(self.posting_date, + self.company, self.doctype, self.name) + +def is_reposting_pending(): + return frappe.db.exists("Repost Item Valuation", + {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) + def check_if_future_sle_exists(args): sl_entries = frappe.db.get_all("Stock Ledger Entry", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 226064bae78..159a6085ff3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -408,7 +408,7 @@ class PurchaseReceipt(BuyingController): if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + "\n".join(warehouse_with_no_account)) - + return process_gl_map(gl_entries) def get_asset_gl_entry(self, gl_entries): diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index a942f2edda7..ba2c2c6f446 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.model.document import Document -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from erpnext.stock.stock_ledger import repost_future_sle -from erpnext.accounts.utils import update_gl_entries_after - - +from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced +from frappe.utils.user import get_users_with_role +from frappe import _ class RepostItemValuation(Document): def validate(self): self.set_status() @@ -51,12 +51,20 @@ def repost(doc): repost_sl_entries(doc) repost_gl_entries(doc) + check_if_stock_and_account_balance_synced(doc.posting_date, doc.company) + doc.set_status('Completed') except Exception: frappe.db.rollback() traceback = frappe.get_traceback() frappe.log_error(traceback) - frappe.db.set_value(doc.doctype, doc.name, 'error_log', traceback) + + message = frappe.message_log.pop() + if traceback: + message += "
" + "Traceback:
" + traceback + frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) + + notify_error_to_stock_managers(doc) doc.set_status('Failed') raise finally: @@ -86,4 +94,19 @@ def repost_gl_entries(doc): warehouses = [doc.warehouse] update_gl_entries_after(doc.posting_date, doc.posting_time, - warehouses, items, company=doc.company) \ No newline at end of file + warehouses, items, company=doc.company) + +def notify_error_to_stock_managers(doc, traceback): + recipients = get_users_with_role("Stock Manager") + if not recipients: + get_users_with_role("System Manager") + + subject = _("Error while reposting item valuation") + message = (_("Hi,") + "
" + + _("An error has been appeared while reposting item valuation via {0}") + .format(get_link_to_form(doc.doctype, doc.name)) + "
" + + _("Please check the error message and take necessary actions to fix the error and then restart the reposting again.") + ) + frappe.sendmail(recipients=recipients, subject=subject, message=message) + + diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 579b8c5fe1d..92d268f0993 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -442,6 +442,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate) + finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item]) # Set basic rate for incoming items for d in self.get('items'): @@ -451,7 +452,7 @@ class StockEntry(StockController): d.basic_rate = 0.0 elif d.is_finished_item: if self.purpose == "Manufacture": - d.basic_rate = self.get_basic_rate_for_manufactured_item(d.transfer_qty, outgoing_items_cost) + d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost) elif self.purpose == "Repack": d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) @@ -666,7 +667,7 @@ class StockEntry(StockController): production_item, wo_qty = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) - number_of_finished_items = 0 + finished_items = [] for d in self.get('items'): if d.is_finished_item: if d.item_code != production_item: @@ -675,9 +676,9 @@ class StockEntry(StockController): elif flt(d.transfer_qty) > flt(self.fg_completed_qty): frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ format(d.idx, d.transfer_qty, self.fg_completed_qty)) - number_of_finished_items += 1 + finished_items.append(d.item_code) - if number_of_finished_items > 1: + if len(set(finished_items)) > 1: frappe.throw(_("Multiple items cannot be marked as finished item")) if self.purpose == "Manufacture": diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 1a641855aa2..123f0c86471 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -179,22 +179,20 @@ class TestStockEntry(unittest.TestCase): def test_material_transfer_gl_entry(self): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - create_stock_reconciliation(qty=100, rate=100) - mtn = make_stock_entry(item_code="_Test Item", source="Stores - TCP1", - target="Finished Goods - TCP1", qty=45) + target="Finished Goods - TCP1", qty=45, company=company) self.check_stock_ledger_entries("Stock Entry", mtn.name, [["_Test Item", "Stores - TCP1", -45.0], ["_Test Item", "Finished Goods - TCP1", 45.0]]) - stock_in_hand_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) + source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) - fixed_asset_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse) + target_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse) - if stock_in_hand_account == fixed_asset_account: + if source_warehouse_account == target_warehouse_account: # no gl entry as both source and target warehouse has linked to same account. self.assertFalse(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name)) + where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name, as_dict=1)) else: stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", @@ -202,8 +200,8 @@ class TestStockEntry(unittest.TestCase): self.check_gl_entries("Stock Entry", mtn.name, sorted([ - [stock_in_hand_account, 0.0, stock_value_diff], - [fixed_asset_account, stock_value_diff, 0.0], + [source_warehouse_account, 0.0, stock_value_diff], + [target_warehouse_account, stock_value_diff, 0.0], ]) ) @@ -754,37 +752,37 @@ class TestStockEntry(unittest.TestCase): def test_total_basic_amount_zero(self): se = frappe.get_doc({"doctype":"Stock Entry", - "purpose":"Material Receipt", - "stock_entry_type":"Material Receipt", - "posting_date": nowdate(), - "company":"_Test Company with perpetual inventory", - "items":[ - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 1, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 2, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - ], - "additional_costs":[ - {"expense_account":"Miscellaneous Expenses - TCP1", - "amount":100, - "description": "miscellanous"} - ] + "purpose":"Material Receipt", + "stock_entry_type":"Material Receipt", + "posting_date": nowdate(), + "company":"_Test Company with perpetual inventory", + "items":[ + { + "item_code":"_Test Item", + "description":"_Test Item", + "qty": 1, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + { + "item_code":"_Test Item", + "description":"_Test Item", + "qty": 2, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + ], + "additional_costs":[ + {"expense_account":"Miscellaneous Expenses - TCP1", + "amount":100, + "description": "miscellanous" + }] }) se.insert() se.submit() diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 6fe60298eeb..b78ae6d79b3 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -526,7 +526,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-23 17:55:03.384138", + "modified": "2020-12-23 17:55:03.384138", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 1af68dd7f22..14d543b1740 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -57,8 +57,7 @@ def get_gl_data(report_filters, filters): if report_filters.account: stock_accounts = [report_filters.account] else: - stock_accounts = [k.name - for k in get_stock_accounts(report_filters.company)] + stock_accounts = get_stock_accounts(report_filters.company) filters.update({ "account": ("in", stock_accounts) From 0788df412ce9fc8f8803170ec1cede05ba6e91ff Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 25 Dec 2020 20:52:59 +0530 Subject: [PATCH 107/203] fix: Removed permissions from UAE VAT Settings --- .../uae_vat_settings/uae_vat_settings.json | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json index ce2c1d4e142..1ff5680bfe9 100644 --- a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json @@ -29,25 +29,12 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-30 20:08:18.764798", + "modified": "2020-12-25 20:20:22.342426", "modified_by": "Administrator", "module": "Regional", "name": "UAE VAT Settings", "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], + "permissions": [], "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", From 60a1d251969bbad3eaa053bc712b0bcb33c73f46 Mon Sep 17 00:00:00 2001 From: Saqib Date: Sat, 26 Dec 2020 13:01:49 +0530 Subject: [PATCH 108/203] fix: cancelling of asset value adjustement (#24193) --- .../doctype/asset_value_adjustment/asset_value_adjustment.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 74ca62ffdad..14308277c14 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -21,9 +21,6 @@ class AssetValueAdjustment(Document): self.reschedule_depreciations(self.new_asset_value) def on_cancel(self): - if self.journal_entry: - frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry)) - self.reschedule_depreciations(self.current_asset_value) def validate_date(self): From 71f203dbc565d170c993668f6f0f92de2303194a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 28 Dec 2020 12:35:19 +0530 Subject: [PATCH 109/203] fix: template task status, subject in project template task --- .../patches/v13_0/update_project_template_tasks.py | 2 ++ .../doctype/project_template/project_template.js | 10 ++++++++++ .../project_template_task/project_template_task.json | 11 +++++++++-- erpnext/projects/doctype/task/task.json | 4 ++-- erpnext/projects/doctype/task/task.py | 2 ++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 26c42592816..f24a2c62f1f 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -5,6 +5,8 @@ from __future__ import unicode_literals import frappe def execute(): + frappe.reload_doc("projects", "doctype", "project_template") + frappe.reload_doc("projects", "doctype", "project_template_task") frappe.reload_doc("projects", "doctype", "project_template") for template_name in frappe.db.sql(""" select diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index 7668df3e139..04153dc5704 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -15,3 +15,13 @@ frappe.ui.form.on('Project Template', { }); } }); + +frappe.ui.form.on('Project Template Task', { + task: function (frm, cdt, cdn) { + var row = locals[cdt][cdn]; + frappe.db.get_value("Task", row.task, "subject", (value) => { + row.subject = value.subject; + refresh_field("tasks"); + }); + } +}) diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json index 80c510db1b0..7a552945bd5 100644 --- a/erpnext/projects/doctype/project_template_task/project_template_task.json +++ b/erpnext/projects/doctype/project_template_task/project_template_task.json @@ -5,7 +5,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "task" + "task", + "subject" ], "fields": [ { @@ -15,11 +16,17 @@ "label": "Task", "options": "Task", "reqd": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Subject" } ], "istable": 1, "links": [], - "modified": "2020-12-07 13:28:40.961810", + "modified": "2020-12-28 12:10:26.321913", "modified_by": "Administrator", "module": "Projects", "name": "Project Template Task", diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index bb55256f7d9..160cc5812f7 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -115,7 +115,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled" + "options": "Open\nWorking\nPending Review\nOverdue\nTemplate\nCompleted\nCancelled" }, { "fieldname": "priority", @@ -388,7 +388,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2020-12-21 11:59:24.196834", + "modified": "2020-12-28 11:32:58.714991", "modified_by": "Administrator", "module": "Projects", "name": "Task", diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 80b764ba4f0..a2095c95d51 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -56,6 +56,8 @@ class Task(NestedSet): validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") def validate_status(self): + if self.is_template and self.status != "Template": + self.status = "Template" if self.status!=self.get_db_value("status") and self.status == "Completed": for d in self.depends_on: if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): From 40d2c6a0cccea8b08bf3480bcb563099f1429780 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Dec 2020 14:08:45 +0530 Subject: [PATCH 110/203] fix: finished good produced qty validation --- .../stock/doctype/stock_entry/stock_entry.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 92d268f0993..2fc7da83896 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -259,11 +259,16 @@ class StockEntry(StockController): item_code.append(item.item_code) def validate_fg_completed_qty(self): + item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item and d.qty != self.fg_completed_qty: - frappe.throw(_("Finished product quantity {0} and For Quantity {1} cannot be different") - .format(d.qty, self.fg_completed_qty)) + if d.is_finished_item: + item_wise_qty.setdefault(d.item_code, []).append(d.qty) + + for item_code, qty_list in iteritems(item_wise_qty): + if self.fg_completed_qty != sum(qty_list): + frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different") + .format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty))) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -319,7 +324,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.bom_no: + if d.is_finished_item or d.is_scrap_item: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -699,7 +704,7 @@ class StockEntry(StockController): # SLE for target warehouse self.get_sle_for_target_warehouse(sl_entries, finished_item_row) - + # reverse sl entries if cancel if self.docstatus == 2: sl_entries.reverse() @@ -727,9 +732,9 @@ class StockEntry(StockController): sle.dependant_sle_voucher_detail_no = d.name elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse): sle.dependant_sle_voucher_detail_no = finished_item_row.name - + sl_entries.append(sle) - + def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): for d in self.get('items'): if cstr(d.t_warehouse): From 527b7e16e5449e13cd2c68469265dfa10637faf5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 28 Dec 2020 15:29:30 +0530 Subject: [PATCH 111/203] Revert "feat: Batch wise item selling pricing" --- erpnext/public/js/controllers/buying.js | 4 - erpnext/public/js/controllers/transaction.js | 6 - erpnext/selling/sales_common.js | 4 - erpnext/stock/doctype/batch/test_batch.py | 70 +---------- .../stock/doctype/item_price/item_price.json | 116 ++++-------------- .../stock/doctype/item_price/item_price.py | 5 +- erpnext/stock/get_item_details.py | 5 +- 7 files changed, 31 insertions(+), 179 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index fcd7c15a7ca..db85a3ec99e 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -195,10 +195,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this._super(doc, cdt, cdn); }, - batch_no: function(doc, cdt, cdn) { - this._super(doc, cdt, cdn); - }, - received_qty: function(doc, cdt, cdn) { this.calculate_accepted_qty(doc, cdt, cdn) }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7bd72c6b6bd..3bc20f87336 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1104,11 +1104,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, - batch_no: function(doc, cdt, cdn) { - let item = frappe.get_doc(cdt, cdn); - this.apply_price_list(item, true); - }, - toggle_conversion_factor: function(item) { // toggle read only property for conversion factor field if the uom and stock uom are same if(this.frm.get_field('items').grid.fields_map.conversion_factor) { @@ -1413,7 +1408,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ "pricing_rules": d.pricing_rules, "warehouse": d.warehouse, "serial_no": d.serial_no, - "batch_no": d.batch_no, "price_list_rate": d.price_list_rate, "conversion_factor": d.conversion_factor || 1.0 }); diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index ce084646e15..7f00fca8f05 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -399,10 +399,6 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ } }, - batch_no: function(doc, cdt, cdn) { - this._super(doc, cdt, cdn); - }, - qty: function(doc, cdt, cdn) { this._super(doc, cdt, cdn); diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 97f85bafd95..e41f1a8aaaf 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -8,8 +8,6 @@ import unittest from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no from frappe.utils import cint, flt -from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.stock.get_item_details import get_item_details class TestBatch(unittest.TestCase): def test_item_has_batch_enabled(self): @@ -184,7 +182,7 @@ class TestBatch(unittest.TestCase): stock_entry.cancel() current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty) - + @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): '''Make a new stock entry for given target warehouse and batch name of item''' @@ -254,72 +252,6 @@ class TestBatch(unittest.TestCase): return batch - def test_batch_wise_item_price(self): - if not frappe.db.get_value('Item', '_Test Batch Price Item'): - frappe.get_doc({ - 'doctype': 'Item', - 'is_stock_item': 1, - 'item_code': '_Test Batch Price Item', - 'item_group': 'Products', - 'has_batch_no': 1, - 'create_new_batch': 1 - }).insert(ignore_permissions=True) - - batch1 = create_batch('_Test Batch Price Item', 200, 1) - batch2 = create_batch('_Test Batch Price Item', 300, 1) - batch3 = create_batch('_Test Batch Price Item', 400, 0) - - args = frappe._dict({ - "item_code": "_Test Batch Price Item", - "company": "_Test Company with perpetual inventory", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "customer": "_Test Customer", - "name": None - }) - - #test price for batch1 - args.update({'batch_no': batch1}) - details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 200) - - #test price for batch2 - args.update({'batch_no': batch2}) - details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 300) - - #test price for batch3 - args.update({'batch_no': batch3}) - details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 400) - -def create_batch(item_code, rate, create_item_price_for_batch): - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", - warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, - expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) - - batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) - - if not create_item_price_for_batch: - create_price_list_for_batch(item_code, None, rate) - else: - create_price_list_for_batch(item_code, batch, rate) - - return batch - -def create_price_list_for_batch(item_code, batch, rate): - frappe.get_doc({ - 'doctype': 'Item Price', - 'item_code': '_Test Batch Price Item', - 'price_list': '_Test Price List', - 'batch_no': batch, - 'price_list_rate': rate - }).insert() - def make_new_batch(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 83177b372ad..5f62381f8b3 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -18,7 +18,6 @@ "price_list", "customer", "supplier", - "batch_no", "column_break_3", "buying", "selling", @@ -48,41 +47,31 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM", - "show_days": 1, - "show_seconds": 1 + "options": "UOM" }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit", - "show_days": 1, - "show_seconds": 1 + "label": "Packing Unit" }, { "fieldname": "column_break_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fetch_from": "item_code.brand", @@ -90,25 +79,19 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Brand", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tags" }, { "fieldname": "price_list", @@ -117,9 +100,7 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "bold": 1, @@ -127,49 +108,37 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer", - "show_days": 1, - "show_seconds": 1 + "options": "Customer" }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier", - "show_days": 1, - "show_seconds": 1 + "options": "Supplier" }, { "fieldname": "column_break_3", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "bold": 1, @@ -177,15 +146,11 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "price_list_rate", @@ -197,80 +162,53 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From", - "show_days": 1, - "show_seconds": 1 + "label": "Valid From" }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days", - "show_days": 1, - "show_seconds": 1 + "label": "Lead Time in days" }, { "fieldname": "column_break_18", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto", - "show_days": 1, - "show_seconds": 1 + "label": "Valid Upto" }, { "fieldname": "section_break_24", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note", - "show_days": 1, - "show_seconds": 1 + "label": "Note" }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference", - "show_days": 1, - "show_seconds": 1 - }, - { - "fieldname": "batch_no", - "fieldtype": "Link", - "label": "Batch No", - "options": "Batch", - "show_days": 1, - "show_seconds": 1 + "label": "Reference" } ], "icon": "fa fa-flag", "idx": 1, - "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-08 18:12:15.395772", + "modified": "2020-07-06 22:31:32.943475", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index e82a19b0dc0..bed5ea9ab66 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -54,8 +54,7 @@ class ItemPrice(Document): "valid_upto", "packing_unit", "customer", - "supplier", - "batch_no"]: + "supplier",]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: @@ -69,7 +68,7 @@ class ItemPrice(Document): self.as_dict(),) if price_list_rate: - frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) + frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) def before_save(self): if self.selling: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 2d2abd71aa1..08f7a83b893 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -672,8 +672,6 @@ def get_item_price(args, item_code, ignore_party=False): and price_list=%(price_list)s and ifnull(uom, '') in ('', %(uom)s)""" - conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" - if not ignore_party: if args.get("customer"): conditions += " and customer=%(customer)s" @@ -692,7 +690,7 @@ def get_item_price(args, item_code, ignore_party=False): return frappe.db.sql(""" select name, price_list_rate, uom from `tabItem Price` {conditions} - order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, uom desc """.format(conditions=conditions), args) def get_price_list_rate_for(args, item_code): """ @@ -711,7 +709,6 @@ def get_price_list_rate_for(args, item_code): "uom": args.get('uom'), "transaction_date": args.get('transaction_date'), "posting_date": args.get('posting_date'), - "batch_no": args.get('batch_no') } item_price_data = 0 From 88471854d53745a2756e947ac92c411b782d8a27 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 28 Dec 2020 15:40:23 +0530 Subject: [PATCH 112/203] fix: sider --- .../projects/doctype/project_template/project_template.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js index 04153dc5704..3d3c15c6e05 100644 --- a/erpnext/projects/doctype/project_template/project_template.js +++ b/erpnext/projects/doctype/project_template/project_template.js @@ -20,8 +20,8 @@ frappe.ui.form.on('Project Template Task', { task: function (frm, cdt, cdn) { var row = locals[cdt][cdn]; frappe.db.get_value("Task", row.task, "subject", (value) => { - row.subject = value.subject; - refresh_field("tasks"); - }); + row.subject = value.subject; + refresh_field("tasks"); + }); } -}) +}); From 29a03bd5a1d250479369f8539450414bfbef080c Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 28 Dec 2020 16:59:13 +0530 Subject: [PATCH 113/203] feat: Add 'Manual Inspection' checkbox - fix merge conflict in js file - Dont auto set status if manual inspection is checked - Added 'Manual Inspection' checkbox in QI readings table --- .../doctype/quality_inspection/quality_inspection.js | 1 + .../doctype/quality_inspection/quality_inspection.py | 11 ++++++----- .../quality_inspection_reading.json | 10 +++++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 544bc2c307a..f7565fd505c 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -51,6 +51,7 @@ frappe.ui.form.on("Quality Inspection", { }; } }); + }, refresh: function(frm) { // Ignore cancellation of reference doctype on cancel all. diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index f582658d871..9672b623944 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -76,11 +76,12 @@ class QualityInspection(Document): def inspect_and_set_status(self): for reading in self.readings: - if reading.formula_based_criteria: - self.set_status_based_on_acceptance_formula(reading) - else: - # if not formula based check acceptance values set - self.set_status_based_on_acceptance_values(reading) + if not reading.manual_inspection: # dont auto set status if manual + if reading.formula_based_criteria: + self.set_status_based_on_acceptance_formula(reading) + else: + # if not formula based check acceptance values set + self.set_status_based_on_acceptance_values(reading) def set_status_based_on_acceptance_values(self, reading): if cint(reading.non_numeric): diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index 0792f26d2ab..264a6ea634b 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -10,6 +10,7 @@ "status", "value", "non_numeric", + "manual_inspection", "column_break_4", "min_value", "max_value", @@ -201,12 +202,19 @@ "fieldname": "non_numeric", "fieldtype": "Check", "label": "Non-Numeric" + }, + { + "default": "0", + "description": "Set the status manually.", + "fieldname": "manual_inspection", + "fieldtype": "Check", + "label": "Manual Inspection" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-21 11:36:24.885019", + "modified": "2020-12-28 16:40:47.586382", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From a69e81a1510d3dc4d3ece2744224023ed3f14a23 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 28 Dec 2020 18:07:22 +0530 Subject: [PATCH 114/203] chore: Made 'Parameter' a link field in QI and QI Template - Added doctype Quality Inspection Parameter - Made 'Parameter' a link field in QI and QI Template - Added patch to create Quality Inspection Parameter records for every parameter in the system --- erpnext/patches.txt | 1 + .../convert_qi_parameter_to_link_field.py | 23 +++++ .../item_quality_inspection_parameter.json | 5 +- .../quality_inspection_parameter/__init__.py | 0 .../quality_inspection_parameter.js | 8 ++ .../quality_inspection_parameter.json | 86 +++++++++++++++++++ .../quality_inspection_parameter.py | 10 +++ .../test_quality_inspection_parameter.py | 10 +++ .../quality_inspection_reading.json | 5 +- 9 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/__init__.py create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py create mode 100644 erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d69dabf15cd..621f4173ad5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -741,3 +741,4 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.update_returned_qty_in_pr_dn +erpnext.patches.v13_0.convert_qi_parameter_to_link_field #2345 diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py new file mode 100644 index 00000000000..289b6a761e3 --- /dev/null +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter') + + # get all distinct parameters from QI readigs table + reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"]) + reading_params = [d.specification for d in reading_params] + + # get all distinct parameters from QI Template as some may be unused in QI + template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"]) + template_params = [d.specification for d in template_params] + + params = list(set(reading_params + template_params)) + + for parameter in params: + if not frappe.db.exists("Quality Inspection Parameter", parameter): + frappe.get_doc({ + "doctype": "Quality Inspection Parameter", + "parameter": parameter, + "description": parameter + }).insert(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index 9b980a1e013..fc06e89f2fb 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -18,11 +18,12 @@ "fields": [ { "fieldname": "specification", - "fieldtype": "Data", + "fieldtype": "Link", "in_list_view": 1, "label": "Parameter", "oldfieldname": "specification", "oldfieldtype": "Data", + "options": "Quality Inspection Parameter", "print_width": "200px", "reqd": 1, "width": "100px" @@ -79,7 +80,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-21 11:37:55.387677", + "modified": "2020-12-28 17:41:04.350225", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection_parameter/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js new file mode 100644 index 00000000000..47c7e11d237 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Quality Inspection Parameter', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json new file mode 100644 index 00000000000..0b5a9b5b3ce --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json @@ -0,0 +1,86 @@ +{ + "actions": [], + "autoname": "field:parameter", + "creation": "2020-12-28 17:06:00.254129", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parameter", + "description" + ], + "fields": [ + { + "fieldname": "parameter", + "fieldtype": "Data", + "label": "Parameter", + "unique": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-28 18:06:54.897317", + "modified_by": "Administrator", + "module": "Stock", + "name": "Quality Inspection Parameter", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "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/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py new file mode 100644 index 00000000000..86784221a0c --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.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 QualityInspectionParameter(Document): + pass diff --git a/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py new file mode 100644 index 00000000000..cefdc0867b1 --- /dev/null +++ b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.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 TestQualityInspectionParameter(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index 264a6ea634b..739845bcdac 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -36,11 +36,12 @@ { "columns": 3, "fieldname": "specification", - "fieldtype": "Data", + "fieldtype": "Link", "in_list_view": 1, "label": "Parameter", "oldfieldname": "specification", "oldfieldtype": "Data", + "options": "Quality Inspection Parameter", "reqd": 1 }, { @@ -214,7 +215,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-28 16:40:47.586382", + "modified": "2020-12-28 17:40:47.407210", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From 91e3c07d774fae54d7e0d67e9dd21cd8bc65c182 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 28 Dec 2020 19:42:46 +0530 Subject: [PATCH 115/203] fix: assest depreciation ledger --- .../asset_depreciation_ledger.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 16bef565252..2162a02eff9 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -47,21 +47,22 @@ def get_data(filters): for d in gl_entries: asset_data = assets_details.get(d.against_voucher) - if not asset_data.get("accumulated_depreciation_amount"): - asset_data.accumulated_depreciation_amount = d.debit - else: - asset_data.accumulated_depreciation_amount += d.debit + if asset_data: + if not asset_data.get("accumulated_depreciation_amount"): + asset_data.accumulated_depreciation_amount = d.debit + else: + asset_data.accumulated_depreciation_amount += d.debit - row = frappe._dict(asset_data) - row.update({ - "depreciation_amount": d.debit, - "depreciation_date": d.posting_date, - "amount_after_depreciation": (flt(row.gross_purchase_amount) - - flt(row.accumulated_depreciation_amount)), - "depreciation_entry": d.voucher_no - }) + row = frappe._dict(asset_data) + row.update({ + "depreciation_amount": d.debit, + "depreciation_date": d.posting_date, + "amount_after_depreciation": (flt(row.gross_purchase_amount) - + flt(row.accumulated_depreciation_amount)), + "depreciation_entry": d.voucher_no + }) - data.append(row) + data.append(row) return data From 81285204dc5a038a4eafc0525b4956caf821337b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Dec 2020 16:25:45 +0530 Subject: [PATCH 116/203] fix: option name for the field 'Role Allowed to Create/Edit Back-dated Transactions' --- erpnext/stock/doctype/stock_settings/stock_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 859aea2eb60..3ff396ba77e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -217,7 +217,7 @@ "fieldname": "role_allowed_to_create_edit_back_dated_transactions", "fieldtype": "Link", "label": "Role Allowed to Create/Edit Back-dated Transactions", - "options": "User" + "options": "Role" }, { "fieldname": "column_break_26", @@ -234,7 +234,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-23 22:26:54.225608", + "modified": "2020-12-29 12:53:31.162247", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From e061004956bbe585fc0874ba2532c5ac83a4ac11 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Dec 2020 17:00:39 +0530 Subject: [PATCH 117/203] 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 fa700e115ce..e822c0c0171 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 329d6e5aa75..e27bf5e2b35 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 353f8f4d857381baf0bfa8e4b50e698b49f59da9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Dec 2020 22:13:40 +0530 Subject: [PATCH 118/203] fix: Add test case for YTD --- ...test_employee_tax_exemption_declaration.py | 16 ++-- .../doctype/salary_slip/salary_slip.py | 49 ++++++------ .../doctype/salary_slip/test_salary_slip.py | 76 ++++++++++++++----- .../salary_structure/test_salary_structure.py | 18 +++-- 4 files changed, 106 insertions(+), 53 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 0609d191497..311f3527f6e 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -86,19 +86,21 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): self.assertEqual(declaration.total_exemption_amount, 100000) -def create_payroll_period(): - if not frappe.db.exists("Payroll Period", "_Test Payroll Period"): +def create_payroll_period(**args): + args = frappe._dict(args) + name = args.name or "_Test Payroll Period" + if not frappe.db.exists("Payroll Period", name): from datetime import date payroll_period = frappe.get_doc(dict( doctype = 'Payroll Period', - name = "_Test Payroll Period", - company = erpnext.get_default_company(), - start_date = date(date.today().year, 1, 1), - end_date = date(date.today().year, 12, 31) + name = name, + company = args.company or erpnext.get_default_company(), + start_date = args.start_date or date(date.today().year, 1, 1), + end_date = args.end_date or date(date.today().year, 12, 31) )).insert() return payroll_period else: - return frappe.get_doc("Payroll Period", "_Test Payroll Period") + return frappe.get_doc("Payroll Period", name) def create_exemption_category(): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 02e5f2d1d12..9f46d50e588 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -18,6 +18,7 @@ from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_fac from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry +from erpnext.accounts.utils import get_fiscal_year class SalarySlip(TransactionBase): def __init__(self, *args, **kwargs): @@ -1129,21 +1130,25 @@ class SalarySlip(TransactionBase): def compute_year_to_date(self): year_to_date = 0 - payroll_period = frappe.get_list('Payroll Period', - fields = ['start_date','end_date','company'], - filters= {'start_date' : ['<=', self.start_date], - 'end_date' : ['>=', self.end_date], - 'company' : self.company - })[0] - salary_slips_from_current_payroll_period = frappe.get_list('Salary Slip', - fields = ['employee_name', 'start_date', 'end_date', 'net_pay'], - filters = {'employee_name' : self.employee_name, - 'start_date' : ['>=', payroll_period.start_date], - 'end_date' : ['<', self.start_date] - }) + payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) - for salary_slip in salary_slips_from_current_payroll_period: - year_to_date += salary_slip.net_pay + 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 + + salary_slip_sum = frappe.get_list('Salary Slip', + fields = ['sum(net_pay) as sum'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', period_start_date], + 'end_date' : ['<', period_end_date]}) + + + year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 year_to_date += self.net_pay self.year_to_date = year_to_date @@ -1151,14 +1156,14 @@ class SalarySlip(TransactionBase): def compute_month_to_date(self): month_to_date = 0 first_day_of_the_month = get_first_day(self.start_date) - salary_slips_from_this_month = frappe.get_list('Salary Slip', - fields = ['employee_name', 'start_date', 'net_pay'], - filters = {'employee_name' : self.employee_name, - 'start_date' : ['>=', first_day_of_the_month], - 'end_date' : ['<', self.start_date] - }) - for salary_slip in salary_slips_from_this_month: - month_to_date += salary_slip.net_pay + salary_slip_sum = frappe.get_list('Salary Slip', + fields = ['sum(net_pay) as sum'], + filters = {'employee_name' : self.employee_name, + 'start_date' : ['>=', first_day_of_the_month], + 'end_date' : ['<', self.start_date] + }) + + year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 month_to_date += self.net_pay self.month_to_date = month_to_date diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 5daf1d439d1..687d3602a97 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -290,6 +290,35 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(salary_slip.gross_pay, 78000) self.assertEqual(salary_slip.base_gross_pay, 78000*70) + def test_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", + start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + + create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01")) + + 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) + + salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date'], filters={'employee_name': + 'test_ytd@salary.com'}, order_by = 'posting_date') + + net_pay = 70026.00 + month = 1 + for slip in salary_slips: + year_to_date = month * net_pay + self.assertEqual(slip.year_to_date, year_to_date) + month += 1 + def test_tax_for_payroll_period(self): data = {} # test the impact of tax exemption declaration, tax exemption proof submission @@ -631,8 +660,13 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): - frappe.db.sql("""delete from `tabIncome Tax Slab`""") +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None): + # frappe.db.sql("""delete from `tabIncome Tax Slab`""") + + print(payroll_period.name, effective_date) + + if not currency: + currency = erpnext.get_default_currency() slabs = [ { @@ -652,26 +686,32 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = } ] - income_tax_slab = frappe.new_doc("Income Tax Slab") - income_tax_slab.name = "Tax Slab: " + payroll_period.name - income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) - income_tax_slab.currency = currency + income_tax_slab_name = frappe.db.get_value("Income Tax Slab", "Tax Slab: " + payroll_period.name) + if not income_tax_slab_name: + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab: " + payroll_period.name + income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + income_tax_slab.currency = currency - if allow_tax_exemption: - income_tax_slab.allow_tax_exemption = 1 - income_tax_slab.standard_tax_exemption_amount = 50000 + if allow_tax_exemption: + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = 50000 - for item in slabs: - income_tax_slab.append("slabs", item) + for item in slabs: + income_tax_slab.append("slabs", item) - income_tax_slab.append("other_taxes_and_charges", { - "description": "cess", - "percent": 4 - }) + income_tax_slab.append("other_taxes_and_charges", { + "description": "cess", + "percent": 4 + }) - income_tax_slab.save() - if not dont_submit: - income_tax_slab.submit() + income_tax_slab.save() + if not dont_submit: + income_tax_slab.submit() + + return income_tax_slab.name + else: + return income_tax_slab_name def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): deducted_dates = [] diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index abb669740b6..e1c6a008aa0 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -114,7 +114,7 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(sal_struct.currency, 'USD') def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, - test_tax=False, company=None, currency=erpnext.get_default_currency()): + test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) @@ -141,16 +141,22 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do if employee and not frappe.db.get_value("Salary Structure Assignment", {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: - create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) + create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency, + payroll_period=payroll_period) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): +def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(), + payroll_period=None): + if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) - payroll_period = create_payroll_period() - create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) + if not payroll_period: + payroll_period = create_payroll_period() + income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) + else: + income_tax_slab = frappe.db.get_value('Income Tax Slab', "Tax Slab: " + payroll_period.name) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee @@ -162,7 +168,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) - salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" + salary_structure_assignment.income_tax_slab = income_tax_slab salary_structure_assignment.submit() return salary_structure_assignment From 20133bd1dfc7fa983e78af18ea827bfee7c2e86d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Dec 2020 22:19:12 +0530 Subject: [PATCH 119/203] fix: Remove comments --- erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 687d3602a97..d1eaae74900 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -661,10 +661,6 @@ def create_benefit_claim(employee, payroll_period, amount, component): return claim_date def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None): - # frappe.db.sql("""delete from `tabIncome Tax Slab`""") - - print(payroll_period.name, effective_date) - if not currency: currency = erpnext.get_default_currency() From 8b7ebe5044b823ccad309f9cd241bd335af93cba Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 12:45:46 +0530 Subject: [PATCH 120/203] fix: Test Case cleanup and fixes --- .../loan_management/doctype/loan/test_loan.py | 2 +- .../income_tax_slab/income_tax_slab.py | 7 +++-- .../doctype/salary_slip/test_salary_slip.py | 30 +++++++++++-------- .../salary_structure/test_salary_structure.py | 5 ++-- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index a63d06590f8..1d831aeb644 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -45,7 +45,7 @@ class TestLoan(unittest.TestCase): create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) self.applicant1 = make_employee("robert_loan@loan.com") - make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR') + make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company") if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index 253f023f68b..e2b22a6ce96 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -3,8 +3,11 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +import erpnext from frappe.model.document import Document class IncomeTaxSlab(Document): - pass + def validate(self): + if self.company: + self.currency = erpnext.get_company_currency(self.company) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index d1eaae74900..9e3e707f39d 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -240,7 +240,12 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') - make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", + start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + + make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', + payroll_period=payroll_period) + frappe.db.sql("""delete from `tabLoan""") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 @@ -298,7 +303,8 @@ class TestSalarySlip(unittest.TestCase): payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) - create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01")) + 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) @@ -309,15 +315,13 @@ class TestSalarySlip(unittest.TestCase): create_salary_slips_for_payroll_period(applicant, salary_structure.name, payroll_period, deduct_random=False) - salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date'], filters={'employee_name': + salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': 'test_ytd@salary.com'}, order_by = 'posting_date') - net_pay = 70026.00 - month = 1 + year_to_date = 0 for slip in salary_slips: - year_to_date = month * net_pay + year_to_date += slip.net_pay self.assertEqual(slip.year_to_date, year_to_date) - month += 1 def test_tax_for_payroll_period(self): data = {} @@ -439,10 +443,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" employee = frappe.db.get_value("Employee", {"user_id": user}) - if not frappe.db.exists('Salary Structure', salary_structure): - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) - else: - salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure) + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) if not salary_slip_name: @@ -660,7 +661,8 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None): +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None, + company=None): if not currency: currency = erpnext.get_default_currency() @@ -687,6 +689,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab.name = "Tax Slab: " + payroll_period.name income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + + if company: + income_tax_slab.company = company + income_tax_slab.currency = currency if allow_tax_exemption: diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e1c6a008aa0..2b249c7a197 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -154,9 +154,8 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non if not payroll_period: payroll_period = create_payroll_period() - income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) - else: - income_tax_slab = frappe.db.get_value('Income Tax Slab', "Tax Slab: " + payroll_period.name) + + income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee From 97d055dfc34ba987f538a1f794bcc9e6eb97e87e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 14:06:13 +0530 Subject: [PATCH 121/203] fix: Test Case --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +- .../doctype/salary_structure/test_salary_structure.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 9f46d50e588..99d8a8317cd 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1163,7 +1163,7 @@ class SalarySlip(TransactionBase): 'end_date' : ['<', self.start_date] }) - year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 + month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 month_to_date += self.net_pay self.month_to_date = month_to_date diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index 2b249c7a197..f2fb558a14b 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -155,7 +155,10 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non if not payroll_period: payroll_period = create_payroll_period() - income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) + income_tax_slab = frappe.db.get_value("Income Tax Slab", {"currency": currency}) + + if not income_tax_slab: + income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee From f946bbd03294d170d8b1093994fe2e174f381c04 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 30 Dec 2020 14:43:42 +0530 Subject: [PATCH 122/203] fix: minor e-invoicing issues (#24239) * fix: invoice value set to zero if rounded total disabled * fix: unit rate & gross amount calculation * fix: e-invoice-setup patch * chore: no need to re-run the patch * fix: item value & invoice value calculations --- .../sales_invoice/test_sales_invoice.py | 63 +++++++++++++----- erpnext/patches.txt | 1 + .../patches/v12_0/setup_einvoice_fields.py | 1 + .../india/e_invoice/einv_template.json | 2 +- erpnext/regional/india/e_invoice/utils.py | 66 +++++++++---------- 5 files changed, 81 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3c681eeecf2..eb223ee42ca 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1885,8 +1885,8 @@ class TestSalesInvoice(unittest.TestCase): "item_code": "_Test Item", "uom": "Nos", "warehouse": "_Test Warehouse - _TC", - "qty": 2, - "rate": 100, + "qty": 2000, + "rate": 12, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", @@ -1895,31 +1895,52 @@ class TestSalesInvoice(unittest.TestCase): "item_code": "_Test Item 2", "uom": "Nos", "warehouse": "_Test Warehouse - _TC", - "qty": 4, - "rate": 150, + "qty": 420, + "rate": 15, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", }) + si.discount_amount = 100 si.save() einvoice = make_einvoice(si) - total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']]) - total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']]) - total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']]) - total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']]) - total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']]) + total_item_ass_value = 0 + total_item_cgst_value = 0 + total_item_sgst_value = 0 + total_item_igst_value = 0 + total_item_value = 0 + + for item in einvoice['ItemList']: + total_item_ass_value += item['AssAmt'] + total_item_cgst_value += item['CgstAmt'] + total_item_sgst_value += item['SgstAmt'] + total_item_igst_value += item['IgstAmt'] + total_item_value += item['TotItemVal'] + + self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount']) + self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt']) + + value_details = einvoice['ValDtls'] self.assertEqual(einvoice['Version'], '1.1') - self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value) - self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value) - self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value) - self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value) - self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value) + self.assertEqual(value_details['AssVal'], total_item_ass_value) + self.assertEqual(value_details['CgstVal'], total_item_cgst_value) + self.assertEqual(value_details['SgstVal'], total_item_sgst_value) + self.assertEqual(value_details['IgstVal'], total_item_igst_value) + + self.assertEqual( + value_details['TotInvVal'], + value_details['AssVal'] + value_details['CgstVal'] + + value_details['SgstVal'] + value_details['IgstVal'] + + value_details['OthChrg'] - value_details['Discount'] + ) + + self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) -def make_sales_invoice_for_ewaybill(): +def make_test_address_for_ewaybill(): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): address = frappe.get_doc({ "address_line1": "_Test Address Line 1", @@ -1967,7 +1988,8 @@ def make_sales_invoice_for_ewaybill(): }) address.save() - + +def make_test_transporter_for_ewaybill(): if not frappe.db.exists('Supplier', '_Test Transporter'): frappe.get_doc({ "doctype": "Supplier", @@ -1978,12 +2000,17 @@ def make_sales_invoice_for_ewaybill(): "is_transporter": 1 }).insert() +def make_sales_invoice_for_ewaybill(): + make_test_address_for_ewaybill() + make_test_transporter_for_ewaybill() + gst_settings = frappe.get_doc("GST Settings") gst_account = frappe.get_all( "GST Account", fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"}) + filters = {"company": "_Test Company"} + ) if not gst_account: gst_settings.append("gst_accounts", { @@ -1995,7 +2022,7 @@ def make_sales_invoice_for_ewaybill(): gst_settings.save() - si = create_sales_invoice(do_not_save =1, rate = '60000') + si = create_sales_invoice(do_not_save=1, rate='60000') si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d69dabf15cd..f2e4f72d673 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -711,6 +711,7 @@ erpnext.patches.v13_0.delete_old_sales_reports execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 +execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings") erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v12_0.add_taxjar_integration_field diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index d0782765dee..2474bc3b82c 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -8,6 +8,7 @@ def execute(): if not company: return + frappe.reload_doc("custom", "doctype", "custom_field") frappe.reload_doc("regional", "doctype", "e_invoice_settings") custom_fields = { 'Sales Invoice': [ diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json index e5751da5612..60f490d6166 100644 --- a/erpnext/regional/india/e_invoice/einv_template.json +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -59,7 +59,7 @@ {item_list} ], "ValDtls": {{ - "AssVal": "{invoice_value_details.base_net_total}", + "AssVal": "{invoice_value_details.base_total}", "CgstVal": "{invoice_value_details.total_cgst_amt}", "SgstVal": "{invoice_value_details.total_sgst_amt}", "IgstVal": "{invoice_value_details.total_igst_amt}", diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index cb92c42464e..db966100430 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -146,12 +146,12 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.qty = abs(item.qty) - item.description = d.item_name - item.taxable_value = abs(item.base_net_amount) item.discount_amount = abs(item.discount_amount * item.qty) - item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) - item.gross_amount = abs(item.unit_rate * item.qty) + item.description = d.item_name + item.qty = abs(item.qty) + item.unit_rate = abs(item.base_amount / item.qty) + item.gross_amount = abs(item.base_amount) + item.taxable_value = abs(item.base_amount) item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None @@ -180,35 +180,35 @@ def update_item_taxes(invoice, item): item[attr] = 0 for t in invoice.taxes: + # this contains item wise tax rate & tax amount (incl. discount) item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if t.account_head in gst_accounts_list: + item_tax_rate = item_tax_detail[0] + # item tax amount excluding discount amount + item_tax_amount = (item_tax_rate / 100) * item.base_amount + if t.account_head in gst_accounts.cess_account: + item_tax_amount_after_discount = item_tax_detail[1] if t.charge_type == 'On Item Quantity': - item.cess_nadv_amount += abs(item_tax_detail[1]) + item.cess_nadv_amount += abs(item_tax_amount_after_discount) else: - item.cess_rate += item_tax_detail[0] - item.cess_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.igst_account: - item.tax_rate += item_tax_detail[0] - item.igst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.sgst_account: - item.tax_rate += item_tax_detail[0] - item.sgst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.cgst_account: - item.tax_rate += item_tax_detail[0] - item.cgst_amount += abs(item_tax_detail[1]) - + item.cess_rate += item_tax_rate + item.cess_amount += abs(item_tax_amount_after_discount) + + for tax_type in ['igst', 'cgst', 'sgst']: + if t.account_head in gst_accounts[f'{tax_type}_account']: + item.tax_rate += item_tax_rate + item[f'{tax_type}_amount'] += abs(item_tax_amount) + return item def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - invoice_value_details.base_net_total = abs(invoice.base_net_total) - invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 - # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off - invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) - disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') - invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) - invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) + 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.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) invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) @@ -226,15 +226,14 @@ def update_invoice_taxes(invoice, invoice_value_details): for t in invoice.taxes: if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: + # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.igst_account: - invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.sgst_account: - invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) - elif t.account_head in gst_accounts.cgst_account: - invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + + for tax_type in ['igst', 'cgst', 'sgst']: + if t.account_head in gst_accounts[f'{tax_type}_account']: + invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount) else: - invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_other_charges += abs(t.base_tax_amount) return invoice_value_details @@ -358,7 +357,8 @@ def validate_einvoice(validations, einvoice, errors=[]): einvoice[fieldname] = str(value) elif value_type == 'number': is_integer = '.' not in str(field_validation.get('maximum')) - einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) + precision = 3 if '.999' in str(field_validation.get('maximum')) else 2 + einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value) value = einvoice[fieldname] max_length = field_validation.get('maxLength') From 66dcaf3ab2cda4663317827e20ca87dc1fa3181b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 15:47:22 +0530 Subject: [PATCH 123/203] fix: Salary structure assignment in tests --- .../income_tax_slab/income_tax_slab.py | 2 +- .../doctype/salary_slip/test_salary_slip.py | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index e2b22a6ce96..81e364778ca 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +#import frappe import erpnext from frappe.model.document import Document diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 9e3e707f39d..bb310c4d873 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -9,7 +9,7 @@ import calendar import random from erpnext.accounts.utils import get_fiscal_year from frappe.utils.make_random import get_random -from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day +from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.hr.doctype.employee.test_employee import make_employee @@ -240,8 +240,7 @@ class TestSalarySlip(unittest.TestCase): interest_income_account='Interest Income Account - _TC', penalty_income_account='Penalty Income Account - _TC') - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", - start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', payroll_period=payroll_period) @@ -300,8 +299,7 @@ class TestSalarySlip(unittest.TestCase): applicant = make_employee("test_ytd@salary.com", company="_Test Company") - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company", - start_date=getdate("2019-04-01"), end_date=getdate("2020-03-31")) + 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") @@ -666,6 +664,9 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = if not currency: currency = erpnext.get_default_currency() + if company: + currency = erpnext.get_company_currency(company) + slabs = [ { "from_amount": 250000, @@ -684,15 +685,12 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = } ] - income_tax_slab_name = frappe.db.get_value("Income Tax Slab", "Tax Slab: " + payroll_period.name) + income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency}) if not income_tax_slab_name: income_tax_slab = frappe.new_doc("Income Tax Slab") - income_tax_slab.name = "Tax Slab: " + payroll_period.name + income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency) income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) - - if company: - income_tax_slab.company = company - + income_tax_slab.company = company or '' income_tax_slab.currency = currency if allow_tax_exemption: From aa44c754de3f740b79e1fb3c686f7ce99fd6ea85 Mon Sep 17 00:00:00 2001 From: Karthikeyan S Date: Wed, 30 Dec 2020 19:22:37 +0530 Subject: [PATCH 124/203] fix(GST E Invoice): update live URLs for adaequare GSP (#24248) --- erpnext/regional/india/e_invoice/utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index db966100430..102a2f0f568 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -386,15 +386,15 @@ class GSPConnector(): 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 + 'test/enriched/ei/api/master/gstin' - self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' - self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn' - self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' - self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' - self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' - + 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' + self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' + self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi' + self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' + def get_credentials(self): if self.invoice: gstin = self.get_seller_gstin() From a60707873c8c976a99f83cca001d9820a1777418 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 20:24:15 +0530 Subject: [PATCH 125/203] fix: Partial loan security unpledging --- erpnext/loan_management/doctype/loan/loan.py | 6 +-- .../loan_management/doctype/loan/test_loan.py | 37 +++++++++++++++++++ .../loan_security_shortfall.py | 1 - .../loan_security_unpledge.py | 16 +++++--- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index cd40a665d43..578c7f116ac 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -280,10 +280,10 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict return write_off @frappe.whitelist() -def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): - # if loan is passed it will be considered as full unpledge +def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0): + # if no security_map is passed it will be considered as full unpledge if loan: - pledge_qty_map = get_pledged_security_qty(loan) + pledge_qty_map = security_map or get_pledged_security_qty(loan) loan_doc = frappe.get_doc('Loan', loan) unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, loan_doc.applicant_type, loan_doc.applicant) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index a63d06590f8..2b9c7c9d0a8 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -325,6 +325,43 @@ class TestLoan(unittest.TestCase): self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) + def test_partial_loan_security_unpledge(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 2000.00 + }, + { + "loan_security": "Test Security 2", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000) + repayment_entry.submit() + + unpledge_map = {'Test Security 2': 2000} + + unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_request.submit() + unpledge_request.status = 'Approved' + unpledge_request.save() + unpledge_request.submit() + unpledge_request.load_from_db() + self.assertEqual(unpledge_request.docstatus, 1) + def test_disbursal_check_with_shortfall(self): pledges = [{ "loan_security": "Test Security 2", diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 8ec0bfb62c0..64698068842 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -81,7 +81,6 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): process_loan_security_shortfall) def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall): - existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") if existing_shortfall: diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index c29f325bfc9..61c418d3d31 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -30,6 +30,8 @@ class LoanSecurityUnpledge(Document): d.idx, frappe.bold(d.loan_security))) def validate_unpledge_qty(self): + from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import get_ltv_ratio + pledge_qty_map = get_pledged_security_qty(self.loan) ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", @@ -47,6 +49,8 @@ class LoanSecurityUnpledge(Document): pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) security_value = 0 + unpledge_qty_map = {} + ltv_ratio = 0 for security in self.securities: pledged_qty = pledge_qty_map.get(security.loan_security, 0) @@ -57,13 +61,15 @@ class LoanSecurityUnpledge(Document): msg += _("You are trying to unpledge more.") frappe.throw(msg, title=_("Loan Security Unpledge Error")) - qty_after_unpledge = pledged_qty - security.qty - ltv_ratio = ltv_ratio_map.get(security.loan_security_type) + unpledge_qty_map.setdefault(security.loan_security, 0) + unpledge_qty_map[security.loan_security] += security.qty - current_price = loan_security_price_map.get(security.loan_security) - if not current_price: - frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) + for security in pledge_qty_map: + if not ltv_ratio: + ltv_ratio = get_ltv_ratio(security) + qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0) + current_price = loan_security_price_map.get(security) security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: From 97162166325c7cf004b7d4dd5e6d1a0c8ae628e0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Dec 2020 20:33:35 +0530 Subject: [PATCH 126/203] fix: check for string types --- erpnext/loan_management/doctype/loan/loan.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 578c7f116ac..2e0a4d13ab2 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe, math, json import erpnext from frappe import _ +from six import string_types from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController @@ -282,6 +283,9 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict @frappe.whitelist() def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0): # if no security_map is passed it will be considered as full unpledge + if security_map and isinstance(security_map, string_types): + security_map = json.loads(security_map) + if loan: pledge_qty_map = security_map or get_pledged_security_qty(loan) loan_doc = frappe.get_doc('Loan', loan) From 23ab5c5cc0bfac56260acc2be982e594eaaeaffe Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 31 Dec 2020 11:29:06 +0530 Subject: [PATCH 127/203] 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 e60bc60475e..e162e3222d3 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 433bd64d2fb..40f50999adf 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 e399b22f90f..629341ff2a5 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 98f1b504ccc..b635adcd443 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 2635d47f886..c39609bd389 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 c79736d0e19..319cbd2b5d5 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 be9404d9c8c..ac4ed5e75d9 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 d5d571ab9dca8b2853aaa551b5f82faa42f0f54c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Dec 2020 12:55:35 +0530 Subject: [PATCH 128/203] fix: update old loan patch --- erpnext/patches/v13_0/update_old_loans.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index 561e967d6df..8cf09aa6925 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import nowdate +from frappe.utils import nowdate, flt from erpnext.accounts.doctype.account.test_account import create_account from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.loan.loan import make_repayment_entry @@ -113,15 +113,15 @@ def execute(): interest_paid = 0 principal_paid = 0 - if total_interest > entry.interest_amount: - interest_paid = entry.interest_amount + if flt(total_interest) > flt(entry.interest_amount): + interest_paid = flt(entry.interest_amount) else: - interest_paid = total_interest + interest_paid = flt(total_interest) - if total_principal > entry.payable_principal_amount: - principal_paid = entry.payable_principal_amount + if flt(total_principal) > flt(entry.payable_principal_amount): + principal_paid = flt(entry.payable_principal_amount) else: - principal_paid = total_principal + principal_paid = flt(total_principal) frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` + %s, @@ -129,8 +129,8 @@ def execute(): WHERE name = %s""", (principal_paid, interest_paid, entry.name)) - total_principal -= principal_paid - total_interest -= interest_paid + total_principal = flt(total_principal) - principal_paid + total_interest = flt(total_interest) - interest_paid def create_loan_type(loan, loan_type_name, penalty_account): loan_type_doc = frappe.new_doc('Loan Type') From 2b67d57480038a9248dfd76cda736974fc14e64f Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 31 Dec 2020 13:26:05 +0530 Subject: [PATCH 129/203] fix: cannot submit e-invoice if legal name not found --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 102a2f0f568..02ce6c14c90 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -88,7 +88,7 @@ def get_party_details(address_name): gstin = address.get('gstin') gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') + legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName') location = gstin_details.get('AddrLoc') or address.get('city') state_code = gstin_details.get('StateCode') pincode = gstin_details.get('AddrPncd') From 8b60fe6125265fa64cf9f3c29cb63fa61d1ef346 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 31 Dec 2020 16:10:39 +0530 Subject: [PATCH 130/203] fix: setting correct account for sal struct assignment if not specified. --- .../salary_structure_assignment/salary_structure_assignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index dccb5df1a11..a0c3013061d 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -43,7 +43,7 @@ class SalaryStructureAssignment(Document): def set_payroll_payable_account(self): if not self.payroll_payable_account: - payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') + payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account') if not payroll_payable_account: payroll_payable_account = frappe.db.get_value( "Account", { From e69148c266ead416c03626ee6442af6aba556723 Mon Sep 17 00:00:00 2001 From: Afshan Date: Thu, 31 Dec 2020 16:45:00 +0530 Subject: [PATCH 131/203] fix: allow leave policy assignment to be cancelled. --- .../leave_policy_assignment/leave_policy_assignment.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index bbb42227154..a0327bdaa0b 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -111,13 +111,14 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-12-17 16:27:20.311060", + "modified": "2020-12-31 16:43:30.695206", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -131,6 +132,7 @@ "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -144,6 +146,7 @@ "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, From 8c39ab68df1e8871be0f92e96ad5d42119e3047c Mon Sep 17 00:00:00 2001 From: vorasmit Date: Fri, 1 Jan 2021 10:54:57 +0530 Subject: [PATCH 132/203] Delete update_sales_invoice_remarks.py --- .../v12_0/update_sales_invoice_remarks.py | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 erpnext/patches/v12_0/update_sales_invoice_remarks.py diff --git a/erpnext/patches/v12_0/update_sales_invoice_remarks.py b/erpnext/patches/v12_0/update_sales_invoice_remarks.py deleted file mode 100644 index 7e8feaaca6c..00000000000 --- a/erpnext/patches/v12_0/update_sales_invoice_remarks.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import unicode_literals -import frappe - -from frappe import _ -from frappe.utils import formatdate - -def execute(): - si_list = frappe.db.get_all('Sales Invoice', filters = { - 'docstatus': 1, - 'remarks': 'No Remarks', - 'po_no' : ['!=', ''], - 'po_date' : ['!=', ''] - }, - fields = ['name', 'po_no', 'po_date'] - ) - - for doc in si_list: - remarks = _("Against Customer Order {0} dated {1}").format(doc.po_no, - formatdate(doc.po_date)) - - frappe.db.set_value('Sales Invoice', doc.name, 'remarks', remarks) - - gl_entry_list = frappe.db.get_all('GL Entry', filters = { - 'voucher_type': 'Sales Invoice', - 'remarks': 'No Remarks', - 'voucher_no' : doc.name - }, - fields = ['name'] - ) - - for entry in gl_entry_list: - frappe.db.set_value('GL Entry', entry.name, 'remarks', remarks) \ No newline at end of file From 3ef965f2531b6cb89e749a0edb18998085294eb2 Mon Sep 17 00:00:00 2001 From: vorasmit Date: Fri, 1 Jan 2021 10:55:20 +0530 Subject: [PATCH 133/203] Update patches.txt --- erpnext/patches.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4a38cb3ab80..25be8841174 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,4 +735,3 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") -erpnext.patches.v12_0.update_sales_invoice_remarks \ No newline at end of file From a245f667d068257b15d95648d8698048ce0be661 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Sat, 2 Jan 2021 10:30:22 +0530 Subject: [PATCH 134/203] fix: pos error pop up (#24237) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index d486ff60285..ac98dccdb5e 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -267,6 +267,8 @@ class POSInvoice(SalesInvoice): from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} + if not pos_profile: + frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) self.pos_profile = pos_profile.get('name') profile = {} From 7877d5a7c243b0e98be9e1f1362b8dfbd18acd2e Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Jan 2021 11:10:04 +0530 Subject: [PATCH 135/203] fix: Create QI Parameters (links) in test cases --- erpnext/controllers/tests/test_item_variant.py | 3 +++ .../test_quality_inspection.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py index c257215e718..813f0a00758 100644 --- a/erpnext/controllers/tests/test_item_variant.py +++ b/erpnext/controllers/tests/test_item_variant.py @@ -6,6 +6,7 @@ import unittest from erpnext.stock.doctype.item.test_item import set_item_variant_settings from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code +from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter from six import string_types @@ -56,6 +57,8 @@ def make_quality_inspection_template(): qc = frappe.new_doc("Quality Inspection Template") qc.quality_inspection_template_name = qc_template + + create_quality_inspection_parameter("Moisture") qc.append('item_quality_inspection_parameter', { "specification": "Moisture", "value": "< 5%", diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index d0bfb466e05..8c5a04b3f06 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -126,12 +126,18 @@ def create_quality_inspection(**args): qa.inspected_by = frappe.session.user qa.status = args.status or "Accepted" - readings = args.readings or {"specification": "Size", "min_value": 0, "max_value": 10} + if not args.readings: + create_quality_inspection_parameter("Size") + readings = {"specification": "Size", "min_value": 0, "max_value": 10} + else: + readings = args.readings + if args.status == "Rejected": readings["reading_1"] = "12" # status is auto set in child on save if isinstance(readings, list): for entry in readings: + create_quality_inspection_parameter(entry["specification"]) qa.append("readings", entry) else: qa.append("readings", readings) @@ -142,3 +148,11 @@ def create_quality_inspection(**args): qa.submit() return qa + +def create_quality_inspection_parameter(parameter): + if not frappe.db.exists("Quality Inspection Parameter", parameter): + frappe.get_doc({ + "doctype": "Quality Inspection Parameter", + "parameter": parameter, + "description": parameter + }).insert() \ No newline at end of file From 03b25be9e9cd6080ddece0b330ea2e1442049da7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 Jan 2021 11:16:59 +0530 Subject: [PATCH 136/203] feat: Allow Discharge despite Unbilled Healthcare Services --- .../healthcare_settings.json | 15 +++- .../healthcare_settings.py | 2 +- .../inpatient_record/inpatient_record.py | 69 +++++++++++++++---- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json index 01043867141..b33c326313d 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json @@ -17,6 +17,8 @@ "enable_free_follow_ups", "max_visits", "valid_days", + "inpatient_settings_section", + "allow_discharge_despite_unbilled_services", "healthcare_service_items", "inpatient_visit_charge_item", "op_consulting_charge_item", @@ -302,11 +304,22 @@ "fieldname": "enable_free_follow_ups", "fieldtype": "Check", "label": "Enable Free Follow-ups" + }, + { + "fieldname": "inpatient_settings_section", + "fieldtype": "Section Break", + "label": "Inpatient Settings" + }, + { + "default": "0", + "fieldname": "allow_discharge_despite_unbilled_services", + "fieldtype": "Check", + "label": "Allow Discharge Despite Unbilled Healthcare Services" } ], "issingle": 1, "links": [], - "modified": "2020-07-08 15:17:21.543218", + "modified": "2021-01-04 10:19:22.329272", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Settings", diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py index a16fceb74dd..e2ccc34a74b 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py @@ -11,7 +11,7 @@ import json class HealthcareSettings(Document): def validate(self): - for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', + for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', 'allow_discharge_despite_unbilled_services', 'lab_test_approval_required', 'create_sample_collection_for_lab_test', 'default_medical_code_standard']: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index bc769706018..6a32aca9d0c 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -from frappe.utils import today, now_datetime, getdate, get_datetime +from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form from frappe.model.document import Document from frappe.desk.reportview import get_match_cond @@ -113,6 +113,7 @@ def schedule_inpatient(args): inpatient_record.status = 'Admission Scheduled' inpatient_record.save(ignore_permissions = True) + @frappe.whitelist() def schedule_discharge(args): discharge_order = json.loads(args) @@ -126,16 +127,19 @@ def schedule_discharge(args): frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status) + def set_details_from_ip_order(inpatient_record, ip_order): for key in ip_order: inpatient_record.set(key, ip_order[key]) + def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child): for item in encounter_child: table = inpatient_record.append(inpatient_record_child) for df in table.meta.get('fields'): table.set(df.fieldname, item.get(df.fieldname)) + def check_out_inpatient(inpatient_record): if inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies: @@ -144,54 +148,88 @@ def check_out_inpatient(inpatient_record): inpatient_occupancy.check_out = now_datetime() frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") + def discharge_patient(inpatient_record): - validate_invoiced_inpatient(inpatient_record) + validate_inpatient_invoicing(inpatient_record) inpatient_record.discharge_date = today() inpatient_record.status = "Discharged" inpatient_record.save(ignore_permissions = True) -def validate_invoiced_inpatient(inpatient_record): - pending_invoices = [] + +def validate_inpatient_invoicing(inpatient_record): + if frappe.db.get_default("allow_discharge_despite_unbilled_services"): + return + + pending_invoices = get_pending_invoices(inpatient_record) + + if pending_invoices: + message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ") + + formatted_doc_rows = '' + + for doctype, docnames in pending_invoices.items(): + formatted_doc_rows += """ + {0} + {1} + """.format(doctype, docnames) + + message += """ + + + + + + {2} +
{0}{1}
+ """.format(_("Healthcare Service"), _("Documents"), formatted_doc_rows) + + frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True) + + +def get_pending_invoices(inpatient_record): + pending_invoices = {} if inpatient_record.inpatient_occupancies: service_unit_names = False for inpatient_occupancy in inpatient_record.inpatient_occupancies: - if inpatient_occupancy.invoiced != 1: + if not inpatient_occupancy.invoiced: if service_unit_names: service_unit_names += ", " + inpatient_occupancy.service_unit else: service_unit_names = inpatient_occupancy.service_unit if service_unit_names: - pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")") + pending_invoices["Inpatient Occupancy"] = service_unit_names docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"] for doc in docs: - doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record) + doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record) if doc_name_list: pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices) - if pending_invoices: - frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", " - .join(pending_invoices)), title=_('Unbilled Invoices')) + return pending_invoices + def get_pending_doc(doc, doc_name_list, pending_invoices): if doc_name_list: doc_ids = False for doc_name in doc_name_list: + doc_link = get_link_to_form(doc, doc_name.name) if doc_ids: - doc_ids += ", "+doc_name.name + doc_ids += ", " + doc_link else: - doc_ids = doc_name.name + doc_ids = doc_link if doc_ids: - pending_invoices.append(doc + " (" + doc_ids + ")") + pending_invoices[doc] = doc_ids return pending_invoices -def get_inpatient_docs_not_invoiced(doc, inpatient_record): + +def get_unbilled_inpatient_docs(doc, inpatient_record): return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient, 'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0}) + def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None): inpatient_record.admitted_datetime = check_in inpatient_record.status = 'Admitted' @@ -203,6 +241,7 @@ def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=N frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted') frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name) + def transfer_patient(inpatient_record, service_unit, check_in): item_line = inpatient_record.append('inpatient_occupancies', {}) item_line.service_unit = service_unit @@ -212,6 +251,7 @@ def transfer_patient(inpatient_record, service_unit, check_in): frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied") + def patient_leave_service_unit(inpatient_record, check_out, leave_from): if inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies: @@ -221,6 +261,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from): frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") inpatient_record.save(ignore_permissions = True) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_leave_from(doctype, txt, searchfield, start, page_len, filters): From 7206e12c2f2d9ed46d11748d9ebb098e034bba28 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 Jan 2021 12:11:00 +0530 Subject: [PATCH 137/203] test: Allow Discharge despite Unbilled Services --- .../healthcare_settings.py | 2 +- .../inpatient_record/inpatient_record.py | 2 +- .../inpatient_record/test_inpatient_record.py | 35 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py index e2ccc34a74b..a16fceb74dd 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.py @@ -11,7 +11,7 @@ import json class HealthcareSettings(Document): def validate(self): - for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', 'allow_discharge_despite_unbilled_services', + for key in ['collect_registration_fee', 'link_customer_to_patient', 'patient_name_by', 'lab_test_approval_required', 'create_sample_collection_for_lab_test', 'default_medical_code_standard']: frappe.db.set_default(key, self.get(key, "")) diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index 6a32aca9d0c..dc549a65db6 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -158,7 +158,7 @@ def discharge_patient(inpatient_record): def validate_inpatient_invoicing(inpatient_record): - if frappe.db.get_default("allow_discharge_despite_unbilled_services"): + if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"): return pending_invoices = get_pending_invoices(inpatient_record) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index 70706adb2e4..e8a9444fecd 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -40,6 +40,31 @@ 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")) + def test_allow_discharge_despite_unbilled_services(self): + frappe.db.sql("""delete from `tabInpatient Record`""") + setup_inpatient_settings() + 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()) + + # 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) + # Should not validate Pending Invoices + ip_record.discharge() + + self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) + self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) + + def test_validate_overlap_admission(self): frappe.db.sql("""delete from `tabInpatient Record`""") patient = create_patient() @@ -63,6 +88,13 @@ def mark_invoiced_inpatient_occupancy(ip_record): inpatient_occupancy.invoiced = 1 ip_record.save(ignore_permissions = True) + +def setup_inpatient_settings(): + settings = frappe.get_single("Healthcare Settings") + settings.allow_discharge_despite_unbilled_services = 1 + settings.save() + + def create_inpatient(patient): patient_obj = frappe.get_doc('Patient', patient) inpatient_record = frappe.new_doc('Inpatient Record') @@ -78,6 +110,7 @@ def create_inpatient(patient): inpatient_record.scheduled_date = today() return inpatient_record + def get_healthcare_service_unit(): service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) if not service_unit: @@ -105,6 +138,7 @@ def get_healthcare_service_unit(): return service_unit.name return service_unit + def get_service_unit_type(): service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1}) @@ -116,6 +150,7 @@ def get_service_unit_type(): return service_unit_type.name return service_unit_type + def create_patient(): patient = frappe.db.exists('Patient', '_Test IPD Patient') if not patient: From a873ae0d9f0157ce69c48a26277097eff710af9a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 4 Jan 2021 14:23:31 +0530 Subject: [PATCH 138/203] 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 319cbd2b5d5..96e181788e3 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 27fd9e4d7def5cf817773b1f4ac6168d0537767d Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 4 Jan 2021 18:18:00 +0530 Subject: [PATCH 139/203] fix: added empty value in Quality Inspection Reading status --- .../quality_inspection_reading.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index c1976dd1fb5..9baa702754f 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -130,7 +130,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Accepted\nRejected" + "options": "\nAccepted\nRejected" }, { "fieldname": "section_break_3", @@ -158,7 +158,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-11-16 16:34:29.947856", + "modified": "2021-01-04 18:16:53.978410", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From 0c883853b3acc8c22b2c44cff212b6ba13ac5b25 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Jan 2021 18:48:50 +0530 Subject: [PATCH 140/203] 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 2fc7da83896..4782a9df0f1 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 0da201c6a5d6467d3e7a392ae1bb4c616681d364 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 4 Jan 2021 19:04:05 +0530 Subject: [PATCH 141/203] fix: set company in leave allocation and leave ledger entry --- .../doctype/leave_allocation/leave_allocation.json | 12 +++++++++++- .../leave_ledger_entry/leave_ledger_entry.json | 14 +++++++++++++- erpnext/patches.txt | 1 + .../v13_0/set_company_in_leave_ledger_entry.py | 7 +++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 4b315014dae..3a300c0d632 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -11,6 +11,7 @@ "employee", "employee_name", "department", + "company", "column_break1", "leave_type", "from_date", @@ -219,6 +220,15 @@ "label": "Leave Policy Assignment", "options": "Leave Policy Assignment", "read_only": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 } ], "icon": "fa fa-ok", @@ -226,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-08-20 14:25:10.314323", + "modified": "2021-01-04 18:46:13.184104", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index 4abba5f2d4a..d74760a5cf8 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-09 15:47:39.760406", "doctype": "DocType", "engine": "InnoDB", @@ -8,6 +9,7 @@ "leave_type", "transaction_type", "transaction_name", + "company", "leaves", "column_break_7", "from_date", @@ -106,12 +108,22 @@ "fieldtype": "Link", "label": "Holiday List", "options": "Holiday List" + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2020-09-04 12:16:36.569066", + "links": [], + "modified": "2021-01-04 18:47:45.146652", "modified_by": "Administrator", "module": "HR", "name": "Leave Ledger Entry", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f2e4f72d673..923ed2f3390 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -742,3 +742,4 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.update_returned_qty_in_pr_dn +erpnext.patches.v13_0.set_company_in_leave_ledger_entry \ No newline at end of file diff --git a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py new file mode 100644 index 00000000000..66857c4e659 --- /dev/null +++ b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + frappe.reload_doc('HR', 'doctype', 'Leave Allocation') + frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry') + frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""") + frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""") \ No newline at end of file From 517fd8b9e6a59a1b0945d07950d60904041cc9d9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 Jan 2021 09:23:39 +0530 Subject: [PATCH 142/203] fix: Ignore customer and supplier while deleting company transactions (#24279) * fix: Ignore customer and supplier while deleting company transactions * fix: Test cases fixed based on Travis --- erpnext/accounts/doctype/budget/test_budget.py | 12 ++++++------ .../doctype/payroll_entry/test_payroll_entry.py | 8 ++++---- .../doctype/salary_slip/test_salary_slip.py | 17 +++++++++-------- .../company/delete_company_transactions.py | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 0f115f9cc20..cd88b117614 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -159,10 +159,10 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Cost Center") month = now_datetime().month - if month > 10: - month = 10 + if month > 9: + month = 9 - for i in range(month): + for i in range(month+1): jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) @@ -181,10 +181,10 @@ class TestBudget(unittest.TestCase): budget = make_budget(budget_against="Project") month = now_datetime().month - if month > 10: - month = 10 + if month > 9: + month = 9 - for i in range(month): + for i in range(month + 1): 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") diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 54106c8d166..e098ec79b0f 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -22,7 +22,7 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) - make_deduction_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"]) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) @@ -107,9 +107,9 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC") - - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) + currency=frappe.db.get_value("Company", "_Test Company", "default_currency") + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) + make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index bb310c4d873..d6fb4195988 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -585,14 +585,6 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "amount": 200, "exempted_from_income_tax": 1 - }, - { - "salary_component": 'TDS', - "abbr":'T', - "type": "Deduction", - "depends_on_payment_days": 0, - "variable_based_on_taxable_salary": 1, - "round_to_the_nearest_integer": 1 } ] if not test_tax: @@ -603,6 +595,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "type": "Deduction", "round_to_the_nearest_integer": 1 }) + else: + data.append({ + "salary_component": 'TDS', + "abbr":'T', + "type": "Deduction", + "depends_on_payment_days": 0, + "variable_based_on_taxable_salary": 1, + "round_to_the_nearest_integer": 1 + }) if setup or test_tax: make_salary_component(data, test_tax, company_list) diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 566f20cfa12..7a72fe31023 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -28,7 +28,7 @@ def delete_company_transactions(company_name): "Party Account", "Employee", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", "POS Profile", "BOM", "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", - "Item Default"): + "Item Default", "Customer", "Supplier"): delete_for_doctype(doctype, company_name) # reset company values From 06a401ffbfa812452c9aafc4fca5b2b5d3702554 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 5 Jan 2021 11:54:34 +0530 Subject: [PATCH 143/203] fix: incoming rate attribute error (#24287) Co-authored-by: pateljannat --- erpnext/controllers/selling_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 85cfb951fcc..812021f5c86 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -233,7 +233,7 @@ class SellingController(StockController): 'allow_zero_valuation': d.allow_zero_valuation_rate, 'sales_invoice_item': d.get("sales_invoice_item"), 'dn_detail': d.get("dn_detail"), - 'incoming_rate': p.incoming_rate + 'incoming_rate': p.get("incoming_rate") })) else: il.append(frappe._dict({ @@ -252,7 +252,7 @@ class SellingController(StockController): 'allow_zero_valuation': d.allow_zero_valuation_rate, 'sales_invoice_item': d.get("sales_invoice_item"), 'dn_detail': d.get("dn_detail"), - 'incoming_rate': d.incoming_rate + 'incoming_rate': d.get("incoming_rate") })) return il From 16a809483b69b20403aefc679f39a2018d187ee6 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 5 Jan 2021 12:28:45 +0530 Subject: [PATCH 144/203] fix: indentation --- erpnext/selling/doctype/sales_order/sales_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ee87afd673f..9a3c260a2aa 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -830,7 +830,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - doc = get_mapped_doc("Sales Order", source_name, { + doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { "doctype": "Purchase Order", "field_no_map": [ @@ -1087,4 +1087,4 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item): if not total_produced_qty and frappe.flags.in_patch: return - frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) \ No newline at end of file + frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) From 1f591ab02e40ab884899059ae95f7d86315da83b Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 Jan 2021 13:53:51 +0530 Subject: [PATCH 145/203] fix(e-invoicing): minor calculation fixes (#24285) --- .../controllers/sales_and_purchase_return.py | 2 + erpnext/public/js/controllers/transaction.js | 1 + erpnext/regional/india/e_invoice/utils.py | 41 +++++++++++++------ erpnext/stock/get_item_details.py | 4 +- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 79792262c0a..a048d6e2dfb 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -328,6 +328,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail target_doc.purchase_invoice_item = source_doc.name + target_doc.price_list_rate = 0 elif doctype == "Delivery Note": returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) @@ -353,6 +354,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.dn_detail = source_doc.dn_detail target_doc.expense_account = source_doc.expense_account target_doc.sales_invoice_item = source_doc.name + target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3bc20f87336..bed9c14141e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -543,6 +543,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ company: me.frm.doc.company, order_type: me.frm.doc.order_type, is_pos: cint(me.frm.doc.is_pos), + is_return: cint(me.frm.doc.is_return), is_subcontracted: me.frm.doc.is_subcontracted, transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 02ce6c14c90..e5f7d2d78c8 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -92,21 +92,18 @@ def get_party_details(address_name): location = gstin_details.get('AddrLoc') or address.get('city') state_code = gstin_details.get('StateCode') pincode = gstin_details.get('AddrPncd') - address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) - address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) - email_id = address.get('email_id') - phone = address.get('phone') - # get last 10 digit - phone = phone.replace(" ", "")[-10:] if phone else '' + address_line1 = '{} {}'.format(gstin_details.get('AddrBno') or "", gstin_details.get('AddrFlno') or "") + address_line2 = '{} {}'.format(gstin_details.get('AddrBnm') or "", gstin_details.get('AddrSt') or "") if state_code == 97: # according to einvoice standard pincode = 999999 return frappe._dict(dict( - gstin=gstin, legal_name=legal_name, location=location, - pincode=pincode, state_code=state_code, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone + gstin=gstin, legal_name=legal_name, + location=location, pincode=pincode, + state_code=state_code, address_line1=address_line1, + address_line2=address_line2 )) def get_gstin_details(gstin): @@ -146,9 +143,10 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.discount_amount = abs(item.discount_amount * item.qty) - item.description = d.item_name + item.description = d.item_name.replace('"', '\\"') + item.qty = abs(item.qty) + item.discount_amount = abs(item.discount_amount * item.qty) item.unit_rate = abs(item.base_amount / item.qty) item.gross_amount = abs(item.base_amount) item.taxable_value = abs(item.base_amount) @@ -156,6 +154,7 @@ def get_item_list(invoice): item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' + item.serial_no = "" item = update_item_taxes(invoice, item) @@ -272,7 +271,25 @@ def get_eway_bill_details(invoice): vehicle_type=vehicle_type[invoice.gst_vehicle_type] )) +def validate_mandatory_fields(invoice): + if not invoice.company_address: + frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields')) + if not invoice.customer_address: + frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields')) + if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): + frappe.throw( + _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), + title=_('Missing Fields') + ) + if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'): + frappe.throw( + _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'), + title=_('Missing Fields') + ) + def make_einvoice(invoice): + validate_mandatory_fields(invoice) + schema = read_json('einv_template') transaction_details = get_transaction_details(invoice) @@ -351,7 +368,7 @@ def validate_einvoice(validations, einvoice, errors=[]): # remove empty dicts einvoice.pop(fieldname, None) continue - + # convert to int or str if value_type == 'string': einvoice[fieldname] = str(value) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 08f7a83b893..bf45251c9d8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -74,7 +74,9 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) - get_price_list_rate(args, item, out) + if not doc or cint(doc.get('is_return')) == 0: + # get price list rate only if the invoice is not a credit or debit note + get_price_list_rate(args, item, out) if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args)) From a56a5ccefa48e4764d058e3e0b46b99f22ea7282 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 Jan 2021 15:59:17 +0530 Subject: [PATCH 146/203] refactor: fetch & validate address from erpnext rather than gst portal (#24297) --- erpnext/regional/india/e_invoice/utils.py | 54 ++++++++++++++--------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e5f7d2d78c8..abe15043af8 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -15,7 +15,7 @@ from frappe import _, bold from pyqrcode import create as qrcreate from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply -from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date +from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form def validate_einvoice_fields(doc): einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) @@ -84,26 +84,32 @@ def get_doc_details(invoice): )) def get_party_details(address_name): - address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - gstin = address.get('gstin') + d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] - gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName') - location = gstin_details.get('AddrLoc') or address.get('city') - state_code = gstin_details.get('StateCode') - pincode = gstin_details.get('AddrPncd') - address_line1 = '{} {}'.format(gstin_details.get('AddrBno') or "", gstin_details.get('AddrFlno') or "") - address_line2 = '{} {}'.format(gstin_details.get('AddrBnm') or "", gstin_details.get('AddrSt') or "") + if (not d.gstin + or not d.city + or not d.pincode + or not d.address_title + or not d.address_line1 + or not d.gst_state_number): - if state_code == 97: + frappe.throw( + msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format( + get_link_to_form('Address', address_name) + ), + title=_('Missing Address Fields') + ) + + if d.gst_state_number == 97: # according to einvoice standard pincode = 999999 return frappe._dict(dict( - gstin=gstin, legal_name=legal_name, - location=location, pincode=pincode, - state_code=state_code, address_line1=address_line1, - address_line2=address_line2 + gstin=d.gstin, legal_name=d.address_title, + location=d.city, pincode=d.pincode, + state_code=d.gst_state_number, + address_line1=d.address_line1, + address_line2=d.address_line2 )) def get_gstin_details(gstin): @@ -124,14 +130,22 @@ def get_gstin_details(gstin): return GSPConnector.get_gstin_details(gstin) def get_overseas_address_details(address_name): - address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( - 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] + address_title, address_line1, address_line2, city = frappe.db.get_value( + 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] ) + if not address_title or not address_line1 or not city: + frappe.throw( + msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format( + get_link_to_form('Address', address_name) + ), + title=_('Missing Address Fields') + ) + return frappe._dict(dict( - gstin='URP', legal_name=address_title, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone, - pincode=999999, state_code=96, place_of_supply=96, location=city + gstin='URP', legal_name=address_title, location=city, + address_line1=address_line1, address_line2=address_line2, + pincode=999999, state_code=96, place_of_supply=96 )) def get_item_list(invoice): From b01b108dfa7baf53562f361a69657fe2ec1fc981 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 5 Jan 2021 17:34:16 +0530 Subject: [PATCH 147/203] fix: do not consider current salary slip in sum --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 99d8a8317cd..3bb1f62b08e 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1145,7 +1145,9 @@ class SalarySlip(TransactionBase): fields = ['sum(net_pay) as sum'], filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', period_start_date], - 'end_date' : ['<', period_end_date]}) + 'end_date' : ['<', period_end_date], + 'name': ['!=', self.name] + }) year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 @@ -1160,7 +1162,8 @@ class SalarySlip(TransactionBase): fields = ['sum(net_pay) as sum'], filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', first_day_of_the_month], - 'end_date' : ['<', self.start_date] + 'end_date' : ['<', self.start_date], + 'name': ['!=', self.name] }) month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 From f7b9b0687ead28f180839c19fe04a02f6829be35 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 Jan 2021 20:43:11 +0530 Subject: [PATCH 148/203] fix: tax calculation on salary slip for the first month (#24272) * fix: tax calculation on salary slip for the first month * fix: net pay precision issue * fix: net pay precision issue Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> --- erpnext/hr/doctype/employee/employee.json | 3 +- .../doctype/salary_slip/salary_slip.js | 1 - .../doctype/salary_slip/salary_slip.py | 46 +++++++++++-------- .../doctype/salary_slip/test_salary_slip.py | 2 +- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4f1c04ff5d0..dc2aaa4a067 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-16 15:02:04.283657", + "modified": "2021-01-01 16:54:33.477439", "modified_by": "Administrator", "module": "HR", "name": "Employee", @@ -855,7 +855,6 @@ "write": 1 } ], - "quick_entry": 1, "search_fields": "employee_name", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 8e05bb2057e..51fb3596e9b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -151,7 +151,6 @@ frappe.ui.form.on("Salary Slip", { var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); - calculate_totals(frm); frm.trigger("set_dynamic_labels"); }, diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 3bb1f62b08e..d725f68a6bd 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -143,8 +143,8 @@ class SalarySlip(TransactionBase): self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.set_time_sheet() self.pull_sal_struct() - payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"]) - return [payroll_based_on, consider_unmarked_attendance_as] + ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1) + return [ps.payroll_based_on, ps.consider_unmarked_attendance_as] def set_time_sheet(self): if self.salary_slip_based_on_timesheet: @@ -424,16 +424,19 @@ class SalarySlip(TransactionBase): def calculate_net_pay(self): if self.salary_structure: self.calculate_component_amounts("earnings") - self.gross_pay = self.get_component_totals("earnings") + self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1) self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) if self.salary_structure: self.calculate_component_amounts("deductions") + + self.set_loan_repayment() + self.set_component_amounts_based_on_payment_days() + self.set_net_pay() + + def set_net_pay(self): self.total_deduction = self.get_component_totals("deductions") self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) - - self.set_loan_repayment() - self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.rounded_total = rounded(self.net_pay) self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) @@ -455,8 +458,6 @@ class SalarySlip(TransactionBase): else: self.add_tax_components(payroll_period) - self.set_component_amounts_based_on_payment_days(component_type) - def add_structure_components(self, component_type): data = self.get_data_for_eval() for struct_row in self._salary_structure_doc.get(component_type): @@ -813,7 +814,7 @@ class SalarySlip(TransactionBase): cint(row.depends_on_payment_days) and cint(self.total_working_days) and (not self.salary_slip_based_on_timesheet or getdate(self.start_date) < joining_date or - getdate(self.end_date) > relieving_date + (relieving_date and getdate(self.end_date) > relieving_date) )): additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days) / cint(self.total_working_days)), row.precision("additional_amount")) @@ -946,15 +947,21 @@ class SalarySlip(TransactionBase): struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary return struct_row - def get_component_totals(self, component_type): + def get_component_totals(self, component_type, depends_on_payment_days=0): + joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, + ["date_of_joining", "relieving_date"]) + total = 0.0 for d in self.get(component_type): if not d.do_not_include_in_total: - d.amount = flt(d.amount, d.precision("amount")) - total += d.amount + if depends_on_payment_days: + amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] + else: + amount = flt(d.amount, d.precision("amount")) + total += amount return total - def set_component_amounts_based_on_payment_days(self, component_type): + def set_component_amounts_based_on_payment_days(self): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) @@ -964,8 +971,9 @@ class SalarySlip(TransactionBase): if not joining_date: frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) - for d in self.get(component_type): - d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] + for component_type in ("earnings", "deductions"): + for d in self.get(component_type): + d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount")) def set_loan_repayment(self): self.total_loan_repayment = 0 @@ -1089,17 +1097,17 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() def set_totals(self): - self.gross_pay = 0 + self.gross_pay = 0.0 if self.salary_slip_based_on_timesheet == 1: self.calculate_total_for_salary_slip_based_on_timesheet() else: - self.total_deduction = 0 + self.total_deduction = 0.0 if self.earnings: for earning in self.earnings: - self.gross_pay += flt(earning.amount) + self.gross_pay += flt(earning.amount, earning.precision("amount")) if self.deductions: for deduction in self.deductions: - self.total_deduction += flt(deduction.amount) + self.total_deduction += flt(deduction.amount, deduction.precision("amount")) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) self.set_base_totals() diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index d6fb4195988..4368c03c2ae 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -318,7 +318,7 @@ class TestSalarySlip(unittest.TestCase): year_to_date = 0 for slip in salary_slips: - year_to_date += slip.net_pay + year_to_date += flt(slip.net_pay) self.assertEqual(slip.year_to_date, year_to_date) def test_tax_for_payroll_period(self): From 5eef19723d9bc139f2ef1cad03c17f6c42b17b2f Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Tue, 5 Jan 2021 18:47:11 +0100 Subject: [PATCH 149/203] Update item_tax_template_dashboard.py added missing backlink. In Item Groups I can set Item Tax Temple in Table taxes. --- .../doctype/item_tax_template/item_tax_template_dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py index acc308e0e68..3d80a9785f0 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py @@ -20,7 +20,8 @@ def get_data(): 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] }, { - 'items': ['Item'] + 'label': _('Stock'), + 'items': ['Item Groups', 'Item'] } ] } From dd768a07c5ff7d4efa243351f2a1fb1be23b044e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 5 Jan 2021 23:55:00 +0530 Subject: [PATCH 150/203] fix: Sanctioned loan security unpledge --- .../loan_management/doctype/loan/test_loan.py | 21 +++++++++++++++++++ .../loan_security_unpledge.py | 12 ++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 8b1f9a2266f..2abd7d84d97 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -362,6 +362,27 @@ class TestLoan(unittest.TestCase): unpledge_request.load_from_db() self.assertEqual(unpledge_request.docstatus, 1) + def test_santined_loan_security_unpledge(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + unpledge_map = {'Test Security 1': 4000} + unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_request.submit() + unpledge_request.status = 'Approved' + unpledge_request.save() + unpledge_request.submit() + def test_disbursal_check_with_shortfall(self): pledges = [{ "loan_security": "Test Security 2", diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 61c418d3d31..ae88a07e251 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -44,10 +44,16 @@ class LoanSecurityUnpledge(Document): "valid_upto": (">=", get_datetime()) }, as_list=1)) - total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', - 'total_interest_payable', 'written_off_amount']) + loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1) + + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.interest_payable) \ + - flt(loan_details.principal_paid) - flt(loan_details.written_off_amount) + else: + pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount) - pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount) security_value = 0 unpledge_qty_map = {} ltv_ratio = 0 From 05fe7ac29cde423616f15b05643705d4bab026f0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Jan 2021 09:10:28 +0530 Subject: [PATCH 151/203] fix: fieldname --- .../doctype/loan_security_unpledge/loan_security_unpledge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index ae88a07e251..c4c2d683780 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -48,8 +48,8 @@ class LoanSecurityUnpledge(Document): 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1) if loan_details.status == 'Disbursed': - pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.interest_payable) \ - - flt(loan_details.principal_paid) - flt(loan_details.written_off_amount) + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount) else: pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \ - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount) From ad8be7c1fedd6bc41afaef23f86b44bffa3a9a1f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Jan 2021 09:29:03 +0530 Subject: [PATCH 152/203] fix: Consider only submitted salary slips --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index d725f68a6bd..47c9d31bf4b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -429,11 +429,11 @@ class SalarySlip(TransactionBase): if self.salary_structure: self.calculate_component_amounts("deductions") - + self.set_loan_repayment() self.set_component_amounts_based_on_payment_days() self.set_net_pay() - + def set_net_pay(self): self.total_deduction = self.get_component_totals("deductions") self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) @@ -1154,10 +1154,10 @@ class SalarySlip(TransactionBase): filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', period_start_date], 'end_date' : ['<', period_end_date], - 'name': ['!=', self.name] + 'name': ['!=', self.name], + 'docstatus': 1 }) - year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 year_to_date += self.net_pay @@ -1171,7 +1171,8 @@ class SalarySlip(TransactionBase): filters = {'employee_name' : self.employee_name, 'start_date' : ['>=', first_day_of_the_month], 'end_date' : ['<', self.start_date], - 'name': ['!=', self.name] + 'name': ['!=', self.name], + 'docstatus': 1 }) month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 From 5a579089c2ba5fa6ffb00538bcee66246e4f07d2 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 6 Jan 2021 11:21:13 +0530 Subject: [PATCH 153/203] fix: indentation --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 9a3c260a2aa..e5a8a7196cc 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -830,7 +830,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) for supplier in suppliers: - doc = get_mapped_doc("Sales Order", source_name, { + doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { "doctype": "Purchase Order", "field_no_map": [ From e7fa6f6a1cb571544708f30d6456891132c27115 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 6 Jan 2021 13:15:30 +0530 Subject: [PATCH 154/203] fix: edditable employee grid --- .../doctype/payroll_entry/payroll_entry.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index a25a6e7a32c..6bcd4e0c006 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -21,6 +21,9 @@ class PayrollEntry(Document): if cint(entries) == len(self.employees): self.set_onload("submitted_ss", True) + def validate(self): + self.number_of_employees = len(self.employees) + def on_submit(self): self.create_salary_slips() @@ -113,7 +116,7 @@ class PayrollEntry(Document): for d in employees: self.append('employees', d) - self.number_of_employees = len(employees) + self.number_of_employees = len(self.employees) if self.validate_attendance: return self.validate_employee_attendance() @@ -145,8 +148,8 @@ class PayrollEntry(Document): """ self.check_permission('write') self.created = 1 - emp_list = [d.employee for d in self.get_emp_list()] - if emp_list: + employees = [emp.employee for emp in self.employees] + if employees: args = frappe._dict({ "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, "payroll_frequency": self.payroll_frequency, @@ -160,10 +163,10 @@ class PayrollEntry(Document): "exchange_rate": self.exchange_rate, "currency": self.currency }) - if len(emp_list) > 30: - frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) + if len(employees) > 30: + frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args) else: - create_salary_slips_for_employees(emp_list, args, publish_progress=False) + create_salary_slips_for_employees(employees, args, publish_progress=False) # since this method is called via frm.call this doc needs to be updated manually self.reload() From fd5ebe9d4a757c5ded79b2cc2e698695e21f2ce8 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 7 Jan 2021 15:16:15 +0530 Subject: [PATCH 155/203] fix: patch and columns --- erpnext/patches/v13_0/update_project_template_tasks.py | 9 +++++++-- .../project_template_task/project_template_task.json | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index f24a2c62f1f..886616e0caf 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -8,6 +8,8 @@ def execute(): frappe.reload_doc("projects", "doctype", "project_template") frappe.reload_doc("projects", "doctype", "project_template_task") frappe.reload_doc("projects", "doctype", "project_template") + frappe.reload_doc("projects", "doctype", "task") + for template_name in frappe.db.sql(""" select name @@ -30,11 +32,14 @@ def execute(): description = task.description, is_template = 1 )).insert() - new_tasks.append(new_task.name) + print(new_task) + new_tasks.append(new_task) if replace_tasks: template.tasks = [] for tsk in new_tasks: + print(tsk.name, tsk.subject) template.append("tasks", { - "task": tsk + "task": tsk.name, + "subject": tsk.subject }) template.save() \ No newline at end of file diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json index 7a552945bd5..69530b15b40 100644 --- a/erpnext/projects/doctype/project_template_task/project_template_task.json +++ b/erpnext/projects/doctype/project_template_task/project_template_task.json @@ -10,6 +10,7 @@ ], "fields": [ { + "columns": 2, "fieldname": "task", "fieldtype": "Link", "in_list_view": 1, @@ -18,6 +19,7 @@ "reqd": 1 }, { + "columns": 6, "fieldname": "subject", "fieldtype": "Read Only", "in_list_view": 1, @@ -26,7 +28,7 @@ ], "istable": 1, "links": [], - "modified": "2020-12-28 12:10:26.321913", + "modified": "2021-01-07 15:13:40.995071", "modified_by": "Administrator", "module": "Projects", "name": "Project Template Task", From fab080c875f7542fb4f989a77a8bbbb2e791db8b Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 7 Jan 2021 15:25:39 +0530 Subject: [PATCH 156/203] fix: Company Wise Valuation Rate for RM in BOM --- erpnext/manufacturing/doctype/bom/bom.js | 17 ++++++++----- erpnext/manufacturing/doctype/bom/bom.py | 24 +++++++++++++++---- .../doctype/work_order/work_order.py | 1 + 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 1c4b7a1e1ce..15affd84e10 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -411,7 +411,7 @@ cur_frm.cscript.hour_rate = function(doc) { cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate; -cur_frm.cscript.bom_no = function(doc, cdt, cdn) { +cur_frm.cscript.bom_no = function(doc, cdt, cdn) { get_bom_material_detail(doc, cdt, cdn, false); }; @@ -419,17 +419,22 @@ cur_frm.cscript.is_default = function(doc) { if (doc.is_default) cur_frm.set_value("is_active", 1); }; -var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { +var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { + if (!doc.company) { + frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")}); + } + var d = locals[cdt][cdn]; if (d.item_code) { return frappe.call({ doc: doc, method: "get_bom_material_detail", args: { - 'item_code': d.item_code, - 'bom_no': d.bom_no != null ? d.bom_no: '', + "company": doc.company, + "item_code": d.item_code, + "bom_no": d.bom_no != null ? d.bom_no: '', "scrap_items": scrap_items, - 'qty': d.qty, + "qty": d.qty, "stock_qty": d.stock_qty, "include_item_in_manufacturing": d.include_item_in_manufacturing, "uom": d.uom, @@ -468,7 +473,7 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { } if (d.bom_no) { - frappe.msgprint(__("You can not change rate if BOM mentioned agianst any item")); + frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item.")); get_bom_material_detail(doc, cdt, cdn, scrap_items); } else { erpnext.bom.calculate_rm_cost(doc); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6363242b0a6..03beedb6635 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -65,6 +65,10 @@ class BOM(WebsiteGenerator): def validate(self): self.route = frappe.scrub(self.name).replace('_', '-') + + if not self.company: + frappe.throw(_("Please select a Company first."), title=_("Mandatory")) + self.clear_operations() self.validate_main_item() self.validate_currency() @@ -125,6 +129,7 @@ class BOM(WebsiteGenerator): self.validate_bom_currecny(item) ret = self.get_bom_material_detail({ + "company": self.company, "item_code": item.item_code, "item_name": item.item_name, "bom_no": item.bom_no, @@ -213,6 +218,7 @@ class BOM(WebsiteGenerator): for d in self.get("items"): rate = self.get_rm_rate({ + "company": self.company, "item_code": d.item_code, "bom_no": d.bom_no, "qty": d.qty, @@ -611,10 +617,20 @@ def get_valuation_rate(args): """ Get weighted average of valuation rate from all warehouses """ total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 - for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` - where item_code=%s""", args['item_code'], as_dict=1): - total_qty += flt(d.actual_qty) - total_value += flt(d.stock_value) + item_bins = frappe.db.sql(""" + select + bin.actual_qty, bin.stock_value + from + `tabBin` bin, `tabWarehouse` warehouse + where + bin.item_code=%(item)s + and bin.warehouse = warehouse.name + and warehouse.company=%(company)s""", + {"item": args['item_code'], "company": args['company']}, as_dict=1) + + for d in item_bins: + total_qty += flt(d.actual_qty) + total_value += flt(d.stock_value) if total_qty: valuation_rate = total_value / total_qty diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index cc93bf9fd63..8e7fac8ce88 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -725,6 +725,7 @@ def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): args.update(item_data) args["rate"] = get_bom_item_rate({ + "company": wo_doc.company, "item_code": args.get("item_code"), "qty": args.get("required_qty"), "uom": args.get("stock_uom"), From ff6ee9d4e712ac1d573e010c5a36193abacbcf50 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 Jan 2021 09:14:43 +0530 Subject: [PATCH 157/203] fix: Formula field description and Rearrange grid view - Missing closing quote in Formula field description - In grid view of child table in QI, show only input fields --- .../item_quality_inspection_parameter.json | 4 ++-- .../quality_inspection_reading.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json index fc06e89f2fb..3e81619cfd1 100644 --- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json +++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json @@ -43,7 +43,7 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
\nNumeric eg. 2: mean > 3.5 (mean of populated fields)
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
\nNumeric eg. 2: mean > 3.5 (mean of populated fields)
\nValue based eg.: reading_value in (\"A\", \"B\", \"C\")", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -80,7 +80,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-28 17:41:04.350225", + "modified": "2021-01-07 21:32:49.866439", "modified_by": "Administrator", "module": "Stock", "name": "Item Quality Inspection Parameter", diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json index 739845bcdac..dddb3d517dd 100644 --- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json +++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json @@ -49,7 +49,6 @@ "depends_on": "eval:(!doc.formula_based_criteria && doc.non_numeric)", "fieldname": "value", "fieldtype": "Data", - "in_list_view": 1, "label": "Acceptance Criteria Value", "oldfieldname": "value", "oldfieldtype": "Data" @@ -76,7 +75,6 @@ "columns": 1, "fieldname": "reading_3", "fieldtype": "Data", - "in_list_view": 1, "label": "Reading 3", "oldfieldname": "reading_3", "oldfieldtype": "Data" @@ -153,7 +151,7 @@ }, { "depends_on": "formula_based_criteria", - "description": "Simple Python formula applied on Reading fields.
Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
\nNumeric eg. 2: mean > 3.5 (mean of populated fields)
\nValue based eg.: reading_value in (\"A\", \"B\", \"C)", + "description": "Simple Python formula applied on Reading fields.
Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
\nNumeric eg. 2: mean > 3.5 (mean of populated fields)
\nValue based eg.: reading_value in (\"A\", \"B\", \"C\")", "fieldname": "acceptance_formula", "fieldtype": "Code", "label": "Acceptance Criteria Formula" @@ -190,6 +188,7 @@ "depends_on": "non_numeric", "fieldname": "reading_value", "fieldtype": "Data", + "in_list_view": 1, "label": "Reading Value" }, { @@ -202,6 +201,7 @@ "default": "0", "fieldname": "non_numeric", "fieldtype": "Check", + "in_list_view": 1, "label": "Non-Numeric" }, { @@ -215,7 +215,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-12-28 17:40:47.407210", + "modified": "2021-01-07 21:56:40.235579", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection Reading", From c4963bfdb24819e2edd4a28c11551190965d9cd1 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 Jan 2021 09:56:04 +0530 Subject: [PATCH 158/203] fix: Back Update from QC based on Batch No --- .../quality_inspection/quality_inspection.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index ae4eb9b9956..b30d48d5475 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -69,11 +69,21 @@ class QualityInspection(Document): doctype = 'Stock Entry Detail' if self.reference_type and self.reference_name: + conditions = "" + if self.batch_no: + conditions += " and t1.batch_no = '%s'"%(self.batch_no) + frappe.db.sql(""" - UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 - SET t1.quality_inspection = %s, t2.modified = %s - WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name - """.format(parent_doc=self.reference_type, child_doc=doctype), + UPDATE + `tab{child_doc}` t1, `tab{parent_doc}` t2 + SET + t1.quality_inspection = %s, t2.modified = %s + WHERE + t1.parent = %s + and t1.item_code = %s + and t1.parent = t2.name + {conditions} + """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions), (quality_inspection, self.modified, self.reference_name, self.item_code)) def set_status_based_on_acceptance_formula(self): From a93151502c5f27b8492449d83d95eba25ff2887e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 8 Jan 2021 12:10:26 +0530 Subject: [PATCH 159/203] fix: Components formulated from additional salary not being fetched in Payroll Entry --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 47c9d31bf4b..183ad13411a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -577,7 +577,7 @@ class SalarySlip(TransactionBase): 'default_amount': amount if not struct_row.get("is_additional_component") else 0, 'depends_on_payment_days' : struct_row.depends_on_payment_days, 'salary_component' : struct_row.salary_component, - 'abbr' : struct_row.abbr, + 'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"), 'additional_salary': additional_salary, 'do_not_include_in_total' : struct_row.do_not_include_in_total, 'is_tax_applicable': struct_row.is_tax_applicable, From 3777c6aa381f35af5359039db211875c784dab50 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 8 Jan 2021 12:36:51 +0530 Subject: [PATCH 160/203] fix: payment entry multi-currency issue --- .../accounts/doctype/payment_entry/payment_entry.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index e1174717382..9bdd26b805b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -401,6 +401,8 @@ frappe.ui.form.on('Payment Entry', { set_account_currency_and_balance: function(frm, account, currency_field, balance_field, callback_function) { + + var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.posting_date && account) { frappe.call({ method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details", @@ -427,6 +429,14 @@ frappe.ui.form.on('Payment Entry', { if(!frm.doc.paid_amount && frm.doc.received_amount) frm.events.received_amount(frm); + + if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency + && frm.doc.paid_amount != frm.doc.received_amount) { + if (company_currency != frm.doc.paid_from_account_currency && + frm.doc.payment_type == "Pay") { + frm.doc.paid_amount = frm.doc.received_amount; + } + } } }, () => { From b7637f49cd543e8a4fdbfc0d840323583d5c3651 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 8 Jan 2021 18:35:49 +0530 Subject: [PATCH 161/203] fix: Remove QI link on cancel wherever same QI name exists --- .../stock/doctype/quality_inspection/quality_inspection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index b30d48d5475..2084e3fa545 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -70,9 +70,12 @@ class QualityInspection(Document): if self.reference_type and self.reference_name: conditions = "" - if self.batch_no: + if self.batch_no and self.docstatus == 1: conditions += " and t1.batch_no = '%s'"%(self.batch_no) + if self.docstatus == 2: # if cancel, then remove qi link wherever same name + conditions += " and t1.quality_inspection = '%s'"%(self.name) + frappe.db.sql(""" UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 From 2ffa4b9ce4614273f06bc74aa62e3c85ca304712 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 8 Jan 2021 20:58:00 +0530 Subject: [PATCH 162/203] 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 4c2edf4f03a..b28682184ef 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 163/203] 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 f157f0df8f0..185bf7a6663 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 d17f5af4907..7d7992d40ae 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 46a64405539..85e008ac293 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 415ba993c7b..ac30c91b670 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 164/203] 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 00000000000..e69de29bb2d 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 00000000000..852e3ca366f --- /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 00000000000..321d6064e33 --- /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 00000000000..e039fe82d38 --- /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 2460e101a70f7eb1ef91a30984b2f32f0b78ceff Mon Sep 17 00:00:00 2001 From: pateljannat Date: Mon, 11 Jan 2021 10:05:54 +0530 Subject: [PATCH 165/203] fix: indicator ofor template task --- erpnext/patches/v13_0/update_project_template_tasks.py | 3 +-- erpnext/projects/doctype/task/task_list.js | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 886616e0caf..5fa062306cf 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -32,12 +32,11 @@ def execute(): description = task.description, is_template = 1 )).insert() - print(new_task) new_tasks.append(new_task) + if replace_tasks: template.tasks = [] for tsk in new_tasks: - print(tsk.name, tsk.subject) template.append("tasks", { "task": tsk.name, "subject": tsk.subject diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js index 941fe975468..39734ee8b1c 100644 --- a/erpnext/projects/doctype/task/task_list.js +++ b/erpnext/projects/doctype/task/task_list.js @@ -20,7 +20,8 @@ frappe.listview_settings['Task'] = { "Pending Review": "orange", "Working": "orange", "Completed": "green", - "Cancelled": "dark grey" + "Cancelled": "dark grey", + "Template": "blue" } return [__(doc.status), colors[doc.status], "status,=," + doc.status]; }, From aff3f611d35dd5c6538a16e4bcebf32c2f06d138 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 11 Jan 2021 12:48:11 +0530 Subject: [PATCH 166/203] fix: allow medication entries to be deleted from the table --- .../inpatient_medication_entry.js | 1 + .../inpatient_medication_entry.json | 3 +-- .../inpatient_medication_entry.py | 18 ------------------ 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js index ca97489b8d8..a7b06b1718b 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js @@ -5,6 +5,7 @@ frappe.ui.form.on('Inpatient Medication Entry', { refresh: function(frm) { // Ignore cancellation of doctype on cancel all frm.ignore_doctypes_on_cancel_all = ['Stock Entry']; + frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide(); frm.set_query('item_code', () => { return { diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json index dd4c423a9e0..b1a6ee4ed14 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json @@ -139,7 +139,6 @@ "fieldtype": "Table", "label": "Inpatient Medication Orders", "options": "Inpatient Medication Entry Detail", - "read_only": 1, "reqd": 1 }, { @@ -180,7 +179,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-03 13:22:37.820707", + "modified": "2021-01-11 12:37:46.749659", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Medication Entry", diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index 70ae7138662..bba521313df 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -15,8 +15,6 @@ class InpatientMedicationEntry(Document): self.validate_medication_orders() def get_medication_orders(self): - self.validate_datetime_filters() - # pull inpatient medication orders based on selected filters orders = get_pending_medication_orders(self) @@ -27,22 +25,6 @@ class InpatientMedicationEntry(Document): self.set('medication_orders', []) frappe.msgprint(_('No pending medication orders found for selected criteria')) - def validate_datetime_filters(self): - if self.from_date and self.to_date: - self.validate_from_to_dates('from_date', 'to_date') - - if self.from_date and getdate(self.from_date) > getdate(): - frappe.throw(_('From Date cannot be after the current date.')) - - if self.to_date and getdate(self.to_date) > getdate(): - frappe.throw(_('To Date cannot be after the current date.')) - - if self.from_time and self.from_time > nowtime(): - frappe.throw(_('From Time cannot be after the current time.')) - - if self.to_time and self.to_time > nowtime(): - frappe.throw(_('To Time cannot be after the current time.')) - def add_mo_to_table(self, orders): # Add medication orders in the child table self.set('medication_orders', []) From dcda8b9e8cfaac31056792ff3ab9a70975c2727a Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Mon, 11 Jan 2021 12:50:39 +0530 Subject: [PATCH 167/203] feat: Patient appointment status changes (#24201) * feat: patient appointment status changes * fix: sider * fix: sider * fix: test status on cancel of docs and test refactor Co-authored-by: pateljannat Co-authored-by: Rucha Mahabal --- .../patient_appointment/test_patient_appointment.py | 4 +++- .../doctype/therapy_plan/test_therapy_plan.py | 13 +++++++++++-- .../healthcare/doctype/therapy_plan/therapy_plan.py | 3 ++- .../doctype/therapy_session/therapy_session.js | 9 +++++++++ .../doctype/therapy_session/therapy_session.py | 7 +++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 3df7ba15314..b681ed1a226 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -23,8 +23,10 @@ class TestPatientAppointment(unittest.TestCase): self.assertEquals(appointment.status, 'Open') appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2)) self.assertEquals(appointment.status, 'Scheduled') - create_encounter(appointment) + encounter = create_encounter(appointment) self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') + encounter.cancel() + self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') def test_start_encounter(self): patient, medical_department, practitioner = create_healthcare_docs() diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py index a061c66a54d..7fb159d6b50 100644 --- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py @@ -5,10 +5,10 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import getdate, flt +from frappe.utils import getdate, flt, nowdate from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice -from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment class TestTherapyPlan(unittest.TestCase): def test_creation_on_encounter_submission(self): @@ -28,6 +28,15 @@ class TestTherapyPlan(unittest.TestCase): frappe.get_doc(session).submit() self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') + patient, medical_department, practitioner = create_healthcare_docs() + appointment = create_appointment(patient, practitioner, nowdate()) + session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name) + session = frappe.get_doc(session) + session.submit() + self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed') + session.cancel() + self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open') + def test_therapy_plan_from_template(self): patient = create_patient() template = create_therapy_plan_template() diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index bc0ff1a5057..ac01c604dda 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -47,7 +47,7 @@ class TherapyPlan(Document): @frappe.whitelist() -def make_therapy_session(therapy_plan, patient, therapy_type, company): +def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None): therapy_type = frappe.get_doc('Therapy Type', therapy_type) therapy_session = frappe.new_doc('Therapy Session') @@ -58,6 +58,7 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company): therapy_session.duration = therapy_type.default_duration therapy_session.rate = therapy_type.rate therapy_session.exercises = therapy_type.exercises + therapy_session.appointment = appointment if frappe.flags.in_test: therapy_session.start_date = today() diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js index a2b01c9c181..fd200036935 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js @@ -19,6 +19,15 @@ frappe.ui.form.on('Therapy Session', { } }; }); + + frm.set_query('appointment', function() { + + return { + filters: { + 'status': ['in', ['Open', 'Scheduled']] + } + }; + }); }, refresh: function(frm) { diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py index 85d09701774..c00054421dc 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -43,7 +43,14 @@ class TherapySession(Document): self.update_sessions_count_in_therapy_plan() insert_session_medical_record(self) + def on_update(self): + if self.appointment: + frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed') + def on_cancel(self): + if self.appointment: + frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open') + self.update_sessions_count_in_therapy_plan(on_cancel=True) def update_sessions_count_in_therapy_plan(self, on_cancel=False): From 7646d7b741ecd24ea42909cc7e0c2e128a69d393 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Jan 2021 10:27:05 +0530 Subject: [PATCH 168/203] fix: Batch/Serial Selector for Batched Item --- erpnext/stock/doctype/stock_entry/stock_entry.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 98116ec1832..f75e8b727db 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -524,7 +524,7 @@ frappe.ui.form.on('Stock Entry', { }) ); } - + for (let i in frm.doc.items) { let item = frm.doc.items[i]; @@ -675,7 +675,13 @@ frappe.ui.form.on('Stock Entry Detail', { }); refresh_field("items"); - if (!d.serial_no) { + let no_batch_serial_number_value = !d.serial_no; + if (d.has_batch_no && !d.has_serial_no) { + // check only batch_no for batched item + no_batch_serial_number_value = !d.batch_no; + } + + if (no_batch_serial_number_value) { erpnext.stock.select_batch_and_serial_no(frm, d); } } From 36fa0512d227693cdc0a904f92d41a93554349c5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 12 Jan 2021 17:02:19 +0530 Subject: [PATCH 169/203] fix: not able to create dunning from sales invoice --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 50eb400775e..566734e7d14 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -179,7 +179,7 @@ class SalesInvoice(SellingController): # this sequence because outstanding may get -ve self.make_gl_entries() - + if self.update_stock == 1: self.repost_future_sle_and_gle() @@ -261,10 +261,10 @@ class SalesInvoice(SellingController): self.update_stock_ledger() self.make_gl_entries_on_cancel() - + if self.update_stock == 1: self.repost_future_sle_and_gle() - + frappe.db.set(self, 'status', 'Cancelled') if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": @@ -551,7 +551,7 @@ class SalesInvoice(SellingController): def add_remarks(self): if not self.remarks: if self.po_no and self.po_date: - self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, + self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, formatdate(self.po_date)) else: self.remarks = _("No Remarks") @@ -1699,6 +1699,7 @@ def get_mode_of_payment_info(mode_of_payment, company): where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", (company, mode_of_payment), as_dict=1) +@frappe.whitelist() def create_dunning(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount From 1354197c72f7e402a38fb0dd8812a155f9b8ac45 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 13 Jan 2021 09:12:50 +0530 Subject: [PATCH 170/203] 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 b33c326313d..ddf1bce4927 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 96282f50a92..50f6f476220 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 171/203] 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 e8a9444fecd..10990d412d8 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 50f6f476220..6a499aab7fa 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 172/203] 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 6fbcd8aa97f..886a7d85d8b 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 173/203] 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 f256a66266d..0d8263835d2 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 174/203] 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 28410f3a71a..18cf87ab0ba 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 00000000000..e69de29bb2d 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 00000000000..684482ac8d2 --- /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 00000000000..b8a580ccef1 --- /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 00000000000..3d735314f4e --- /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 175/203] 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 8e7fac8ce88..ca530bbaddc 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 176/203] 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 5c1c79dc047..3034370feac 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 4dcb22a54c7..db8bda75bfd 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 abe15043af8..d0cac90e4df 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 177/203] 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 552a5d476b0..e023b47caca 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 178/203] 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 75ebcbc971b..1c6758e6f36 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 179/203] 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 d7893d06572..46f6cd842c3 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 180/203] 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 b63cc8ed5ac..c6f6b990cc5 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 181/203] 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 7830cfd3702..3863768a8b2 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 5efc32e11d9..89b716c180d 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 47483c9d1c3..38532d18f36 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 661e107e1e9..5a0d9c90655 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 5f2658c1028..cb1e31b15b6 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 814858061f36487641eac763cb53e9982ddcaaf7 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 14 Jan 2021 12:13:08 +0530 Subject: [PATCH 182/203] 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 0a29fa05e1a..72a3a5e67c7 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 bc9eaac3428435f336c7e722c3268c8716ba605a Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 14 Jan 2021 13:47:40 +0530 Subject: [PATCH 183/203] 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 715cdd92858..d85c82612a2 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 184/203] 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 b068245a8ba..b123af5d188 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 185/203] 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 47b48665b60..a73cb0d62ec 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 186/203] 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 5321a9a3b5a..526198424f3 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 187/203] 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 c4412749080..288111b1b69 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 188/203] 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 57a1231f5a9..98731d32ae9 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 189/203] 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 98731d32ae9..7195c7e0b8b 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 190/203] 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 22a4004955f..f26b130805c 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 e535e81bdef..e12d9ee46cc 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 191/203] 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 c13548ab82e..1360fd1890a 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 4437e02fc84..b8f6df6f7a6 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 00883d75f19..1e897671770 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 5da8cc82a2e..c015101600a 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 f630570c4c2..512ba5c5555 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 f92e72eaa7e..c562db3c25a 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 192/203] 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 40f50999adf..ac66acd00f5 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 193/203] 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 e822c0c0171..5bb4b6f6b59 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 194/203] 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 5bb4b6f6b59..7877abd0263 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 195/203] 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 00000000000..e69de29bb2d 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 00000000000..e954a3942cf --- /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 00000000000..a778cd7055d --- /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 00000000000..6b7f3ad3284 --- /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 e039fe82d38..e38770b4ed2 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 196/203] 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 00000000000..e69de29bb2d 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 00000000000..777f29624a7 --- /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 00000000000..d4dca08212d --- /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 00000000000..dc880f72666 --- /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 197/203] 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 ffc36711324..3feb3055a6a 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 e954a3942cf..73d60c40458 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 6b7f3ad3284..0aff2e3623a 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 852e3ca366f..a227b6d7973 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 e38770b4ed2..730176f4a96 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 dc880f72666..2f4d98066b8 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 198/203] 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 2e0a4d13ab2..e607d4f3cbf 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 18a97315f0a..3ef53044c20 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 199/203] 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 fc59c193251..75036bd0972 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 200/203] 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 2abd7d84d97..f3c9db62338 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 a55b482bd66..b6e87637567 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 801e3a31173..c23479c8251 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 3e7e778a259..a0b3a79b56c 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 00356685eb9..0091e6c43d4 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 201/203] 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 0aff2e3623a..6d7c3b730db 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 730176f4a96..aa0325ef35c 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 2f4d98066b8..3ef10c0f419 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 202/203] 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 18cf87ab0ba..dba2b1463f5 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 62b39cced53..02d10a4ddad 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 c962dc6b317..483bb155dbf 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 00000000000..e69de29bb2d 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 00000000000..f87b2c2dddc --- /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 00000000000..dd18498d1da --- /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 00000000000..0b629151a6b --- /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 00000000000..432906db9b9 --- /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 203/203] 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 de9498e0752..49b2b186c4b 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 8d4b5104905..66d0e5f77db 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 f26b130805c..bbc2ca8846c 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 e12d9ee46cc..70c7f3fe5d7 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)