From a4043c035d4116e206e486d4eecabe897cf24630 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:27:06 +0530 Subject: [PATCH 01/99] fix: Prevent clearing of Depreciation Schedule on adding more than one Finance Book --- erpnext/assets/doctype/asset/asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2f..7031ed15bdf 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -185,11 +185,11 @@ class Asset(AccountsController): if not self.available_for_use_date: return + start = self.clear_depreciation_schedule() + for d in self.get('finance_books'): self.validate_asset_finance_books(d) - start = self.clear_depreciation_schedule() - # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - From 059d1f3b74b2bee707387cea518eeb394bfce3d8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:35:41 +0530 Subject: [PATCH 02/99] fix: Rename loop variable --- erpnext/assets/doctype/asset/asset.py | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7031ed15bdf..c00d6836a63 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -187,23 +187,23 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() - for d in self.get('finance_books'): - self.validate_asset_finance_books(d) + for finance_book in self.get('finance_books'): + self.validate_asset_finance_books(finance_book) # value_after_depreciation - current Asset value - if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - + if self.docstatus == 1 and finance_book.value_after_depreciation: + value_after_depreciation = (flt(finance_book.value_after_depreciation) - flt(self.opening_accumulated_depreciation)) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) - d.value_after_depreciation = value_after_depreciation + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ cint(self.number_of_depreciations_booked) - has_pro_rata = self.check_is_pro_rata(d) + has_pro_rata = self.check_is_pro_rata(finance_book) if has_pro_rata: number_of_pending_depreciations += 1 @@ -213,56 +213,56 @@ class Asset(AccountsController): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(d.depreciation_start_date, - n * cint(d.frequency_of_depreciation)) + schedule_date = add_months(finance_book.depreciation_start_date, + n * cint(finance_book.frequency_of_depreciation)) # schedule date will be a year later from start date # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) + monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) # if asset is being sold if date_of_sale: - from_date = self.get_from_date(d.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, + from_date = self.get_from_date(finance_book.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, from_date, date_of_sale) if depreciation_amount > 0: self.append("schedules", { "schedule_date": date_of_sale, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) break # For first row if has_pro_rata and n==0: - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, - self.available_for_use_date, d.depreciation_start_date) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + self.available_for_use_date, finance_book.depreciation_start_date) # For first depr schedule date will be the start date # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) + monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + n * cint(finance_book.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount - depreciation_amount, days, months = self.get_pro_rata_amt(d, + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, schedule_date, self.to_date) depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, d.finance_book) + depreciation_amount, finance_book.finance_book) monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) @@ -273,10 +273,10 @@ class Asset(AccountsController): self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != d.expected_value_after_useful_life) - or value_after_depreciation < d.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life) + if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life) + or value_after_depreciation < finance_book.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) skip_row = True if depreciation_amount > 0: @@ -286,7 +286,7 @@ class Asset(AccountsController): # In pro rata case, for first and last depreciation, month range would be different month_range = months \ if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ - else d.frequency_of_depreciation + else finance_book.frequency_of_depreciation for r in range(month_range): if (has_pro_rata and n == 0): @@ -312,17 +312,17 @@ class Asset(AccountsController): self.append("schedules", { "schedule_date": date, "depreciation_amount": amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) else: self.append("schedules", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) # used when depreciation schedule needs to be modified due to increase in asset life From 475d8394e413d530ec57d9c7f713fecd706b2271 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 22 Nov 2021 21:23:27 +0530 Subject: [PATCH 03/99] fix: Clear Depreciation Schedule entries that aren't linked with Journal Entries before modifying the schedule --- erpnext/assets/doctype/asset/asset.py | 42 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c00d6836a63..f3825ea6826 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -209,7 +209,8 @@ class Asset(AccountsController): number_of_pending_depreciations += 1 skip_row = False - for n in range(start, number_of_pending_depreciations): + + for n in range(start[finance_book.idx-1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -325,14 +326,39 @@ class Asset(AccountsController): "finance_book_id": finance_book.idx }) - # used when depreciation schedule needs to be modified due to increase in asset life + # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales + # JE: Journal Entry, FB: Finance Book def clear_depreciation_schedule(self): - start = 0 - for n in range(len(self.schedules)): - if not self.schedules[n].journal_entry: - del self.schedules[n:] - start = n - break + start = [] + num_of_depreciations_completed = 0 + depr_schedule = [] + + for schedule in self.get('schedules'): + + # to ensure that start will only be updated once for each FB + if len(start) == (int(schedule.finance_book_id) - 1): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + elif len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when all the schedule rows corresponding to the last FB are linked with JEs + if len(start) == (len(self.finance_books) - 1): + start.append(num_of_depreciations_completed) + + # when the Depreciation Schedule is being created for the first time + if start == []: + start = [0] * len(self.finance_books) + else: + self.schedules = depr_schedule + return start def get_from_date(self, finance_book): From f455de2924f7c7554b9eefc757c354992901136b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 23:26:32 +0530 Subject: [PATCH 04/99] fix: Test if multiple Depreciation Schedules are set up for multiple Finance Books --- erpnext/assets/doctype/asset/test_asset.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec70..fd4d9ff239f 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,38 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.save() + + self.assertEqual(len(asset.schedules), 9) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, 1) + else: + self.assertEqual(schedule.finance_book_id, 2) + def test_depreciation_entry_cancellation(self): asset = create_asset( item_code = "Macbook Pro", From 33a0b1db2ced1fe84386dbd6b1e5eacb535e5098 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 02:45:17 +0530 Subject: [PATCH 05/99] fix: Retain depreciation schedule rows that are linked with JEs while clearing the schedule --- erpnext/assets/doctype/asset/asset.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f3825ea6826..351556c76c1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -335,6 +335,11 @@ class Asset(AccountsController): for schedule in self.get('schedules'): + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + if len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + # to ensure that start will only be updated once for each FB if len(start) == (int(schedule.finance_book_id) - 1): if schedule.journal_entry: @@ -344,11 +349,6 @@ class Asset(AccountsController): start.append(num_of_depreciations_completed) num_of_depreciations_completed = 0 - # to update start when there are JEs linked with all the schedule rows corresponding to an FB - elif len(start) == (int(schedule.finance_book_id) - 2): - start.append(num_of_depreciations_completed) - num_of_depreciations_completed = 0 - # to update start when all the schedule rows corresponding to the last FB are linked with JEs if len(start) == (len(self.finance_books) - 1): start.append(num_of_depreciations_completed) From 6ec5a190631b563804748fe43532b70d0f230ff9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 03:19:36 +0530 Subject: [PATCH 06/99] fix: Test if clear_depreciation_schedule() works for multiple finance books --- erpnext/assets/doctype/asset/test_asset.py | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fd4d9ff239f..c1a978572ec 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,50 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_clear_depreciation_schedule_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2023-12-31" + }) + asset.submit() + + post_depreciation_entries(date="2023-01-01") + asset.load_from_db() + + asset.clear_depreciation_schedule() + + self.assertEqual(len(asset.schedules), 6) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, "1") + else: + self.assertEqual(schedule.finance_book_id, "2") + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): asset = create_asset( item_code = "Macbook Pro", From 4311936b7dd2bf7e3f5e11fe5ce6eeff03a7d49b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 05:21:31 +0530 Subject: [PATCH 07/99] fix: Edit dates and frequency of depreciation --- erpnext/assets/doctype/asset/test_asset.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index c1a978572ec..a297a5a5013 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -900,28 +900,28 @@ class TestDepreciationBasics(AssetSetup): asset.calculate_depreciation = 1 asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2023-12-31" + "depreciation_start_date": "2020-12-31" }) asset.submit() - post_depreciation_entries(date="2023-01-01") + post_depreciation_entries(date="2020-04-01") asset.load_from_db() asset.clear_depreciation_schedule() From 9047e2b9dd8b6d8b1819404d98480d86b598d562 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 13:52:20 +0530 Subject: [PATCH 08/99] refactor: do not set default priority in isssues --- erpnext/support/doctype/issue/issue.json | 4 ++-- erpnext/support/doctype/issue/issue_list.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 14712f89feb..75b6d0f4f92 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -123,7 +123,6 @@ "search_index": 1 }, { - "default": "Medium", "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, @@ -410,10 +409,11 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2021-06-10 03:22:27.098898", + "modified": "2021-11-24 13:13:10.276630", "modified_by": "Administrator", "module": "Support", "name": "Issue", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js index e04498e29ee..5bfecb019cc 100644 --- a/erpnext/support/doctype/issue/issue_list.js +++ b/erpnext/support/doctype/issue/issue_list.js @@ -18,7 +18,6 @@ frappe.listview_settings['Issue'] = { }, get_indicator: function(doc) { if (doc.status === 'Open') { - if (!doc.priority) doc.priority = 'Medium'; const color = { 'Low': 'yellow', 'Medium': 'orange', From 1f060c0b0a2d1925ebfda8b269517462d428ef35 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 13:53:39 +0530 Subject: [PATCH 09/99] feat: find SLA based on customer group's ancestors --- .../service_level_agreement.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 5f8f83d89ba..9dfe339c519 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -22,6 +22,7 @@ from frappe.utils import ( time_diff_in_seconds, to_timedelta, ) +from frappe.utils.nestedset import get_ancestors_of from frappe.utils.safe_exec import get_safe_globals from erpnext.support.doctype.issue.issue import get_holidays @@ -248,7 +249,7 @@ def get_active_service_level_agreement_for(doc): customer = doc.get('customer') or_filters.append( - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] + ["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)] ) default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] @@ -275,11 +276,23 @@ def get_context(doc): return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} def get_customer_group(customer): - return frappe.db.get_value("Customer", customer, "customer_group") if customer else None + customer_groups = [] + customer_group = frappe.db.get_value("Customer", customer, "customer_group") if customer else None + if customer_group: + ancestors = get_ancestors_of("Customer Group", customer_group) + customer_groups = [customer_group] + ancestors + + return customer_groups def get_customer_territory(customer): - return frappe.db.get_value("Customer", customer, "territory") if customer else None + customer_territories = [] + customer_territory = frappe.db.get_value("Customer", customer, "territory") if customer else None + if customer_territory: + ancestors = get_ancestors_of("Territory", customer_territory) + customer_territories = [customer_territory] + ancestors + + return customer_territories @frappe.whitelist() @@ -299,7 +312,7 @@ def get_service_level_agreement_filters(doctype, name, customer=None): if customer: # Include SLA with No Entity and Entity Type or_filters.append( - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]] + ["Service Level Agreement", "entity", "in", [""] + [customer] + get_customer_group(customer) + get_customer_territory(customer)] ) return { @@ -343,6 +356,8 @@ def apply(doc, method=None): service_level_agreement = get_active_service_level_agreement_for(doc) + print(service_level_agreement) + if not service_level_agreement: return From 5b2ba245637befb7c6a01abcd9fbc5d83318a551 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 24 Nov 2021 19:19:45 +0530 Subject: [PATCH 10/99] fix: company tour --- .../selling/form_tour/customer/customer.json | 29 ++++++++ .../form_tour/quotation/quotation.json | 67 +++++++++++++++++++ erpnext/setup/form_tour/company/company.json | 67 +++++++++++++++++++ .../setup/module_onboarding/home/home.json | 62 +++++++++++++++++ .../company_set_up/company_set_up.json | 21 ++++++ .../create_a_customer/create_a_customer.json | 21 ++++++ .../create_a_quotation.json | 21 ++++++ .../create_a_supplier/create_a_supplier.json | 21 ++++++ .../create_an_item/create_an_item.json | 22 ++++++ .../data_import/data_import.json | 21 ++++++ .../letterhead/letterhead.json | 21 ++++++ .../navigation_help/navigation_help.json | 21 ++++++ erpnext/setup/workspace/home/home.json | 13 +++- erpnext/stock/form_tour/item/item.json | 30 +++++++-- 14 files changed, 427 insertions(+), 10 deletions(-) create mode 100644 erpnext/selling/form_tour/customer/customer.json create mode 100644 erpnext/selling/form_tour/quotation/quotation.json create mode 100644 erpnext/setup/form_tour/company/company.json create mode 100644 erpnext/setup/module_onboarding/home/home.json create mode 100644 erpnext/setup/onboarding_step/company_set_up/company_set_up.json create mode 100644 erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json create mode 100644 erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json create mode 100644 erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json create mode 100644 erpnext/setup/onboarding_step/create_an_item/create_an_item.json create mode 100644 erpnext/setup/onboarding_step/data_import/data_import.json create mode 100644 erpnext/setup/onboarding_step/letterhead/letterhead.json create mode 100644 erpnext/setup/onboarding_step/navigation_help/navigation_help.json diff --git a/erpnext/selling/form_tour/customer/customer.json b/erpnext/selling/form_tour/customer/customer.json new file mode 100644 index 00000000000..1de45b7f5d2 --- /dev/null +++ b/erpnext/selling/form_tour/customer/customer.json @@ -0,0 +1,29 @@ +{ + "creation": "2021-11-23 10:44:13.185982", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-11-23 10:54:09.602358", + "modified_by": "Administrator", + "module": "Selling", + "name": "Customer", + "owner": "Administrator", + "reference_doctype": "Customer", + "save_on_complete": 1, + "steps": [ + { + "description": "Enter the Full Name of the Customer", + "field": "", + "fieldname": "customer_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Full Name", + "parent_field": "", + "position": "Left", + "title": "Full Name" + } + ], + "title": "Customer" +} \ No newline at end of file diff --git a/erpnext/selling/form_tour/quotation/quotation.json b/erpnext/selling/form_tour/quotation/quotation.json new file mode 100644 index 00000000000..2a2aa5e63e4 --- /dev/null +++ b/erpnext/selling/form_tour/quotation/quotation.json @@ -0,0 +1,67 @@ +{ + "creation": "2021-11-23 12:00:36.138824", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-11-23 12:02:48.010298", + "modified_by": "Administrator", + "module": "Selling", + "name": "Quotation", + "owner": "Administrator", + "reference_doctype": "Quotation", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.", + "field": "", + "fieldname": "quotation_to", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Quotation To", + "parent_field": "", + "position": "Right", + "title": "Quotation To" + }, + { + "description": "Select a specific Customer to whom this quotation will be sent.", + "field": "", + "fieldname": "party_name", + "fieldtype": "Dynamic Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Party", + "parent_field": "", + "position": "Right", + "title": "Party" + }, + { + "child_doctype": "Quotation Item", + "description": "Select an item for which you will be quoting a price.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "parent_fieldname": "items", + "position": "Bottom", + "title": "Items" + }, + { + "description": "You can select pre-populated Sales Taxes and Charges from here.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Sales Taxes and Charges" + } + ], + "title": "Quotation" +} \ No newline at end of file diff --git a/erpnext/setup/form_tour/company/company.json b/erpnext/setup/form_tour/company/company.json new file mode 100644 index 00000000000..c66abc0a720 --- /dev/null +++ b/erpnext/setup/form_tour/company/company.json @@ -0,0 +1,67 @@ +{ + "creation": "2021-11-24 10:17:18.534917", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 1, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2021-11-24 15:38:21.026582", + "modified_by": "Administrator", + "module": "Setup", + "name": "Company", + "owner": "Administrator", + "reference_doctype": "Company", + "save_on_complete": 0, + "steps": [ + { + "description": "This is the default currency for this company.", + "field": "", + "fieldname": "default_currency", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Currency", + "parent_field": "", + "position": "Right", + "title": "Default Currency" + }, + { + "description": "Here, you can add multiple addresses of the company", + "field": "", + "fieldname": "company_info", + "fieldtype": "Section Break", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Address & Contact", + "parent_field": "", + "position": "Top", + "title": "Address & Contact" + }, + { + "description": "Here, you can set default Accounts, which will ease the creation of accounting entries.", + "field": "", + "fieldname": "default_settings", + "fieldtype": "Section Break", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Accounts Settings", + "parent_field": "", + "position": "Top", + "title": "Accounts Settings" + }, + { + "description": "This setting is recommended if you wish to track the real-time stock balance in your books of account. This will allow the creation of a General Ledger entry for every stock transaction.", + "field": "", + "fieldname": "enable_perpetual_inventory", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Enable Perpetual Inventory", + "parent_field": "", + "position": "Right", + "title": "Enable Perpetual Inventory" + } + ], + "title": "Company" +} \ No newline at end of file diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json new file mode 100644 index 00000000000..fbff98ff8a8 --- /dev/null +++ b/erpnext/setup/module_onboarding/home/home.json @@ -0,0 +1,62 @@ +{ + "allow_roles": [ + { + "role": "Accounts Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Sales Manager" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + } + ], + "creation": "2021-11-22 12:19:15.888642", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", + "idx": 0, + "is_complete": 0, + "modified": "2021-11-24 16:40:15.154081", + "modified_by": "Administrator", + "module": "Setup", + "name": "Home", + "owner": "Administrator", + "steps": [ + { + "step": "Company Set Up" + }, + { + "step": "Create an Item" + }, + { + "step": "Create a Customer" + }, + { + "step": "Create a Supplier" + }, + { + "step": "Navigation Help" + }, + { + "step": "Data import" + }, + { + "step": "Create a Quotation" + }, + { + "step": "Letterhead" + } + ], + "subtitle": "Company, Item, Customer, Supplier, Navigation Help, Data Import, Letter Head, Quotation", + "success_message": "Masters are all set up!", + "title": "Let's Set Up Some Masters" +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json new file mode 100644 index 00000000000..dc851897649 --- /dev/null +++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let's create a Company", + "creation": "2021-11-22 11:55:48.931427", + "description": "# Set Up a Company\n\nA company is a legal entity for which you will set up your books of account and create accounting transactions. In ERPNext, you can create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:41:01.620963", + "modified_by": "Administrator", + "name": "Company Set Up", + "owner": "Administrator", + "reference_document": "Company", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Set Up a Company", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json new file mode 100644 index 00000000000..c3162f2b874 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Customer", + "creation": "2020-05-14 17:46:41.831517", + "description": "The Customer master is at the heart of your sales transactions. Customers are linked in Quotations, Sales Orders, Invoices, and Payments. Customers can be either numbered or identified by name (you would typically do this based on the number of customers you have).\n\nThrough Customer\u2019s master, you can effectively track essentials like:\n - Customer\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:20.882675", + "modified_by": "Administrator", + "name": "Create a Customer", + "owner": "Administrator", + "reference_document": "Customer", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Manage Customers", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json new file mode 100644 index 00000000000..12d4372470c --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Quotation", + "creation": "2020-06-01 13:34:58.958641", + "description": "# Quotation\n\nLet\u2019s get started with business transactions by creating your first Quotation. You can create a Quotation for an existing customer or a prospect. It will be an approved document, with items you sell and the proposed price + taxes applied. After completing the instructions, you will get a Quotation in a ready to share print format.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:55:28.950596", + "modified_by": "Administrator", + "name": "Create a Quotation", + "owner": "Administrator", + "reference_document": "Quotation", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create your first Quotation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json new file mode 100644 index 00000000000..cc87d97ae42 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first Supplier", + "creation": "2020-05-14 22:09:10.043554", + "description": "# Supplier\n\nAlso known as Vendor, is a master at the center of your purchase transactions. Suppliers are linked in Request for Quotation, Purchase Orders, Receipts, and Payments. Suppliers can be either numbered or identified by name.\n\nThrough Supplier\u2019s master, you can effectively track essentials like:\n - Supplier\u2019s multiple address and contacts\n - Account Receivables\n - Credit Limit and Credit Period\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:36.656949", + "modified_by": "Administrator", + "name": "Create a Supplier", + "owner": "Administrator", + "reference_document": "Supplier", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Manage Suppliers", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json new file mode 100644 index 00000000000..0d650de6654 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "Create a new Item", + "creation": "2021-05-17 13:47:18.515052", + "description": "Item is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:51:21.446969", + "modified_by": "Administrator", + "name": "Create an Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Manage Items", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json new file mode 100644 index 00000000000..5f4e8db4ab4 --- /dev/null +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -0,0 +1,21 @@ +{ + "action": "Watch Video", + "action_label": "Learn more about data migration", + "creation": "2021-05-19 05:29:16.809610", + "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:54:37.622063", + "modified_by": "Administrator", + "name": "Data import", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Import Data from Spreadsheet", + "validate_action": 1, + "video_url": "https://youtu.be/DQyqeurPI64" +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json new file mode 100644 index 00000000000..b8d430365f8 --- /dev/null +++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s setup your first Letter Head", + "creation": "2021-11-22 12:36:34.583783", + "description": "# Letter Head\n\nA Letter Head contains your organization's name, logo, address, etc which appears at the header and footer portion in documents. You can learn more about Setting up Letter Head in ERPNext here.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 16:19:25.379798", + "modified_by": "Administrator", + "name": "Letterhead", + "owner": "Administrator", + "reference_document": "Letter Head", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Setup Your Letterhead", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json new file mode 100644 index 00000000000..1271a86cb9c --- /dev/null +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -0,0 +1,21 @@ +{ + "action": "Watch Video", + "action_label": "Learn about Navigation options", + "creation": "2021-11-22 12:09:52.233872", + "description": "Ease of navigating and browsing around the ERPNext is one of our core strengths. In the following video, you will learn how to reach a specific feature in ERPNext via module page or awesome bar\u2019s shortcut.\n", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-11-24 15:52:40.641949", + "modified_by": "Administrator", + "name": "Navigation Help", + "owner": "Administrator", + "show_form_tour": 0, + "show_full_form": 0, + "title": "How to Navigate in ERPNext", + "validate_action": 1, + "video_url": "https://youtu.be/j60xyNFqX_A" +} \ No newline at end of file diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 4e1ccf9b94f..f9c585c015c 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -1,13 +1,18 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", "creation": "2020-01-23 13:46:38.833076", + "developer_mode_only": 0, + "disable_user_customization": 0, "docstatus": 0, "doctype": "Workspace", + "extends_another_page": 0, "for_user": "", "hide_custom": 0, "icon": "getting-started", "idx": 0, + "is_default": 0, + "is_standard": 0, "label": "Home", "links": [ { @@ -271,12 +276,14 @@ "type": "Link" } ], - "modified": "2021-08-10 15:33:20.704741", + "modified": "2021-11-22 12:50:15.771366", "modified_by": "Administrator", "module": "Setup", "name": "Home", "owner": "Administrator", "parent_page": "", + "pin_to_bottom": 0, + "pin_to_top": 0, "public": 1, "restrict_to_domain": "", "roles": [], @@ -309,4 +316,4 @@ } ], "title": "Home" -} \ No newline at end of file +} diff --git a/erpnext/stock/form_tour/item/item.json b/erpnext/stock/form_tour/item/item.json index 821e91b28d4..5369366edba 100644 --- a/erpnext/stock/form_tour/item/item.json +++ b/erpnext/stock/form_tour/item/item.json @@ -2,15 +2,17 @@ "creation": "2021-08-24 17:56:40.754909", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-08-24 18:04:50.928431", + "modified": "2021-11-24 17:59:44.559001", "modified_by": "Administrator", "module": "Stock", "name": "Item", "owner": "Administrator", "reference_doctype": "Item", - "save_on_complete": 0, + "save_on_complete": 1, "steps": [ { "description": "Enter code for Asset Item", @@ -36,14 +38,27 @@ "position": "Bottom", "title": "Asset Item Name" }, + { + "description": "Select an Item Group", + "field": "", + "fieldname": "item_group", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Group", + "parent_field": "", + "position": "Right", + "title": "Item Group" + }, { "description": "Check this field to make this an Asset Item", "field": "", "fieldname": "is_fixed_asset", "fieldtype": "Check", - "has_next_condition": 0, + "has_next_condition": 1, "is_table_field": 0, "label": "Is Fixed Asset", + "next_step_condition": "eval:doc.is_fixed_asset", "parent_field": "", "position": "Bottom", "title": "Is this a Fixed Asset?" @@ -53,9 +68,10 @@ "field": "", "fieldname": "auto_create_assets", "fieldtype": "Check", - "has_next_condition": 0, + "has_next_condition": 1, "is_table_field": 0, "label": "Auto Create Assets on Purchase", + "next_step_condition": "eval:doc.auto_create_assets", "parent_field": "", "position": "Bottom", "title": "Auto Create Asset on Purchase" @@ -69,7 +85,7 @@ "is_table_field": 0, "label": "Asset Category", "parent_field": "", - "position": "Bottom", + "position": "Left", "title": "Asset Category" }, { @@ -81,9 +97,9 @@ "is_table_field": 0, "label": "Asset Naming Series", "parent_field": "", - "position": "Bottom", + "position": "Left", "title": "Asset Naming Series" } ], "title": "Item" -} +} \ No newline at end of file From c46c8dd6c57a1d12df977b02c14affaf479e1bb3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 24 Nov 2021 19:23:31 +0530 Subject: [PATCH 11/99] feat: do not change variance if response or resolution is set --- .../service_level_agreement.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9dfe339c519..a25608b2759 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -515,17 +515,21 @@ def set_service_level_agreement_variance(doctype, doc=None): if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) - frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) + else: + variance = round(time_diff_in_seconds(current_doc.response_by, current_doc.first_responded_on), 2) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) + if variance < 0: + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) - frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) + elif apply_sla_for_resolution and current_doc.get("resolution_date"): + variance = round(time_diff_in_seconds(current_doc.resolution_by, current_doc.get("resolution_date")), 2) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) + frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) + if variance < 0: + frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) def set_user_resolution_time(doc, meta): @@ -808,10 +812,18 @@ def update_agreement_status_on_custom_status(doc): # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) + if meta.has_field("first_responded_on") and doc.first_responded_on: + # first_responded_on set when first reply is sent to customer + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) + if meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) + if meta.has_field("resolution_date") and doc.resolution_date: + # resolution_date set when issue has been closed + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) + if meta.has_field("agreement_status"): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" @@ -857,6 +869,8 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) + elif meta.has_field("response_by_variance") and doc.get('first_responded_on'): + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by"): From 210f593a492ba4332c526fac6579df9adc3afc1c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 15:49:17 +0530 Subject: [PATCH 12/99] refactor: SLA form fields --- .../service_level_agreement.js | 31 ++++++++++ .../service_level_agreement.json | 61 ++++++++----------- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index ae2080c3b53..7e260dbbf56 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -22,10 +22,41 @@ frappe.ui.form.on('Service Level Agreement', { refresh: function(frm) { frm.trigger('fetch_status_fields'); frm.trigger('toggle_resolution_fields'); + frm.trigger('default_service_level_agreement'); + frm.trigger('entity'); + }, + + default_service_level_agreement: function(frm) { + const field = frm.get_field('default_service_level_agreement'); + if (frm.doc.default_service_level_agreement) { + field.set_description(__('SLA will be applied on every {0}', [frm.doc.document_type])); + } else { + field.set_description(__('Enable to apply SLA on every {0}', [frm.doc.document_type])); + } }, document_type: function(frm) { frm.trigger('fetch_status_fields'); + frm.trigger('default_service_level_agreement'); + }, + + entity_type: function(frm) { + frm.set_value('entity', undefined); + }, + + entity: function(frm) { + const field = frm.get_field('entity'); + if (frm.doc.entity) { + const and_descendants = frm.doc.entity_type != 'Customer' ? __(' or its descendants') : ''; + field.set_description( + __('SLA will be applied if {1} is set as {2}{3}', [ + frm.doc.document_type, frm.doc.entity_type, + frm.doc.entity, and_descendants + ]) + ); + } else { + field.set_description(''); + } }, fetch_status_fields: function(frm) { diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 5f470aad672..1698e2380f7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -6,22 +6,17 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "enabled", - "section_break_2", "document_type", - "default_service_level_agreement", "default_priority", "column_break_2", "service_level", - "holiday_list", - "entity_section", - "entity_type", - "column_break_10", - "entity", + "enabled", "filters_section", - "condition", + "default_service_level_agreement", + "entity_type", + "entity", "column_break_15", - "condition_description", + "condition", "agreement_details_section", "start_date", "column_break_7", @@ -31,8 +26,10 @@ "priorities", "status_details", "sla_fulfilled_on", + "column_break_22", "pause_sla_on", "support_and_resolution_section_break", + "holiday_list", "support_and_resolution" ], "fields": [ @@ -42,7 +39,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Service Level Name", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "fieldname": "holiday_list", @@ -56,10 +54,10 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: !doc.default_service_level_agreement", + "depends_on": "eval: doc.document_type", "fieldname": "agreement_details_section", "fieldtype": "Section Break", - "label": "Agreement Details" + "label": "Valid From" }, { "fieldname": "start_date", @@ -72,7 +70,6 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "end_date", "fieldtype": "Date", "label": "End Date" @@ -80,7 +77,7 @@ { "fieldname": "response_and_resolution_time_section", "fieldtype": "Section Break", - "label": "Response and Resolution Time" + "label": "Response and Resolution" }, { "fieldname": "support_and_resolution_section_break", @@ -90,6 +87,7 @@ { "fieldname": "support_and_resolution", "fieldtype": "Table", + "label": "Working Hours", "options": "Service Day", "reqd": 1 }, @@ -101,10 +99,7 @@ "reqd": 1 }, { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, - { + "depends_on": "eval: !doc.default_service_level_agreement", "fieldname": "entity", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -114,22 +109,12 @@ }, { "depends_on": "eval: !doc.default_service_level_agreement", - "fieldname": "entity_section", - "fieldtype": "Section Break", - "label": "Entity" - }, - { "fieldname": "entity_type", "fieldtype": "Select", "in_standard_filter": 1, "label": "Entity Type", "options": "\nCustomer\nCustomer Group\nTerritory" }, - { - "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "default": "0", "fieldname": "default_service_level_agreement", @@ -152,7 +137,7 @@ { "fieldname": "document_type", "fieldtype": "Link", - "label": "Document Type", + "label": "Apply On", "options": "DocType", "reqd": 1, "set_only_once": 1 @@ -164,6 +149,7 @@ "label": "Enabled" }, { + "depends_on": "document_type", "fieldname": "status_details", "fieldtype": "Section Break", "label": "Status Details" @@ -182,28 +168,31 @@ "label": "Apply SLA for Resolution Time" }, { + "depends_on": "document_type", "fieldname": "filters_section", "fieldtype": "Section Break", - "label": "Assignment Condition" + "label": "Assignment Conditions" }, { "fieldname": "column_break_15", "fieldtype": "Column Break" }, { + "depends_on": "eval: !doc.default_service_level_agreement", + "description": "Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'", "fieldname": "condition", "fieldtype": "Code", "label": "Condition", - "options": "Python" + "max_height": "7rem", + "options": "PythonExpression" }, { - "fieldname": "condition_description", - "fieldtype": "HTML", - "options": "

