diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py index 421c86dba01..b2abffc79d7 100644 --- a/erpnext/accounts/dashboard_fixtures.py +++ b/erpnext/accounts/dashboard_fixtures.py @@ -5,7 +5,22 @@ import frappe import json from frappe.utils import nowdate, add_months, get_date_str from frappe import _ -from erpnext.accounts.utils import get_fiscal_year, get_account_name +from erpnext.accounts.utils import get_fiscal_year, get_account_name, FiscalYearError + +def _get_fiscal_year(date=None): + try: + fiscal_year = get_fiscal_year(date=nowdate(), as_dict=True) + return fiscal_year + + except FiscalYearError: + #if no fiscal year for current date then get default fiscal year + try: + fiscal_year = get_fiscal_year(as_dict=True) + return fiscal_year + + except FiscalYearError: + #if still no fiscal year found then no accounting data created, return + return None def get_company_for_dashboards(): company = frappe.defaults.get_defaults().company @@ -18,10 +33,16 @@ def get_company_for_dashboards(): return None def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards() + "charts": get_charts(fiscal_year), + "number_cards": get_number_cards(fiscal_year) }) def get_dashboards(): @@ -46,10 +67,9 @@ def get_dashboards(): ] }] -def get_charts(): +def get_charts(fiscal_year): company = frappe.get_doc("Company", get_company_for_dashboards()) bank_account = company.default_bank_account or get_account_name("Bank", company=company.name) - fiscal_year = get_fiscal_year(date=nowdate()) default_cost_center = company.cost_center return [ @@ -61,8 +81,8 @@ def get_charts(): "filters_json": json.dumps({ "company": company.name, "filter_based_on": "Fiscal Year", - "from_fiscal_year": fiscal_year[0], - "to_fiscal_year": fiscal_year[0], + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), "periodicity": "Monthly", "include_default_book_entries": 1 }), @@ -158,8 +178,8 @@ def get_charts(): "report_name": "Budget Variance Report", "filters_json": json.dumps({ "company": company.name, - "from_fiscal_year": fiscal_year[0], - "to_fiscal_year": fiscal_year[0], + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), "period": "Monthly", "budget_against": "Cost Center" }), @@ -190,10 +210,10 @@ def get_charts(): }, ] -def get_number_cards(): - fiscal_year = get_fiscal_year(date=nowdate()) - year_start_date = get_date_str(fiscal_year[1]) - year_end_date = get_date_str(fiscal_year[2]) +def get_number_cards(fiscal_year): + + year_start_date = get_date_str(fiscal_year.get("year_start_date")) + year_end_date = get_date_str(fiscal_year.get("year_end_date")) return [ { "doctype": "Number Card", diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 894ec5bdec5..8834385135f 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -72,7 +72,11 @@ def make_dimension_in_accounting_doctypes(doc): if doctype == "Budget": add_dimension_to_budget_doctype(df, doc) else: - create_custom_field(doctype, df) + meta = frappe.get_meta(doctype, cached=False) + fieldnames = [d.fieldname for d in meta.get("fields")] + + if df['fieldname'] not in fieldnames: + create_custom_field(doctype, df) count += 1 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c82a2498432..6cdf9b57d1d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1745,53 +1745,6 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") - def test_deferred_error_email(self): - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") - - item = create_item("_Test Item for Deferred Accounting") - item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_account - item.no_of_months = 12 - item.save() - - si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True) - si.items[0].enable_deferred_revenue = 1 - si.items[0].service_start_date = "2019-01-10" - si.items[0].service_end_date = "2019-03-15" - si.items[0].deferred_revenue_account = deferred_account - si.save() - si.submit() - - from erpnext.accounts.deferred_revenue import convert_deferred_revenue_to_income - - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - acc_settings.acc_frozen_upto = '2019-01-31' - acc_settings.save() - - pda = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) - - pda.insert() - pda.submit() - - email = frappe.db.sql(""" select name from `tabEmail Queue` - where message like %(txt)s """, { - 'txt': "%%%s%%" % "Error while processing deferred accounting for {0}".format(pda.name) - }) - - self.assertTrue(email) - - acc_settings.load_from_db() - acc_settings.acc_frozen_upto = None - acc_settings.save() - def test_inter_company_transaction(self): if not frappe.db.exists("Customer", "_Test Internal Customer"): diff --git a/erpnext/assets/dashboard_fixtures.py b/erpnext/assets/dashboard_fixtures.py index 9af45d16b62..7f3c1de406a 100644 --- a/erpnext/assets/dashboard_fixtures.py +++ b/erpnext/assets/dashboard_fixtures.py @@ -5,14 +5,23 @@ import frappe import json from frappe.utils import nowdate, add_months, get_date_str from frappe import _ -from erpnext.accounts.utils import get_fiscal_year - +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year +from erpnext.buying.dashboard_fixtures import get_company_for_dashboards def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + year_start_date = get_date_str(fiscal_year.get('year_start_date')) + year_end_date = get_date_str(fiscal_year.get('year_end_date')) + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), + "charts": get_charts(fiscal_year, year_start_date, year_end_date), + "number_cards": get_number_cards(fiscal_year, year_start_date, year_end_date), }) def get_dashboards(): @@ -31,12 +40,7 @@ def get_dashboards(): ] }] -fiscal_year = get_fiscal_year(date=nowdate()) -year_start_date = get_date_str(fiscal_year[1]) -year_end_date = get_date_str(fiscal_year[2]) - - -def get_charts(): +def get_charts(fiscal_year, year_start_date, year_end_date): company = get_company_for_dashboards() return [ { @@ -55,8 +59,8 @@ def get_charts(): "company": company, "status": "In Location", "filter_based_on": "Fiscal Year", - "from_fiscal_year": fiscal_year[0], - "to_fiscal_year": fiscal_year[0], + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), "period_start_date": year_start_date, "period_end_date": year_end_date, "date_based_on": "Purchase Date", @@ -134,7 +138,7 @@ def get_charts(): } ] -def get_number_cards(): +def get_number_cards(fiscal_year, year_start_date, year_end_date): return [ { "name": "Total Assets", @@ -172,14 +176,4 @@ def get_number_cards(): "filters_json": "[]", "doctype": "Number Card" } - ] - -def get_company_for_dashboards(): - company = frappe.defaults.get_defaults().company - if company: - return company - else: - company_list = frappe.get_list("Company") - if company_list: - return company_list[0].name - return None \ No newline at end of file + ] \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index d6adde6a371..1869a29c8dd 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -41,7 +41,7 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex team_member = frappe.db.get_value('User', assign_to_member, "email") args = { 'doctype' : 'Asset Maintenance', - 'assign_to' : team_member, + 'assign_to' : [team_member], 'name' : asset_maintenance_name, 'description' : maintenance_task, 'date' : next_due_date diff --git a/erpnext/buying/dashboard_fixtures.py b/erpnext/buying/dashboard_fixtures.py index 186bfb23af9..172c936bd2b 100644 --- a/erpnext/buying/dashboard_fixtures.py +++ b/erpnext/buying/dashboard_fixtures.py @@ -5,13 +5,24 @@ import frappe import json from frappe import _ from frappe.utils import nowdate -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + company = frappe.get_doc("Company", get_company_for_dashboards()) + fiscal_year_name = fiscal_year.get("name") + start_date = str(fiscal_year.get("year_start_date")) + end_date = str(fiscal_year.get("year_end_date")) + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), + "charts": get_charts(company, fiscal_year_name, start_date, end_date), + "number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date), }) def get_company_for_dashboards(): @@ -24,12 +35,6 @@ def get_company_for_dashboards(): return company_list[0].name return None -company = frappe.get_doc("Company", get_company_for_dashboards()) -fiscal_year = get_fiscal_year(nowdate(), as_dict=1) -fiscal_year_name = fiscal_year.get("name") -start_date = str(fiscal_year.get("year_start_date")) -end_date = str(fiscal_year.get("year_end_date")) - def get_dashboards(): return [{ "name": "Buying", @@ -48,7 +53,7 @@ def get_dashboards(): ] }] -def get_charts(): +def get_charts(company, fiscal_year_name, start_date, end_date): return [ { "name": "Purchase Order Analysis", @@ -139,7 +144,7 @@ def get_charts(): } ] -def get_number_cards(): +def get_number_cards(company, fiscal_year_name, start_date, end_date): return [ { "name": "Annual Purchase", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index f62df20ae1a..c7efb8a1a17 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -71,6 +71,15 @@ class PurchaseOrder(BuyingController): "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="], ["conversion_factor", "="]], "is_child_table": True + }, + "Material Request": { + "ref_dn_field": "material_request", + "compare_fields": [["company", "="]], + }, + "Material Request Item": { + "ref_dn_field": "material_request_item", + "compare_fields": [["project", "="], ["item_code", "="]], + "is_child_table": True } }) diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index bebf0ccec56..c7204a1f341 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -15,7 +15,7 @@ class TestProcurementTracker(unittest.TestCase): def test_result_for_procurement_tracker(self): filters = { 'company': '_Test Procurement Company', - 'cost_center': '_Test Cost Center - _TC' + 'cost_center': 'Main - _TPC' } expected_data = self.generate_expected_data() report = execute(filters) @@ -33,24 +33,27 @@ class TestProcurementTracker(unittest.TestCase): country="Pakistan" )).insert() warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company") - mr = make_material_request(company="_Test Procurement Company", warehouse=warehouse) + mr = make_material_request(company="_Test Procurement Company", warehouse=warehouse, cost_center="Main - _TPC") po = make_purchase_order(mr.name) po.supplier = "_Test Supplier" - po.get("items")[0].cost_center = "_Test Cost Center - _TC" + po.get("items")[0].cost_center = "Main - _TPC" po.submit() pr = make_purchase_receipt(po.name) + pr.get("items")[0].cost_center = "Main - _TPC" pr.submit() frappe.db.commit() date_obj = datetime.date(datetime.now()) + po.load_from_db() + expected_data = { "material_request_date": date_obj, - "cost_center": "_Test Cost Center - _TC", + "cost_center": "Main - _TPC", "project": None, "requesting_site": "_Test Procurement Warehouse - _TPC", "requestor": "Administrator", "material_request_no": mr.name, - "description": '_Test Item 1', + "item_code": '_Test Item', "quantity": 10.0, "unit_of_measurement": "_Test UOM", "status": "To Bill", @@ -58,9 +61,9 @@ class TestProcurementTracker(unittest.TestCase): "purchase_order": po.name, "supplier": "_Test Supplier", "estimated_cost": 0.0, - "actual_cost": None, - "purchase_order_amt": 5000.0, - "purchase_order_amt_in_company_currency": 300000.0, + "actual_cost": 0.0, + "purchase_order_amt": po.net_total, + "purchase_order_amt_in_company_currency": po.base_net_total, "expected_delivery_date": date_obj, "actual_delivery_date": date_obj } diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 608e537e1bb..89b48f07ee8 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -349,7 +349,7 @@ class BuyingController(StockController): }) if not rm.rate: - rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse, + rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse, self.doctype, self.name, currency=self.company_currency, company=self.company) rm.amount = qty * flt(rm.rate) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index 4c2d3f692a2..2bef5fb5bdd 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -15,6 +15,7 @@ class TestInpatientRecord(unittest.TestCase): patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 ip_record.save(ignore_permissions = True) self.assertEqual(ip_record.name, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(ip_record.status, frappe.db.get_value("Patient", patient, "inpatient_status")) @@ -26,7 +27,7 @@ class TestInpatientRecord(unittest.TestCase): self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) # Discharge - schedule_discharge(patient=patient) + schedule_discharge(frappe.as_json({'patient': patient})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) @@ -44,8 +45,10 @@ class TestInpatientRecord(unittest.TestCase): patient = create_patient() ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 ip_record.save(ignore_permissions = True) ip_record_new = create_inpatient(patient) + ip_record_new.expected_length_of_stay = 0 self.assertRaises(frappe.ValidationError, ip_record_new.save) service_unit = get_healthcare_service_unit() diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index f575765f69b..2c2b2f6a17b 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -205,7 +205,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nActive\nLeft", + "options": "Active\nLeft", "reqd": 1, "search_index": 1 }, @@ -667,6 +667,7 @@ "oldfieldtype": "Date" }, { + "depends_on": "eval:doc.status == \"Left\"", "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", @@ -803,7 +804,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-05-05 18:51:03.152503", + "modified": "2020-06-15 12:26:30.003741", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 96baaab5950..fa28470af89 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -66,6 +66,7 @@ "fieldname": "employee", "fieldtype": "Link", "in_global_search": 1, + "in_standard_filter": 1, "label": "From Employee", "oldfieldname": "employee", "oldfieldtype": "Link", @@ -164,6 +165,7 @@ "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", + "in_standard_filter": 1, "label": "Posting Date", "oldfieldname": "posting_date", "oldfieldtype": "Date", @@ -236,6 +238,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Company", "oldfieldname": "company", "oldfieldtype": "Link", @@ -368,7 +371,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2019-12-14 23:52:05.388458", + "modified": "2020-06-15 12:43:04.099803", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py index 4a17fd07fbb..64e4bc6ed06 100644 --- a/erpnext/manufacturing/dashboard_fixtures.py +++ b/erpnext/manufacturing/dashboard_fixtures.py @@ -4,7 +4,6 @@ import frappe, erpnext, json from frappe import _ from frappe.utils import nowdate, get_first_day, get_last_day, add_months -from erpnext.accounts.utils import get_fiscal_year def get_data(): return frappe._dict({ diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a0707b77ca7..db9610b62ba 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -697,3 +697,4 @@ execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) erpnext.patches.v12_0.update_uom_conversion_factor erpnext.patches.v13_0.delete_old_purchase_reports erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions +erpnext.patches.v13_0.update_sla_enhancements \ No newline at end of file diff --git a/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py index f1ffaf9d2d4..6013eaa29c6 100644 --- a/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py +++ b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py @@ -6,4 +6,6 @@ import frappe def execute(): ''' Move from due_advance_amount to pending_amount ''' - frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''') + + if frappe.db.has_column("Employee Advance", "due_advance_amount"): + frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''') diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py new file mode 100644 index 00000000000..c156ba95772 --- /dev/null +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -0,0 +1,93 @@ +# Copyright (c) 2018, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + # add holiday list and employee group fields in SLA + # change response and resolution time in priorities child table + if frappe.db.exists('DocType', 'Service Level Agreement'): + sla_details = frappe.db.get_all('Service Level Agreement', fields=['name', 'service_level']) + priorities = frappe.db.get_all('Service Level Priority', fields=['*'], filters={ + 'parenttype': ('in', ['Service Level Agreement', 'Service Level']) + }) + + frappe.reload_doc('support', 'doctype', 'service_level_agreement') + frappe.reload_doc('support', 'doctype', 'pause_sla_on_status') + frappe.reload_doc('support', 'doctype', 'service_level_priority') + frappe.reload_doc('support', 'doctype', 'service_day') + + for entry in sla_details: + values = frappe.db.get_value('Service Level', entry.service_level, ['holiday_list', 'employee_group']) + if values: + holiday_list = values[0] + employee_group = values[1] + frappe.db.set_value('Service Level Agreement', entry.name, { + 'holiday_list': holiday_list, + 'employee_group': employee_group + }) + + priority_dict = {} + + for priority in priorities: + if priority.parenttype == 'Service Level Agreement': + response_time = convert_to_seconds(priority.response_time, priority.response_time_period) + resolution_time = convert_to_seconds(priority.resolution_time, priority.resolution_time_period) + frappe.db.set_value('Service Level Priority', priority.name, { + 'response_time': response_time, + 'resolution_time': resolution_time + }) + if priority.parenttype == 'Service Level': + if not priority.parent in priority_dict: + priority_dict[priority.parent] = [] + priority_dict[priority.parent].append(priority) + + + # copy Service Levels to Service Level Agreements + sl = [entry.service_level for entry in sla_details] + if frappe.db.exists('DocType', 'Service Level'): + service_levels = frappe.db.get_all('Service Level', filters={'service_level': ('not in', sl)}, fields=['*']) + for entry in service_levels: + sla = frappe.new_doc('Service Level Agreement') + sla.service_level = entry.service_level + sla.holiday_list = entry.holiday_list + sla.employee_group = entry.employee_group + sla.flags.ignore_validate = True + sla = sla.insert(ignore_mandatory=True) + + frappe.db.sql(""" + UPDATE + `tabService Day` + SET + parent = %(new_parent)s , parentfield = 'support_and_resolution', parenttype = 'Service Level Agreement' + WHERE + parent = %(old_parent)s + """, {'new_parent': sla.name, 'old_parent': entry.name}, as_dict = 1) + + priority_list = priority_dict.get(entry.name) + if priority_list: + sla = frappe.get_doc('Service Level Agreement', sla.name) + for priority in priority_list: + row = sla.append('priorities', { + 'priority': priority.priority, + 'default_priority': priority.default_priority, + 'response_time': convert_to_seconds(priority.response_time, priority.response_time_period), + 'resolution_time': convert_to_seconds(priority.resolution_time, priority.resolution_time_period) + }) + row.db_update() + sla.db_update() + + frappe.delete_doc_if_exists('DocType', 'Service Level') + + +def convert_to_seconds(value, unit): + seconds = 0 + if unit == "Hour": + seconds = value * 3600 + if unit == "Day": + seconds = value * 3600 * 24 + if unit == "Week": + seconds = value * 3600 * 24 * 7 + return seconds diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 0993e69e042..6b6b8c579be 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -1,4 +1,5 @@ import frappe +from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager def get_field_filter_data(): @@ -243,6 +244,8 @@ def get_next_attribute_and_values(item_code, selected_attributes): else: product_info = None + product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) + return { 'next_attribute': next_attribute, 'valid_options_for_attributes': valid_options_for_attributes, diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index bd3369447ba..47a28fd1114 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -64,7 +64,7 @@ class TestTask(unittest.TestCase): def assign(): from frappe.desk.form import assign_to assign_to.add({ - "assign_to": "test@example.com", + "assign_to": ["test@example.com"], "doctype": task.doctype, "name": task.name, "description": "Close this task" diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 32f0428fcd8..cbc624c064f 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -13,7 +13,7 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.hr.doctype.salary_structure.test_salary_structure \ import make_salary_structure, create_salary_structure_assignment - +from erpnext.hr.doctype.employee.test_employee import make_employee class TestTimesheet(unittest.TestCase): def setUp(self): @@ -25,8 +25,10 @@ class TestTimesheet(unittest.TestCase): def test_timesheet_billing_amount(self): - make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, billable=1) self.assertEqual(timesheet.total_hours, 2) self.assertEqual(timesheet.total_billable_hours, 2) @@ -35,8 +37,10 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.total_billable_amount, 100) def test_timesheet_billing_amount_not_billable(self): - make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=0) + emp = make_employee("test_employee_6@salary.com") + + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, billable=0) self.assertEqual(timesheet.total_hours, 2) self.assertEqual(timesheet.total_billable_hours, 0) @@ -45,8 +49,10 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.total_billable_amount, 0) def test_salary_slip_from_timesheet(self): - salary_structure = make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate = True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + salary_structure = make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate = True, billable=1) salary_slip = make_salary_slip(timesheet.name) salary_slip.submit() @@ -65,7 +71,9 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.status, 'Submitted') def test_sales_invoice_from_timesheet(self): - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + timesheet = make_timesheet(emp, simulate=True, billable=1) sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer') sales_invoice.due_date = nowdate() sales_invoice.submit() @@ -80,7 +88,9 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(item.rate, 50.00) def test_timesheet_billing_based_on_project(self): - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1, project = '_Test Project', company='_Test Company') + emp = make_employee("test_employee_6@salary.com") + + timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company') sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice.project = '_Test Project' sales_invoice.submit() @@ -90,6 +100,8 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(ts.time_logs[0].sales_invoice, sales_invoice.name) def test_timesheet_time_overlap(self): + emp = make_employee("test_employee_6@salary.com") + settings = frappe.get_single('Projects Settings') initial_setting = settings.ignore_employee_time_overlap settings.ignore_employee_time_overlap = 0 @@ -97,7 +109,7 @@ class TestTimesheet(unittest.TestCase): update_activity_type("_Test Activity Type") timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" + timesheet.employee = emp timesheet.append( 'time_logs', { @@ -129,12 +141,14 @@ class TestTimesheet(unittest.TestCase): settings.save() def test_timesheet_std_working_hours(self): + emp = make_employee("test_employee_6@salary.com") + company = frappe.get_doc('Company', "_Test Company") company.standard_working_hours = 8 company.save() timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" + timesheet.employee = emp timesheet.company = '_Test Company' timesheet.append( 'time_logs', @@ -156,7 +170,7 @@ class TestTimesheet(unittest.TestCase): company.save() timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" + timesheet.employee = emp timesheet.company = '_Test Company' timesheet.append( 'time_logs', diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index 6d344025d26..cae28bee8bc 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -9,14 +9,14 @@ def setup(company=None, patch=True): make_custom_fields() add_print_formats() -def make_custom_fields(): +def make_custom_fields(update=True): custom_fields = { 'Supplier': [ dict(fieldname='irs_1099', fieldtype='Check', insert_after='tax_id', label='Is IRS 1099 reporting required for supplier?') ] } - create_custom_fields(custom_fields) + create_custom_fields(custom_fields, update=update) def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index e666a41f300..74ff0ecfd8f 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -105,3 +105,4 @@ def add_company_to_session_defaults(): "ref_doctype": "Company" }) settings.save() + diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 7096c17fb18..5bd30ab2e89 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -78,8 +78,10 @@ def place_order(): if is_stock_item: item_stock = get_qty_in_stock(item.item_code, "website_warehouse") + if not cint(item_stock.in_stock): + throw(_("{1} Not in Stock").format(item.item_code)) if item.qty > item_stock.stock_qty[0][0]: - throw(_("Only {0} in stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) + throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) sales_order.flags.ignore_permissions = True sales_order.insert() diff --git a/erpnext/stock/dashboard_fixtures.py b/erpnext/stock/dashboard_fixtures.py index 0f1fd128f0b..7625b1ad283 100644 --- a/erpnext/stock/dashboard_fixtures.py +++ b/erpnext/stock/dashboard_fixtures.py @@ -5,31 +5,26 @@ import frappe import json from frappe import _ from frappe.utils import nowdate -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year +from erpnext.buying.dashboard_fixtures import get_company_for_dashboards def get_data(): + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + company = frappe.get_doc("Company", get_company_for_dashboards()) + fiscal_year_name = fiscal_year.get("name") + start_date = str(fiscal_year.get("year_start_date")) + end_date = str(fiscal_year.get("year_end_date")) + return frappe._dict({ "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), + "charts": get_charts(company, fiscal_year_name, start_date, end_date), + "number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date), }) -def get_company_for_dashboards(): - company = frappe.defaults.get_defaults().company - if company: - return company - else: - company_list = frappe.get_list("Company") - if company_list: - return company_list[0].name - return None - -company = frappe.get_doc("Company", get_company_for_dashboards()) -fiscal_year = get_fiscal_year(nowdate(), as_dict=1) -fiscal_year_name = fiscal_year.get("name") -start_date = str(fiscal_year.get("year_start_date")) -end_date = str(fiscal_year.get("year_end_date")) - def get_dashboards(): return [{ "name": "Stock", @@ -48,7 +43,7 @@ def get_dashboards(): ] }] -def get_charts(): +def get_charts(company, fiscal_year_name, start_date, end_date): return [ { "doctype": "Dashboard Chart", @@ -133,7 +128,7 @@ def get_charts(): } ] -def get_number_cards(): +def get_number_cards(company, fiscal_year_name, start_date, end_date): return [ { "name": "Total Active Items", diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index a3fe72d0519..b1ad7c8aa0a 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Service Level Agreement", - "links": "[\n {\n \"description\": \"Service Level.\",\n \"label\": \"Service Level\",\n \"name\": \"Service Level\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-05-28 13:51:23.869954", + "modified": "2020-06-04 11:54:56.124219", "modified_by": "Administrator", "module": "Support", "name": "Support", @@ -65,8 +65,8 @@ "type": "DocType" }, { - "label": "Service Level", - "link_to": "Service Level", + "label": "Service Level Agreement", + "link_to": "Service Level Agreement", "type": "DocType" } ] diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index bad40cc37fc..e7e5bd312bc 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -38,10 +38,35 @@ frappe.ui.form.on("Issue", { }, refresh: function (frm) { - if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { if (frm.doc.service_level_agreement) { - set_time_to_resolve_and_response(frm); + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement + }, + callback: function(data) { + let statuses = data.message.pause_sla_on; + const hold_statuses = []; + $.each(statuses, (_i, entry) => { + hold_statuses.push(entry.status); + }); + if (hold_statuses.includes(frm.doc.status)) { + frm.dashboard.clear_headline(); + let message = {"indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)])}; + frm.dashboard.set_headline_alert( + '
' + + '
' + + ''+ message.msg +' ' + + '
' + + '
' + ); + } else { + set_time_to_resolve_and_response(frm); + } + } + }); } frm.add_custom_button(__("Close"), function () { @@ -55,6 +80,7 @@ frappe.ui.form.on("Issue", { frm: frm }); }, __("Make")); + } else { if (frm.doc.service_level_agreement) { frm.dashboard.clear_headline(); diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index c12cef4a5f3..6525ab27d30 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -31,9 +31,13 @@ "resolution_by", "resolution_by_variance", "service_level_agreement_creation", + "on_hold_since", + "total_hold_time", "response", "mins_to_first_response", "first_responded_on", + "column_break_26", + "avg_response_time", "additional_info", "lead", "contact", @@ -50,7 +54,9 @@ "resolution_date", "content_type", "attachment", - "via_customer_portal" + "via_customer_portal", + "resolution_time", + "user_resolution_time" ], "fields": [ { @@ -114,7 +120,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nReplied\nHold\nClosed", + "options": "Open\nReplied\nHold\nResolved\nClosed", "search_index": 1 }, { @@ -161,6 +167,7 @@ "options": "Service Level Agreement" }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "response_by", "fieldtype": "Datetime", "label": "Response By", @@ -174,6 +181,7 @@ "read_only": 1 }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "resolution_by", "fieldtype": "Datetime", "label": "Resolution By", @@ -328,7 +336,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.service_level_agreement", + "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", "description": "in hours", "fieldname": "response_by_variance", "fieldtype": "Float", @@ -336,7 +344,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.service_level_agreement", + "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", "description": "in hours", "fieldname": "resolution_by_variance", "fieldtype": "Float", @@ -362,12 +370,48 @@ "label": "Issue Split From", "options": "Issue", "read_only": 1 + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "fieldname": "avg_response_time", + "fieldtype": "Duration", + "label": "Average Response 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": "on_hold_since", + "fieldtype": "Datetime", + "hidden": 1, + "label": "On Hold Since", + "read_only": 1 + }, + { + "fieldname": "total_hold_time", + "fieldtype": "Duration", + "label": "Total Hold Time", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-03-13 02:19:49.477928", + "modified": "2020-06-10 12:47:37.146914", "modified_by": "Administrator", "module": "Support", "name": "Issue", @@ -395,4 +439,4 @@ "title_field": "subject", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 117267f1a42..883e603fd31 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -7,7 +7,7 @@ import json from frappe import _ from frappe import utils from frappe.model.document import Document -from frappe.utils import now, time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime +from frappe.utils import time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime, time_diff_in_seconds, time_diff from datetime import datetime, timedelta from frappe.model.mapper import get_mapped_doc from frappe.utils.user import is_website_user @@ -47,8 +47,8 @@ class Issue(Document): self.contact = frappe.db.get_value("Contact", {"email_id": email_id}) if self.contact: - contact = frappe.get_doc('Contact', self.contact) - self.customer = contact.get_link_for('Customer') + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") if not self.company: self.company = frappe.db.get_value("Lead", self.lead, "company") or \ @@ -56,18 +56,70 @@ class Issue(Document): def update_status(self): status = frappe.db.get_value("Issue", self.name, "status") - if self.status!="Open" and status =="Open" and not self.first_responded_on: + if self.status != "Open" and status == "Open" and not self.first_responded_on: self.first_responded_on = frappe.flags.current_time or now_datetime() - if self.status=="Closed" and status !="Closed": + if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: self.resolution_date = frappe.flags.current_time or now_datetime() if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing": set_service_level_agreement_variance(issue=self.name) self.update_agreement_status() + set_resolution_time(issue=self) + set_user_resolution_time(issue=self) - if self.status=="Open" and status !="Open": + if self.status == "Open" and status != "Open": # if no date, it should be set as None and not a blank string "", as per mysql strict config self.resolution_date = None + self.reset_issue_metrics() + # enable SLA and variance on Reopen + self.agreement_fulfilled = "Ongoing" + set_service_level_agreement_variance(issue=self.name) + + self.handle_hold_time(status) + + def handle_hold_time(self, status): + if self.service_level_agreement: + # set response and resolution variance as None as the issue is on Hold for status as Replied + pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"], + filters={"parent": self.service_level_agreement}) + hold_statuses = [entry.status for entry in pause_sla_on] + update_values = {} + + if self.status in hold_statuses and status not in hold_statuses: + update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() + if not self.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 + + # calculate hold time when status is changed from Replied to any other status + if self.status not in hold_statuses and status in hold_statuses: + hold_time = self.total_hold_time if self.total_hold_time else 0 + now_time = frappe.flags.current_time or now_datetime() + update_values['total_hold_time'] = hold_time + time_diff_in_seconds(now_time, self.on_hold_since) + + # re-calculate SLA variables after issue changes from Replied to Open + # add hold time to SLA variables + if self.status == "Open" and status in hold_statuses: + start_date_time = get_datetime(self.service_level_agreement_creation) + priority = get_priority(self) + now_time = frappe.flags.current_time or now_datetime() + hold_time = time_diff_in_seconds(now_time, self.on_hold_since) + + if not self.first_responded_on: + response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + update_values['response_by'] = add_to_date(response_by, seconds=round(hold_time)) + response_by_variance = round(time_diff_in_hours(self.response_by, now_time)) + update_values['response_by_variance'] = response_by_variance + (hold_time // 3600) + + resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + update_values['resolution_by'] = add_to_date(resolution_by, seconds=round(hold_time)) + resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_time)) + update_values['resolution_by_variance'] = resolution_by_variance + (hold_time // 3600) + update_values['on_hold_since'] = None + + self.db_set(update_values) def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": @@ -128,6 +180,7 @@ class Issue(Document): 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() @@ -137,7 +190,7 @@ class Issue(Document): communications = frappe.get_all("Communication", filters={"reference_doctype": "Issue", "reference_name": comm_to_split_from.reference_name, - "creation": ('>=', comm_to_split_from.creation)}) + "creation": (">=", comm_to_split_from.creation)}) for communication in communications: doc = frappe.get_doc("Communication", communication.name) @@ -173,20 +226,15 @@ class Issue(Document): self.service_level_agreement = service_level_agreement.name self.priority = service_level_agreement.default_priority if not priority else priority - service_level_agreement = frappe.get_doc("Service Level Agreement", service_level_agreement.name) - priority = service_level_agreement.get_service_level_agreement_priority(self.priority) - priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list - }) + priority = get_priority(self) if not self.creation: self.creation = now_datetime() self.service_level_agreement_creation = now_datetime() start_date_time = get_datetime(self.service_level_agreement_creation) - self.response_by = get_expected_time_for(parameter='response', service_level=priority, start_date_time=start_date_time) - self.resolution_by = get_expected_time_for(parameter='resolution', service_level=priority, start_date_time=start_date_time) + self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime())) self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) @@ -221,36 +269,41 @@ class Issue(Document): self.agreement_fulfilled = "Ongoing" self.save() + def reset_issue_metrics(self): + self.db_set("resolution_time", None) + self.db_set("user_resolution_time", None) + + +def get_priority(issue): + service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement) + priority = service_level_agreement.get_service_level_agreement_priority(issue.priority) + priority.update({ + "support_and_resolution": service_level_agreement.support_and_resolution, + "holiday_list": service_level_agreement.holiday_list + }) + return priority + + def get_expected_time_for(parameter, service_level, start_date_time): current_date_time = start_date_time expected_time = current_date_time start_time = None end_time = None - # lets assume response time is in days by default - if parameter == 'response': - allotted_days = service_level.get("response_time") - time_period = service_level.get("response_time_period") - elif parameter == 'resolution': - allotted_days = service_level.get("resolution_time") - time_period = service_level.get("resolution_time_period") + if parameter == "response": + allotted_seconds = service_level.get("response_time") + elif parameter == "resolution": + allotted_seconds = service_level.get("resolution_time") else: frappe.throw(_("{0} parameter is invalid").format(parameter)) - allotted_hours = 0 - if time_period == 'Hour': - allotted_hours = allotted_days - allotted_days = 0 - elif time_period == 'Week': - allotted_days *= 7 - - expected_time_is_set = 1 if allotted_days == 0 and time_period in ['Day', 'Week'] else 0 + expected_time_is_set = 0 support_days = {} for service in service_level.get("support_and_resolution"): support_days[service.workday] = frappe._dict({ - 'start_time': service.start_time, - 'end_time': service.end_time, + "start_time": service.start_time, + "end_time": service.end_time, }) holidays = get_holidays(service_level.get("holiday_list")) @@ -264,25 +317,22 @@ def get_expected_time_for(parameter, service_level, start_date_time): if getdate(current_date_time) == getdate(start_date_time) and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time \ else support_days[current_weekday].start_time end_time = support_days[current_weekday].end_time - time_left_today = time_diff_in_hours(end_time, start_time) + time_left_today = time_diff_in_seconds(end_time, start_time) # no time left for support today - if time_left_today < 0: pass - elif time_period == 'Hour': - if time_left_today >= allotted_hours: + if time_left_today <= 0: pass + elif allotted_seconds: + if time_left_today >= allotted_seconds: expected_time = datetime.combine(getdate(current_date_time), get_time(start_time)) - expected_time = add_to_date(expected_time, hours=allotted_hours) + expected_time = add_to_date(expected_time, seconds=allotted_seconds) expected_time_is_set = 1 else: - allotted_hours = allotted_hours - time_left_today - else: - allotted_days -= 1 - expected_time_is_set = allotted_days <= 0 + allotted_seconds = allotted_seconds - time_left_today if not expected_time_is_set: current_date_time = add_to_date(current_date_time, days=1) - if end_time and time_period != 'Hour': + if end_time and allotted_seconds >= 86400: current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time)) else: current_date_time = expected_time @@ -311,6 +361,36 @@ def set_service_level_agreement_variance(issue=None): if variance < 0: frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) + +def set_resolution_time(issue): + # total time taken from issue creation to closing + resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) + issue.db_set("resolution_time", resolution_time) + + +def set_user_resolution_time(issue): + # total time taken by a user to close the issue apart from wait_time + communications = frappe.get_list("Communication", filters={ + "reference_doctype": issue.doctype, + "reference_name": issue.name + }, + fields=["sent_or_received", "name", "creation"], + order_by="creation" + ) + + pending_time = [] + for i in range(len(communications)): + if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": + wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if wait_time > 0: + pending_time.append(wait_time) + + total_pending_time = sum(pending_time) + resolution_time_in_secs = time_diff_in_seconds(issue.resolution_date, issue.creation) + user_resolution_time = resolution_time_in_secs - total_pending_time + issue.db_set("user_resolution_time", user_resolution_time) + + def get_list_context(context=None): return { "title": _("Issues"), @@ -318,7 +398,7 @@ def get_list_context(context=None): "row_template": "templates/includes/issue_row.html", "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True + "no_breadcrumbs": True } @@ -326,12 +406,12 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord from frappe.www.list import get_list user = frappe.session.user - contact = frappe.db.get_value('Contact', {'user': user}, 'name') + contact = frappe.db.get_value("Contact", {"user": user}, "name") customer = None if contact: - contact_doc = frappe.get_doc('Contact', contact) - customer = contact_doc.get_link_for('Customer') + contact_doc = frappe.get_doc("Contact", contact) + customer = contact_doc.get_link_for("Customer") ignore_permissions = False if is_website_user(): diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 7a5e3e300db..fb8ceb53b21 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -5,15 +5,18 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues -from frappe.utils import now_datetime, get_datetime +from frappe.utils import now_datetime, get_datetime, flt import datetime from datetime import timedelta class TestIssue(unittest.TestCase): - def test_response_time_and_resolution_time_based_on_different_sla(self): + def setUp(self): + frappe.db.sql("delete from `tabService Level Agreement`") + frappe.db.sql("delete from `tabEmployee`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) create_service_level_agreements_for_issues() + def test_response_time_and_resolution_time_based_on_different_sla(self): creation = datetime.datetime(2019, 3, 4, 12, 0) # make issue with customer specific SLA @@ -72,8 +75,67 @@ class TestIssue(unittest.TestCase): self.assertEqual(issue.agreement_fulfilled, 'Fulfilled') -def make_issue(creation=None, customer=None, index=0): + def test_issue_metrics(self): + creation = datetime.datetime(2020, 3, 4, 4, 0) + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 4, 15) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + creation = datetime.datetime(2020, 3, 4, 5, 0) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 5, 5) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + 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 = datetime.datetime(2020, 3, 4, 4, 0) + + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 4, 15) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) + issue.reload() + issue.status = 'Replied' + issue.save() + + self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + + creation = datetime.datetime(2020, 3, 4, 5, 0) + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 0) + create_communication(issue.name, "test@example.com", "Received", creation) + + issue.reload() + self.assertEqual(flt(issue.total_hold_time, 2), 2700) + self.assertEqual(issue.resolution_by, datetime.datetime(2020, 3, 4, 16, 45)) + + creation = datetime.datetime(2020, 3, 4, 5, 5) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + issue.reload() + issue.status = 'Closed' + issue.save() + + issue.reload() + self.assertEqual(flt(issue.total_hold_time, 2), 2700) + + +def make_issue(creation=None, customer=None, index=0): issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), @@ -86,6 +148,7 @@ def make_issue(creation=None, customer=None, index=0): return issue + def create_customer(name, customer_group, territory): create_customer_group(customer_group) @@ -99,6 +162,7 @@ def create_customer(name, customer_group, territory): "territory": territory }).insert(ignore_permissions=True) + def create_customer_group(customer_group): if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}): @@ -107,6 +171,7 @@ def create_customer_group(customer_group): "customer_group_name": customer_group }).insert(ignore_permissions=True) + def create_territory(territory): if not frappe.db.exists("Territory", {"territory_name": territory}): @@ -114,3 +179,21 @@ def create_territory(territory): "doctype": "Territory", "territory_name": territory, }).insert(ignore_permissions=True) + + +def create_communication(reference_name, sender, sent_or_received, creation): + issue = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "communication_medium": "Email", + "sent_or_received": sent_or_received, + "email_status": "Open", + "subject": "Test Issue", + "sender": sender, + "content": "Test", + "status": "Linked", + "reference_doctype": "Issue", + "creation": creation, + "reference_name": reference_name + }) + issue.save() diff --git a/erpnext/support/doctype/service_level/__init__.py b/erpnext/support/doctype/pause_sla_on_status/__init__.py similarity index 100% rename from erpnext/support/doctype/service_level/__init__.py rename to erpnext/support/doctype/pause_sla_on_status/__init__.py diff --git a/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json new file mode 100644 index 00000000000..5b03f25f489 --- /dev/null +++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-06-05 13:59:43.265588", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-05 15:15:29.986608", + "modified_by": "Administrator", + "module": "Support", + "name": "Pause SLA On Status", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py new file mode 100644 index 00000000000..a3b547e4801 --- /dev/null +++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class PauseSLAOnStatus(Document): + pass diff --git a/erpnext/support/doctype/service_level/service_level.js b/erpnext/support/doctype/service_level/service_level.js deleted file mode 100644 index abe254bd036..00000000000 --- a/erpnext/support/doctype/service_level/service_level.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Service Level', { - -}); diff --git a/erpnext/support/doctype/service_level/service_level.json b/erpnext/support/doctype/service_level/service_level.json deleted file mode 100644 index dced3aa9e90..00000000000 --- a/erpnext/support/doctype/service_level/service_level.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "autoname": "field:service_level", - "creation": "2018-11-19 12:44:30.407502", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "service_level", - "employee_group", - "column_break_2", - "holiday_list", - "default_priority", - "response_and_resoution_time", - "priorities", - "section_break_01", - "support_and_resolution" - ], - "fields": [ - { - "fieldname": "service_level", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Level", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "holiday_list", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Holiday List (ignored during SLA calculation)", - "options": "Holiday List", - "reqd": 1 - }, - { - "fieldname": "employee_group", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Employee Group", - "options": "Employee Group" - }, - { - "fieldname": "response_and_resoution_time", - "fieldtype": "Section Break", - "label": "Response and Resoution Time" - }, - { - "fieldname": "section_break_01", - "fieldtype": "Section Break", - "label": "Support Hours" - }, - { - "fieldname": "support_and_resolution", - "fieldtype": "Table", - "label": "Support and Resolution", - "options": "Service Day", - "reqd": 1 - }, - { - "fieldname": "priorities", - "fieldtype": "Table", - "label": "Priorities", - "options": "Service Level Priority", - "reqd": 1 - }, - { - "fieldname": "default_priority", - "fieldtype": "Link", - "label": "Default Priority", - "options": "Issue Priority", - "read_only": 1 - } - ], - "modified": "2019-06-06 12:58:03.464056", - "modified_by": "Administrator", - "module": "Support", - "name": "Service Level", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "All", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/service_level.py b/erpnext/support/doctype/service_level/service_level.py deleted file mode 100644 index 89fa25c2338..00000000000 --- a/erpnext/support/doctype/service_level/service_level.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.model.document import Document -from datetime import datetime -from frappe.utils import get_weekdays - -class ServiceLevel(Document): - - def validate(self): - self.check_priorities() - self.check_support_and_resolution() - - def check_priorities(self): - default_priority = [] - priorities = [] - - for priority in self.priorities: - # Check if response and resolution time is set for every priority - if not (priority.response_time or priority.resolution_time): - frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx)) - - priorities.append(priority.priority) - - if priority.default_priority: - default_priority.append(priority.default_priority) - - if priority.response_time_period == "Hour": - response = priority.response_time * 0.0416667 - elif priority.response_time_period == "Day": - response = priority.response_time - elif priority.response_time_period == "Week": - response = priority.response_time * 7 - - if priority.resolution_time_period == "Hour": - resolution = priority.resolution_time * 0.0416667 - elif priority.resolution_time_period == "Day": - resolution = priority.resolution_time - elif priority.resolution_time_period == "Week": - resolution = priority.resolution_time * 7 - - if response > resolution: - frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) - - # Check if repeated priority - if not len(set(priorities)) == len(priorities): - repeated_priority = get_repeated(priorities) - frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority)) - - # Check if repeated default priority - if not len(set(default_priority)) == len(default_priority): - frappe.throw(_("Select only one Priority as Default.")) - - # set default priority from priorities - try: - self.default_priority = next(d.priority for d in self.priorities if d.default_priority) - except Exception: - frappe.throw(_("Select a Default Priority.")) - - def check_support_and_resolution(self): - week = get_weekdays() - support_days = [] - - for support_and_resolution in self.support_and_resolution: - # Check if start and end time is set for every support day - if not (support_and_resolution.start_time or support_and_resolution.end_time): - frappe.throw(_("Set Start Time and End Time for \ - Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx))) - - support_days.append(support_and_resolution.workday) - support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 - - if support_and_resolution.start_time >= support_and_resolution.end_time: - frappe.throw(_("Start Time can't be greater than or equal to End Time \ - for {0}.".format(support_and_resolution.workday))) - - # Check for repeated workday - if not len(set(support_days)) == len(support_days): - repeated_days = get_repeated(support_days) - frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) - -def get_repeated(values): - unique_list = [] - diff = [] - for value in values: - if value not in unique_list: - unique_list.append(str(value)) - else: - if value not in diff: - diff.append(str(value)) - return " ".join(diff) diff --git a/erpnext/support/doctype/service_level/service_level_dashboard.py b/erpnext/support/doctype/service_level/service_level_dashboard.py deleted file mode 100644 index 393095e1179..00000000000 --- a/erpnext/support/doctype/service_level/service_level_dashboard.py +++ /dev/null @@ -1,12 +0,0 @@ -from frappe import _ - -def get_data(): - return { - 'fieldname': 'service_level', - 'transactions': [ - { - 'label': _('Service Level Agreement'), - 'items': ['Service Level Agreement'] - } - ] - } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/test_service_level.py b/erpnext/support/doctype/service_level/test_service_level.py deleted file mode 100644 index 09577df1663..00000000000 --- a/erpnext/support/doctype/service_level/test_service_level.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals -from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group -from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities - -import frappe -import unittest - -class TestServiceLevel(unittest.TestCase): - - def test_service_level(self): - employee_group = make_employee_group() - make_holiday_list() - make_priorities() - - # Default Service Level - test_make_service_level = create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6) - get_make_service_level = get_service_level("__Test Service Level") - - self.assertEqual(test_make_service_level.name, get_make_service_level.name) - self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list) - self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group) - - # Service Level - test_make_service_level = create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3) - get_make_service_level = get_service_level("_Test Service Level") - - self.assertEqual(test_make_service_level.name, get_make_service_level.name) - self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list) - self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group) - - -def create_service_level(service_level, holiday_list, employee_group, response_time, resolution_time): - sl = frappe.get_doc({ - "doctype": "Service Level", - "service_level": service_level, - "holiday_list": holiday_list, - "employee_group": employee_group, - "priorities": [ - { - "priority": "Low", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "Medium", - "response_time": response_time, - "default_priority": 1, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "High", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - } - ], - "support_and_resolution": [ - { - "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Saturday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Sunday", - "start_time": "10:00:00", - "end_time": "18:00:00", - } - ] - }) - - sl_exists = frappe.db.exists("Service Level", {"service_level": service_level}) - - if not sl_exists: - sl.insert() - return sl - else: - return frappe.get_doc("Service Level", {"service_level": service_level}) - -def get_service_level(service_level): - return frappe.get_doc("Service Level", service_level) - -def make_holiday_list(): - holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") - if not holiday_list: - now = frappe.utils.now_datetime() - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "__Test Holiday List", - "from_date": "2019-01-01", - "to_date": "2019-12-31", - "holidays": [ - { - "description": "Test Holiday 1", - "holiday_date": "2019-03-05" - }, - { - "description": "Test Holiday 2", - "holiday_date": "2019-03-07" - }, - { - "description": "Test Holiday 3", - "holiday_date": "2019-02-11" - }, - ] - }).insert() - -def create_service_level_for_sla(): - employee_group = make_employee_group() - make_holiday_list() - make_priorities() - - # Default Service Level - create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6) - - # Service Level - create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3) 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 1d486f48346..5346195a396 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -2,28 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('Service Level Agreement', { - service_level: function(frm) { - frm.fields_dict.support_and_resolution.grid.remove_all(); - frappe.call({ - "method": "frappe.client.get", - args: { - doctype: "Service Level", - name: frm.doc.service_level - }, - callback: function(data){ - let count = Math.max(data.message.priorities.length, data.message.support_and_resolution.length); - let i = 0; - while (i < count){ - if (data.message.priorities[i]) { - frm.add_child("priorities", data.message.priorities[i]); - } - if (data.message.support_and_resolution[i]) { - frm.add_child("support_and_resolution", data.message.support_and_resolution[i]); - } - i++; - } - frm.refresh(); - } + setup: function(frm) { + let allow_statuses = []; + const exclude_statuses = ['Open', 'Closed', 'Resolved']; + + frappe.model.with_doctype('Issue', () => { + let statuses = frappe.meta.get_docfield('Issue', 'status', frm.doc.name).options; + statuses = statuses.split('\n'); + allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); + frappe.meta.get_docfield('Pause SLA On Status', 'status', frm.doc.name).options = [''].concat(allow_statuses); }); - }, -}); + } +}); \ No newline at end of file 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 9a83ca7ac04..939c1999828 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:SLA-{service_level}-{####}", "creation": "2018-12-26 21:08:15.448812", "doctype": "DocType", @@ -6,12 +7,13 @@ "engine": "InnoDB", "field_order": [ "enable", + "section_break_2", "service_level", + "default_priority", "default_service_level_agreement", - "holiday_list", "column_break_2", "employee_group", - "default_priority", + "holiday_list", "entity_section", "entity_type", "column_break_10", @@ -21,49 +23,40 @@ "active", "column_break_7", "end_date", + "section_break_18", + "pause_sla_on", "response_and_resolution_time_section", "priorities", "support_and_resolution_section_break", "support_and_resolution" ], "fields": [ - { - "default": "0", - "depends_on": "eval: !doc.customer;", - "fieldname": "default_service_level_agreement", - "fieldtype": "Check", - "label": "Default Service Level Agreement" - }, { "fieldname": "service_level", - "fieldtype": "Link", + "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, "label": "Service Level", - "options": "Service Level", "reqd": 1 }, { - "fetch_from": "service_level.holiday_list", "fieldname": "holiday_list", "fieldtype": "Link", "label": "Holiday List", "options": "Holiday List", - "read_only": 1 + "reqd": 1 }, { "fieldname": "column_break_2", "fieldtype": "Column Break" }, { - "fetch_from": "service_level.employee_group", "fieldname": "employee_group", "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, "label": "Employee Group", - "options": "Employee Group", - "read_only": 1 + "options": "Employee Group" }, { "fieldname": "agreement_details_section", @@ -103,21 +96,15 @@ "fieldname": "support_and_resolution", "fieldtype": "Table", "label": "Support and Resolution", - "options": "Service Day" + "options": "Service Day", + "reqd": 1 }, { "fieldname": "priorities", "fieldtype": "Table", "label": "Priorities", - "options": "Service Level Priority" - }, - { - "fetch_from": "service_level.default_priority", - "fieldname": "default_priority", - "fieldtype": "Link", - "label": "Default Priority", - "options": "Issue Priority", - "read_only": 1 + "options": "Service Level Priority", + "reqd": 1 }, { "default": "1", @@ -156,9 +143,38 @@ "fieldname": "enable", "fieldtype": "Check", "label": "Enable" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "default_service_level_agreement", + "fieldtype": "Check", + "label": "Default Service Level Agreement" + }, + { + "fieldname": "default_priority", + "fieldtype": "Link", + "label": "Default Priority", + "options": "Issue Priority", + "read_only": 1 + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "pause_sla_on", + "fieldtype": "Table", + "label": "Pause SLA On", + "options": "Pause SLA On Status" } ], - "modified": "2019-07-09 17:22:16.402939", + "links": [], + "modified": "2020-06-10 12:30:15.050785", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", 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 a399c58b168..c6923157064 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,11 +6,73 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import getdate +from frappe.utils import getdate, get_weekdays class ServiceLevelAgreement(Document): def validate(self): + self.validate_doc() + self.check_priorities() + self.check_support_and_resolution() + + def check_priorities(self): + default_priority = [] + priorities = [] + + for priority in self.priorities: + # Check if response and resolution time is set for every priority + if not (priority.response_time or priority.resolution_time): + frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx)) + + priorities.append(priority.priority) + + if priority.default_priority: + default_priority.append(priority.default_priority) + + response = priority.response_time + resolution = priority.resolution_time + + if response > resolution: + frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) + + # Check if repeated priority + if not len(set(priorities)) == len(priorities): + repeated_priority = get_repeated(priorities) + frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority)) + + # Check if repeated default priority + if not len(set(default_priority)) == len(default_priority): + frappe.throw(_("Select only one Priority as Default.")) + + # set default priority from priorities + try: + self.default_priority = next(d.priority for d in self.priorities if d.default_priority) + except Exception: + frappe.throw(_("Select a Default Priority.")) + + def check_support_and_resolution(self): + week = get_weekdays() + support_days = [] + + for support_and_resolution in self.support_and_resolution: + # Check if start and end time is set for every support day + if not (support_and_resolution.start_time or support_and_resolution.end_time): + frappe.throw(_("Set Start Time and End Time for \ + Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx))) + + support_days.append(support_and_resolution.workday) + support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 + + if support_and_resolution.start_time >= support_and_resolution.end_time: + frappe.throw(_("Start Time can't be greater than or equal to End Time \ + for {0}.".format(support_and_resolution.workday))) + + # Check for repeated workday + if not len(set(support_days)) == len(support_days): + repeated_days = get_repeated(support_days) + frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) + + def validate_doc(self): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): frappe.throw(_("Service Level Agreement tracking is not enabled.")) @@ -35,9 +97,7 @@ class ServiceLevelAgreement(Document): return frappe._dict({ "priority": priority.priority, "response_time": priority.response_time, - "response_time_period": priority.response_time_period, - "resolution_time": priority.resolution_time, - "resolution_time_period": priority.resolution_time_period + "resolution_time": priority.resolution_time }) def check_agreement_status(): @@ -110,4 +170,15 @@ def get_service_level_agreement_filters(name, customer=None): return { "priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])], "service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)] - } \ No newline at end of file + } + +def get_repeated(values): + unique_list = [] + diff = [] + for value in values: + if value not in unique_list: + unique_list.append(str(value)) + else: + if value not in diff: + diff.append(str(value)) + return " ".join(diff) 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 4a741ea4e18..07ef368cbe3 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 @@ -5,19 +5,20 @@ from __future__ import unicode_literals import frappe import unittest -from erpnext.support.doctype.service_level.test_service_level import create_service_level_for_sla +from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group +from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities class TestServiceLevelAgreement(unittest.TestCase): - - def test_service_level_agreement(self): + def setUp(self): + frappe.db.sql("delete from `tabService Level Agreement`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) - create_service_level_for_sla() - + def test_service_level_agreement(self): # Default Service Level Agreement create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, - service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=4, resolution_time=6) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type=None, entity=None, response_time=14400, resolution_time=21600) + get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1) self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name) @@ -28,8 +29,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Customer customer = create_customer() create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity=customer, response_time=2, resolution_time=3) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type="Customer", entity=customer, response_time=7200, resolution_time=10800) get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer) self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name) @@ -40,8 +41,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Customer Group customer_group = create_customer_group() create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800) get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group) self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name) @@ -52,8 +53,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Territory territory = create_territory() create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity=territory, response_time=2, resolution_time=3) + holiday_list="__Test Holiday List", employee_group="_Test Employee Group", + entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800) get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory) self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name) @@ -71,14 +72,19 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ service_level_agreement = frappe.get_doc("Service Level Agreement", filters) return service_level_agreement -def create_service_level_agreement(default_service_level_agreement, service_level, holiday_list, employee_group, +def create_service_level_agreement(default_service_level_agreement, holiday_list, employee_group, response_time, entity_type, entity, resolution_time): + employee_group = make_employee_group() + make_holiday_list() + make_priorities() + service_level_agreement = frappe.get_doc({ "doctype": "Service Level Agreement", "enable": 1, + "service_level": "__Test Service Level", "default_service_level_agreement": default_service_level_agreement, - "service_level": service_level, + "default_priority": "Medium", "holiday_list": holiday_list, "employee_group": employee_group, "entity_type": entity_type, @@ -109,6 +115,11 @@ def create_service_level_agreement(default_service_level_agreement, service_leve "resolution_time_period": "Hour", } ], + "pause_sla_on": [ + { + "status": "Replied" + } + ], "support_and_resolution": [ { "workday": "Monday", @@ -167,6 +178,7 @@ def create_service_level_agreement(default_service_level_agreement, service_leve else: return frappe.get_doc("Service Level Agreement", service_level_agreement_exists) + def create_customer(): customer = frappe.get_doc({ "doctype": "Customer", @@ -206,23 +218,41 @@ def create_territory(): return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}) def create_service_level_agreements_for_issues(): - create_service_level_for_sla() - - create_service_level_agreement(default_service_level_agreement=1, - service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=4, resolution_time=6) + create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=14400, resolution_time=21600) create_customer() - create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity="_Test Customer", response_time=2, resolution_time=3) + create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800) create_customer_group() - create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=2, resolution_time=3) + create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800) create_territory() - create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity="_Test SLA Territory", response_time=2, resolution_time=3) + create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + +def make_holiday_list(): + holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") + if not holiday_list: + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "holiday_list_name": "__Test Holiday List", + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "holidays": [ + { + "description": "Test Holiday 1", + "holiday_date": "2019-03-05" + }, + { + "description": "Test Holiday 2", + "holiday_date": "2019-03-07" + }, + { + "description": "Test Holiday 3", + "holiday_date": "2019-02-11" + }, + ] + }).insert() diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json index cd87a1c1131..65d51694cc3 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-04 05:54:03.658991", "doctype": "DocType", "editable_grid": 1, @@ -9,10 +10,8 @@ "default_priority", "sb_00", "response_time", - "response_time_period", "cb_00", - "resolution_time", - "resolution_time_period" + "resolution_time" ], "fields": [ { @@ -28,16 +27,11 @@ "fieldtype": "Section Break" }, { - "columns": 1, - "fieldname": "response_time", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Response Time" - }, - { - "columns": 1, + "columns": 2, "fieldname": "resolution_time", - "fieldtype": "Int", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Resolution Time" }, @@ -45,36 +39,31 @@ "fieldname": "cb_00", "fieldtype": "Column Break" }, - { - "columns": 2, - "fieldname": "response_time_period", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Response Time Period", - "options": "Hour\nDay\nWeek" - }, - { - "columns": 2, - "fieldname": "resolution_time_period", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Resolution Time Period", - "options": "Hour\nDay\nWeek" - }, { "fieldname": "cb_01", "fieldtype": "Column Break" }, { + "columns": 1, "default": "0", "fieldname": "default_priority", "fieldtype": "Check", "in_list_view": 1, "label": "Default Priority" + }, + { + "columns": 2, + "fieldname": "response_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "in_list_view": 1, + "label": "First Response Time" } ], "istable": 1, - "modified": "2019-05-21 06:54:42.674377", + "links": [], + "modified": "2020-06-10 12:45:47.545915", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", @@ -84,4 +73,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/support/doctype/support_settings/support_settings.js b/erpnext/support/doctype/support_settings/support_settings.js index 1d1069d58bb..78adca81ca5 100644 --- a/erpnext/support/doctype/support_settings/support_settings.js +++ b/erpnext/support/doctype/support_settings/support_settings.js @@ -3,6 +3,6 @@ frappe.ui.form.on('Support Settings', { refresh: function(frm) { - + // } }); diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json index be9e0645915..1c1b0c3517e 100644 --- a/erpnext/support/doctype/support_settings/support_settings.json +++ b/erpnext/support/doctype/support_settings/support_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2017-02-17 13:07:35.686409", "doctype": "DocType", "editable_grid": 1, @@ -122,13 +123,15 @@ }, { "default": "0", + "depends_on": "eval:doc.track_service_level_agreement;", "fieldname": "allow_resetting_service_level_agreement", "fieldtype": "Check", "label": "Allow Resetting Service Level Agreement" } ], "issingle": 1, - "modified": "2019-07-10 22:52:39.663873", + "links": [], + "modified": "2020-06-05 17:56:17.491684", "modified_by": "Administrator", "module": "Support", "name": "Support Settings", diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index 5fd901169f0..163c955c566 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -193,14 +193,17 @@ class ItemConfigure { filtered_items_count === 1 ? filtered_items[0] : ''; + // Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock. + const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock; + const add_to_cart = `${__('Add to cart')}`; + const product_action = in_stock ? add_to_cart : `${__('Not in Stock')}`; + const item_add_to_cart = one_item ? ` `: '';