Merge branch 'develop' into website_theme_scss

This commit is contained in:
Raffael Meyer 2020-09-28 10:15:55 +02:00 committed by GitHub
commit 21266fe4a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 373 additions and 253 deletions

View File

@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', {
setup: function(frm) {
frm.set_query("paid_from", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return {
filters: {
"account_type": ["in", account_types],
@ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', {
}
}
});
frm.set_query("party_type", function() {
frm.events.validate_company(frm);
return{
filters: {
"name": ["in", Object.keys(frappe.boot.party_account_types)],
}
}
});
frm.set_query("party_bank_account", function() {
return {
filters: {
@ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', {
}
}
});
frm.set_query("bank_account", function() {
return {
filters: {
@ -47,6 +52,7 @@ frappe.ui.form.on('Payment Entry', {
}
}
});
frm.set_query("contact_person", function() {
if (frm.doc.party) {
return {
@ -58,10 +64,12 @@ frappe.ui.form.on('Payment Entry', {
};
}
});
frm.set_query("paid_to", function() {
frm.events.validate_company(frm);
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
return {
filters: {
"account_type": ["in", account_types],
@ -150,6 +158,12 @@ frappe.ui.form.on('Payment Entry', {
frm.events.show_general_ledger(frm);
},
validate_company: (frm) => {
if (!frm.doc.company){
frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")});
}
},
company: function(frm) {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);

View File

@ -320,7 +320,6 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
entry['remarks'] = "On cancellation of " + entry['voucher_no']
entry['is_cancelled'] = 1
entry['posting_date'] = today()
if entry['debit'] or entry['credit']:
make_entry(entry, adv_adj, "Yes")

View File

@ -268,9 +268,9 @@ class GrossProfitGenerator(object):
def get_last_purchase_rate(self, item_code, row):
condition = ''
if row.project:
condition += " AND a.project='%s'" % (row.project)
condition += " AND a.project=%s" % (frappe.db.escape(row.project))
elif row.cost_center:
condition += " AND a.cost_center='%s'" % (row.cost_center)
condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
if self.filters.to_date:
condition += " AND modified='%s'" % (self.filters.to_date)

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext, math, json
from frappe import _
from six import string_types
from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day
from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day, get_datetime
from frappe.model.document import Document
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset.depreciation \
@ -140,6 +140,10 @@ class Asset(AccountsController):
def make_asset_movement(self):
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
reference_docname = self.purchase_receipt or self.purchase_invoice
transaction_date = getdate(self.purchase_date)
if reference_docname:
posting_date, posting_time = frappe.db.get_value(reference_doctype, reference_docname, ["posting_date", "posting_time"])
transaction_date = get_datetime("{} {}".format(posting_date, posting_time))
assets = [{
'asset': self.name,
'asset_name': self.asset_name,
@ -151,7 +155,7 @@ class Asset(AccountsController):
'assets': assets,
'purpose': 'Receipt',
'company': self.company,
'transaction_date': getdate(self.purchase_date),
'transaction_date': transaction_date,
'reference_doctype': reference_doctype,
'reference_name': reference_docname
}).insert()

View File

@ -178,6 +178,7 @@ def make_all_scorecards(docname):
period_card = make_supplier_scorecard(docname, None)
period_card.start_date = start_date
period_card.end_date = end_date
period_card.insert(ignore_permissions=True)
period_card.submit()
scp_count = scp_count + 1
if start_date < first_start_date:

View File

@ -106,7 +106,7 @@ def make_supplier_scorecard(source_name, target_doc=None):
"doctype": "Supplier Scorecard Scoring Criteria",
"postprocess": update_criteria_fields,
}
}, target_doc, post_process)
}, target_doc, post_process, ignore_permissions=True)
return doc

View File

@ -8,7 +8,7 @@
{
"hidden": 0,
"label": "Reports",
"links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Opportunity\",\n \"name\": \"Minutes to First Response for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Opportunity\",\n \"name\": \"First Response Time for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
@ -42,7 +42,7 @@
"idx": 0,
"is_standard": 1,
"label": "CRM",
"modified": "2020-05-28 13:33:52.906750",
"modified": "2020-08-11 18:55:18.238900",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",

View File

@ -27,9 +27,6 @@ class Lead(SellingController):
def after_insert(self):
self.update_links()
# after the address and contact are created, flush the field values
# to avoid inconsistent reporting in case the documents are changed
self.flush_address_and_contact_fields()
def validate(self):
self.set_lead_name()
@ -210,14 +207,6 @@ class Lead(SellingController):
})
self.contact_doc.save()
def flush_address_and_contact_fields(self):
fields = ['address_type', 'address_line1', 'address_line2', 'address_title',
'city', 'county', 'country', 'fax', 'pincode', 'state']
for field in fields:
self.set(field, None)
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
return _make_customer(source_name, target_doc)

View File

@ -24,7 +24,7 @@
"converted_by",
"sales_stage",
"order_lost_reason",
"mins_to_first_response",
"first_response_time",
"expected_closing",
"next_contact",
"contact_by",
@ -152,13 +152,6 @@
"no_copy": 1,
"read_only": 1
},
{
"bold": 1,
"fieldname": "mins_to_first_response",
"fieldtype": "Float",
"label": "Mins to first response",
"read_only": 1
},
{
"fieldname": "expected_closing",
"fieldtype": "Date",
@ -419,12 +412,19 @@
"fieldtype": "Link",
"label": "Converted By",
"options": "User"
},
{
"bold": 1,
"fieldname": "first_response_time",
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
"modified": "2020-08-11 17:34:35.066961",
"modified": "2020-08-12 17:34:35.066961",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",

View File

@ -1,33 +1,44 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Minutes to First Response for Opportunity"] = {
frappe.query_reports["First Response Time for Opportunity"] = {
"filters": [
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
'reqd': 1,
"reqd": 1,
"default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
'reqd': 1,
"reqd": 1,
"default": frappe.datetime.nowdate()
},
],
get_chart_data: function (columns, result) {
get_chart_data: function (_columns, result) {
return {
data: {
labels: result.map(d => d[0]),
datasets: [{
name: 'Mins to first response',
name: "First Response Time",
values: result.map(d => d[1])
}]
},
type: 'line',
type: "line",
tooltipOptions: {
formatTooltipY: d => {
let duration_options = {
hide_days: 0,
hide_seconds: 0
};
value = frappe.utils.get_formatted_duration(d, duration_options);
return value;
}
}
}
}
}
};

View File

@ -0,0 +1,28 @@
{
"add_total_row": 0,
"creation": "2020-08-10 18:34:19.083872",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"letter_head": "Test 2",
"modified": "2020-08-10 18:34:19.083872",
"modified_by": "Administrator",
"module": "CRM",
"name": "First Response Time for Opportunity",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Opportunity",
"report_name": "First Response Time for Opportunity",
"report_type": "Script Report",
"roles": [
{
"role": "Sales User"
},
{
"role": "Sales Manager"
}
]
}

View File

@ -0,0 +1,35 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
def execute(filters=None):
columns = [
{
'fieldname': 'creation_date',
'label': 'Date',
'fieldtype': 'Date',
'width': 300
},
{
'fieldname': 'first_response_time',
'fieldtype': 'Duration',
'label': 'First Response Time',
'width': 300
},
]
data = frappe.db.sql('''
SELECT
date(creation) as creation_date,
avg(first_response_time) as avg_response_time
FROM tabOpportunity
WHERE
date(creation) between %s and %s
and first_response_time > 0
GROUP BY creation_date
ORDER BY creation_date desc
''', (filters.from_date, filters.to_date))
return columns, data

View File

@ -1,26 +0,0 @@
{
"add_total_row": 0,
"apply_user_permissions": 0,
"creation": "2016-06-17 11:28:25.867258",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:06:08.801109",
"modified_by": "Administrator",
"module": "CRM",
"name": "Minutes to First Response for Opportunity",
"owner": "Administrator",
"ref_doctype": "Opportunity",
"report_name": "Minutes to First Response for Opportunity",
"report_type": "Script Report",
"roles": [
{
"role": "Sales User"
},
{
"role": "Sales Manager"
}
]
}

View File

@ -1,28 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
def execute(filters=None):
columns = [
{
'fieldname': 'creation_date',
'label': 'Date',
'fieldtype': 'Date'
},
{
'fieldname': 'mins',
'fieldtype': 'Float',
'label': 'Mins to First Response'
},
]
data = frappe.db.sql('''select date(creation) as creation_date,
avg(mins_to_first_response) as mins
from tabOpportunity
where date(creation) between %s and %s
and mins_to_first_response > 0
group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date))
return columns, data

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from frappe.utils import getdate, get_time
from frappe.utils import getdate, get_time, flt
from frappe.model.mapper import get_mapped_doc
from frappe import _
import datetime
@ -45,7 +45,7 @@ class PatientAppointment(Document):
def validate_overlaps(self):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \
+ datetime.timedelta(minutes=float(self.duration))
+ datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql("""
select

View File

@ -726,5 +726,6 @@ erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.drop_razorpay_payload_column
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports
erpnext.patches.v13_0.rename_issue_doctype_fields
erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id

View File

@ -0,0 +1,65 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.exists('DocType', 'Issue'):
issues = frappe.db.get_all('Issue', fields=['name', 'response_by_variance', 'resolution_by_variance', 'mins_to_first_response'],
order_by='creation desc')
frappe.reload_doc('support', 'doctype', 'issue')
# rename fields
rename_map = {
'agreement_fulfilled': 'agreement_status',
'mins_to_first_response': 'first_response_time'
}
for old, new in rename_map.items():
rename_field('Issue', old, new)
# change fieldtype to duration
count = 0
for entry in issues:
response_by_variance = convert_to_seconds(entry.response_by_variance, 'Hours')
resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, 'Hours')
mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes')
frappe.db.set_value('Issue', entry.name, {
'response_by_variance': response_by_variance,
'resolution_by_variance': resolution_by_variance,
'first_response_time': mins_to_first_response
})
# commit after every 100 updates
count += 1
if count%100 == 0:
frappe.db.commit()
if frappe.db.exists('DocType', 'Opportunity'):
opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
frappe.reload_doc('crm', 'doctype', 'opportunity')
rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
# change fieldtype to duration
count = 0
for entry in opportunities:
mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes')
frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response)
# commit after every 100 updates
count += 1
if count%100 == 0:
frappe.db.commit()
# renamed reports from "Minutes to First Response for Issues" to "First Response Time for Issues". Same for Opportunity
for report in ['Minutes to First Response for Issues', 'Minutes to First Response for Opportunity']:
if frappe.db.exists('Report', report):
frappe.delete_doc('Report', report)
def convert_to_seconds(value, unit):
seconds = 0
if unit == 'Hours':
seconds = value * 3600
if unit == 'Minutes':
seconds = value * 60
return seconds

View File

@ -120,7 +120,7 @@ frappe.ui.form.on('Salary Structure', {
var get_payment_mode_account = function(frm, mode_of_payment, callback) {
if(!frm.doc.company) {
frappe.throw(__("Please select the Company first"));
frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")});
}
if(!mode_of_payment) {

View File

@ -26,19 +26,19 @@ frappe.ui.form.on("Item", {
refresh: function(frm) {
if (frm.doc.is_stock_item) {
frm.add_custom_button(__("Balance"), function() {
frm.add_custom_button(__("Stock Balance"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
frappe.set_route("query-report", "Stock Balance");
}, __("View"));
frm.add_custom_button(__("Ledger"), function() {
frm.add_custom_button(__("Stock Ledger"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}
frappe.set_route("query-report", "Stock Ledger");
}, __("View"));
frm.add_custom_button(__("Projected"), function() {
frm.add_custom_button(__("Stock Projected Qty"), function() {
frappe.route_options = {
"item_code": frm.doc.name
}

View File

@ -156,6 +156,7 @@ frappe.ui.form.on('Stock Entry', {
mr_item.item_code = item.item_code;
mr_item.item_name = item.item_name;
mr_item.uom = item.uom;
mr_item.stock_uom = item.stock_uom;
mr_item.conversion_factor = item.conversion_factor;
mr_item.item_group = item.item_group;
mr_item.description = item.description;

View File

@ -28,7 +28,7 @@
{
"hidden": 0,
"label": "Reports",
"links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Issues\",\n \"name\": \"Minutes to First Response for Issues\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@ -43,7 +43,7 @@
"idx": 0,
"is_standard": 1,
"label": "Support",
"modified": "2020-06-04 11:54:56.124219",
"modified": "2020-08-11 15:49:34.307341",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",

View File

@ -2,10 +2,14 @@ frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
frappe.db.get_value("Support Settings", {name: "Support Settings"}, "allow_resetting_service_level_agreement", (r) => {
if (!r.allow_resetting_service_level_agreement) {
frm.set_df_property("reset_service_level_agreement", "hidden", 1) ;
}
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
if (r && r.track_service_level_agreement == "0") {
frm.set_df_property("service_level_section", "hidden", 1);
}
if (r && r.allow_resetting_service_level_agreement == "0") {
frm.set_df_property("reset_service_level_agreement", "hidden", 1);
}
});
if (frm.doc.service_level_agreement) {
@ -38,7 +42,7 @@ frappe.ui.form.on("Issue", {
},
refresh: function (frm) {
if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") {
if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
if (frm.doc.service_level_agreement) {
frappe.call({
'method': 'frappe.client.get',
@ -85,14 +89,14 @@ frappe.ui.form.on("Issue", {
if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
let agreement_fulfilled = (frm.doc.agreement_fulfilled == "Fulfilled") ?
let agreement_status = (frm.doc.agreement_status == "Fulfilled") ?
{"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} :
{"indicator": "red", "msg": "Service Level Agreement Failed"};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ agreement_fulfilled.indicator +'"><span class="hidden-xs">'+ agreement_fulfilled.msg +'</span></span> ' +
'<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
'</div>' +
'</div>'
);
@ -198,13 +202,13 @@ function set_time_to_resolve_and_response(frm) {
frm.dashboard.clear_headline();
var time_to_respond = get_status(frm.doc.response_by_variance);
if (!frm.doc.first_responded_on && frm.doc.agreement_fulfilled === "Ongoing") {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled);
if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
}
var time_to_resolve = get_status(frm.doc.resolution_by_variance);
if (!frm.doc.resolution_date && frm.doc.agreement_fulfilled === "Ongoing") {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled);
if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
}
frm.dashboard.set_headline_alert(
@ -219,10 +223,10 @@ function set_time_to_resolve_and_response(frm) {
);
}
function get_time_left(timestamp, agreement_fulfilled) {
function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
let indicator = (diff_display == 'Failed' && agreement_fulfilled != "Fulfilled") ? "red" : "green";
let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green";
return {"diff_display": diff_display, "indicator": indicator};
}

View File

@ -27,17 +27,25 @@
"response_by_variance",
"reset_service_level_agreement",
"cb",
"agreement_fulfilled",
"agreement_status",
"resolution_by",
"resolution_by_variance",
"service_level_agreement_creation",
"on_hold_since",
"total_hold_time",
"response",
"mins_to_first_response",
"first_response_time",
"first_responded_on",
"column_break_26",
"avg_response_time",
"section_break_19",
"resolution_details",
"column_break1",
"opening_date",
"opening_time",
"resolution_date",
"resolution_time",
"user_resolution_time",
"additional_info",
"lead",
"contact",
@ -46,23 +54,14 @@
"customer_name",
"project",
"company",
"section_break_19",
"resolution_details",
"column_break1",
"opening_date",
"opening_time",
"resolution_date",
"content_type",
"attachment",
"via_customer_portal",
"resolution_time",
"user_resolution_time"
"attachment",
"content_type"
],
"fields": [
{
"fieldname": "subject_section",
"fieldtype": "Section Break",
"label": "Subject",
"options": "fa fa-flag"
},
{
@ -158,7 +157,7 @@
"collapsible": 1,
"fieldname": "service_level_section",
"fieldtype": "Section Break",
"label": "Service Level"
"label": "Service Level Agreement Details"
},
{
"fieldname": "service_level_agreement",
@ -191,14 +190,7 @@
"collapsible": 1,
"fieldname": "response",
"fieldtype": "Section Break",
"label": "Response"
},
{
"bold": 1,
"fieldname": "mins_to_first_response",
"fieldtype": "Float",
"label": "Mins to First Response",
"read_only": 1
"label": "Response Details"
},
{
"fieldname": "first_responded_on",
@ -261,7 +253,7 @@
"collapsible": 1,
"fieldname": "section_break_19",
"fieldtype": "Section Break",
"label": "Resolution"
"label": "Resolution Details"
},
{
"depends_on": "eval:!doc.__islocal",
@ -326,28 +318,19 @@
"fieldtype": "Check",
"label": "Via Customer Portal"
},
{
"default": "Ongoing",
"depends_on": "eval: doc.service_level_agreement",
"fieldname": "agreement_fulfilled",
"fieldtype": "Select",
"label": "Service Level Agreement Fulfilled",
"options": "Ongoing\nFulfilled\nFailed",
"read_only": 1
},
{
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"description": "in hours",
"fieldname": "response_by_variance",
"fieldtype": "Float",
"fieldtype": "Duration",
"hide_seconds": 1,
"label": "Response By Variance",
"read_only": 1
},
{
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
"description": "in hours",
"fieldname": "resolution_by_variance",
"fieldtype": "Float",
"fieldtype": "Duration",
"hide_seconds": 1,
"label": "Resolution By Variance",
"read_only": 1
},
@ -406,12 +389,28 @@
"fieldtype": "Duration",
"label": "Total Hold Time",
"read_only": 1
},
{
"default": "Ongoing",
"depends_on": "eval: doc.service_level_agreement",
"fieldname": "agreement_status",
"fieldtype": "Select",
"label": "Service Level Agreement Status",
"options": "Ongoing\nFulfilled\nFailed",
"read_only": 1
},
{
"bold": 1,
"fieldname": "first_response_time",
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
}
],
"icon": "fa fa-ticket",
"idx": 7,
"links": [],
"modified": "2020-06-10 12:47:37.146914",
"modified": "2020-08-11 18:49:07.574769",
"modified_by": "Administrator",
"module": "Support",
"name": "Issue",

View File

@ -61,7 +61,7 @@ class Issue(Document):
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":
if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing":
set_service_level_agreement_variance(issue=self.name)
self.update_agreement_status()
set_resolution_time(issue=self)
@ -72,7 +72,7 @@ class Issue(Document):
self.resolution_date = None
self.reset_issue_metrics()
# enable SLA and variance on Reopen
self.agreement_fulfilled = "Ongoing"
self.agreement_status = "Ongoing"
set_service_level_agreement_variance(issue=self.name)
self.handle_hold_time(status)
@ -113,39 +113,39 @@ class Issue(Document):
if not self.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_hours(response_by, now_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 // 3600)
update_values['response_by_variance'] = response_by_variance + last_hold_time
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_hours(resolution_by, now_time))
resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time))
update_values['resolution_by'] = resolution_by
update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600)
update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time
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":
if self.service_level_agreement and self.agreement_status == "Ongoing":
if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \
frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0:
self.agreement_fulfilled = "Failed"
self.agreement_status = "Failed"
else:
self.agreement_fulfilled = "Fulfilled"
self.agreement_status = "Fulfilled"
def update_agreement_fulfilled_on_custom_status(self):
def update_agreement_status_on_custom_status(self):
"""
Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status
"""
if not self.first_responded_on: # first_responded_on set when first reply is sent to customer
self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()), 2)
self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2)
if not self.resolution_date: # resolution_date set when issue has been closed
self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()), 2)
self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2)
self.agreement_fulfilled = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed"
def create_communication(self):
communication = frappe.new_doc("Communication")
@ -172,7 +172,7 @@ class Issue(Document):
replicated_issue = deepcopy(self)
replicated_issue.subject = subject
replicated_issue.issue_split_from = self.name
replicated_issue.mins_to_first_response = 0
replicated_issue.first_response_time = 0
replicated_issue.first_responded_on = None
replicated_issue.creation = now_datetime()
@ -180,7 +180,7 @@ 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_fulfilled = "Ongoing"
replicated_issue.agreement_status = "Ongoing"
replicated_issue.response_by = None
replicated_issue.response_by_variance = None
replicated_issue.resolution_by = None
@ -241,8 +241,8 @@ class Issue(Document):
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()))
self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()))
self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()))
def change_service_level_agreement_and_priority(self):
if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \
@ -271,7 +271,7 @@ class Issue(Document):
self.service_level_agreement_creation = now_datetime()
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
self.agreement_fulfilled = "Ongoing"
self.agreement_status = "Ongoing"
self.save()
def reset_issue_metrics(self):
@ -347,7 +347,7 @@ def get_expected_time_for(parameter, service_level, start_date_time):
def set_service_level_agreement_variance(issue=None):
current_time = frappe.flags.current_time or now_datetime()
filters = {"status": "Open", "agreement_fulfilled": "Ongoing"}
filters = {"status": "Open", "agreement_status": "Ongoing"}
if issue:
filters = {"name": issue}
@ -358,13 +358,13 @@ def set_service_level_agreement_variance(issue=None):
variance = round(time_diff_in_hours(doc.response_by, current_time), 2)
frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False)
if variance < 0:
frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
if not doc.resolution_date: # resolution_date set when issue has been closed
variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2)
frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False)
if variance < 0:
frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False)
frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False)
def set_resolution_time(issue):

View File

@ -73,7 +73,7 @@ class TestIssue(unittest.TestCase):
issue.status = 'Closed'
issue.save()
self.assertEqual(issue.agreement_fulfilled, 'Fulfilled')
self.assertEqual(issue.agreement_status, 'Fulfilled')
def test_issue_metrics(self):
creation = datetime.datetime(2020, 3, 4, 4, 0)

View File

@ -0,0 +1,44 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["First Response Time for Issues"] = {
"filters": [
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default":frappe.datetime.nowdate()
}
],
get_chart_data: function(_columns, result) {
return {
data: {
labels: result.map(d => d[0]),
datasets: [{
name: 'First Response Time',
values: result.map(d => d[1])
}]
},
type: "line",
tooltipOptions: {
formatTooltipY: d => {
let duration_options = {
hide_days: 0,
hide_seconds: 0
};
value = frappe.utils.get_formatted_duration(d, duration_options);
return value;
}
}
}
}
};

View File

@ -0,0 +1,26 @@
{
"add_total_row": 0,
"creation": "2020-08-10 18:12:42.391224",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"letter_head": "Test 2",
"modified": "2020-08-10 18:12:42.391224",
"modified_by": "Administrator",
"module": "Support",
"name": "First Response Time for Issues",
"owner": "Administrator",
"prepared_report": 0,
"query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;",
"ref_doctype": "Issue",
"report_name": "First Response Time for Issues",
"report_type": "Script Report",
"roles": [
{
"role": "Support Team"
}
]
}

View File

@ -0,0 +1,35 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
def execute(filters=None):
columns = [
{
'fieldname': 'creation_date',
'label': 'Date',
'fieldtype': 'Date',
'width': 300
},
{
'fieldname': 'first_response_time',
'fieldtype': 'Duration',
'label': 'First Response Time',
'width': 300
},
]
data = frappe.db.sql('''
SELECT
date(creation) as creation_date,
avg(first_response_time) as avg_response_time
FROM tabIssue
WHERE
date(creation) between %s and %s
and first_response_time > 0
GROUP BY creation_date
ORDER BY creation_date desc
''', (filters.from_date, filters.to_date))
return columns, data

View File

@ -1,30 +0,0 @@
frappe.query_reports["Minutes to First Response for Issues"] = {
"filters": [
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
'reqd': 1,
"default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
'reqd': 1,
"default":frappe.datetime.nowdate()
},
],
get_chart_data: function(columns, result) {
return {
data: {
labels: result.map(d => d[0]),
datasets: [{
name: 'Mins to first response',
values: result.map(d => d[1])
}]
},
type: 'line',
}
}
}

View File

@ -1,24 +0,0 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-06-14 17:44:26.034112",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:06:18.391100",
"modified_by": "Administrator",
"module": "Support",
"name": "Minutes to First Response for Issues",
"owner": "Administrator",
"query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;",
"ref_doctype": "Issue",
"report_name": "Minutes to First Response for Issues",
"report_type": "Script Report",
"roles": [
{
"role": "Support Team"
}
]
}

View File

@ -1,28 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
def execute(filters=None):
columns = [
{
'fieldname': 'creation_date',
'label': 'Date',
'fieldtype': 'Date'
},
{
'fieldname': 'mins',
'fieldtype': 'Float',
'label': 'Mins to First Response'
},
]
data = frappe.db.sql('''select date(creation) as creation_date,
avg(mins_to_first_response) as mins
from tabIssue
where date(creation) between %s and %s
and mins_to_first_response > 0
group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date))
return columns, data