Condition Examples:

\n
doc.status==\"Open\"
doc.due_date==nowdate()
doc.total > 40000\n
" + "fieldname": "column_break_22", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2021-10-02 11:32:55.556024", + "modified": "2021-11-26 15:45:33.289911", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", From a8c75b68628d29cb354ae3cb62424d3a01923586 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 20:16:21 +0530 Subject: [PATCH 13/99] refactor: application of SLA and its metrics --- erpnext/hooks.py | 3 +- .../service_level_agreement.py | 339 +++++++----------- 2 files changed, 137 insertions(+), 205 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a277ee0350..186dbb5847d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,8 +341,7 @@ scheduler_events = { "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", - "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", - "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance" + "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts" ], "hourly_long": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index a25608b2759..864640204b3 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -10,7 +10,6 @@ from frappe.core.utils import get_parent_doc from frappe.model.document import Document from frappe.utils import ( add_to_date, - cint, get_datetime, get_datetime_str, get_link_to_form, @@ -350,86 +349,113 @@ def set_documents_with_active_service_level_agreement(): def apply(doc, method=None): # Applies SLA to document on validate - if frappe.flags.in_patch or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_setup_wizard or \ - doc.doctype not in get_documents_with_active_service_level_agreement(): + if ( + frappe.flags.in_patch + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_setup_wizard + or doc.doctype not in get_documents_with_active_service_level_agreement() + ): return service_level_agreement = get_active_service_level_agreement_for(doc) - print(service_level_agreement) - if not service_level_agreement: return - set_sla_properties(doc, service_level_agreement) + process_sla(doc, service_level_agreement) -def set_sla_properties(doc, service_level_agreement): - if frappe.db.exists(doc.doctype, doc.name): - from_db = frappe.get_doc(doc.doctype, doc.name) - else: - from_db = frappe._dict({}) - - meta = frappe.get_meta(doc.doctype) - - if meta.has_field("customer") and service_level_agreement.customer and doc.get("customer") and \ - not service_level_agreement.customer == doc.get("customer"): - frappe.throw(_("Service Level Agreement {0} is specific to Customer {1}").format(service_level_agreement.name, - service_level_agreement.customer)) - - doc.service_level_agreement = service_level_agreement.name - doc.priority = doc.get("priority") or service_level_agreement.default_priority - priority = get_priority(doc) +def process_sla(doc, service_level_agreement): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) - - if meta.has_field("service_level_agreement_creation"): + if doc.meta.has_field("service_level_agreement_creation"): doc.service_level_agreement_creation = now_datetime(doc.get("owner")) + doc.service_level_agreement = service_level_agreement.name + doc.priority = doc.get("priority") or service_level_agreement.default_priority + + prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') + handle_status_change(doc, prev_status, service_level_agreement.apply_sla_for_resolution) + update_response_and_resolution_metrics(doc, service_level_agreement.apply_sla_for_resolution) + update_agreement_status(doc, service_level_agreement.apply_sla_for_resolution) + + +def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): + priority = get_response_and_resolution_duration(doc) start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - - set_response_by_and_variance(doc, meta, start_date_time, priority) - if service_level_agreement.apply_sla_for_resolution: - set_resolution_by_and_variance(doc, meta, start_date_time, priority) - - update_status(doc, from_db, meta) + set_response_by_and_variance(doc, start_date_time, priority) + if apply_sla_for_resolution: + set_resolution_by_and_variance(doc, start_date_time, priority) -def update_status(doc, from_db, meta): - if meta.has_field("status"): - if meta.has_field("first_responded_on") and doc.status != "Open" and \ - from_db.status == "Open" and not doc.first_responded_on: +def get_fulfillment_statuses(service_level_agreement): + return [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ + "parent": service_level_agreement + }, fields=["status"])] + + +def get_hold_statuses(service_level_agreement): + return [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ + "parent": service_level_agreement + }, fields=["status"])] + + +def handle_status_change(doc, prev_status, apply_sla_for_resolution): + + if doc.status != "Open" and prev_status == "Open": + # status changed from Open to something else + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + # status changed to something other than Open doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("service_level_agreement") and doc.service_level_agreement: - # mark sla status as fulfilled based on the configuration - fulfillment_statuses = [entry.status for entry in frappe.db.get_all("SLA Fulfilled On Status", filters={ - "parent": doc.service_level_agreement - }, fields=["status"])] + if doc.status == "Open" and prev_status != "Open": + # status changed from something else to Open + reset_resolution_metrics(doc) - if doc.status in fulfillment_statuses and from_db.status not in fulfillment_statuses: - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, - "apply_sla_for_resolution") + handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution) + handle_hold_status(doc, prev_status) - if apply_sla_for_resolution and meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("agreement_status") and from_db.agreement_status == "Ongoing": - set_service_level_agreement_variance(doc.doctype, doc.name) - update_agreement_status(doc, meta) +def handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution): + fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) + if ( + doc.status in fulfillment_statuses + and prev_status not in fulfillment_statuses + and apply_sla_for_resolution + ): + # status changed to any fulfillment_statuses + if doc.meta.has_field("resolution_date"): + doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) + if doc.meta.has_field("resolution_time"): + doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) + set_user_resolution_time(doc) - if apply_sla_for_resolution: - set_resolution_time(doc, meta) - set_user_resolution_time(doc, meta) - if doc.status == "Open" and from_db.status != "Open": - # if no date, it should be set as None and not a blank string "", as per mysql strict config - # enable SLA and variance on Reopen - reset_metrics(doc, meta) - set_service_level_agreement_variance(doc.doctype, doc.name) +def handle_hold_status(doc, prev_status): + hold_statuses = get_hold_statuses(doc.service_level_agreement) + if doc.status in hold_statuses: + # reset if status is a hold status, regardless of previous status + reset_expected_response_and_resolution(doc) + if prev_status not in hold_statuses: + # set on_hold_since status changed from any non-hold status + # for eg. doc.status changed from Open to Replied + if doc.meta.has_field("on_hold_since"): + doc.on_hold_since = frappe.flags.current_time or now_datetime(doc.get("owner")) - handle_hold_time(doc, meta, from_db.status) + if doc.status not in hold_statuses and prev_status in hold_statuses: + # status changed to any non-hold status + # for eg. doc.status changed from Replied to Closed + if doc.meta.has_field("on_hold_since") and doc.on_hold_since: + cumulate_hold_time(doc) + doc.on_hold_since = None + + +def cumulate_hold_time(doc): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + on_hold_duration = time_diff_in_seconds(now_time, doc.on_hold_since) + doc.total_hold_time = (doc.total_hold_time or 0) + on_hold_duration def get_expected_time_for(parameter, service_level, start_date_time): @@ -500,41 +526,9 @@ def get_support_days(service_level): return support_days -def set_service_level_agreement_variance(doctype, doc=None): - - filters = {"status": "Open", "agreement_status": "Ongoing"} - - if doc: - filters = {"name": doc} - - for entry in frappe.get_all(doctype, filters=filters): - current_doc = frappe.get_doc(doctype, entry.name) - current_time = frappe.flags.current_time or now_datetime(current_doc.get("owner")) - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", current_doc.service_level_agreement, - "apply_sla_for_resolution") - - if not current_doc.first_responded_on: # first_responded_on set when first reply is sent to customer - variance = round(time_diff_in_seconds(current_doc.response_by, current_time), 2) - else: - variance = round(time_diff_in_seconds(current_doc.response_by, current_doc.first_responded_on), 2) - - frappe.db.set_value(current_doc.doctype, current_doc.name, "response_by_variance", variance, update_modified=False) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - - if apply_sla_for_resolution and not current_doc.get("resolution_date"): # resolution_date set when issue has been closed - variance = round(time_diff_in_seconds(current_doc.resolution_by, current_time), 2) - elif apply_sla_for_resolution and current_doc.get("resolution_date"): - variance = round(time_diff_in_seconds(current_doc.resolution_by, current_doc.get("resolution_date")), 2) - - frappe.db.set_value(current_doc.doctype, current_doc.name, "resolution_by_variance", variance, update_modified=False) - if variance < 0: - frappe.db.set_value(current_doc.doctype, current_doc.name, "agreement_status", "Failed", update_modified=False) - - -def set_user_resolution_time(doc, meta): +def set_user_resolution_time(doc): # total time taken by a user to close the issue apart from wait_time - if not meta.has_field("user_resolution_time"): + if not doc.meta.has_field("user_resolution_time"): return communications = frappe.get_all("Communication", filters={ @@ -567,7 +561,7 @@ def change_service_level_agreement_and_priority(self): frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) -def get_priority(doc): +def get_response_and_resolution_duration(doc): service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) priority.update({ @@ -596,115 +590,81 @@ def reset_service_level_agreement(doc, reason, user): doc.save() -def reset_metrics(doc, meta): - if meta.has_field("resolution_date"): +def reset_resolution_metrics(doc): + if doc.meta.has_field("resolution_date"): doc.resolution_date = None - if not meta.has_field("resolution_time"): + if doc.meta.has_field("resolution_time"): doc.resolution_time = None - if not meta.has_field("user_resolution_time"): + if doc.meta.has_field("user_resolution_time"): doc.user_resolution_time = None - if meta.has_field("agreement_status"): + if doc.meta.has_field("agreement_status"): doc.agreement_status = "Ongoing" -def set_resolution_time(doc, meta): - # total time taken from issue creation to closing - if not meta.has_field("resolution_time"): - return - - doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) - - # called via hooks on communication update def update_hold_time(doc, status): + if doc.communication_type == "Comment" or doc.sent_or_received != "Received": + return + parent = get_parent_doc(doc) if not parent: return - if doc.communication_type == "Comment": + if not parent.meta.has_field('service_level_agreement'): return - status_field = parent.meta.get_field("status") - if status_field: - options = (status_field.options or "").splitlines() + apply_sla_for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - # if status has a "Replied" option, then handle hold time - if ("Replied" in options) and doc.sent_or_received == "Received": - meta = frappe.get_meta(parent.doctype) - handle_hold_time(parent, meta, 'Replied') + handle_status_change(parent, 'Replied', apply_sla_for_resolution) + update_response_and_resolution_metrics(parent, apply_sla_for_resolution) + update_agreement_status(parent, apply_sla_for_resolution) + + parent.save() -def handle_hold_time(doc, meta, status): - if meta.has_field("service_level_agreement") and doc.service_level_agreement: - # set response and resolution variance as None as the issue is on Hold for status as Replied - hold_statuses = [entry.status for entry in frappe.db.get_all("Pause SLA On Status", filters={ - "parent": doc.service_level_agreement - }, fields=["status"])] +def reset_expected_response_and_resolution(doc): + update_values = {} - if not hold_statuses: - return - - if meta.has_field("status") and doc.status in hold_statuses and status not in hold_statuses: - apply_hold_status(doc, meta) - - # calculate hold time when status is changed from any hold status to any non-hold status - if meta.has_field("status") and doc.status not in hold_statuses and status in hold_statuses: - reset_hold_status_and_update_hold_time(doc, meta) - - -def apply_hold_status(doc, meta): - update_values = {'on_hold_since': frappe.flags.current_time or now_datetime(doc.get("owner"))} - - if meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: update_values['response_by'] = None update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 + if doc.meta.has_field("resolution_by") and not doc.resolution_date: + update_values['resolution_by'] = None + update_values['resolution_by_variance'] = 0 doc.db_set(update_values) -def reset_hold_status_and_update_hold_time(doc, meta): - hold_time = doc.total_hold_time if meta.has_field("total_hold_time") and doc.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - last_hold_time = 0 - update_values = {} +def set_response_by_and_variance(doc, start_date_time, priority): + if doc.meta.has_field("response_by"): + doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + if doc.meta.has_field("total_hold_time") and doc.total_hold_time: + doc.response_by = add_to_date(doc.response_by, seconds=round(doc.total_hold_time)) - if meta.has_field("on_hold_since") and doc.on_hold_since: - # last_hold_time will be added to the sla variables - last_hold_time = time_diff_in_seconds(now_time, doc.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time + if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - # re-calculate SLA variables after issue changes from any hold status to any non-hold status - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - priority = get_priority(doc) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + if doc.meta.has_field("response_by_variance") and doc.get('first_responded_on'): + doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - # add hold time to response by variance - if meta.has_field("first_responded_on") and not doc.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time +def set_resolution_by_and_variance(doc, start_date_time, priority): + if doc.meta.has_field("resolution_by"): + doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + if doc.meta.has_field("total_hold_time") and doc.total_hold_time: + doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.total_hold_time)) - # add hold time to resolution by variance - if frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, "apply_sla_for_resolution"): - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) + if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - - update_values['on_hold_since'] = None - - doc.db_set(update_values) + if doc.meta.has_field("resolution_by_variance") and doc.get('resolution_date'): + doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.get('resolution_date')), 2) def get_service_level_agreement_fields(): @@ -808,45 +768,37 @@ def update_agreement_status_on_custom_status(doc): meta = frappe.get_meta(doc.doctype) now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - if meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - if meta.has_field("first_responded_on") and doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and doc.first_responded_on: # first_responded_on set when first reply is sent to customer doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) - if meta.has_field("resolution_date") and not doc.resolution_date: + if doc.meta.has_field("resolution_date") and not doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - if meta.has_field("resolution_date") and doc.resolution_date: + if doc.meta.has_field("resolution_date") and doc.resolution_date: # resolution_date set when issue has been closed doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) - if meta.has_field("agreement_status"): + if doc.meta.has_field("agreement_status"): doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" -def update_agreement_status(doc, meta): - if meta.has_field("service_level_agreement") and meta.has_field("agreement_status") and \ - doc.service_level_agreement and doc.agreement_status == "Ongoing": - - apply_sla_for_resolution = frappe.db.get_value("Service Level Agreement", doc.service_level_agreement, - "apply_sla_for_resolution") - +def update_agreement_status(doc, apply_sla_for_resolution): + if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if meta.has_field("response_by_variance") and meta.has_field("resolution_by_variance"): - if cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value(doc.doctype, doc.name, "resolution_by_variance")) < 0: - + if doc.meta.has_field("response_by_variance") and doc.meta.has_field("resolution_by_variance"): + if doc.response_by_variance < 0 or doc.resolution_by_variance < 0: doc.agreement_status = "Failed" else: doc.agreement_status = "Fulfilled" else: - if meta.has_field("response_by_variance") and \ - cint(frappe.db.get_value(doc.doctype, doc.name, "response_by_variance")) < 0: + if doc.meta.has_field("response_by_variance") and doc.response_by_variance < 0: doc.agreement_status = "Failed" else: doc.agreement_status = "Fulfilled" @@ -862,25 +814,6 @@ def get_time_in_timedelta(time): return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) -def set_response_by_and_variance(doc, meta, start_date_time, priority): - if meta.has_field("response_by"): - doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - - if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - elif meta.has_field("response_by_variance") and doc.get('first_responded_on'): - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - -def set_resolution_by_and_variance(doc, meta, start_date_time, priority): - if meta.has_field("resolution_by"): - doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - - if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - def now_datetime(user): dt = convert_utc_to_user_timezone(datetime.utcnow(), user) return dt.replace(tzinfo=None) From 214d0e367f5290dd82bc2f1dc7cdbd8e02951283 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Nov 2021 20:20:38 +0530 Subject: [PATCH 14/99] fix: remove leading whitespace --- .../doctype/service_level_agreement/service_level_agreement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 7e260dbbf56..bfbffe22ad7 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -47,7 +47,7 @@ frappe.ui.form.on('Service Level Agreement', { entity: function(frm) { const field = frm.get_field('entity'); if (frm.doc.entity) { - const and_descendants = frm.doc.entity_type != 'Customer' ? __(' or its descendants') : ''; + const and_descendants = frm.doc.entity_type != 'Customer' ? ' ' + __('or its descendants') : ''; field.set_description( __('SLA will be applied if {1} is set as {2}{3}', [ frm.doc.document_type, frm.doc.entity_type, From 267cc3585013c5a9454df0c25b65d90bf8fef171 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 27 Nov 2021 16:47:45 +0530 Subject: [PATCH 15/99] fix: failing tests --- .../service_level_agreement/service_level_agreement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 864640204b3..565f05083b1 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -642,8 +642,8 @@ def reset_expected_response_and_resolution(doc): def set_response_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.total_hold_time: - doc.response_by = add_to_date(doc.response_by, seconds=round(doc.total_hold_time)) + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) @@ -656,8 +656,8 @@ def set_response_by_and_variance(doc, start_date_time, priority): def set_resolution_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.total_hold_time: - doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.total_hold_time)) + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) From 79f8159ab95e357aa7b094901a9da36a44281c1c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 27 Nov 2021 17:14:58 +0530 Subject: [PATCH 16/99] test: issue closing after being on hold --- erpnext/support/doctype/issue/test_issue.py | 26 +++++++++++++++++++ .../service_level_agreement.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index ab9a444bc34..0559b15649d 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -142,6 +142,32 @@ class TestIssue(TestSetUp): issue.reload() self.assertEqual(flt(issue.total_hold_time, 2), 2700) + def test_issue_close_after_on_hold(self): + creation = get_datetime("2021-11-01 19:00") + + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + # send a reply within SLA + creation = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = creation + issue.reload() + issue.status = 'Replied' + issue.save() + + self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + + # close the issue after being on hold for 20 days + frappe.flags.current_time = get_datetime("2021-11-22 01:00") + issue.status = 'Closed' + issue.save() + + self.assertEqual(issue.resolution_by, get_datetime('2021-11-22 06:00:00')) + self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00')) + self.assertEqual(issue.agreement_status, 'Fulfilled') + class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm # all dates are in the mm-dd-yyyy format diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 565f05083b1..9c1e5360786 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -642,7 +642,7 @@ def reset_expected_response_and_resolution(doc): def set_response_by_and_variance(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): + if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'): doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): From 6f1cf94c9fb2c7f7abda9802c4868b427a339ed9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:18:30 +0530 Subject: [PATCH 17/99] fix: Fix 'Adjust Asset Value' button --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c0c437f8d2f..a053c7fab96 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,7 +724,7 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.get_doc("Asset Value Adjustment") + asset_maintenance = frappe.new_doc("Asset Value Adjustment") asset_maintenance.update({ "asset": asset, "company": company, From 5b224f841b0419d0250f02549300e0754fa816b5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:19:35 +0530 Subject: [PATCH 18/99] fix: Rename variable --- erpnext/assets/doctype/asset/asset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a053c7fab96..ff5964be680 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,13 +724,13 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.new_doc("Asset Value Adjustment") - asset_maintenance.update({ + asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") + asset_value_adjustment.update({ "asset": asset, "company": company, "asset_category": asset_category }) - return asset_maintenance + return asset_value_adjustment @frappe.whitelist() def transfer_asset(args): From 4629308d94b4fc2a667a7eb7d526b2a810bb486a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:22:18 +0530 Subject: [PATCH 19/99] fix: Rename function --- erpnext/assets/doctype/asset/asset.js | 6 +++--- erpnext/assets/doctype/asset/asset.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d5..9fa50a9aded 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', { if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { - frm.trigger("create_asset_adjustment"); + frm.trigger("create_asset_value_adjustment"); }, __("Manage")); } @@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', { }); }, - create_asset_adjustment: function(frm) { + create_asset_value_adjustment: function(frm) { frappe.call({ args: { "asset": frm.doc.name, "asset_category": frm.doc.asset_category, "company": frm.doc.company }, - method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment", + method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment", freeze: 1, callback: function(r) { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ff5964be680..f2b02cb9c56 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -723,7 +723,7 @@ def create_asset_repair(asset, asset_name): return asset_repair @frappe.whitelist() -def create_asset_adjustment(asset, asset_category, company): +def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") asset_value_adjustment.update({ "asset": asset, From 427938f87bd06ff0803ce7c1a079cb455fd4f96d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 2 Dec 2021 11:25:47 +0530 Subject: [PATCH 20/99] fix: new item form tour for home onboarding --- .../form_tour/item_general/item_general.json | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 erpnext/stock/form_tour/item_general/item_general.json diff --git a/erpnext/stock/form_tour/item_general/item_general.json b/erpnext/stock/form_tour/item_general/item_general.json new file mode 100644 index 00000000000..b468d270de6 --- /dev/null +++ b/erpnext/stock/form_tour/item_general/item_general.json @@ -0,0 +1,79 @@ +{ + "creation": "2021-12-02 10:37:55.433087", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2021-12-02 10:37:55.433087", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item General", + "owner": "Administrator", + "reference_doctype": "Item", + "save_on_complete": 1, + "steps": [ + { + "description": "Enter code for the Item", + "field": "", + "fieldname": "item_code", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Code", + "parent_field": "", + "position": "Right", + "title": "Item Code" + }, + { + "description": "Enter name for the Item", + "field": "", + "fieldname": "item_name", + "fieldtype": "Data", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Name", + "parent_field": "", + "position": "Right", + "title": "Item Name" + }, + { + "description": "Select an Item Group", + "field": "", + "fieldname": "item_group", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Group", + "parent_field": "", + "position": "Right", + "title": "Item Group" + }, + { + "description": "This is the default measuring unit that you will use for your product. It could be Nos, Kgs, Meters, etc.", + "field": "", + "fieldname": "stock_uom", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Unit of Measure", + "parent_field": "", + "position": "Right", + "title": "Default Unit of Measurement" + }, + { + "description": "When creating an Item, entering a value for this field will automatically create an Item Price at the backend. Entering a value after the Item has been saved will not work. In this case, the Item Price is created from any transactions with the Item.", + "field": "", + "fieldname": "standard_rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Standard Selling Rate", + "parent_field": "", + "position": "Left", + "title": "Standard Selling Rate" + } + ], + "title": "Item General" +} \ No newline at end of file From a108df8efc9e26fcece1f2f742902e20c9dd30c4 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 2 Dec 2021 11:27:37 +0530 Subject: [PATCH 21/99] fix: assigned proper form tour to onboarding steps --- erpnext/assets/module_onboarding/assets/assets.json | 2 +- .../onboarding_step/asset_category/asset_category.json | 2 +- .../assets/onboarding_step/asset_item/asset_item.json | 9 +++++---- .../onboarding_step/asset_purchase/asset_purchase.json | 2 +- .../fixed_asset_accounts/fixed_asset_accounts.json | 2 +- erpnext/setup/module_onboarding/home/home.json | 2 +- .../onboarding_step/company_set_up/company_set_up.json | 2 +- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_quotation/create_a_quotation.json | 2 +- .../create_a_supplier/create_a_supplier.json | 2 +- .../onboarding_step/create_an_item/create_an_item.json | 3 ++- .../setup/onboarding_step/data_import/data_import.json | 2 +- erpnext/setup/onboarding_step/letterhead/letterhead.json | 2 +- .../onboarding_step/navigation_help/navigation_help.json | 2 +- 14 files changed, 19 insertions(+), 17 deletions(-) diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json index e6df88b000e..796245df0cd 100644 --- a/erpnext/assets/module_onboarding/assets/assets.json +++ b/erpnext/assets/module_onboarding/assets/assets.json @@ -13,7 +13,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset", "idx": 0, "is_complete": 0, - "modified": "2021-08-24 17:50:41.573281", + "modified": "2021-12-02 11:24:37.963746", "modified_by": "Administrator", "module": "Assets", "name": "Assets", diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json index 033e86669cf..58f322eecf7 100644 --- a/erpnext/assets/onboarding_step/asset_category/asset_category.json +++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 12:49:37.665239", + "modified": "2021-11-23 10:02:03.242127", "modified_by": "Administrator", "name": "Asset Category", "owner": "Administrator", diff --git a/erpnext/assets/onboarding_step/asset_item/asset_item.json b/erpnext/assets/onboarding_step/asset_item/asset_item.json index 8a174c5b77b..13e3e2e838e 100644 --- a/erpnext/assets/onboarding_step/asset_item/asset_item.json +++ b/erpnext/assets/onboarding_step/asset_item/asset_item.json @@ -1,21 +1,22 @@ { - "action": "Show Form Tour", + "action": "Create Entry", "action_label": "Let's create a new Asset item", "creation": "2021-08-13 14:27:07.277167", "description": "# Asset Item\n\nAsset items are created based on Asset Category. You can create one or multiple items against once Asset Category. The sales and purchase transaction for Asset is done via Asset Item. ", "docstatus": 0, "doctype": "Onboarding Step", + "form_tour": "Item", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-16 13:59:18.362233", + "modified": "2021-12-02 11:23:48.158504", "modified_by": "Administrator", "name": "Asset Item", "owner": "Administrator", "reference_document": "Item", - "show_form_tour": 0, - "show_full_form": 0, + "show_form_tour": 1, + "show_full_form": 1, "title": "Create an Asset Item", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json index 54611edc296..69fa337ae07 100644 --- a/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json +++ b/erpnext/assets/onboarding_step/asset_purchase/asset_purchase.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 17:26:57.180637", + "modified": "2021-11-23 10:02:03.235498", "modified_by": "Administrator", "name": "Asset Purchase", "owner": "Administrator", diff --git a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json index cebee7a7eaf..2fc6c46c147 100644 --- a/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json +++ b/erpnext/assets/onboarding_step/fixed_asset_accounts/fixed_asset_accounts.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-24 17:46:37.646174", + "modified": "2021-11-23 10:02:03.229566", "modified_by": "Administrator", "name": "Fixed Asset Accounts", "owner": "Administrator", diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index fbff98ff8a8..e3868163cf8 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -25,7 +25,7 @@ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2021-11-24 16:40:15.154081", + "modified": "2021-12-02 11:24:49.108119", "modified_by": "Administrator", "module": "Setup", "name": "Home", diff --git a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json index dc851897649..57a3a3c2ed5 100644 --- a/erpnext/setup/onboarding_step/company_set_up/company_set_up.json +++ b/erpnext/setup/onboarding_step/company_set_up/company_set_up.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:41:01.620963", + "modified": "2021-11-24 18:01:53.528032", "modified_by": "Administrator", "name": "Company Set Up", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json index c3162f2b874..3f67809773e 100644 --- a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:20.882675", + "modified": "2021-11-24 18:01:53.508901", "modified_by": "Administrator", "name": "Create a Customer", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json index 12d4372470c..039d32b69ef 100644 --- a/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/setup/onboarding_step/create_a_quotation/create_a_quotation.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:55:28.950596", + "modified": "2021-12-02 11:16:01.553277", "modified_by": "Administrator", "name": "Create a Quotation", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json index cc87d97ae42..02e8a4021a8 100644 --- a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:36.656949", + "modified": "2021-11-24 18:01:53.499200", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json index 0d650de6654..177525e5fb6 100644 --- a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -5,12 +5,13 @@ "description": "Item is a product, of a or service offered by your company, or something you buy as a part of your supplies or raw materials.\n\nItems are integral to everything you do in ERPNext - from billing, purchasing to managing inventory. Everything you buy or sell, whether it is a physical product or a service is an Item. Items can be stock, non-stock, variants, serialized, batched, assets etc.\n", "docstatus": 0, "doctype": "Onboarding Step", + "form_tour": "Item General", "idx": 0, "intro_video_url": "", "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:51:21.446969", + "modified": "2021-12-02 11:16:01.451477", "modified_by": "Administrator", "name": "Create an Item", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json index 5f4e8db4ab4..f4628b7f089 100644 --- a/erpnext/setup/onboarding_step/data_import/data_import.json +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:54:37.622063", + "modified": "2021-11-24 18:01:53.480435", "modified_by": "Administrator", "name": "Data import", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/letterhead/letterhead.json b/erpnext/setup/onboarding_step/letterhead/letterhead.json index b8d430365f8..05ca5ca6285 100644 --- a/erpnext/setup/onboarding_step/letterhead/letterhead.json +++ b/erpnext/setup/onboarding_step/letterhead/letterhead.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 16:19:25.379798", + "modified": "2021-11-24 18:01:53.459675", "modified_by": "Administrator", "name": "Letterhead", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json index 1271a86cb9c..1cc58cb08b5 100644 --- a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-24 15:52:40.641949", + "modified": "2021-11-24 18:01:53.490470", "modified_by": "Administrator", "name": "Navigation Help", "owner": "Administrator", From 6f67bbca69a1dcd7a9dd9918f0c656704ecc14a7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 13:39:05 +0530 Subject: [PATCH 22/99] refactor(SLA): remove response_by_variance & resolution_by_variance --- erpnext/support/doctype/issue/issue.json | 22 +- erpnext/support/doctype/issue/issue.py | 4 +- erpnext/support/doctype/issue/test_issue.py | 109 +++++-- .../service_level_agreement.py | 269 ++++++++---------- .../test_service_level_agreement.py | 68 ++--- 5 files changed, 221 insertions(+), 251 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 75b6d0f4f92..1da22fd58f9 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -24,12 +24,10 @@ "service_level_section", "service_level_agreement", "response_by", - "response_by_variance", "reset_service_level_agreement", "cb", "agreement_status", "resolution_by", - "resolution_by_variance", "service_level_agreement_creation", "on_hold_since", "total_hold_time", @@ -44,8 +42,6 @@ "opening_date", "opening_time", "resolution_date", - "resolution_time", - "user_resolution_time", "additional_info", "lead", "contact", @@ -317,22 +313,6 @@ "fieldtype": "Check", "label": "Via Customer Portal" }, - { - "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "fieldname": "response_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Response By Variance", - "read_only": 1 - }, - { - "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "fieldname": "resolution_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Resolution By Variance", - "read_only": 1 - }, { "fieldname": "service_level_agreement_creation", "fieldtype": "Datetime", @@ -395,7 +375,7 @@ "fieldname": "agreement_status", "fieldtype": "Select", "label": "Service Level Agreement Status", - "options": "Ongoing\nFulfilled\nFailed", + "options": "First Response Due\nResolution Due\nFulfilled\nFailed", "read_only": 1 }, { diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 0dc3639f1eb..d5e5b782880 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -87,11 +87,9 @@ class Issue(Document): if replicated_issue.service_level_agreement: replicated_issue.service_level_agreement_creation = now_datetime() replicated_issue.service_level_agreement = None - replicated_issue.agreement_status = "Ongoing" + replicated_issue.agreement_status = "First Response Due" replicated_issue.response_by = None - replicated_issue.response_by_variance = None replicated_issue.resolution_by = None - replicated_issue.resolution_by_variance = None replicated_issue.reset_issue_metrics() frappe.get_doc(replicated_issue).insert() diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 0559b15649d..da9953df552 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -83,30 +83,6 @@ class TestIssue(TestSetUp): self.assertEqual(issue.agreement_status, 'Fulfilled') - def test_issue_metrics(self): - creation = get_datetime("2020-03-04 4:00") - - issue = make_issue(creation, index=1) - create_communication(issue.name, "test@example.com", "Received", creation) - - creation = get_datetime("2020-03-04 4:15") - create_communication(issue.name, "test@admin.com", "Sent", creation) - - creation = get_datetime("2020-03-04 5:00") - create_communication(issue.name, "test@example.com", "Received", creation) - - creation = get_datetime("2020-03-04 5:05") - create_communication(issue.name, "test@admin.com", "Sent", creation) - - frappe.flags.current_time = get_datetime("2020-03-04 5:05") - issue.reload() - issue.status = 'Closed' - issue.save() - - self.assertEqual(issue.avg_response_time, 600) - self.assertEqual(issue.resolution_time, 3900) - self.assertEqual(issue.user_resolution_time, 1200) - def test_hold_time_on_replied(self): creation = get_datetime("2020-03-04 4:00") @@ -143,16 +119,15 @@ class TestIssue(TestSetUp): self.assertEqual(flt(issue.total_hold_time, 2), 2700) def test_issue_close_after_on_hold(self): - creation = get_datetime("2021-11-01 19:00") + frappe.flags.current_time = get_datetime("2021-11-01 19:00") - issue = make_issue(creation, index=1) - create_communication(issue.name, "test@example.com", "Received", creation) + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) # send a reply within SLA - creation = get_datetime("2021-11-02 11:00") - create_communication(issue.name, "test@admin.com", "Sent", creation) + frappe.flags.current_time = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) - frappe.flags.current_time = creation issue.reload() issue.status = 'Replied' issue.save() @@ -168,6 +143,75 @@ class TestIssue(TestSetUp): self.assertEqual(issue.resolution_date, get_datetime('2021-11-22 01:00:00')) self.assertEqual(issue.agreement_status, 'Fulfilled') + def test_issue_open_after_closed(self): + + # Created on -> 1 pm, Response Time -> 4 hrs, Resolution Time -> 6 hrs + frappe.flags.current_time = get_datetime("2021-11-01 13:00") + issue = make_issue(frappe.flags.current_time, index=1, issue_type='Critical') # Applies 24hr working time SLA + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + self.assertEquals(issue.agreement_status, 'First Response Due') + self.assertEquals(issue.response_by, get_datetime("2021-11-01 17:00")) + self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 19:00")) + + # Replied on โ†’ 2 pm + frappe.flags.current_time = get_datetime("2021-11-01 14:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + issue.reload() + issue.status = 'Replied' + issue.save() + self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertEquals(issue.on_hold_since, frappe.flags.current_time) + self.assertEquals(issue.first_responded_on, frappe.flags.current_time) + + # Customer Replied โ†’ 3 pm + frappe.flags.current_time = get_datetime("2021-11-01 15:00") + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + issue.reload() + self.assertEquals(issue.status, 'Open') + # Hold Time + 1 Hrs + self.assertEquals(issue.total_hold_time, 3600) + # Resolution By should increase by one hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-01 20:00")) + + # Replied on โ†’ 4 pm, Open โ†’ 1 hr, Resolution Due โ†’ 8 pm + frappe.flags.current_time = get_datetime("2021-11-01 16:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + issue.reload() + issue.status = 'Replied' + issue.save() + self.assertEquals(issue.agreement_status, 'Resolution Due') + + # Customer Closed โ†’ 10 pm + frappe.flags.current_time = get_datetime("2021-11-01 22:00") + issue.status = 'Closed' + issue.save() + # Hold Time + 6 Hrs + self.assertEquals(issue.total_hold_time, 3600 + 21600) + # Resolution By should increase by 6 hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 02:00")) + self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.resolution_date, frappe.flags.current_time) + + # Customer Open โ†’ 3 am i.e after resolution by is crossed + frappe.flags.current_time = get_datetime("2021-11-02 03:00") + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + issue.reload() + # Since issue was Resolved, Resolution By should be increased by 5 hrs (3am - 10pm) + self.assertEquals(issue.total_hold_time, 3600 + 21600 + 18000) + # Resolution By should increase by 5 hrs + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEquals(issue.agreement_status, 'Resolution Due') + self.assertFalse(issue.resolution_date) + + # We Closed โ†’ 4 am, SLA should be Fulfilled + frappe.flags.current_time = get_datetime("2021-11-02 04:00") + issue.status = 'Closed' + issue.save() + self.assertEquals(issue.resolution_by, get_datetime("2021-11-02 07:00")) + self.assertEquals(issue.agreement_status, 'Fulfilled') + self.assertEquals(issue.resolution_date, frappe.flags.current_time) + + class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm # all dates are in the mm-dd-yyyy format @@ -386,7 +430,10 @@ def create_issue_and_communication(issue_creation, first_responded_on): return issue -def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None, do_not_insert=False): + if not frappe.db.exists('Issue Type', issue_type): + frappe.get_doc(dict(doctype='Issue Type', name=issue_type)).insert() + issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9c1e5360786..e2326ac2df1 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -358,36 +358,102 @@ def apply(doc, method=None): ): return - service_level_agreement = get_active_service_level_agreement_for(doc) + sla = get_active_service_level_agreement_for(doc) - if not service_level_agreement: + if not sla: return - process_sla(doc, service_level_agreement) + process_sla(doc, sla) -def process_sla(doc, service_level_agreement): +def process_sla(doc, sla): if not doc.creation: doc.creation = now_datetime(doc.get("owner")) if doc.meta.has_field("service_level_agreement_creation"): doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.service_level_agreement = service_level_agreement.name - doc.priority = doc.get("priority") or service_level_agreement.default_priority + doc.service_level_agreement = sla.name + doc.priority = doc.get("priority") or sla.default_priority prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') - handle_status_change(doc, prev_status, service_level_agreement.apply_sla_for_resolution) - update_response_and_resolution_metrics(doc, service_level_agreement.apply_sla_for_resolution) - update_agreement_status(doc, service_level_agreement.apply_sla_for_resolution) + handle_status_change(doc, prev_status, sla.apply_sla_for_resolution) + update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution) + update_agreement_status(doc, sla.apply_sla_for_resolution) -def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): - priority = get_response_and_resolution_duration(doc) - start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) - set_response_by_and_variance(doc, start_date_time, priority) - if apply_sla_for_resolution: - set_resolution_by_and_variance(doc, start_date_time, priority) +def handle_status_change(doc, prev_status, apply_sla_for_resolution): + now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + + hold_statuses = get_hold_statuses(doc.service_level_agreement) + fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) + + def is_hold_status(status): + return status in hold_statuses + + def is_fulfilled_status(status): + return status in fulfillment_statuses + + def is_open_status(status): + return status not in hold_statuses and status not in fulfillment_statuses + + def calculate_hold_hours(): + # In case issue was closed and after few days it has been opened + # The hold time should be calculated from resolution_date + + on_hold_since = doc.resolution_date or doc.on_hold_since + if on_hold_since: + current_hold_hours = time_diff_in_seconds(now_time, on_hold_since) + doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours + doc.on_hold_since = None + + if is_open_status(prev_status) and not is_open_status(doc.status): + # status changed from Open to something else + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + doc.first_responded_on = now_time + + # Open to Replied + if is_open_status(prev_status) and is_hold_status(doc.status): + # Issue is on hold -> Set on_hold_since + doc.on_hold_since = now_time + + # Replied to Open + if is_hold_status(prev_status) and is_open_status(doc.status): + # Issue was on hold -> Calculate Total Hold Time + calculate_hold_hours() + # Issue is open -> reset resolution_date + reset_expected_response_and_resolution(doc) + reset_resolution_metrics(doc) + + # Open to Closed + if is_open_status(prev_status) and is_fulfilled_status(doc.status): + # Issue is closed -> Set resolution_date + doc.resolution_date = now_time + set_resolution_time(doc) + + # Closed to Open + if is_fulfilled_status(prev_status) and is_open_status(doc.status): + # Issue was closed -> Calculate Total Hold Time from resolution_date + calculate_hold_hours() + # Issue is open -> reset resolution_date + reset_expected_response_and_resolution(doc) + reset_resolution_metrics(doc) + + # Closed to Replied + if is_fulfilled_status(prev_status) and is_hold_status(doc.status): + # Issue was closed -> Calculate Total Hold Time from resolution_date + calculate_hold_hours() + # Issue is on hold -> Set on_hold_since + doc.on_hold_since = now_time + + # Replied to Closed + if is_hold_status(prev_status) and is_fulfilled_status(doc.status): + # Issue was on hold -> Calculate Total Hold Time + calculate_hold_hours() + # Issue is closed -> Set resolution_date + if apply_sla_for_resolution: + doc.resolution_date = now_time + set_resolution_time(doc) def get_fulfillment_statuses(service_level_agreement): @@ -402,60 +468,12 @@ def get_hold_statuses(service_level_agreement): }, fields=["status"])] -def handle_status_change(doc, prev_status, apply_sla_for_resolution): - - if doc.status != "Open" and prev_status == "Open": - # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: - # status changed to something other than Open - doc.first_responded_on = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if doc.status == "Open" and prev_status != "Open": - # status changed from something else to Open - reset_resolution_metrics(doc) - - handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution) - handle_hold_status(doc, prev_status) - - -def handle_fulfillment_status(doc, prev_status, apply_sla_for_resolution): - fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) - if ( - doc.status in fulfillment_statuses - and prev_status not in fulfillment_statuses - and apply_sla_for_resolution - ): - # status changed to any fulfillment_statuses - if doc.meta.has_field("resolution_date"): - doc.resolution_date = frappe.flags.current_time or now_datetime(doc.get("owner")) - if doc.meta.has_field("resolution_time"): - doc.resolution_time = time_diff_in_seconds(doc.resolution_date, doc.creation) - set_user_resolution_time(doc) - - -def handle_hold_status(doc, prev_status): - hold_statuses = get_hold_statuses(doc.service_level_agreement) - if doc.status in hold_statuses: - # reset if status is a hold status, regardless of previous status - reset_expected_response_and_resolution(doc) - if prev_status not in hold_statuses: - # set on_hold_since status changed from any non-hold status - # for eg. doc.status changed from Open to Replied - if doc.meta.has_field("on_hold_since"): - doc.on_hold_since = frappe.flags.current_time or now_datetime(doc.get("owner")) - - if doc.status not in hold_statuses and prev_status in hold_statuses: - # status changed to any non-hold status - # for eg. doc.status changed from Replied to Closed - if doc.meta.has_field("on_hold_since") and doc.on_hold_since: - cumulate_hold_time(doc) - doc.on_hold_since = None - - -def cumulate_hold_time(doc): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - on_hold_duration = time_diff_in_seconds(now_time, doc.on_hold_since) - doc.total_hold_time = (doc.total_hold_time or 0) + on_hold_duration +def update_response_and_resolution_metrics(doc, apply_sla_for_resolution): + priority = get_response_and_resolution_duration(doc) + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + set_response_by(doc, start_date_time, priority) + if apply_sla_for_resolution: + set_resolution_by(doc, start_date_time, priority) def get_expected_time_for(parameter, service_level, start_date_time): @@ -526,7 +544,11 @@ def get_support_days(service_level): return support_days -def set_user_resolution_time(doc): +def set_resolution_time(doc): + start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation) + if doc.meta.has_field("resolution_time"): + doc.resolution_time = time_diff_in_seconds(doc.resolution_date, start_date_time) + # total time taken by a user to close the issue apart from wait_time if not doc.meta.has_field("user_resolution_time"): return @@ -544,7 +566,7 @@ def set_user_resolution_time(doc): pending_time.append(wait_time) total_pending_time = sum(pending_time) - resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, doc.creation) + resolution_time_in_secs = time_diff_in_seconds(doc.resolution_date, start_date_time) doc.user_resolution_time = resolution_time_in_secs - total_pending_time @@ -562,11 +584,11 @@ def change_service_level_agreement_and_priority(self): def get_response_and_resolution_duration(doc): - service_level_agreement = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) - priority = service_level_agreement.get_service_level_agreement_priority(doc.priority) + sla = frappe.get_doc("Service Level Agreement", doc.service_level_agreement) + priority = sla.get_service_level_agreement_priority(doc.priority) priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list + "support_and_resolution": sla.support_and_resolution, + "holiday_list": sla.holiday_list }) return priority @@ -585,8 +607,6 @@ def reset_service_level_agreement(doc, reason, user): }).insert(ignore_permissions=True) doc.service_level_agreement_creation = now_datetime(doc.get("owner")) - doc.set_response_and_resolution_time(priority=doc.priority, service_level_agreement=doc.service_level_agreement) - doc.agreement_status = "Ongoing" doc.save() @@ -616,56 +636,37 @@ def update_hold_time(doc, status): if not parent.meta.has_field('service_level_agreement'): return - apply_sla_for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') + for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - handle_status_change(parent, 'Replied', apply_sla_for_resolution) - update_response_and_resolution_metrics(parent, apply_sla_for_resolution) - update_agreement_status(parent, apply_sla_for_resolution) + handle_status_change(parent, 'Replied', for_resolution) + update_response_and_resolution_metrics(parent, for_resolution) + update_agreement_status(parent, for_resolution) parent.save() def reset_expected_response_and_resolution(doc): update_values = {} - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: update_values['response_by'] = None - update_values['response_by_variance'] = 0 - if doc.meta.has_field("resolution_by") and not doc.resolution_date: update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - doc.db_set(update_values) -def set_response_by_and_variance(doc, start_date_time, priority): +def set_response_by(doc, start_date_time, priority): if doc.meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time') and not doc.get('first_responded_on'): doc.response_by = add_to_date(doc.response_by, seconds=round(doc.get('total_hold_time'))) - if doc.meta.has_field("response_by_variance") and not doc.get('first_responded_on'): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - if doc.meta.has_field("response_by_variance") and doc.get('first_responded_on'): - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.get('first_responded_on')), 2) - - -def set_resolution_by_and_variance(doc, start_date_time, priority): +def set_resolution_by(doc, start_date_time, priority): if doc.meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) if doc.meta.has_field("total_hold_time") and doc.get('total_hold_time'): doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) - if doc.meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - if doc.meta.has_field("resolution_by_variance") and doc.get('resolution_date'): - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.get('resolution_date')), 2) - def get_service_level_agreement_fields(): return [ @@ -693,17 +694,11 @@ def get_service_level_agreement_fields(): "label": "Response By", "read_only": 1 }, - { - "fieldname": "response_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Response By Variance", - "read_only": 1 - }, { "fieldname": "first_responded_on", "fieldtype": "Datetime", "label": "First Responded On", + "no_copy": 1, "read_only": 1 }, { @@ -725,11 +720,11 @@ def get_service_level_agreement_fields(): "read_only": 1 }, { - "default": "Ongoing", + "default": "First Response Due", "fieldname": "agreement_status", "fieldtype": "Select", "label": "Service Level Agreement Status", - "options": "Ongoing\nFulfilled\nFailed", + "options": "First Response Due\nResolution Due\nFulfilled\nFailed", "read_only": 1 }, { @@ -738,13 +733,6 @@ def get_service_level_agreement_fields(): "label": "Resolution By", "read_only": 1 }, - { - "fieldname": "resolution_by_variance", - "fieldtype": "Duration", - "hide_seconds": 1, - "label": "Resolution By Variance", - "read_only": 1 - }, { "fieldname": "service_level_agreement_creation", "fieldtype": "Datetime", @@ -765,43 +753,28 @@ def get_service_level_agreement_fields(): def update_agreement_status_on_custom_status(doc): # Update Agreement Fulfilled status using Custom Scripts for Custom Status - - meta = frappe.get_meta(doc.doctype) - now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: - # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) - - if doc.meta.has_field("first_responded_on") and doc.first_responded_on: - # first_responded_on set when first reply is sent to customer - doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, doc.first_responded_on), 2) - - if doc.meta.has_field("resolution_date") and not doc.resolution_date: - # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) - - if doc.meta.has_field("resolution_date") and doc.resolution_date: - # resolution_date set when issue has been closed - doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, doc.resolution_date), 2) - - if doc.meta.has_field("agreement_status"): - doc.agreement_status = "Fulfilled" if doc.response_by_variance > 0 and doc.resolution_by_variance > 0 else "Failed" + update_agreement_status(doc) def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if doc.meta.has_field("response_by_variance") and doc.meta.has_field("resolution_by_variance"): - if doc.response_by_variance < 0 or doc.resolution_by_variance < 0: - doc.agreement_status = "Failed" - else: - doc.agreement_status = "Fulfilled" - else: - if doc.meta.has_field("response_by_variance") and doc.response_by_variance < 0: - doc.agreement_status = "Failed" - else: + if not doc.first_responded_on: + doc.agreement_status = "First Response Due" + elif not doc.resolution_date: + doc.agreement_status = "Resolution Due" + elif get_datetime(doc.resolution_date) <= get_datetime(doc.resolution_by): doc.agreement_status = "Fulfilled" + else: + doc.agreement_status = "Failed" + else: + if not doc.first_responded_on: + doc.agreement_status = "First Response Due" + elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): + doc.agreement_status = "Fulfilled" + else: + doc.agreement_status = "Failed" def is_holiday(date, holidays): diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index cfbe7446c0b..ce564c4dae7 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -220,42 +220,6 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') - def test_changing_of_variance_after_response(self): - # create lead - doctype = "Lead" - lead_sla = create_service_level_agreement( - default_service_level_agreement=1, - holiday_list="__Test Holiday List", - entity_type=None, entity=None, - response_time=14400, - doctype=doctype, - sla_fulfilled_on=[{"status": "Replied"}], - apply_sla_for_resolution=0 - ) - creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=2) - self.assertEqual(lead.service_level_agreement, lead_sla.name) - - # set lead as replied to set first responded on - frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30) - lead.reload() - lead.status = 'Replied' - lead.save() - lead.reload() - self.assertEqual(lead.agreement_status, 'Fulfilled') - - # check response_by_variance - self.assertEqual(lead.first_responded_on, frappe.flags.current_time) - self.assertEqual(lead.response_by_variance, 1800.0) - - # make a change on the document & - # check response_by_variance is unchanged - frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30) - lead.status = 'Open' - lead.save() - lead.reload() - self.assertEqual(lead.response_by_variance, 1800.0) - def test_service_level_agreement_filters(self): doctype = "Lead" lead_sla = create_service_level_agreement( @@ -295,7 +259,8 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ return service_level_agreement def create_service_level_agreement(default_service_level_agreement, holiday_list, response_time, entity_type, - entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1): + entity, resolution_time=0, doctype="Issue", condition="", sla_fulfilled_on=[], pause_sla_on=[], apply_sla_for_resolution=1, + service_level=None, start_time="10:00:00", end_time="18:00:00"): make_holiday_list() make_priorities() @@ -312,7 +277,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "doctype": "Service Level Agreement", "enabled": 1, "document_type": doctype, - "service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"), + "service_level": service_level or "__Test {} SLA".format(entity_type if entity_type else "Default"), "default_service_level_agreement": default_service_level_agreement, "condition": condition, "default_priority": "Medium", @@ -345,28 +310,28 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "support_and_resolution": [ { "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, }, { "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", + "start_time": start_time, + "end_time": end_time, } ] }) @@ -443,6 +408,13 @@ def create_service_level_agreements_for_issues(): create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, holiday_list="__Test Holiday List", + entity_type=None, entity=None, response_time=14400, resolution_time=21600, + service_level="24-hour-SLA", start_time="00:00:00", end_time="23:59:59", + condition="doc.issue_type == 'Critical'" + ) + def make_holiday_list(): holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") if not holiday_list: From a1cedc3ea027820f185c163333b69e5669cccd93 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 14:48:24 +0530 Subject: [PATCH 23/99] fix: failing tests --- .../service_level_agreement/service_level_agreement.py | 8 ++++---- .../test_service_level_agreement.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index e2326ac2df1..62b21474f03 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -647,9 +647,9 @@ def update_hold_time(doc, status): def reset_expected_response_and_resolution(doc): update_values = {} - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): update_values['response_by'] = None - if doc.meta.has_field("resolution_by") and not doc.resolution_date: + if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'): update_values['resolution_by'] = None doc.db_set(update_values) @@ -760,7 +760,7 @@ def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: doc.agreement_status = "First Response Due" elif not doc.resolution_date: doc.agreement_status = "Resolution Due" @@ -769,7 +769,7 @@ def update_agreement_status(doc, apply_sla_for_resolution): else: doc.agreement_status = "Failed" else: - if not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: doc.agreement_status = "First Response Due" elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): doc.agreement_status = "Fulfilled" diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index ce564c4dae7..b07c862c7b0 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -351,7 +351,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list if sla: frappe.delete_doc("Service Level Agreement", sla, force=1) - return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) + return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True, ignore_if_duplicate=True) def create_customer(): From 6736a89b4ee9b2f9125a29378700dd9a547ac19e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 2 Dec 2021 15:54:02 +0530 Subject: [PATCH 24/99] fix: failing tests --- .../service_level_agreement.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 62b21474f03..662e42cc9f5 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -409,7 +409,7 @@ def handle_status_change(doc, prev_status, apply_sla_for_resolution): if is_open_status(prev_status) and not is_open_status(doc.status): # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.first_responded_on = now_time # Open to Replied @@ -760,18 +760,18 @@ def update_agreement_status(doc, apply_sla_for_resolution): if (doc.meta.has_field("agreement_status")): # if SLA is applied for resolution check for response and resolution, else only response if apply_sla_for_resolution: - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.agreement_status = "First Response Due" - elif not doc.resolution_date: + elif doc.meta.has_field("resolution_date") and not doc.get('resolution_date'): doc.agreement_status = "Resolution Due" - elif get_datetime(doc.resolution_date) <= get_datetime(doc.resolution_by): + elif get_datetime(doc.get('resolution_date')) <= get_datetime(doc.get('resolution_by')): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" else: - if doc.meta.has_field("first_responded_on") and not doc.first_responded_on: + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.agreement_status = "First Response Due" - elif get_datetime(doc.first_responded_on) <= get_datetime(doc.response_by): + elif get_datetime(doc.get('first_responded_on')) <= get_datetime(doc.get('response_by')): doc.agreement_status = "Fulfilled" else: doc.agreement_status = "Failed" From 1e550d3e46c633e24ba6f49f2883a506aa96b471 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 3 Dec 2021 11:36:53 +0530 Subject: [PATCH 25/99] fix: failing tests --- erpnext/support/doctype/issue/test_issue.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index da9953df552..d566f33f24a 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -430,9 +430,11 @@ def create_issue_and_communication(issue_creation, first_responded_on): return issue -def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None, do_not_insert=False): - if not frappe.db.exists('Issue Type', issue_type): - frappe.get_doc(dict(doctype='Issue Type', name=issue_type)).insert() +def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): + if issue_type and not frappe.db.exists('Issue Type', issue_type): + doc = frappe.new_doc('Issue Type') + doc.name = issue_type + doc.insert() issue = frappe.get_doc({ "doctype": "Issue", From defa01edac2ebb52047f4fe060f23a9e81db0d3c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 3 Dec 2021 16:22:10 +0530 Subject: [PATCH 26/99] fix: undo removing of resolution_time fields --- erpnext/support/doctype/issue/issue.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 1da22fd58f9..38b5395adfd 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -42,6 +42,8 @@ "opening_date", "opening_time", "resolution_date", + "resolution_time", + "user_resolution_time", "additional_info", "lead", "contact", @@ -345,16 +347,16 @@ "read_only": 1 }, { - "fieldname": "resolution_time", - "fieldtype": "Duration", - "label": "Resolution Time", - "read_only": 1 + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 }, { - "fieldname": "user_resolution_time", - "fieldtype": "Duration", - "label": "User Resolution Time", - "read_only": 1 + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 }, { "fieldname": "on_hold_since", From 36a2d8ee0df17ce2f23a2afe5f63b406aaeb2ace Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 14:42:27 +0530 Subject: [PATCH 27/99] feat: Exit Interview --- erpnext/hr/doctype/exit_interview/__init__.py | 0 .../doctype/exit_interview/exit_interview.js | 20 ++ .../exit_interview/exit_interview.json | 190 ++++++++++++++++++ .../doctype/exit_interview/exit_interview.py | 17 ++ .../exit_interview/test_exit_interview.py | 8 + 5 files changed, 235 insertions(+) create mode 100644 erpnext/hr/doctype/exit_interview/__init__.py create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.js create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.json create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview.py create mode 100644 erpnext/hr/doctype/exit_interview/test_exit_interview.py diff --git a/erpnext/hr/doctype/exit_interview/__init__.py b/erpnext/hr/doctype/exit_interview/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js new file mode 100644 index 00000000000..d8ce7018d5d --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -0,0 +1,20 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Exit Interview', { + refresh: function(frm) { + + }, + + employee: function(frm) { + frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date').then(({ relieving_date }) => { + if (!relieving_date) { + frappe.throw({ + message: __('Please set the relieving date for employee {0}', + ['' + frm.doc.employee + '']), + title: __('Relieving Date Missing') + }); + } + }); + } +}); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json new file mode 100644 index 00000000000..696820ee490 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -0,0 +1,190 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2021-12-05 13:56:36.241690", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "employee", + "employee_name", + "column_break_5", + "company", + "date", + "employee_details_section", + "department", + "designation", + "reports_to", + "column_break_9", + "date_of_joining", + "relieving_date", + "exit_questionnaire_section", + "ref_doctype", + "column_break_10", + "reference_document_name", + "interview_summary_section", + "interviewers", + "text_editor_12" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fetch_from": "employee.relieving_date", + "fieldname": "relieving_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Relieving Date", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "exit_questionnaire_section", + "fieldtype": "Section Break", + "label": "Exit Questionnaire" + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType" + }, + { + "fieldname": "reference_document_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Document Name", + "options": "ref_doctype" + }, + { + "fieldname": "interview_summary_section", + "fieldtype": "Section Break", + "label": "Interview Details" + }, + { + "fieldname": "text_editor_12", + "fieldtype": "Text Editor", + "label": "Interview Summary" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "interviewers", + "fieldtype": "Table MultiSelect", + "label": "Interviewers", + "options": "Interviewer", + "reqd": 1 + }, + { + "fetch_from": "employee.date_of_joining", + "fieldname": "date_of_joining", + "fieldtype": "Date", + "label": "Date of Joining", + "read_only": 1 + }, + { + "fieldname": "reports_to", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Reports To", + "options": "Employee", + "read_only": 1 + }, + { + "fieldname": "employee_details_section", + "fieldtype": "Section Break", + "label": "Employee Details" + }, + { + "fetch_from": "employee.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "HR-EXIT-INT-" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-12-05 14:25:40.416023", + "modified_by": "Administrator", + "module": "HR", + "name": "Exit Interview", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py new file mode 100644 index 00000000000..677141b6970 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_link_to_form + +class ExitInterview(Document): + def validate(self): + self.validate_relieving_date() + + def validate_relieving_date(self): + if not frappe.db.get_value('Employee', self.employee, 'relieving_date'): + frappe.throw(_('Please set the relieving date for employee {0}').format( + get_link_to_form('Employee', self.employee)), + title=_('Relieving Date Missing')) diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py new file mode 100644 index 00000000000..daf3d66290d --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestExitInterview(unittest.TestCase): + pass From 7412accf6d4d6b932b24e40828a886b12111c1b4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:02:19 +0530 Subject: [PATCH 28/99] feat: sending Exit Questionnaire --- .../doctype/exit_interview/exit_interview.js | 38 +++++++++++++++++-- .../exit_interview/exit_interview.json | 21 +++++++++- .../doctype/exit_interview/exit_interview.py | 36 ++++++++++++++++++ .../hr/doctype/hr_settings/hr_settings.json | 29 +++++++++++++- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index d8ce7018d5d..31c961063f4 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -3,12 +3,16 @@ frappe.ui.form.on('Exit Interview', { refresh: function(frm) { - + if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent) { + frm.add_custom_button(__('Send Exit Questionnaire'), function () { + frm.trigger('send_exit_questionnaire'); + }); + } }, employee: function(frm) { - frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date').then(({ relieving_date }) => { - if (!relieving_date) { + frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => { + if (!message.relieving_date) { frappe.throw({ message: __('Please set the relieving date for employee {0}', ['' + frm.doc.employee + '']), @@ -16,5 +20,33 @@ frappe.ui.form.on('Exit Interview', { }); } }); + }, + + send_exit_questionnaire: function(frm) { + frappe.db.get_value('HR Settings', 'HR Settings', + ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], (r) => { + if (!r.exit_questionnaire_web_form || !r.exit_questionnaire_notification_template) { + frappe.throw({ + message: __('Please set {0} and {1} in {2}.', + ['Exit Questionnaire Web Form'.bold(), + 'Notification Template'.bold(), + 'HR Settings'] + ), + title: __('Settings Missing') + }); + } else { + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + args: { + 'exit_interview': frm.doc.name + }, + callback: function(r) { + if (!r.exc) { + frm.refresh_field('questionnaire_email_sent'); + } + } + }); + } + }); } }); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 696820ee490..0712b3d2a58 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -5,11 +5,13 @@ "creation": "2021-12-05 13:56:36.241690", "doctype": "DocType", "editable_grid": 1, + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "naming_series", "employee", "employee_name", + "email", "column_break_5", "company", "date", @@ -22,6 +24,7 @@ "relieving_date", "exit_questionnaire_section", "ref_doctype", + "questionnaire_email_sent", "column_break_10", "reference_document_name", "interview_summary_section", @@ -130,6 +133,7 @@ "read_only": 1 }, { + "fetch_from": "employee.reports_to", "fieldname": "reports_to", "fieldtype": "Link", "in_standard_filter": 1, @@ -159,11 +163,25 @@ "fieldtype": "Select", "label": "Naming Series", "options": "HR-EXIT-INT-" + }, + { + "default": "0", + "fieldname": "questionnaire_email_sent", + "fieldtype": "Check", + "label": "Questionnaire Email Sent", + "read_only": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email ID", + "options": "Email", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-05 14:25:40.416023", + "modified": "2021-12-05 16:50:05.933394", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", @@ -183,6 +201,7 @@ "write": 1 } ], + "sender_field": "email", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 677141b6970..a59146a8989 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -6,12 +6,48 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import get_link_to_form +from erpnext.hr.doctype.employee.employee import get_employee_email + + class ExitInterview(Document): def validate(self): self.validate_relieving_date() + self.set_employee_email() def validate_relieving_date(self): if not frappe.db.get_value('Employee', self.employee, 'relieving_date'): frappe.throw(_('Please set the relieving date for employee {0}').format( get_link_to_form('Employee', self.employee)), title=_('Relieving Date Missing')) + + def set_employee_email(self): + employee = frappe.get_doc('Employee', self.employee) + self.email = get_employee_email(employee) + + +@frappe.whitelist() +def send_exit_questionnaire(exit_interview): + exit_interview = frappe.get_doc('Exit Interview', exit_interview) + context = exit_interview.as_dict() + + employee = frappe.get_doc('Employee', exit_interview.employee) + context.update(employee.as_dict()) + + email = get_employee_email(employee) + template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') + template = frappe.get_doc('Email Template', template_name) + + if email: + frappe.sendmail( + recipients=email, + subject=template.subject, + message=frappe.render_template(template.response, context), + reference_doctype=exit_interview.doctype, + reference_name=exit_interview.name + ) + frappe.msgprint(_('Exit Questionnaire sent to {0}').format(email), + title='Success', indicator='green') + exit_interview.db_set('questionnaire_email_sent', True) + exit_interview.notify_update() + else: + frappe.msgprint(_('Email IDs for employee not found.')) \ No newline at end of file diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 5148435c130..f9a3e05fc34 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -36,7 +36,11 @@ "remind_before", "column_break_4", "send_interview_feedback_reminder", - "feedback_reminder_notification_template" + "feedback_reminder_notification_template", + "employee_exit_section", + "exit_questionnaire_web_form", + "column_break_34", + "exit_questionnaire_notification_template" ], "fields": [ { @@ -226,13 +230,34 @@ "fieldname": "check_vacancies", "fieldtype": "Check", "label": "Check Vacancies On Job Offer Creation" + }, + { + "fieldname": "employee_exit_section", + "fieldtype": "Section Break", + "label": "Employee Exit Settings" + }, + { + "fieldname": "exit_questionnaire_web_form", + "fieldtype": "Link", + "label": "Exit Questionnaire Web Form", + "options": "Web Form" + }, + { + "fieldname": "exit_questionnaire_notification_template", + "fieldtype": "Link", + "label": "Exit Questionnaire Notification Template", + "options": "Email Template" + }, + { + "fieldname": "column_break_34", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-10-01 23:46:11.098236", + "modified": "2021-12-05 14:48:10.884253", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", From 235b707417316dc787fe4fb154f6db81896a9c13 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:06:29 +0530 Subject: [PATCH 29/99] feat: add default Exit Questionnaire email template --- ...t_questionnaire_notification_template.html | 16 +++++++++++ erpnext/patches.txt | 1 + ...xit_questionnaire_notification_template.py | 27 +++++++++++++++++++ .../setup_wizard/operations/defaults_setup.py | 2 ++ .../operations/install_fixtures.py | 5 ++++ 5 files changed, 51 insertions(+) create mode 100644 erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html create mode 100644 erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py diff --git a/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html new file mode 100644 index 00000000000..0317b1a1026 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html @@ -0,0 +1,16 @@ +

Exit Questionnaire

+
+ +

+ Dear {{ employee_name }}, +

+ + Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us. + Request you to take out a few minutes to fill up this Exit Questionnaire. + + {% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %} + {% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %} + +

+ {{ _('Submit Now') }} +

diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce256..717965a569e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -312,3 +312,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template \ No newline at end of file diff --git a/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py new file mode 100644 index 00000000000..8b1752b2c73 --- /dev/null +++ b/erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py @@ -0,0 +1,27 @@ +import os + +import frappe +from frappe import _ + + +def execute(): + frappe.reload_doc("email", "doctype", "email_template") + frappe.reload_doc("hr", "doctype", "hr_settings") + + template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification")) + if not template: + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html")) + + template = frappe.get_doc({ + "doctype": "Email Template", + "name": _("Exit Questionnaire Notification"), + "response": response, + "subject": _("Exit Questionnaire Notification"), + "owner": frappe.session.user, + }).insert(ignore_permissions=True) + template = template.name + + hr_settings = frappe.get_doc("HR Settings") + hr_settings.exit_questionnaire_notification_template = template + hr_settings.save() diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index e4b1fa26ae0..ca1f57eb1d4 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -68,6 +68,8 @@ def set_default_settings(args): hr_settings.send_interview_feedback_reminder = 1 hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") + + hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification") hr_settings.save() def set_no_copy_fields_in_variant_settings(): diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 503aeacd015..323a7940b80 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -278,6 +278,11 @@ def install(country=None): records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response, 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}] + response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html')) + + records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response, + 'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}] + base_path = frappe.get_app_path("erpnext", "stock", "doctype") response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) From 1347187a30b1c485d5d602bd5aca78c3efa11563 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 17:20:39 +0530 Subject: [PATCH 30/99] feat: track status and final decision (Retained/Exit Confirmed) --- .../exit_interview/exit_interview.json | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 0712b3d2a58..bed0e776f1d 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -14,6 +14,7 @@ "email", "column_break_5", "company", + "status", "date", "employee_details_section", "department", @@ -29,7 +30,9 @@ "reference_document_name", "interview_summary_section", "interviewers", - "text_editor_12" + "text_editor_12", + "employee_status_section", + "employee_status" ], "fields": [ { @@ -72,7 +75,6 @@ { "fieldname": "company", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Company", "options": "Company", @@ -84,6 +86,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Date", + "mandatory_depends_on": "eval:doc.status==='Scheduled';", "reqd": 1 }, { @@ -168,6 +171,7 @@ "default": "0", "fieldname": "questionnaire_email_sent", "fieldtype": "Check", + "in_standard_filter": 1, "label": "Questionnaire Email Sent", "read_only": 1 }, @@ -177,11 +181,33 @@ "label": "Email ID", "options": "Email", "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Pending\nScheduled\nCompleted", + "reqd": 1 + }, + { + "fieldname": "employee_status_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "employee_status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Final Decision", + "mandatory_depends_on": "eval:doc.status==='Completed';", + "options": "\nEmployee Retained\nExit Confirmed" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-05 16:50:05.933394", + "modified": "2021-12-05 17:17:20.033950", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", From 09fdfed1633110db19ffb978263c57b143349195 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 18:28:23 +0530 Subject: [PATCH 31/99] fix: make Exit Interview submittable --- .../doctype/exit_interview/exit_interview.json | 17 ++++++++++++++--- .../hr/doctype/exit_interview/exit_interview.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index bed0e776f1d..d4d4aaceea3 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -32,7 +32,8 @@ "interviewers", "text_editor_12", "employee_status_section", - "employee_status" + "employee_status", + "amended_from" ], "fields": [ { @@ -188,7 +189,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Pending\nScheduled\nCompleted", + "options": "Pending\nScheduled\nCompleted\nCancelled", "reqd": 1 }, { @@ -203,11 +204,21 @@ "label": "Final Decision", "mandatory_depends_on": "eval:doc.status==='Completed';", "options": "\nEmployee Retained\nExit Confirmed" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Exit Interview", + "print_hide": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, + "is_submittable": 1, "links": [], - "modified": "2021-12-05 17:17:20.033950", + "modified": "2021-12-05 17:49:44.839277", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index a59146a8989..878380b9ec7 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -12,6 +12,7 @@ from erpnext.hr.doctype.employee.employee import get_employee_email class ExitInterview(Document): def validate(self): self.validate_relieving_date() + self.validate_duplicate_interview() self.set_employee_email() def validate_relieving_date(self): @@ -20,10 +21,21 @@ class ExitInterview(Document): get_link_to_form('Employee', self.employee)), title=_('Relieving Date Missing')) + def validate_duplicate_interview(self): + doc = frappe.db.exists('Exit Interview', {'employee': self.employee, 'name': ('!=', self.name)}) + if doc: + frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( + get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), + title=_('Duplicate Document')) + def set_employee_email(self): employee = frappe.get_doc('Employee', self.employee) self.email = get_employee_email(employee) + def on_submit(self): + if self.status != 'Completed': + frappe.throw(_('Only Completed documents can be submitted')) + @frappe.whitelist() def send_exit_questionnaire(exit_interview): From e30187f2469bb82fe1ed98abaa4f126053ba9371 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 19:32:33 +0530 Subject: [PATCH 32/99] feat: bulk questionnaire sending --- .../doctype/exit_interview/exit_interview.js | 32 ++----- .../exit_interview/exit_interview.json | 6 +- .../doctype/exit_interview/exit_interview.py | 90 ++++++++++++++----- .../exit_interview/exit_interview_list.js | 27 ++++++ 4 files changed, 108 insertions(+), 47 deletions(-) create mode 100644 erpnext/hr/doctype/exit_interview/exit_interview_list.js diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index 31c961063f4..849e8542d2f 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -23,29 +23,15 @@ frappe.ui.form.on('Exit Interview', { }, send_exit_questionnaire: function(frm) { - frappe.db.get_value('HR Settings', 'HR Settings', - ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], (r) => { - if (!r.exit_questionnaire_web_form || !r.exit_questionnaire_notification_template) { - frappe.throw({ - message: __('Please set {0} and {1} in {2}.', - ['Exit Questionnaire Web Form'.bold(), - 'Notification Template'.bold(), - 'HR Settings'] - ), - title: __('Settings Missing') - }); - } else { - frappe.call({ - method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', - args: { - 'exit_interview': frm.doc.name - }, - callback: function(r) { - if (!r.exc) { - frm.refresh_field('questionnaire_email_sent'); - } - } - }); + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + args: { + 'interviews': [frm.doc] + }, + callback: function(r) { + if (!r.exc) { + frm.refresh_field('questionnaire_email_sent'); + } } }); } diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index d4d4aaceea3..4b396402dbf 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -87,8 +87,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Date", - "mandatory_depends_on": "eval:doc.status==='Scheduled';", - "reqd": 1 + "mandatory_depends_on": "eval:doc.status==='Scheduled';" }, { "fieldname": "exit_questionnaire_section", @@ -174,6 +173,7 @@ "fieldtype": "Check", "in_standard_filter": 1, "label": "Questionnaire Email Sent", + "no_copy": 1, "read_only": 1 }, { @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-05 17:49:44.839277", + "modified": "2021-12-05 18:56:34.856854", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 878380b9ec7..b2ada4d83ca 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -38,28 +38,76 @@ class ExitInterview(Document): @frappe.whitelist() -def send_exit_questionnaire(exit_interview): - exit_interview = frappe.get_doc('Exit Interview', exit_interview) - context = exit_interview.as_dict() +def send_exit_questionnaire(interviews): + interviews = get_interviews(interviews) + validate_questionnaire_settings() - employee = frappe.get_doc('Employee', exit_interview.employee) - context.update(employee.as_dict()) + email_success = [] + email_failure = [] - email = get_employee_email(employee) - template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') - template = frappe.get_doc('Email Template', template_name) + for exit_interview in interviews: + interview = frappe.get_doc('Exit Interview', exit_interview.get('name')) + if interview.get('questionnaire_email_sent'): + continue - if email: - frappe.sendmail( - recipients=email, - subject=template.subject, - message=frappe.render_template(template.response, context), - reference_doctype=exit_interview.doctype, - reference_name=exit_interview.name + employee = frappe.get_doc('Employee', interview.employee) + email = get_employee_email(employee) + + context = interview.as_dict() + context.update(employee.as_dict()) + template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template') + template = frappe.get_doc('Email Template', template_name) + + if email: + frappe.sendmail( + recipients=email, + subject=template.subject, + message=frappe.render_template(template.response, context), + reference_doctype=interview.doctype, + reference_name=interview.name + ) + interview.db_set('questionnaire_email_sent', True) + interview.notify_update() + email_success.append(email) + else: + email_failure.append(get_link_to_form('Employee', employee.name)) + + show_email_summary(email_success, email_failure) + + +def get_interviews(interviews): + import json + + if isinstance(interviews, str): + interviews = json.loads(interviews) + + if not len(interviews): + frappe.throw(_('Atleast one interview has to be selected.')) + + return interviews + + +def validate_questionnaire_settings(): + settings = frappe.db.get_value('HR Settings', 'HR Settings', + ['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True) + + if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template: + frappe.throw( + message=_('Please set {0} and {1} in {2}.').format( + frappe.bold('Exit Questionnaire Web Form'), + frappe.bold('Notification Template'), + get_link_to_form('HR Settings', 'HR Settings')), + title=_('Settings Missing') ) - frappe.msgprint(_('Exit Questionnaire sent to {0}').format(email), - title='Success', indicator='green') - exit_interview.db_set('questionnaire_email_sent', True) - exit_interview.notify_update() - else: - frappe.msgprint(_('Email IDs for employee not found.')) \ No newline at end of file + + +def show_email_summary(email_success, email_failure): + message = '' + if email_success: + message += _('{0}: {1}').format( + frappe.bold('Sent Successfully'), ', '.join(email_success)) + if email_failure: + message += + '

' + _('{0} due to missing email information for employee(s): {1}').format( + frappe.bold('Sending Failed'), ', '.join(email_failure)) + + frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/exit_interview_list.js b/erpnext/hr/doctype/exit_interview/exit_interview_list.js new file mode 100644 index 00000000000..93d7b213f20 --- /dev/null +++ b/erpnext/hr/doctype/exit_interview/exit_interview_list.js @@ -0,0 +1,27 @@ +frappe.listview_settings['Exit Interview'] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + let status_color = { + 'Pending': 'orange', + 'Scheduled': 'yellow', + 'Completed': 'green', + 'Cancelled': 'red', + }; + return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status]; + }, + + onload: function(listview) { + if (frappe.boot.user.can_write.includes('Exit Interview')) { + listview.page.add_action_item(__('Send Exit Questionnaires'), function() { + const interviews = listview.get_checked_items(); + frappe.call({ + method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire', + freeze: true, + args: { + 'interviews': interviews + } + }); + }); + } + } +}; From d67536cc8d11ce7ec6e4fa0f46f633eba724ba33 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 19:55:25 +0530 Subject: [PATCH 33/99] feat: update Exit Interview date in employee master on submission --- .../hr/doctype/employee/employee_dashboard.py | 6 +++++- .../doctype/exit_interview/exit_interview.py | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index a4c0af0a4bf..a1247d9eb17 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -21,7 +21,11 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] + 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance'] + }, + { + 'label': _('Exit'), + 'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement'] }, { 'label': _('Shift'), diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index b2ada4d83ca..59dd4631c78 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -22,7 +22,11 @@ class ExitInterview(Document): title=_('Relieving Date Missing')) def validate_duplicate_interview(self): - doc = frappe.db.exists('Exit Interview', {'employee': self.employee, 'name': ('!=', self.name)}) + doc = frappe.db.exists('Exit Interview', { + 'employee': self.employee, + 'name': ('!=', self.name), + 'docstatus': ('!=', 2) + }) if doc: frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), @@ -36,6 +40,18 @@ class ExitInterview(Document): if self.status != 'Completed': frappe.throw(_('Only Completed documents can be submitted')) + self.update_interview_date_in_employee() + + def on_cancel(self): + self.update_interview_date_in_employee() + self.db_set('status', 'Cancelled') + + def update_interview_date_in_employee(self): + if self.docstatus == 1: + frappe.db.set_value('Employee', self.employee, 'held_on', self.date) + elif self.docstatus == 2: + frappe.db.set_value('Employee', self.employee, 'held_on', None) + @frappe.whitelist() def send_exit_questionnaire(interviews): From 3437f568be7ceafa3dc00af163c185aeec865c77 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 21:55:13 +0530 Subject: [PATCH 34/99] feat: Employee Exits report --- erpnext/hr/report/employee_exits/__init__.py | 0 .../report/employee_exits/employee_exits.js | 77 ++++++ .../report/employee_exits/employee_exits.json | 33 +++ .../report/employee_exits/employee_exits.py | 231 ++++++++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 erpnext/hr/report/employee_exits/__init__.py create mode 100644 erpnext/hr/report/employee_exits/employee_exits.js create mode 100644 erpnext/hr/report/employee_exits/employee_exits.json create mode 100644 erpnext/hr/report/employee_exits/employee_exits.py diff --git a/erpnext/hr/report/employee_exits/__init__.py b/erpnext/hr/report/employee_exits/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/report/employee_exits/employee_exits.js b/erpnext/hr/report/employee_exits/employee_exits.js new file mode 100644 index 00000000000..ac677d87e78 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.js @@ -0,0 +1,77 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Employee Exits"] = { + filters: [ + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12) + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.nowdate() + }, + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company" + }, + { + "fieldname": "department", + "label": __("Department"), + "fieldtype": "Link", + "options": "Department" + }, + { + "fieldname": "designation", + "label": __("Designation"), + "fieldtype": "Link", + "options": "Designation" + }, + { + "fieldname": "employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "reports_to", + "label": __("Reports To"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "interview_status", + "label": __("Interview Status"), + "fieldtype": "Select", + "options": ["", "Pending", "Scheduled", "Completed"] + }, + { + "fieldname": "final_decision", + "label": __("Final Decision"), + "fieldtype": "Select", + "options": ["", "Employee Retained", "Exit Confirmed"] + }, + { + "fieldname": "exit_interview_pending", + "label": __("Exit Interview Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "questionnaire_pending", + "label": __("Exit Questionnaire Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "fnf_pending", + "label": __("FnF Pending"), + "fieldtype": "Check" + } + ] +}; diff --git a/erpnext/hr/report/employee_exits/employee_exits.json b/erpnext/hr/report/employee_exits/employee_exits.json new file mode 100644 index 00000000000..4fe9a853c0c --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-12-05 19:47:18.332319", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test", + "modified": "2021-12-05 19:47:18.332319", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Exits", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Exit Interview", + "report_name": "Employee Exits", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "HR Manager" + }, + { + "role": "HR User" + } + ] +} \ No newline at end of file diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py new file mode 100644 index 00000000000..fd49543d084 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -0,0 +1,231 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _ +from frappe.utils import getdate + + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + chart = get_chart_data(data) + report_summary = get_report_summary(data) + + return columns, data, None, chart, report_summary + +def get_columns(): + return [ + { + 'label': _('Employee'), + 'fieldname': 'employee', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 150 + }, + { + 'label': _('Employee Name'), + 'fieldname': 'employee_name', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _('Date of Joining'), + 'fieldname': 'date_of_joining', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Relieving Date'), + 'fieldname': 'relieving_date', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Department'), + 'fieldname': 'department', + 'fieldtype': 'Link', + 'options': 'Department', + 'width': 120 + }, + { + 'label': _('Designation'), + 'fieldname': 'designation', + 'fieldtype': 'Link', + 'options': 'Designation', + 'width': 120 + }, + { + 'label': _('Reports To'), + 'fieldname': 'reports_to', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 120 + }, + { + 'label': _('Exit Interview'), + 'fieldname': 'exit_interview', + 'fieldtype': 'Link', + 'options': 'Exit Interview', + 'width': 98 + }, + { + 'label': _('Interview Status'), + 'fieldname': 'interview_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Final Decision'), + 'fieldname': 'employee_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Full and Final Statement'), + 'fieldname': 'full_and_final_statement', + 'fieldtype': 'Link', + 'options': 'Full and Final Statement', + 'width': 150 + } + ] + +def get_data(filters): + data = [] + + employee = frappe.qb.DocType('Employee') + interview = frappe.qb.DocType('Exit Interview') + fnf = frappe.qb.DocType('Full and Final Statement') + + query = ( + frappe.qb.from_(employee) + .left_join(interview).on(interview.employee == employee.name) + .left_join(fnf).on(fnf.employee == employee.name) + .select( + employee.name.as_('employee'), employee.employee_name.as_('employee_name'), + employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'), + employee.department.as_('department'), employee.designation.as_('designation'), + employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'), + interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), + interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement') + ).distinct() + .where( + ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) + & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) + & ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2))) + ) + ) + + query = get_conditions(filters, query, employee, interview, fnf) + result = query.run(as_dict=True) + + return result + + +def get_conditions(filters, query, employee, interview, fnf): + if filters.get('from_date') and filters.get('to_date'): + query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))]) + + elif filters.get('from_date'): + query = query.where(employee.relieving_date >= filters.get('from_date')) + + elif filters.get('to_date'): + query = query.where(employee.relieving_date <= filters.get('to_date')) + + if filters.get('company'): + query = query.where(employee.company == filters.get('company')) + + if filters.get('department'): + query = query.where(employee.department == filters.get('department')) + + if filters.get('designation'): + query = query.where(employee.designation == filters.get('designation')) + + if filters.get('employee'): + query = query.where(employee.name == filters.get('employee')) + + if filters.get('reports_to'): + query = query.where(employee.reports_to == filters.get('reports_to')) + + if filters.get('interview_status'): + query = query.where(interview.status == filters.get('interview_status')) + + if filters.get('final_decision'): + query = query.where(interview.employee_status == filters.get('final_decision')) + + if filters.get('exit_interview_pending'): + query = query.where((interview.name == '') | (interview.name.isnull())) + + if filters.get('questionnaire_pending'): + query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull())) + + if filters.get('fnf_pending'): + query = query.where((fnf.name == '') | (interview.name.isnull())) + + return query + + +def get_chart_data(data): + if not data: + return None + + retained = 0 + exit_confirmed = 0 + pending = 0 + + for entry in data: + if entry.employee_status == 'Employee Retained': + retained += 1 + elif entry.employee_status == 'Exit Confirmed': + exit_confirmed += 1 + else: + pending += 1 + + chart = { + 'data': { + 'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')], + 'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}] + }, + 'type': 'donut', + 'colors': ['green', 'red', 'blue'], + } + + return chart + + +def get_report_summary(data): + if not data: + return None + + total_resignations = len(data) + interviews_pending = len([entry.name for entry in data if not entry.exit_interview]) + fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement]) + questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire]) + + return [ + { + 'value': total_resignations, + 'label': _('Total Resignations'), + 'indicator': 'Red' if total_resignations > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': interviews_pending, + 'label': _('Pending Interviews'), + 'indicator': 'Blue' if interviews_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': fnf_pending, + 'label': _('Pending FnF'), + 'indicator': 'Blue' if fnf_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': questionnaires_pending, + 'label': _('Pending Questionnaires'), + 'indicator': 'Blue' if questionnaires_pending > 0 else 'Green', + 'datatype': 'Int' + }, + ] + From 1c09439d037b2694c309f497a0b3d611e8241798 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 22:06:19 +0530 Subject: [PATCH 35/99] chore: update HR workspace --- erpnext/hr/workspace/hr/hr.json | 871 +++++++++++++++++++++++++++++--- 1 file changed, 789 insertions(+), 82 deletions(-) diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index 7408d63eee5..85e641c856c 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -5,7 +5,7 @@ "label": "Outgoing Salary" } ], - "content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-03-02 15:48:58.322521", "docstatus": 0, "doctype": "Workspace", @@ -15,14 +15,6 @@ "idx": 0, "label": "HR", "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Employee", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -111,14 +103,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Employee Lifecycle", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Job Applicant", "hidden": 0, @@ -227,14 +211,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Shift Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -268,14 +244,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Leaves", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -386,14 +354,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Attendance", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Employee", "hidden": 0, @@ -449,14 +409,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Expense Claims", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Employee", "hidden": 0, @@ -489,14 +441,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Settings", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -530,14 +474,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Fleet Management", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "hidden": 0, "is_query_report": 0, @@ -581,14 +517,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Recruitment", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -808,14 +736,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Key Reports", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "Attendance", "hidden": 0, @@ -933,9 +853,796 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Lifecycle", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Job Applicant", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Onboarding", + "link_count": 0, + "link_to": "Employee Onboarding", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Skill Map", + "link_count": 0, + "link_to": "Employee Skill Map", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Promotion", + "link_count": 0, + "link_to": "Employee Promotion", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Transfer", + "link_count": 0, + "link_to": "Employee Transfer", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grievance Type", + "link_count": 0, + "link_to": "Grievance Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grievance", + "link_count": 0, + "link_to": "Employee Grievance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Onboarding Template", + "link_count": 0, + "link_to": "Employee Onboarding Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Exit", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Separation Template", + "link_count": 0, + "link_to": "Employee Separation Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Separation", + "link_count": 0, + "link_to": "Employee Separation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Full and Final Statement", + "link_count": 0, + "link_to": "Full and Final Statement", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Exit Interview", + "link_count": 0, + "link_to": "Exit Interview", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee", + "link_count": 8, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee", + "link_count": 0, + "link_to": "Employee", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employment Type", + "link_count": 0, + "link_to": "Employment Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Branch", + "link_count": 0, + "link_to": "Branch", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Department", + "link_count": 0, + "link_to": "Department", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Designation", + "link_count": 0, + "link_to": "Designation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grade", + "link_count": 0, + "link_to": "Employee Grade", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Group", + "link_count": 0, + "link_to": "Employee Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Health Insurance", + "link_count": 0, + "link_to": "Employee Health Insurance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Key Reports", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Attendance", + "hidden": 0, + "is_query_report": 1, + "label": "Monthly Attendance Sheet", + "link_count": 0, + "link_to": "Monthly Attendance Sheet", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Staffing Plan", + "hidden": 0, + "is_query_report": 1, + "label": "Recruitment Analytics", + "link_count": 0, + "link_to": "Recruitment Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Analytics", + "link_count": 0, + "link_to": "Employee Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance", + "link_count": 0, + "link_to": "Employee Leave Balance", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Leave Balance Summary", + "link_count": 0, + "link_to": "Employee Leave Balance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee Advance", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Advance Summary", + "link_count": 0, + "link_to": "Employee Advance Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Exits", + "link_count": 0, + "link_to": "Employee Exits", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Recruitment", + "link_count": 11, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Opening", + "link_count": 0, + "link_to": "Job Opening", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Referral", + "link_count": 0, + "link_to": "Employee Referral", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Applicant", + "link_count": 0, + "link_to": "Job Applicant", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Job Offer", + "link_count": 0, + "link_to": "Job Offer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Staffing Plan", + "link_count": 0, + "link_to": "Staffing Plan", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter", + "link_count": 0, + "link_to": "Appointment Letter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Appointment Letter Template", + "link_count": 0, + "link_to": "Appointment Letter Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Type", + "link_count": 0, + "link_to": "Interview Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Round", + "link_count": 0, + "link_to": "Interview Round", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview", + "link_count": 0, + "link_to": "Interview", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Feedback", + "link_count": 0, + "link_to": "Interview Feedback", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Fleet Management", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Driver", + "link_count": 0, + "link_to": "Driver", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Vehicle", + "link_count": 0, + "link_to": "Vehicle", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Vehicle Log", + "link_count": 0, + "link_to": "Vehicle Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Vehicle", + "hidden": 0, + "is_query_report": 1, + "label": "Vehicle Expenses", + "link_count": 0, + "link_to": "Vehicle Expenses", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "HR Settings", + "link_count": 0, + "link_to": "HR Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Daily Work Summary Group", + "link_count": 0, + "link_to": "Daily Work Summary Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Team Updates", + "link_count": 0, + "link_to": "team-updates", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Expense Claims", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Expense Claim", + "link_count": 0, + "link_to": "Expense Claim", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Advance", + "link_count": 0, + "link_to": "Employee Advance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Travel Request", + "link_count": 0, + "link_to": "Travel Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Attendance", + "link_count": 5, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Attendance Tool", + "link_count": 0, + "link_to": "Employee Attendance Tool", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Attendance", + "link_count": 0, + "link_to": "Attendance", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Attendance Request", + "link_count": 0, + "link_to": "Attendance Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Upload Attendance", + "link_count": 0, + "link_to": "Upload Attendance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Employee Checkin", + "link_count": 0, + "link_to": "Employee Checkin", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Leaves", + "link_count": 10, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Holiday List", + "link_count": 0, + "link_to": "Holiday List", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Type", + "link_count": 0, + "link_to": "Leave Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Period", + "link_count": 0, + "link_to": "Leave Period", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Type", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy", + "link_count": 0, + "link_to": "Leave Policy", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Leave Policy", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Policy Assignment", + "link_count": 0, + "link_to": "Leave Policy Assignment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Application", + "link_count": 0, + "link_to": "Leave Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Allocation", + "link_count": 0, + "link_to": "Leave Allocation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Encashment", + "link_count": 0, + "link_to": "Leave Encashment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Leave Block List", + "link_count": 0, + "link_to": "Leave Block List", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Employee", + "hidden": 0, + "is_query_report": 0, + "label": "Compensatory Leave Request", + "link_count": 0, + "link_to": "Compensatory Leave Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Shift Management", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Type", + "link_count": 0, + "link_to": "Shift Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Request", + "link_count": 0, + "link_to": "Shift Request", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Shift Assignment", + "link_count": 0, + "link_to": "Shift Assignment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2021-08-31 12:18:59.842919", + "modified": "2021-12-05 22:05:13.004462", "modified_by": "Administrator", "module": "HR", "name": "HR", From b69e0d2c6353c924591c79c320e17b71c32104b0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 5 Dec 2021 22:28:22 +0530 Subject: [PATCH 36/99] feat: default Notification - a day before Exit Interview --- .../exit_interview_scheduled/__init__.py | 0 .../exit_interview_scheduled.json | 29 +++++++++++++++ .../exit_interview_scheduled.md | 37 +++++++++++++++++++ .../exit_interview_scheduled.py | 5 +++ 4 files changed, 71 insertions(+) create mode 100644 erpnext/hr/notification/exit_interview_scheduled/__init__.py create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md create mode 100644 erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py diff --git a/erpnext/hr/notification/exit_interview_scheduled/__init__.py b/erpnext/hr/notification/exit_interview_scheduled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json new file mode 100644 index 00000000000..8323ef06945 --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json @@ -0,0 +1,29 @@ +{ + "attach_print": 0, + "channel": "Email", + "condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'", + "creation": "2021-12-05 22:11:47.263933", + "date_changed": "date", + "days_in_advance": 1, + "docstatus": 0, + "doctype": "Notification", + "document_type": "Exit Interview", + "enabled": 1, + "event": "Days Before", + "idx": 0, + "is_standard": 1, + "message": "\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n
\n\t\t\t
\n\t\t\t\t{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}\n\t\t\t
\n\t\t
\n\n\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n
\n\t\t\t
\n\t\t\t\t
    \n\t\t\t\t\t
  • {{_(\"Employee\")}}: {{ doc.employee }} - {{ doc.employee_name }}
  • \n\t\t\t\t\t
  • {{_(\"Date\")}}: {{ doc.date }}
  • \n\t\t\t\t\t
  • {{_(\"Interviewers\")}}:
  • \n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t
    • {{ entry.user }}
    • \n\t\t\t\t\t\t
    \n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t
  • {{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • \n\t\t\t\t
\n\t\t\t
\n\t\t
\n", + "modified": "2021-12-05 22:26:57.096159", + "modified_by": "Administrator", + "module": "HR", + "name": "Exit Interview Scheduled", + "owner": "Administrator", + "recipients": [ + { + "receiver_by_document_field": "email" + } + ], + "send_system_notification": 0, + "send_to_all_assignees": 1, + "subject": "Exit Interview Scheduled: {{ doc.name }}" +} \ No newline at end of file diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md new file mode 100644 index 00000000000..6d6db4014b7 --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md @@ -0,0 +1,37 @@ + + + + + + + + +
+
+

{{_("Exit Interview Scheduled:")}} {{ doc.name }}

+
+
+ + + + + + + + + +
+
+
    +
  • {{_("Employee")}}: {{ doc.employee }} - {{ doc.employee_name }}
  • +
  • {{_("Date")}}: {{ frappe.utils.formatdate(doc.date) }}
  • +
  • {{_("Interviewers")}}:
  • + {% for entry in doc.interviewers %} +
      +
    • {{ entry.user }}
    • +
    + {% endfor %} +
  • {{ _("Interview Document") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
+
+
diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py new file mode 100644 index 00000000000..e1ada61927b --- /dev/null +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py @@ -0,0 +1,5 @@ +import frappe + +def get_context(context): + # do your magic here + pass From 3230741cdeca72daa018a3eaa903e4c59a24856c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Dec 2021 10:08:51 +0530 Subject: [PATCH 37/99] fix: email summary --- erpnext/hr/doctype/exit_interview/exit_interview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index 59dd4631c78..ba75100a3b3 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -123,7 +123,7 @@ def show_email_summary(email_success, email_failure): message += _('{0}: {1}').format( frappe.bold('Sent Successfully'), ', '.join(email_success)) if email_failure: - message += + '

' + _('{0} due to missing email information for employee(s): {1}').format( + message += '

' + _('{0} due to missing email information for employee(s): {1}').format( frappe.bold('Sending Failed'), ', '.join(email_failure)) frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file From f9408d170a8f7e863144003e5966f7b5a3219d3d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 12:38:25 +0530 Subject: [PATCH 38/99] patch: split 'ongoing' sla status --- erpnext/patches.txt | 1 + .../rename_ongoing_status_in_sla_documents.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 897e70ce256..d85f2339ae6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -312,3 +312,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v13_0.create_ksa_vat_custom_fields +erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents \ No newline at end of file diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py new file mode 100644 index 00000000000..dddf4a3778d --- /dev/null +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -0,0 +1,27 @@ +import frappe + + +def execute(): + active_sla_documents = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])] + + for doctype in active_sla_documents: + doctype = frappe.qb.DocType(doctype) + try: + query = ( + frappe.qb + .update(doctype) + .set(doctype.agreement_status, 'First Response Due') + .where( + (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + ) + ) + query.run() + query = ( + frappe.qb + .update(doctype) + .set(doctype.agreement_status, 'Resolution Due') + .where(doctype.agreement_status == 'Ongoing') + ) + query.run() + except Exception as e: + frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file From c1d8877a838775ed04fb210ae629c34371ffe17b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 14:34:59 +0530 Subject: [PATCH 39/99] refactor: handle special cases on communication creation --- erpnext/hooks.py | 2 +- .../service_level_agreement.py | 34 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 546166fe192..09037f0f6c8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -234,7 +234,7 @@ doc_events = { }, "Communication": { "on_update": [ - "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time", + "erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update", "erpnext.support.doctype.issue.issue.set_first_response_time" ] }, diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 662e42cc9f5..9ae2d64d6f4 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -376,14 +376,14 @@ def process_sla(doc, sla): doc.service_level_agreement = sla.name doc.priority = doc.get("priority") or sla.default_priority - prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') - handle_status_change(doc, prev_status, sla.apply_sla_for_resolution) + handle_status_change(doc, sla.apply_sla_for_resolution) update_response_and_resolution_metrics(doc, sla.apply_sla_for_resolution) update_agreement_status(doc, sla.apply_sla_for_resolution) -def handle_status_change(doc, prev_status, apply_sla_for_resolution): +def handle_status_change(doc, apply_sla_for_resolution): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) + prev_status = frappe.db.get_value(doc.doctype, doc.name, 'status') hold_statuses = get_hold_statuses(doc.service_level_agreement) fulfillment_statuses = get_fulfillment_statuses(doc.service_level_agreement) @@ -407,7 +407,7 @@ def handle_status_change(doc, prev_status, apply_sla_for_resolution): doc.total_hold_time = (doc.total_hold_time or 0) + current_hold_hours doc.on_hold_since = None - if is_open_status(prev_status) and not is_open_status(doc.status): + if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply): # status changed from Open to something else if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): doc.first_responded_on = now_time @@ -625,8 +625,8 @@ def reset_resolution_metrics(doc): # called via hooks on communication update -def update_hold_time(doc, status): - if doc.communication_type == "Comment" or doc.sent_or_received != "Received": +def on_communication_update(doc, status): + if doc.communication_type == "Comment": return parent = get_parent_doc(doc) @@ -638,7 +638,27 @@ def update_hold_time(doc, status): for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - handle_status_change(parent, 'Replied', for_resolution) + if ( + doc.sent_or_received == "Received" # a reply is received + and parent.get('status') == 'Open' # issue status is set as open from communication.py + and parent._doc_before_save + and parent.get('status') != parent._doc_before_save.get('status') # status changed + ): + # undo the status change in db + # since prev status is fetched from db + frappe.db.set_value(parent.doctype, parent.name, 'status', parent._doc_before_save.get('status')) + + elif ( + doc.sent_or_received == "Sent" # a reply is sent + and parent.get('first_responded_on') # first_responded_on is set from communication.py + and parent._doc_before_save + and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set + ): + # reset first_responded_on since it will be handled/set later on + parent.first_responded_on = None + parent.flags.on_first_reply = True + + handle_status_change(parent, for_resolution) update_response_and_resolution_metrics(parent, for_resolution) update_agreement_status(parent, for_resolution) From 812572d250e5dd18ebdd9e043826ba7616e504fe Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 14:35:35 +0530 Subject: [PATCH 40/99] feat: record assignment on first response failure --- erpnext/support/doctype/issue/test_issue.py | 38 +++++++++++++++++++ .../service_level_agreement.py | 22 +++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index d566f33f24a..477ac7260ce 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -211,6 +211,43 @@ class TestIssue(TestSetUp): self.assertEquals(issue.agreement_status, 'Fulfilled') self.assertEquals(issue.resolution_date, frappe.flags.current_time) + def test_recording_of_assignment_on_first_reponse_failure(self): + from frappe.desk.form.assign_to import add as add_assignment + + frappe.flags.current_time = get_datetime("2021-11-01 19:00") + + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + add_assignment({ + 'doctype': issue.doctype, + 'name': issue.name, + 'assign_to': ['test@admin.com'] + }) + issue.reload() + + # send a reply failing response SLA + frappe.flags.current_time = get_datetime("2021-11-02 15:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + + # assert if a new timeline item has been added + # to record the assignment + comment = frappe.get_last_doc('Comment') + self.assertTrue('First Response SLA Failed' in comment.content) + + def test_agreement_status_on_response(self): + frappe.flags.current_time = get_datetime("2021-11-01 19:00") + + issue = make_issue(frappe.flags.current_time, index=1) + create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time) + self.assertTrue(issue.status == 'Open') + + # send a reply within response SLA + frappe.flags.current_time = get_datetime("2021-11-02 11:00") + create_communication(issue.name, "test@admin.com", "Sent", frappe.flags.current_time) + + issue.reload() + self.assertEquals(issue.first_responded_on, frappe.flags.current_time) + self.assertEquals(issue.agreement_status, 'Resolution Due') class TestFirstResponseTime(TestSetUp): # working hours used in all cases: Mon-Fri, 10am to 6pm @@ -425,6 +462,7 @@ class TestFirstResponseTime(TestSetUp): def create_issue_and_communication(issue_creation, first_responded_on): issue = make_issue(issue_creation, index=1) sender = create_user("test@admin.com") + frappe.flags.current_time = first_responded_on create_communication(issue.name, sender.email, "Sent", first_responded_on) issue.reload() diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 9ae2d64d6f4..19aa5783f79 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -397,6 +397,12 @@ def handle_status_change(doc, apply_sla_for_resolution): def is_open_status(status): return status not in hold_statuses and status not in fulfillment_statuses + def set_first_response(): + if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): + doc.first_responded_on = now_time + if get_datetime(doc.get('first_responded_on')) > get_datetime(doc.get('response_by')): + record_assigned_users_on_failure(doc) + def calculate_hold_hours(): # In case issue was closed and after few days it has been opened # The hold time should be calculated from resolution_date @@ -408,9 +414,7 @@ def handle_status_change(doc, apply_sla_for_resolution): doc.on_hold_since = None if ((is_open_status(prev_status) and not is_open_status(doc.status)) or doc.flags.on_first_reply): - # status changed from Open to something else - if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): - doc.first_responded_on = now_time + set_first_response() # Open to Replied if is_open_status(prev_status) and is_hold_status(doc.status): @@ -688,6 +692,18 @@ def set_resolution_by(doc, start_date_time, priority): doc.resolution_by = add_to_date(doc.resolution_by, seconds=round(doc.get('total_hold_time'))) +def record_assigned_users_on_failure(doc): + assigned_users = doc.get_assigned_users() + if assigned_users: + from frappe.utils import get_fullname + assigned_users = ', '.join((get_fullname(user) for user in assigned_users)) + message = _(f'First Response SLA Failed by {assigned_users}') + doc.add_comment( + comment_type='Assigned', + text=message + ) + + def get_service_level_agreement_fields(): return [ { From 3dabac15edf4baedfc3da288400e16933d497e70 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Dec 2021 15:05:20 +0530 Subject: [PATCH 41/99] fix: Ageing in AR/AP report for advances --- .../report/accounts_receivable/accounts_receivable.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 353f9087f1b..a990f23cd6b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -545,7 +545,9 @@ class ReceivablePayableReport(object): def set_ageing(self, row): if self.filters.ageing_based_on == "Due Date": - entry_date = row.due_date + # use posting date as a fallback for advances posted via journal and payment entry + # when ageing viewed by due date + entry_date = row.due_date or row.posting_date elif self.filters.ageing_based_on == "Supplier Invoice Date": entry_date = row.bill_date else: From 32c81818f619c25878ab5620b3f5e505ef43c1e7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 15:38:01 +0530 Subject: [PATCH 42/99] fix: transalations --- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 19aa5783f79..7527adf2bf8 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -697,7 +697,7 @@ def record_assigned_users_on_failure(doc): if assigned_users: from frappe.utils import get_fullname assigned_users = ', '.join((get_fullname(user) for user in assigned_users)) - message = _(f'First Response SLA Failed by {assigned_users}') + message = _('First Response SLA Failed by {}').format(assigned_users) doc.add_comment( comment_type='Assigned', text=message From 10b87e081cd0523fe63f7c6254d683ea4fb48439 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 15:38:19 +0530 Subject: [PATCH 43/99] fix: sider issues --- .../rename_ongoing_status_in_sla_documents.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py index dddf4a3778d..b5296fbdb0c 100644 --- a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -7,21 +7,21 @@ def execute(): for doctype in active_sla_documents: doctype = frappe.qb.DocType(doctype) try: - query = ( - frappe.qb - .update(doctype) - .set(doctype.agreement_status, 'First Response Due') - .where( - (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') - ) - ) - query.run() - query = ( - frappe.qb - .update(doctype) - .set(doctype.agreement_status, 'Resolution Due') - .where(doctype.agreement_status == 'Ongoing') - ) - query.run() - except Exception as e: + frappe.qb.update( + doctype + ).set( + doctype.agreement_status, 'First Response Due' + ).where( + (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + ).run() + + frappe.qb.update( + doctype + ).set( + doctype.agreement_status, 'Resolution Due' + ).where( + doctype.agreement_status == 'Ongoing' + ).run() + + except Exception: frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file From 1a76b3801ac1e933087b2cf14a1a3e74c6be8383 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 16:16:13 +0530 Subject: [PATCH 44/99] fix: test --- erpnext/support/doctype/issue/test_issue.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 477ac7260ce..14cec46ad4f 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -1,10 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt -import datetime import unittest import frappe +from frappe import _ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.utils import flt, get_datetime @@ -231,8 +231,13 @@ class TestIssue(TestSetUp): # assert if a new timeline item has been added # to record the assignment - comment = frappe.get_last_doc('Comment') - self.assertTrue('First Response SLA Failed' in comment.content) + comment = frappe.db.exists('Comment', { + 'reference_doctype': 'Issue', + 'reference_name': issue.name, + 'comment_type': 'Assigned', + 'content': _('First Response SLA Failed by {}').format('test') + }) + self.assertTrue(comment) def test_agreement_status_on_response(self): frappe.flags.current_time = get_datetime("2021-11-01 19:00") From 476e81a6312f9d38ae8b525b52f47317cbceefe5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 18:55:30 +0530 Subject: [PATCH 45/99] fix: time to respond & resolve indicators --- .../rename_ongoing_status_in_sla_documents.py | 4 ++-- erpnext/public/js/utils.js | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py index b5296fbdb0c..1cc5f38f427 100644 --- a/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py +++ b/erpnext/patches/v14_0/rename_ongoing_status_in_sla_documents.py @@ -12,7 +12,7 @@ def execute(): ).set( doctype.agreement_status, 'First Response Due' ).where( - (doctype.first_responded_on.isnull()) | (doctype.first_responded_on == '') + doctype.first_responded_on.isnull() ).run() frappe.qb.update( @@ -24,4 +24,4 @@ def execute(): ).run() except Exception: - frappe.log_error('Failed to Patch SLA Status') \ No newline at end of file + frappe.log_error(title='Failed to Patch SLA Status') \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f0facdd3a10..2d8b1be93f0 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -831,7 +831,7 @@ $(document).on('app_ready', function() { refresh: function(frm) { if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement - && frm.doc.agreement_status === 'Ongoing') { + && ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { frappe.call({ 'method': 'frappe.client.get', args: { @@ -884,8 +884,8 @@ $(document).on('app_ready', function() { function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { frm.dashboard.clear_headline(); - let time_to_respond = get_status(frm.doc.response_by_variance); - if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') { + let time_to_respond = get_status(frm.doc.response_by); + if (!frm.doc.first_responded_on) { time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); } @@ -899,8 +899,8 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { if (apply_sla_for_resolution) { - let time_to_resolve = get_status(frm.doc.resolution_by_variance); - if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') { + let time_to_resolve = get_status(frm.doc.resolution_by); + if (!frm.doc.resolution_date) { time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); } @@ -924,8 +924,9 @@ function get_time_left(timestamp, agreement_status) { return {'diff_display': diff_display, 'indicator': indicator}; } -function get_status(variance) { - if (variance > 0) { +function get_status(timestamp) { + const time_left = moment(timestamp).diff(moment()); + if (time_left >= 0) { return {'diff_display': 'Fulfilled', 'indicator': 'green'}; } else { return {'diff_display': 'Failed', 'indicator': 'red'}; From 91aa78707c471f688acb1147956f5aac3b84af99 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:13:31 +0530 Subject: [PATCH 46/99] fix: remove missed 'ongoing' references --- erpnext/support/doctype/issue/issue.json | 2 +- .../doctype/service_level_agreement/service_level_agreement.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 38b5395adfd..30aa7319dfc 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -372,7 +372,7 @@ "read_only": 1 }, { - "default": "Ongoing", + "default": "First Response Due", "depends_on": "eval: doc.service_level_agreement", "fieldname": "agreement_status", "fieldtype": "Select", diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 7527adf2bf8..50f31fde2d6 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -625,7 +625,7 @@ def reset_resolution_metrics(doc): doc.user_resolution_time = None if doc.meta.has_field("agreement_status"): - doc.agreement_status = "Ongoing" + doc.agreement_status = "First Response Due" # called via hooks on communication update From 3446f7545ff8178999a93fd2042c188905de1db2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:17:38 +0530 Subject: [PATCH 47/99] fix: remove 'ongoing' status from issue summary report --- erpnext/support/report/issue_summary/issue_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index 39a5c407cd4..67fe345d5fe 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -82,7 +82,8 @@ class IssueSummary(object): self.sla_status_map = { 'SLA Failed': 'failed', 'SLA Fulfilled': 'fulfilled', - 'SLA Ongoing': 'ongoing' + 'First Response Due': 'first_response_due', + 'Resolution Due': 'resolution_due' } for label, fieldname in self.sla_status_map.items(): From 2f7d8ac29e58898e7fe7bd05b460abdcc1ea7a5a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 6 Dec 2021 19:19:34 +0530 Subject: [PATCH 48/99] fix: indentation --- erpnext/support/doctype/issue/issue.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 30aa7319dfc..3ff7d02f1ae 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -347,16 +347,16 @@ "read_only": 1 }, { - "fieldname": "resolution_time", - "fieldtype": "Duration", - "label": "Resolution Time", - "read_only": 1 + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 }, { - "fieldname": "user_resolution_time", - "fieldtype": "Duration", - "label": "User Resolution Time", - "read_only": 1 + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 }, { "fieldname": "on_hold_since", From d106d59c3f750a2545cb0c82f86c75fd88ab8276 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 6 Dec 2021 20:36:48 +0530 Subject: [PATCH 49/99] fix: TDS Monthly payable report --- .../report/tds_payable_monthly/tds_payable_monthly.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index a3a45d1e79c..caee1a10bbb 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): posting_date = entry.posting_date voucher_type = entry.voucher_type + if not tax_withholding_category: + tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category') + rate = tax_rate_map.get(tax_withholding_category) + if entry.account in tds_accounts: tds_deducted += (entry.credit - entry.debit) total_amount_credited += (entry.credit - entry.debit) - if rate and tds_deducted: + if tds_deducted: row = { 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'), 'supplier': supplier_map.get(supplier, {}).get('name') @@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): def get_supplier_pan_map(): supplier_map = frappe._dict() - suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name']) + suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category']) for d in suppliers: supplier_map[d.name] = d From 0a937dc0509e26d7f88e205902212c3b8987e202 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 13:04:23 +0530 Subject: [PATCH 50/99] fix: show Exit Questionnaire button only to the users with write access - fix linter issues --- erpnext/hr/doctype/exit_interview/exit_interview.js | 2 +- erpnext/hr/doctype/exit_interview/exit_interview.py | 4 ++-- erpnext/hr/doctype/exit_interview/test_exit_interview.py | 1 + .../exit_interview_scheduled/exit_interview_scheduled.py | 3 ++- erpnext/hr/report/employee_exits/employee_exits.py | 2 -- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js index 849e8542d2f..502af423a2c 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.js +++ b/erpnext/hr/doctype/exit_interview/exit_interview.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Exit Interview', { refresh: function(frm) { - if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent) { + if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) { frm.add_custom_button(__('Send Exit Questionnaire'), function () { frm.trigger('send_exit_questionnaire'); }); diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index ba75100a3b3..e72c47e8a7b 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -28,9 +28,9 @@ class ExitInterview(Document): 'docstatus': ('!=', 2) }) if doc: - frappe.throw(_('Exit Interview {0} already scheduled for Employee: {1}').format( + frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format( get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)), - title=_('Duplicate Document')) + frappe.DuplicateEntryError) def set_employee_email(self): employee = frappe.get_doc('Employee', self.employee) diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index daf3d66290d..3a6316c2004 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestExitInterview(unittest.TestCase): pass diff --git a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py index e1ada61927b..5f697c9613e 100644 --- a/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py +++ b/erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py @@ -1,4 +1,5 @@ -import frappe +# import frappe + def get_context(context): # do your magic here diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index fd49543d084..93252295f3e 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -91,8 +91,6 @@ def get_columns(): ] def get_data(filters): - data = [] - employee = frappe.qb.DocType('Employee') interview = frappe.qb.DocType('Exit Interview') fnf = frappe.qb.DocType('Full and Final Statement') From 8db21e065c0fceed7a63459ddbc8ee970ecb393b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 14:55:17 +0530 Subject: [PATCH 51/99] test: Exit Interview --- .../exit_interview/exit_interview.json | 14 ++-- .../doctype/exit_interview/exit_interview.py | 6 +- .../exit_interview/test_exit_interview.py | 84 ++++++++++++++++++- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 4b396402dbf..86720a105b6 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -30,7 +30,7 @@ "reference_document_name", "interview_summary_section", "interviewers", - "text_editor_12", + "interview_summary", "employee_status_section", "employee_status", "amended_from" @@ -112,11 +112,6 @@ "fieldtype": "Section Break", "label": "Interview Details" }, - { - "fieldname": "text_editor_12", - "fieldtype": "Text Editor", - "label": "Interview Summary" - }, { "fieldname": "column_break_10", "fieldtype": "Column Break" @@ -213,12 +208,17 @@ "options": "Exit Interview", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "interview_summary", + "fieldtype": "Text Editor", + "label": "Interview Summary" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-05 18:56:34.856854", + "modified": "2021-12-07 14:08:29.355390", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py index e72c47e8a7b..30e19f1c9bb 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/exit_interview.py @@ -109,7 +109,7 @@ def validate_questionnaire_settings(): if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template: frappe.throw( - message=_('Please set {0} and {1} in {2}.').format( + _('Please set {0} and {1} in {2}.').format( frappe.bold('Exit Questionnaire Web Form'), frappe.bold('Notification Template'), get_link_to_form('HR Settings', 'HR Settings')), @@ -122,8 +122,10 @@ def show_email_summary(email_success, email_failure): if email_success: message += _('{0}: {1}').format( frappe.bold('Sent Successfully'), ', '.join(email_success)) + if message and email_failure: + message += '

' if email_failure: - message += '

' + _('{0} due to missing email information for employee(s): {1}').format( + message += _('{0} due to missing email information for employee(s): {1}').format( frappe.bold('Sending Failed'), ', '.join(email_failure)) frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index 3a6316c2004..8eeb4a1b956 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -1,9 +1,89 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.tests.test_webform import create_custom_doctype, create_webform +from frappe.utils import getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire + class TestExitInterview(unittest.TestCase): - pass + def test_duplicate_interview(self): + employee = make_employee('employeeexit1@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + interview = create_exit_interview(employee) + + doc = frappe.copy_doc(interview) + self.assertRaises(frappe.DuplicateEntryError, doc.save) + + def test_relieving_date_validation(self): + employee = make_employee('employeeexit2@example.com') + + interview = create_exit_interview(employee, save=False) + self.assertRaises(frappe.ValidationError, interview.save) + + # set relieving date + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + interview = create_exit_interview(employee) + self.assertTrue(interview.name) + + def test_interview_date_updated_in_employee_master(self): + employee = make_employee('employeeexit3@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + + interview = create_exit_interview(employee) + interview.status = 'Completed' + interview.employee_status = 'Exit Confirmed' + + # exit interview date updated on submit + interview.submit() + self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date) + + # exit interview reset on cancel + interview.reload() + interview.cancel() + self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None) + + def test_send_exit_questionnaire(self): + create_custom_doctype() + create_webform() + + webform = frappe.db.get_all('Web Form', limit=1) + frappe.db.set_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form', webform[0].name) + + employee = make_employee('employeeexit3@example.com') + frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) + + interview = create_exit_interview(employee) + send_exit_questionnaire([interview]) + + email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1) + self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message) + + def tearDown(self): + frappe.db.rollback() + + +def create_exit_interview(employee, save=True): + interviewer = create_user('test_interviewer1@example.com') + + doc = frappe.get_doc({ + 'doctype': 'Exit Interview', + 'employee': employee, + 'company': '_Test Company', + 'status': 'Pending', + 'date': getdate(), + 'interviewers': [{ + 'interviewer': interviewer.name + }], + 'interview_summary': 'Test' + }) + + if save: + return doc.insert() + return doc From ef38b127ae0f592a17070a55087804cad8a44ec0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 15:02:00 +0530 Subject: [PATCH 52/99] chore: fix report column widths --- .../report/employee_exits/employee_exits.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 93252295f3e..56cea4917bc 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -33,13 +33,39 @@ def get_columns(): 'label': _('Date of Joining'), 'fieldname': 'date_of_joining', 'fieldtype': 'Date', - 'width': 100 + 'width': 120 }, { 'label': _('Relieving Date'), 'fieldname': 'relieving_date', 'fieldtype': 'Date', - 'width': 100 + 'width': 120 + }, + { + 'label': _('Exit Interview'), + 'fieldname': 'exit_interview', + 'fieldtype': 'Link', + 'options': 'Exit Interview', + 'width': 150 + }, + { + 'label': _('Interview Status'), + 'fieldname': 'interview_status', + 'fieldtype': 'Data', + 'width': 130 + }, + { + 'label': _('Final Decision'), + 'fieldname': 'employee_status', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _('Full and Final Statement'), + 'fieldname': 'full_and_final_statement', + 'fieldtype': 'Link', + 'options': 'Full and Final Statement', + 'width': 180 }, { 'label': _('Department'), @@ -61,32 +87,6 @@ def get_columns(): 'fieldtype': 'Link', 'options': 'Employee', 'width': 120 - }, - { - 'label': _('Exit Interview'), - 'fieldname': 'exit_interview', - 'fieldtype': 'Link', - 'options': 'Exit Interview', - 'width': 98 - }, - { - 'label': _('Interview Status'), - 'fieldname': 'interview_status', - 'fieldtype': 'Data', - 'width': 98 - }, - { - 'label': _('Final Decision'), - 'fieldname': 'employee_status', - 'fieldtype': 'Data', - 'width': 98 - }, - { - 'label': _('Full and Final Statement'), - 'fieldname': 'full_and_final_statement', - 'fieldtype': 'Link', - 'options': 'Full and Final Statement', - 'width': 150 } ] From c305ff911f2db58b40463ad069a8e99fcc1b2cd9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 16:22:17 +0530 Subject: [PATCH 53/99] test: Employee Exits Report --- .../report/employee_exits/employee_exits.py | 7 +- .../employee_exits/test_employee_exits.py | 221 ++++++++++++++++++ 2 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 erpnext/hr/report/employee_exits/test_employee_exits.py diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 56cea4917bc..d0e1ef9a327 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -105,8 +105,9 @@ def get_data(filters): employee.department.as_('department'), employee.designation.as_('designation'), employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'), interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), - interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement') - ).distinct() + interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement')) + .distinct() + .orderby(employee.relieving_date) .where( ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) @@ -158,7 +159,7 @@ def get_conditions(filters, query, employee, interview, fnf): query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull())) if filters.get('fnf_pending'): - query = query.where((fnf.name == '') | (interview.name.isnull())) + query = query.where((fnf.name == '') | (fnf.name.isnull())) return query diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py new file mode 100644 index 00000000000..1c64b46773b --- /dev/null +++ b/erpnext/hr/report/employee_exits/test_employee_exits.py @@ -0,0 +1,221 @@ +import unittest + +import frappe +from frappe.utils import add_days, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview +from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import create_full_and_final_statement +from erpnext.hr.report.employee_exits.employee_exits import execute + + +class TestEmployeeExits(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.sql("delete from `tabEmployee` where company='Test Company'") + frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'") + frappe.db.sql("delete from `tabExit Interview` where company='Test Company'") + + cls.create_records() + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + @classmethod + def create_records(cls): + cls.emp1 = make_employee('employeeexit1@example.com', + company='Test Company', + date_of_joining=getdate('01-10-2021'), + relieving_date=add_days(getdate(), 14), + designation='Accountant' + ) + cls.emp2 = make_employee('employeeexit2@example.com', + company='Test Company', + date_of_joining=getdate('01-12-2021'), + relieving_date=add_days(getdate(), 15), + designation='Accountant' + ) + + cls.emp3 = make_employee('employeeexit3@example.com', + company='Test Company', + date_of_joining=getdate('02-12-2021'), + relieving_date=add_days(getdate(), 29), + designation='Engineer' + ) + cls.emp4 = make_employee('employeeexit4@example.com', + company='Test Company', + date_of_joining=getdate('01-12-2021'), + relieving_date=add_days(getdate(), 30), + designation='Engineer' + ) + + # exit interview for 3 employees only + cls.interview1 = create_exit_interview(cls.emp1) + cls.interview2 = create_exit_interview(cls.emp2) + cls.interview3 = create_exit_interview(cls.emp3) + + # create fnf for some records + cls.fnf1 = create_full_and_final_statement(cls.emp1) + cls.fnf2 = create_full_and_final_statement(cls.emp2) + + # link questionnaire for a few records + # setting employee doctype as reference instead of creating a questionnaire + # since this is just for a test + frappe.db.set_value('Exit Interview', cls.interview1.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp1 + }) + + frappe.db.set_value('Exit Interview', cls.interview2.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp2 + }) + + frappe.db.set_value('Exit Interview', cls.interview3.name, { + 'ref_doctype': 'Employee', + 'reference_document_name': cls.emp3 + }) + + + def test_employee_exits_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 15), + 'designation': 'Accountant' + } + + report = execute(filters) + + employee1 = frappe.get_doc('Employee', self.emp1) + employee2 = frappe.get_doc('Employee', self.emp2) + expected_data = [{ + 'employee': employee1.name, + 'employee_name': employee1.employee_name, + 'date_of_joining': employee1.date_of_joining, + 'relieving_date': employee1.relieving_date, + 'department': employee1.department, + 'designation': employee1.designation, + 'reports_to': None, + 'exit_interview': self.interview1.name, + 'interview_status': self.interview1.status, + 'employee_status': '', + 'questionnaire': employee1.name, + 'full_and_final_statement': self.fnf1.name + }, + { + 'employee': employee2.name, + 'employee_name': employee2.employee_name, + 'date_of_joining': employee2.date_of_joining, + 'relieving_date': employee2.relieving_date, + 'department': employee2.department, + 'designation': employee2.designation, + 'reports_to': None, + 'exit_interview': self.interview2.name, + 'interview_status': self.interview2.status, + 'employee_status': '', + 'questionnaire': employee2.name, + 'full_and_final_statement': self.fnf2.name + }] + + self.assertEqual(expected_data, report[1]) # rows + + + def test_pending_exit_interviews_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 30), + 'exit_interview_pending': 1 + } + + report = execute(filters) + + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows + + def test_pending_exit_questionnaire_summary(self): + filters = { + 'company': 'Test Company', + 'from_date': getdate(), + 'to_date': add_days(getdate(), 30), + 'questionnaire_pending': 1 + } + + report = execute(filters) + + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows + + + def test_pending_fnf_summary(self): + filters = { + 'company': 'Test Company', + 'fnf_pending': 1 + } + + report = execute(filters) + + employee3 = frappe.get_doc('Employee', self.emp3) + employee4 = frappe.get_doc('Employee', self.emp4) + expected_data = [{ + 'employee': employee3.name, + 'employee_name': employee3.employee_name, + 'date_of_joining': employee3.date_of_joining, + 'relieving_date': employee3.relieving_date, + 'department': employee3.department, + 'designation': employee3.designation, + 'reports_to': None, + 'exit_interview': self.interview3.name, + 'interview_status': self.interview3.status, + 'employee_status': '', + 'questionnaire': employee3.name, + 'full_and_final_statement': None + }, + { + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + }] + + self.assertEqual(expected_data, report[1]) # rows \ No newline at end of file From dcbf0c9eca1ca811a69eb318a0365a38054ac2cc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Dec 2021 23:40:10 +0530 Subject: [PATCH 54/99] fix: tests and sider issues --- .../exit_interview/exit_interview.json | 6 +- .../exit_interview/test_exit_interview.py | 30 +++- .../employee_exits/test_employee_exits.py | 145 ++++++++++-------- 3 files changed, 115 insertions(+), 66 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json index 86720a105b6..989a1b81188 100644 --- a/erpnext/hr/doctype/exit_interview/exit_interview.json +++ b/erpnext/hr/doctype/exit_interview/exit_interview.json @@ -120,8 +120,8 @@ "fieldname": "interviewers", "fieldtype": "Table MultiSelect", "label": "Interviewers", - "options": "Interviewer", - "reqd": 1 + "mandatory_depends_on": "eval:doc.status==='Scheduled';", + "options": "Interviewer" }, { "fetch_from": "employee.date_of_joining", @@ -218,7 +218,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-12-07 14:08:29.355390", + "modified": "2021-12-07 23:39:22.645401", "modified_by": "Administrator", "module": "HR", "name": "Exit Interview", diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index 8eeb4a1b956..b31d593a2d3 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe import _ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.tests.test_webform import create_custom_doctype, create_webform from frappe.utils import getdate @@ -13,6 +14,9 @@ from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionn class TestExitInterview(unittest.TestCase): + def setUp(self): + frappe.db.sql('delete from `tabExit Interview`') + def test_duplicate_interview(self): employee = make_employee('employeeexit1@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) @@ -23,6 +27,8 @@ class TestExitInterview(unittest.TestCase): def test_relieving_date_validation(self): employee = make_employee('employeeexit2@example.com') + # unset relieving date + frappe.db.set_value('Employee', employee, 'relieving_date', None) interview = create_exit_interview(employee, save=False) self.assertRaises(frappe.ValidationError, interview.save) @@ -52,9 +58,13 @@ class TestExitInterview(unittest.TestCase): def test_send_exit_questionnaire(self): create_custom_doctype() create_webform() + template = create_notification_template() webform = frappe.db.get_all('Web Form', limit=1) - frappe.db.set_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form', webform[0].name) + frappe.db.set_value('HR Settings', 'HR Settings', { + 'exit_questionnaire_web_form': webform[0].name, + 'exit_questionnaire_notification_template': template + }) employee = make_employee('employeeexit3@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) @@ -87,3 +97,21 @@ def create_exit_interview(employee, save=True): if save: return doc.insert() return doc + + +def create_notification_template(): + template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification')) + if not template: + base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') + response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html')) + + template = frappe.get_doc({ + 'doctype': 'Email Template', + 'name': _('Exit Questionnaire Notification'), + 'response': response, + 'subject': _('Exit Questionnaire Notification'), + 'owner': frappe.session.user, + }).insert(ignore_permissions=True) + template = template.name + + return template \ No newline at end of file diff --git a/erpnext/hr/report/employee_exits/test_employee_exits.py b/erpnext/hr/report/employee_exits/test_employee_exits.py index 1c64b46773b..d7e95a60d0b 100644 --- a/erpnext/hr/report/employee_exits/test_employee_exits.py +++ b/erpnext/hr/report/employee_exits/test_employee_exits.py @@ -5,13 +5,16 @@ from frappe.utils import add_days, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview -from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import create_full_and_final_statement +from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import ( + create_full_and_final_statement, +) from erpnext.hr.report.employee_exits.employee_exits import execute class TestEmployeeExits(unittest.TestCase): @classmethod def setUpClass(cls): + create_company() frappe.db.sql("delete from `tabEmployee` where company='Test Company'") frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'") frappe.db.sql("delete from `tabExit Interview` where company='Test Company'") @@ -24,26 +27,30 @@ class TestEmployeeExits(unittest.TestCase): @classmethod def create_records(cls): - cls.emp1 = make_employee('employeeexit1@example.com', + cls.emp1 = make_employee( + 'employeeexit1@example.com', company='Test Company', date_of_joining=getdate('01-10-2021'), relieving_date=add_days(getdate(), 14), designation='Accountant' ) - cls.emp2 = make_employee('employeeexit2@example.com', + cls.emp2 = make_employee( + 'employeeexit2@example.com', company='Test Company', date_of_joining=getdate('01-12-2021'), relieving_date=add_days(getdate(), 15), designation='Accountant' ) - cls.emp3 = make_employee('employeeexit3@example.com', + cls.emp3 = make_employee( + 'employeeexit3@example.com', company='Test Company', date_of_joining=getdate('02-12-2021'), relieving_date=add_days(getdate(), 29), designation='Engineer' ) - cls.emp4 = make_employee('employeeexit4@example.com', + cls.emp4 = make_employee( + 'employeeexit4@example.com', company='Test Company', date_of_joining=getdate('01-12-2021'), relieving_date=add_days(getdate(), 30), @@ -90,34 +97,36 @@ class TestEmployeeExits(unittest.TestCase): employee1 = frappe.get_doc('Employee', self.emp1) employee2 = frappe.get_doc('Employee', self.emp2) - expected_data = [{ - 'employee': employee1.name, - 'employee_name': employee1.employee_name, - 'date_of_joining': employee1.date_of_joining, - 'relieving_date': employee1.relieving_date, - 'department': employee1.department, - 'designation': employee1.designation, - 'reports_to': None, - 'exit_interview': self.interview1.name, - 'interview_status': self.interview1.status, - 'employee_status': '', - 'questionnaire': employee1.name, - 'full_and_final_statement': self.fnf1.name - }, - { - 'employee': employee2.name, - 'employee_name': employee2.employee_name, - 'date_of_joining': employee2.date_of_joining, - 'relieving_date': employee2.relieving_date, - 'department': employee2.department, - 'designation': employee2.designation, - 'reports_to': None, - 'exit_interview': self.interview2.name, - 'interview_status': self.interview2.status, - 'employee_status': '', - 'questionnaire': employee2.name, - 'full_and_final_statement': self.fnf2.name - }] + expected_data = [ + { + 'employee': employee1.name, + 'employee_name': employee1.employee_name, + 'date_of_joining': employee1.date_of_joining, + 'relieving_date': employee1.relieving_date, + 'department': employee1.department, + 'designation': employee1.designation, + 'reports_to': None, + 'exit_interview': self.interview1.name, + 'interview_status': self.interview1.status, + 'employee_status': '', + 'questionnaire': employee1.name, + 'full_and_final_statement': self.fnf1.name + }, + { + 'employee': employee2.name, + 'employee_name': employee2.employee_name, + 'date_of_joining': employee2.date_of_joining, + 'relieving_date': employee2.relieving_date, + 'department': employee2.department, + 'designation': employee2.designation, + 'reports_to': None, + 'exit_interview': self.interview2.name, + 'interview_status': self.interview2.status, + 'employee_status': '', + 'questionnaire': employee2.name, + 'full_and_final_statement': self.fnf2.name + } + ] self.assertEqual(expected_data, report[1]) # rows @@ -189,33 +198,45 @@ class TestEmployeeExits(unittest.TestCase): employee3 = frappe.get_doc('Employee', self.emp3) employee4 = frappe.get_doc('Employee', self.emp4) - expected_data = [{ - 'employee': employee3.name, - 'employee_name': employee3.employee_name, - 'date_of_joining': employee3.date_of_joining, - 'relieving_date': employee3.relieving_date, - 'department': employee3.department, - 'designation': employee3.designation, - 'reports_to': None, - 'exit_interview': self.interview3.name, - 'interview_status': self.interview3.status, - 'employee_status': '', - 'questionnaire': employee3.name, - 'full_and_final_statement': None - }, - { - 'employee': employee4.name, - 'employee_name': employee4.employee_name, - 'date_of_joining': employee4.date_of_joining, - 'relieving_date': employee4.relieving_date, - 'department': employee4.department, - 'designation': employee4.designation, - 'reports_to': None, - 'exit_interview': None, - 'interview_status': None, - 'employee_status': None, - 'questionnaire': None, - 'full_and_final_statement': None - }] + expected_data = [ + { + 'employee': employee3.name, + 'employee_name': employee3.employee_name, + 'date_of_joining': employee3.date_of_joining, + 'relieving_date': employee3.relieving_date, + 'department': employee3.department, + 'designation': employee3.designation, + 'reports_to': None, + 'exit_interview': self.interview3.name, + 'interview_status': self.interview3.status, + 'employee_status': '', + 'questionnaire': employee3.name, + 'full_and_final_statement': None + }, + { + 'employee': employee4.name, + 'employee_name': employee4.employee_name, + 'date_of_joining': employee4.date_of_joining, + 'relieving_date': employee4.relieving_date, + 'department': employee4.department, + 'designation': employee4.designation, + 'reports_to': None, + 'exit_interview': None, + 'interview_status': None, + 'employee_status': None, + 'questionnaire': None, + 'full_and_final_statement': None + } + ] - self.assertEqual(expected_data, report[1]) # rows \ No newline at end of file + self.assertEqual(expected_data, report[1]) # rows + + +def create_company(): + if not frappe.db.exists('Company', 'Test Company'): + frappe.get_doc({ + 'doctype': 'Company', + 'company_name': 'Test Company', + 'default_currency': 'INR', + 'country': 'India' + }).insert() \ No newline at end of file From d37541d3fb57923b681753871bfd975bf53b630f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 12:04:10 +0530 Subject: [PATCH 55/99] fix: ensure that reposting is finished before freezing stock/account --- .../accounts_settings/accounts_settings.py | 8 ++++++ erpnext/public/js/utils.js | 4 +++ .../doctype/stock_settings/stock_settings.py | 8 ++++++ erpnext/stock/utils.py | 25 +++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 745191712b2..48392074102 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_ from frappe.model.document import Document from frappe.utils import cint +from erpnext.stock.utils import check_pending_reposting + class AccountsSettings(Document): def on_update(self): @@ -25,6 +27,7 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() self.toggle_discount_accounting_fields() + self.validate_pending_reposts() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -56,3 +59,8 @@ class AccountsSettings(Document): make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) + + + def validate_pending_reposts(self): + if self.acc_frozen_upto: + check_pending_reposting(self.acc_frozen_upto) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f0facdd3a10..cad16595618 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -84,6 +84,10 @@ $.extend(erpnext, { }); }, + route_to_pending_reposts: (args) => { + frappe.set_route('List', 'Repost Item Valuation', args); + }, + proceed_save_with_reminders_frequency_change: () => { frappe.ui.hide_open_dialog(); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 1de48b6f1f1..c1293cbf0fa 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -11,6 +11,8 @@ from frappe.model.document import Document from frappe.utils import cint from frappe.utils.html_utils import clean_html +from erpnext.stock.utils import check_pending_reposting + class StockSettings(Document): def validate(self): @@ -36,6 +38,7 @@ class StockSettings(Document): self.validate_warehouses() self.cant_change_valuation_method() self.validate_clean_description_html() + self.validate_pending_reposts() def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] @@ -64,6 +67,11 @@ class StockSettings(Document): # changed to text frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) + def validate_pending_reposts(self): + if self.stock_frozen_upto: + check_pending_reposting(self.stock_frozen_upto) + + def on_update(self): self.toggle_warehouse_field_for_inter_warehouse_transfer() diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 72d8098d440..b00dbad4769 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -417,3 +417,28 @@ def is_reposting_item_valuation_in_progress(): {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) if reposting_in_progress: frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) + +def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool: + """Check if there are pending reposting job till the specified posting date.""" + + filters = { + "docstatus": 1, + "status": ["in", ["Queued","In Progress", "Failed"]], + "posting_date": ["<=", posting_date], + } + + reposting_pending = frappe.db.exists("Repost Item Valuation", filters) + if reposting_pending and throw_error: + msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") + frappe.msgprint(msg, + raise_exception=frappe.ValidationError, + title="Stock Reposting Ongoing", + indicator="red", + primary_action={ + "label": _("Show pending entries"), + "client_action": "erpnext.route_to_pending_reposts", + "args": filters, + } + ) + + return bool(reposting_pending) From 75bc404cbea8ab0713a4f64e382d35558c453eed Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 12:39:38 +0530 Subject: [PATCH 56/99] test: stock frozen validation --- .../test_repost_item_valuation.py | 24 +++++++++++++++++++ erpnext/stock/utils.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index de793163fdb..78b432d564c 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -4,12 +4,14 @@ import unittest import frappe +from frappe.utils import nowdate from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( in_configured_timeslot, ) +from erpnext.stock.utils import PendingRepostingError class TestRepostItemValuation(unittest.TestCase): @@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase): # to avoid breaking other tests accidentaly riv4.set_status("Skipped") riv3.set_status("Skipped") + + def test_stock_freeze_validation(self): + + today = nowdate() + + riv = frappe.get_doc( + doctype="Repost Item Valuation", + item_code="_Test Item", + warehouse="_Test Warehouse - _TC", + based_on="Item and Warehouse", + posting_date=today, + posting_time="00:01:00", + ) + riv.flags.dont_run_in_test = True # keep it queued + riv.submit() + + stock_settings = frappe.get_doc("Stock Settings") + stock_settings.stock_frozen_upto = today + + self.assertRaises(PendingRepostingError, stock_settings.save) + + riv.set_status("Skipped") diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index b00dbad4769..3b1ae3b43cd 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,6 +12,7 @@ import erpnext class InvalidWarehouseCompany(frappe.ValidationError): pass +class PendingRepostingError(frappe.ValidationError): pass def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -431,7 +432,7 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool if reposting_pending and throw_error: msg = _("Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.") frappe.msgprint(msg, - raise_exception=frappe.ValidationError, + raise_exception=PendingRepostingError, title="Stock Reposting Ongoing", indicator="red", primary_action={ From ffa3d45c42649c7cf4db4c12f67b2181901f0f05 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 14:42:38 +0530 Subject: [PATCH 57/99] chore: update stale rules and apply on issues [skip ci] --- .github/stale.yml | 56 ++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 9322ae87bfc..8b7cb9be3ef 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,34 +1,36 @@ # Configuration for probot-stale - https://github.com/probot/stale -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 15 - -# Number of days of inactivity before a stale Issue or Pull Request is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 3 - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - hotfix - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: true - # Label to use when marking as stale staleLabel: inactive -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This pull request has been automatically marked as stale because it has not had - recent activity. It will be closed within a week if no further activity occurs, but it - only takes a comment to keep a contribution alive :) Also, even if it is closed, - you can always reopen the PR when you're ready. Thank you for contributing. - # Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 30 +limitPerRun: 10 -# Limit to only `issues` or `pulls` -only: pulls +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: true + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +pulls: + daysUntilStale: 15 + daysUntilClose: 3 + exemptLabels: + - hotfix + markComment: > + This pull request has been automatically marked as inactive because it has + not had recent activity. It will be closed within 3 days if no further + activity occurs, but it only takes a comment to keep a contribution alive + :) Also, even if it is closed, you can always reopen the PR when you're + ready. Thank you for contributing. + +issues: + daysUntilStale: 60 + daysUntilClose: 7 + exemptLabels: + - valid + - to-validate + markComment: > + This issue has been automatically marked as inactive because it has not had + recent activity and it wasn't validated by maintainer team. It will be + closed within a week if no further activity occurs. From bf48f176003e2ab6d43de450d5831d941be44caf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 15:58:56 +0530 Subject: [PATCH 58/99] chore: new and improved bug report form [skip ci] --- .github/ISSUE_TEMPLATE/bug_report.md | 47 ----------- .github/ISSUE_TEMPLATE/bug_report.yaml | 106 +++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 47 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c145291b57c..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: Bug report -about: Report a bug encountered while using ERPNext -labels: bug ---- - - - -## Description of the issue - -## Context information (for bug reports) - -**Output of `bench version`** -``` -(paste here) -``` - -## Steps to reproduce the issue - -1. -2. -3. - -### Observed result - -### Expected result - -### Stacktrace / full error message - -``` -(paste here) -``` - -## Additional information - -OS version / distribution, `ERPNext` install method, etc. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000000..df8fcc29893 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,106 @@ +name: Bug Report +description: Report a bug encountered while using ERPNext +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + Welcome to ERPNext issue tracker! Before creating an issue, please heed the following: + + 1. This tracker should only be used to report bugs and request features / enhancements to ERPNext + - For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com) + - For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly. + 2. When making a bug report, make sure you provide all required information. The easier it is for + maintainers to reproduce, the faster it'll be fixed. + 3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR ๐Ÿ˜‰ + + - type: textarea + id: bug-info + attributes: + label: Information about bug + description: Also tell us, what did you expect to happen? + placeholder: Please provide as much information as possible. + validations: + required: true + + - type: dropdown + id: version + attributes: + label: Version + description: Affected versions. + multiple: true + options: + - v12 + - v13 + - v14 + - develop + validations: + required: true + + - type: dropdown + id: module + attributes: + label: Module + description: Select affected module of ERPNext. + multiple: true + options: + - accounts + - stock + - buying + - selling + - ecommerce + - manufacturing + - HR + - projects + - support + - assets + - integrations + - quality + - regional + - portal + - agriculture + - education + - non-profit + validations: + required: true + + - type: textarea + id: exact-version + attributes: + label: Version + description: Share exact version number of Frappe and ERPNext you are using. + placeholder: | + Frappe version - + ERPNext Verion - + validations: + required: true + + - type: Installation method + id: install-method + attributes: + label: Module + options: + - docker + - easy-install + - manual install + - FrappeCloud + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output / Stack trace / Full Error Message. + description: Please copy and paste any relevant log output. This will be automatically formatted. + render: shell + + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true From fcb614d1b763bb950b9c133d0c05f392c3f11b45 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 18:22:55 +0530 Subject: [PATCH 59/99] chore: correct form format for issues --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index df8fcc29893..a6e16a03d8d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -76,10 +76,10 @@ body: validations: required: true - - type: Installation method + - type: dropdown id: install-method attributes: - label: Module + label: Installation method options: - docker - easy-install From fe597cdd3bb663e81e81be2ba9c4fb708294a0d4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Dec 2021 18:33:21 +0530 Subject: [PATCH 60/99] chore: remove unused issue tempaltes [skip ci] --- .github/ISSUE_TEMPLATE/feature_request.md | 3 +++ .../question-about-using-erpnext.md | 17 ----------------- 2 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/question-about-using-erpnext.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6cdad356cd0..418bf3c9417 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,10 @@ --- name: Feature request about: Suggest an idea to improve ERPNext +title: '' labels: feature-request +assignees: '' + ---