mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 06:29:20 +00:00
Merge branch 'develop' into develop
This commit is contained in:
@@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
|
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.set_query("paid_from", function() {
|
frm.set_query("paid_from", function() {
|
||||||
|
frm.events.validate_company(frm);
|
||||||
|
|
||||||
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
|
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
|
||||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"account_type": ["in", account_types],
|
"account_type": ["in", account_types],
|
||||||
@@ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("party_type", function() {
|
frm.set_query("party_type", function() {
|
||||||
|
frm.events.validate_company(frm);
|
||||||
return{
|
return{
|
||||||
filters: {
|
filters: {
|
||||||
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("party_bank_account", function() {
|
frm.set_query("party_bank_account", function() {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("bank_account", function() {
|
frm.set_query("bank_account", function() {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -47,6 +52,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("contact_person", function() {
|
frm.set_query("contact_person", function() {
|
||||||
if (frm.doc.party) {
|
if (frm.doc.party) {
|
||||||
return {
|
return {
|
||||||
@@ -58,10 +64,12 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("paid_to", function() {
|
frm.set_query("paid_to", function() {
|
||||||
|
frm.events.validate_company(frm);
|
||||||
|
|
||||||
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
|
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
|
||||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"account_type": ["in", account_types],
|
"account_type": ["in", account_types],
|
||||||
@@ -150,6 +158,12 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
frm.events.show_general_ledger(frm);
|
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) {
|
company: function(frm) {
|
||||||
frm.events.hide_unhide_fields(frm);
|
frm.events.hide_unhide_fields(frm);
|
||||||
frm.events.set_dynamic_labels(frm);
|
frm.events.set_dynamic_labels(frm);
|
||||||
|
|||||||
@@ -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['remarks'] = "On cancellation of " + entry['voucher_no']
|
||||||
entry['is_cancelled'] = 1
|
entry['is_cancelled'] = 1
|
||||||
entry['posting_date'] = today()
|
|
||||||
|
|
||||||
if entry['debit'] or entry['credit']:
|
if entry['debit'] or entry['credit']:
|
||||||
make_entry(entry, adv_adj, "Yes")
|
make_entry(entry, adv_adj, "Yes")
|
||||||
|
|||||||
@@ -268,9 +268,9 @@ class GrossProfitGenerator(object):
|
|||||||
def get_last_purchase_rate(self, item_code, row):
|
def get_last_purchase_rate(self, item_code, row):
|
||||||
condition = ''
|
condition = ''
|
||||||
if row.project:
|
if row.project:
|
||||||
condition += " AND a.project='%s'" % (row.project)
|
condition += " AND a.project=%s" % (frappe.db.escape(row.project))
|
||||||
elif row.cost_center:
|
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:
|
if self.filters.to_date:
|
||||||
condition += " AND modified='%s'" % (self.filters.to_date)
|
condition += " AND modified='%s'" % (self.filters.to_date)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, erpnext, math, json
|
import frappe, erpnext, math, json
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from six import string_types
|
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 frappe.model.document import Document
|
||||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||||
from erpnext.assets.doctype.asset.depreciation \
|
from erpnext.assets.doctype.asset.depreciation \
|
||||||
@@ -140,6 +140,10 @@ class Asset(AccountsController):
|
|||||||
def make_asset_movement(self):
|
def make_asset_movement(self):
|
||||||
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
|
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
|
||||||
reference_docname = self.purchase_receipt or self.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 = [{
|
assets = [{
|
||||||
'asset': self.name,
|
'asset': self.name,
|
||||||
'asset_name': self.asset_name,
|
'asset_name': self.asset_name,
|
||||||
@@ -151,7 +155,7 @@ class Asset(AccountsController):
|
|||||||
'assets': assets,
|
'assets': assets,
|
||||||
'purpose': 'Receipt',
|
'purpose': 'Receipt',
|
||||||
'company': self.company,
|
'company': self.company,
|
||||||
'transaction_date': getdate(self.purchase_date),
|
'transaction_date': transaction_date,
|
||||||
'reference_doctype': reference_doctype,
|
'reference_doctype': reference_doctype,
|
||||||
'reference_name': reference_docname
|
'reference_name': reference_docname
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ def make_all_scorecards(docname):
|
|||||||
period_card = make_supplier_scorecard(docname, None)
|
period_card = make_supplier_scorecard(docname, None)
|
||||||
period_card.start_date = start_date
|
period_card.start_date = start_date
|
||||||
period_card.end_date = end_date
|
period_card.end_date = end_date
|
||||||
|
period_card.insert(ignore_permissions=True)
|
||||||
period_card.submit()
|
period_card.submit()
|
||||||
scp_count = scp_count + 1
|
scp_count = scp_count + 1
|
||||||
if start_date < first_start_date:
|
if start_date < first_start_date:
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ def make_supplier_scorecard(source_name, target_doc=None):
|
|||||||
"doctype": "Supplier Scorecard Scoring Criteria",
|
"doctype": "Supplier Scorecard Scoring Criteria",
|
||||||
"postprocess": update_criteria_fields,
|
"postprocess": update_criteria_fields,
|
||||||
}
|
}
|
||||||
}, target_doc, post_process)
|
}, target_doc, post_process, ignore_permissions=True)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|||||||
@@ -1264,7 +1264,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
transitions.append(transition.as_dict())
|
transitions.append(transition.as_dict())
|
||||||
|
|
||||||
if not transitions:
|
if not transitions:
|
||||||
frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions"))
|
frappe.throw(
|
||||||
|
_("You are not allowed to update as per the conditions set in {} Workflow.").format(get_link_to_form("Workflow", workflow)),
|
||||||
|
title=_("Insufficient Permissions")
|
||||||
|
)
|
||||||
|
|
||||||
def get_new_child_item(item_row):
|
def get_new_child_item(item_row):
|
||||||
new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
|
new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
|
||||||
|
|||||||
@@ -165,9 +165,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
AND company = %(company)s
|
AND company = %(company)s
|
||||||
AND account_currency = %(currency)s
|
AND account_currency = %(currency)s
|
||||||
AND `{searchfield}` LIKE %(txt)s
|
AND `{searchfield}` LIKE %(txt)s
|
||||||
|
{mcond}
|
||||||
ORDER BY idx DESC, name
|
ORDER BY idx DESC, name
|
||||||
LIMIT %(offset)s, %(limit)s
|
LIMIT %(offset)s, %(limit)s
|
||||||
""".format(account_type_condition=account_type_condition, searchfield=searchfield),
|
""".format(
|
||||||
|
account_type_condition=account_type_condition,
|
||||||
|
searchfield=searchfield,
|
||||||
|
mcond=get_match_cond(doctype)
|
||||||
|
),
|
||||||
dict(
|
dict(
|
||||||
account_types=filters.get("account_type"),
|
account_types=filters.get("account_type"),
|
||||||
company=filters.get("company"),
|
company=filters.get("company"),
|
||||||
@@ -359,9 +364,21 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if filters.get("is_return"):
|
if filters.get("is_return"):
|
||||||
having_clause = ""
|
having_clause = ""
|
||||||
|
|
||||||
|
meta = frappe.get_meta("Batch", cached=True)
|
||||||
|
searchfields = meta.get_search_fields()
|
||||||
|
|
||||||
|
search_columns = ''
|
||||||
|
if searchfields:
|
||||||
|
search_columns = ", " + ", ".join(searchfields)
|
||||||
|
|
||||||
if args.get('warehouse'):
|
if args.get('warehouse'):
|
||||||
|
searchfields = ['batch.' + field for field in searchfields]
|
||||||
|
if searchfields:
|
||||||
|
search_columns = ", " + ", ".join(searchfields)
|
||||||
|
|
||||||
batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom,
|
batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom,
|
||||||
concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date)
|
concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date)
|
||||||
|
{search_columns}
|
||||||
from `tabStock Ledger Entry` sle
|
from `tabStock Ledger Entry` sle
|
||||||
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
|
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
|
||||||
where
|
where
|
||||||
@@ -377,6 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
group by batch_no {having_clause}
|
group by batch_no {having_clause}
|
||||||
order by batch.expiry_date, sle.batch_no desc
|
order by batch.expiry_date, sle.batch_no desc
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(start)s, %(page_len)s""".format(
|
||||||
|
search_columns = search_columns,
|
||||||
cond=cond,
|
cond=cond,
|
||||||
match_conditions=get_match_cond(doctype),
|
match_conditions=get_match_cond(doctype),
|
||||||
having_clause = having_clause
|
having_clause = having_clause
|
||||||
@@ -384,7 +402,9 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
|
|
||||||
return batch_nos
|
return batch_nos
|
||||||
else:
|
else:
|
||||||
return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) from `tabBatch` batch
|
return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date)
|
||||||
|
{search_columns}
|
||||||
|
from `tabBatch` batch
|
||||||
where batch.disabled = 0
|
where batch.disabled = 0
|
||||||
and item = %(item_code)s
|
and item = %(item_code)s
|
||||||
and (name like %(txt)s
|
and (name like %(txt)s
|
||||||
@@ -394,7 +414,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
{0}
|
{0}
|
||||||
{match_conditions}
|
{match_conditions}
|
||||||
order by expiry_date, name desc
|
order by expiry_date, name desc
|
||||||
limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args)
|
limit %(start)s, %(page_len)s""".format(cond, search_columns = search_columns, match_conditions=get_match_cond(doctype)), args)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"label": "Reports",
|
"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,
|
"hidden": 0,
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "CRM",
|
"label": "CRM",
|
||||||
"modified": "2020-05-28 13:33:52.906750",
|
"modified": "2020-08-11 18:55:18.238900",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM",
|
"name": "CRM",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"converted_by",
|
"converted_by",
|
||||||
"sales_stage",
|
"sales_stage",
|
||||||
"order_lost_reason",
|
"order_lost_reason",
|
||||||
"mins_to_first_response",
|
"first_response_time",
|
||||||
"expected_closing",
|
"expected_closing",
|
||||||
"next_contact",
|
"next_contact",
|
||||||
"contact_by",
|
"contact_by",
|
||||||
@@ -152,13 +152,6 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"bold": 1,
|
|
||||||
"fieldname": "mins_to_first_response",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"label": "Mins to first response",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "expected_closing",
|
"fieldname": "expected_closing",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
@@ -419,12 +412,19 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Converted By",
|
"label": "Converted By",
|
||||||
"options": "User"
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"fieldname": "first_response_time",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"label": "First Response Time",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-info-sign",
|
"icon": "fa fa-info-sign",
|
||||||
"idx": 195,
|
"idx": 195,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-11 17:34:35.066961",
|
"modified": "2020-08-12 17:34:35.066961",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity",
|
"name": "Opportunity",
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// 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": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname": "from_date",
|
"fieldname": "from_date",
|
||||||
"label": __("From Date"),
|
"label": __("From Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
'reqd': 1,
|
"reqd": 1,
|
||||||
"default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
|
"default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "to_date",
|
"fieldname": "to_date",
|
||||||
"label": __("To Date"),
|
"label": __("To Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
'reqd': 1,
|
"reqd": 1,
|
||||||
"default": frappe.datetime.nowdate()
|
"default": frappe.datetime.nowdate()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
get_chart_data: function (columns, result) {
|
get_chart_data: function (_columns, result) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
labels: result.map(d => d[0]),
|
labels: result.map(d => d[0]),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
name: 'Mins to first response',
|
name: "First Response Time",
|
||||||
values: result.map(d => d[1])
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -282,6 +282,11 @@ auto_cancel_exempted_doctypes= [
|
|||||||
]
|
]
|
||||||
|
|
||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
|
"cron": {
|
||||||
|
"0/30 * * * *": [
|
||||||
|
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||||
|
]
|
||||||
|
},
|
||||||
"all": [
|
"all": [
|
||||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||||
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
|
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
|
||||||
|
|||||||
@@ -690,6 +690,7 @@ erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation
|
|||||||
erpnext.patches.v13_0.update_old_loans
|
erpnext.patches.v13_0.update_old_loans
|
||||||
erpnext.patches.v12_0.set_serial_no_status #2020-05-21
|
erpnext.patches.v12_0.set_serial_no_status #2020-05-21
|
||||||
erpnext.patches.v12_0.update_price_list_currency_in_bom
|
erpnext.patches.v12_0.update_price_list_currency_in_bom
|
||||||
|
execute:frappe.reload_doctype('Dashboard')
|
||||||
execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts')
|
execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts')
|
||||||
erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo
|
erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo
|
||||||
erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25
|
erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25
|
||||||
@@ -725,4 +726,6 @@ erpnext.patches.v12_0.rename_lost_reason_detail
|
|||||||
erpnext.patches.v13_0.drop_razorpay_payload_column
|
erpnext.patches.v13_0.drop_razorpay_payload_column
|
||||||
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
|
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.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.change_default_pos_print_format
|
||||||
|
erpnext.patches.v13_0.set_youtube_video_id
|
||||||
|
|||||||
65
erpnext/patches/v13_0/rename_issue_doctype_fields.py
Normal file
65
erpnext/patches/v13_0/rename_issue_doctype_fields.py
Normal 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
|
||||||
10
erpnext/patches/v13_0/set_youtube_video_id.py
Normal file
10
erpnext/patches/v13_0/set_youtube_video_id.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from erpnext.utilities.doctype.video.video import get_id_from_url
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("utilities", "doctype","video")
|
||||||
|
|
||||||
|
for video in frappe.get_all("Video", fields=["name", "url", "youtube_video_id"]):
|
||||||
|
if video.url and not video.youtube_video_id:
|
||||||
|
frappe.db.set_value("Video", video.name, "youtube_video_id", get_id_from_url(video.url))
|
||||||
@@ -120,7 +120,7 @@ frappe.ui.form.on('Salary Structure', {
|
|||||||
|
|
||||||
var get_payment_mode_account = function(frm, mode_of_payment, callback) {
|
var get_payment_mode_account = function(frm, mode_of_payment, callback) {
|
||||||
if(!frm.doc.company) {
|
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) {
|
if(!mode_of_payment) {
|
||||||
|
|||||||
@@ -26,19 +26,19 @@ frappe.ui.form.on("Item", {
|
|||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if (frm.doc.is_stock_item) {
|
if (frm.doc.is_stock_item) {
|
||||||
frm.add_custom_button(__("Balance"), function() {
|
frm.add_custom_button(__("Stock Balance"), function() {
|
||||||
frappe.route_options = {
|
frappe.route_options = {
|
||||||
"item_code": frm.doc.name
|
"item_code": frm.doc.name
|
||||||
}
|
}
|
||||||
frappe.set_route("query-report", "Stock Balance");
|
frappe.set_route("query-report", "Stock Balance");
|
||||||
}, __("View"));
|
}, __("View"));
|
||||||
frm.add_custom_button(__("Ledger"), function() {
|
frm.add_custom_button(__("Stock Ledger"), function() {
|
||||||
frappe.route_options = {
|
frappe.route_options = {
|
||||||
"item_code": frm.doc.name
|
"item_code": frm.doc.name
|
||||||
}
|
}
|
||||||
frappe.set_route("query-report", "Stock Ledger");
|
frappe.set_route("query-report", "Stock Ledger");
|
||||||
}, __("View"));
|
}, __("View"));
|
||||||
frm.add_custom_button(__("Projected"), function() {
|
frm.add_custom_button(__("Stock Projected Qty"), function() {
|
||||||
frappe.route_options = {
|
frappe.route_options = {
|
||||||
"item_code": frm.doc.name
|
"item_code": frm.doc.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ frappe.query_reports["Batch-Wise Balance History"] = {
|
|||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"width": "80",
|
"width": "80",
|
||||||
"default": frappe.sys_defaults.year_start_date,
|
"default": frappe.sys_defaults.year_start_date,
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"to_date",
|
"fieldname":"to_date",
|
||||||
"label": __("To Date"),
|
"label": __("To Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"width": "80",
|
"width": "80",
|
||||||
"default": frappe.datetime.get_today()
|
"default": frappe.datetime.get_today(),
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "item",
|
"fieldname": "item",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ from frappe.utils import cint, flt, getdate
|
|||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
if not filters: filters = {}
|
if not filters: filters = {}
|
||||||
|
|
||||||
|
if filters.from_date > filters.to_date:
|
||||||
|
frappe.throw(_("From Date must be before To Date"))
|
||||||
|
|
||||||
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"label": "Reports",
|
"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",
|
"category": "Modules",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Support",
|
"label": "Support",
|
||||||
"modified": "2020-06-04 11:54:56.124219",
|
"modified": "2020-08-11 15:49:34.307341",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Support",
|
"module": "Support",
|
||||||
"name": "Support",
|
"name": "Support",
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ frappe.ui.form.on("Issue", {
|
|||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.email_field = "raised_by";
|
frm.email_field = "raised_by";
|
||||||
|
|
||||||
frappe.db.get_value("Support Settings", {name: "Support Settings"}, "allow_resetting_service_level_agreement", (r) => {
|
frappe.db.get_value("Support Settings", {name: "Support Settings"},
|
||||||
if (!r.allow_resetting_service_level_agreement) {
|
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
|
||||||
frm.set_df_property("reset_service_level_agreement", "hidden", 1) ;
|
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) {
|
if (frm.doc.service_level_agreement) {
|
||||||
@@ -38,7 +42,7 @@ frappe.ui.form.on("Issue", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
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) {
|
if (frm.doc.service_level_agreement) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
'method': 'frappe.client.get',
|
'method': 'frappe.client.get',
|
||||||
@@ -85,14 +89,14 @@ frappe.ui.form.on("Issue", {
|
|||||||
if (frm.doc.service_level_agreement) {
|
if (frm.doc.service_level_agreement) {
|
||||||
frm.dashboard.clear_headline();
|
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": "green", "msg": "Service Level Agreement has been fulfilled"} :
|
||||||
{"indicator": "red", "msg": "Service Level Agreement Failed"};
|
{"indicator": "red", "msg": "Service Level Agreement Failed"};
|
||||||
|
|
||||||
frm.dashboard.set_headline_alert(
|
frm.dashboard.set_headline_alert(
|
||||||
'<div class="row">' +
|
'<div class="row">' +
|
||||||
'<div class="col-xs-12">' +
|
'<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>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
);
|
);
|
||||||
@@ -198,13 +202,13 @@ function set_time_to_resolve_and_response(frm) {
|
|||||||
frm.dashboard.clear_headline();
|
frm.dashboard.clear_headline();
|
||||||
|
|
||||||
var time_to_respond = get_status(frm.doc.response_by_variance);
|
var time_to_respond = get_status(frm.doc.response_by_variance);
|
||||||
if (!frm.doc.first_responded_on && frm.doc.agreement_fulfilled === "Ongoing") {
|
if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") {
|
||||||
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled);
|
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);
|
var time_to_resolve = get_status(frm.doc.resolution_by_variance);
|
||||||
if (!frm.doc.resolution_date && frm.doc.agreement_fulfilled === "Ongoing") {
|
if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") {
|
||||||
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled);
|
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.dashboard.set_headline_alert(
|
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 = moment(timestamp).diff(moment());
|
||||||
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
|
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};
|
return {"diff_display": diff_display, "indicator": indicator};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,17 +27,25 @@
|
|||||||
"response_by_variance",
|
"response_by_variance",
|
||||||
"reset_service_level_agreement",
|
"reset_service_level_agreement",
|
||||||
"cb",
|
"cb",
|
||||||
"agreement_fulfilled",
|
"agreement_status",
|
||||||
"resolution_by",
|
"resolution_by",
|
||||||
"resolution_by_variance",
|
"resolution_by_variance",
|
||||||
"service_level_agreement_creation",
|
"service_level_agreement_creation",
|
||||||
"on_hold_since",
|
"on_hold_since",
|
||||||
"total_hold_time",
|
"total_hold_time",
|
||||||
"response",
|
"response",
|
||||||
"mins_to_first_response",
|
"first_response_time",
|
||||||
"first_responded_on",
|
"first_responded_on",
|
||||||
"column_break_26",
|
"column_break_26",
|
||||||
"avg_response_time",
|
"avg_response_time",
|
||||||
|
"section_break_19",
|
||||||
|
"resolution_details",
|
||||||
|
"column_break1",
|
||||||
|
"opening_date",
|
||||||
|
"opening_time",
|
||||||
|
"resolution_date",
|
||||||
|
"resolution_time",
|
||||||
|
"user_resolution_time",
|
||||||
"additional_info",
|
"additional_info",
|
||||||
"lead",
|
"lead",
|
||||||
"contact",
|
"contact",
|
||||||
@@ -46,23 +54,14 @@
|
|||||||
"customer_name",
|
"customer_name",
|
||||||
"project",
|
"project",
|
||||||
"company",
|
"company",
|
||||||
"section_break_19",
|
|
||||||
"resolution_details",
|
|
||||||
"column_break1",
|
|
||||||
"opening_date",
|
|
||||||
"opening_time",
|
|
||||||
"resolution_date",
|
|
||||||
"content_type",
|
|
||||||
"attachment",
|
|
||||||
"via_customer_portal",
|
"via_customer_portal",
|
||||||
"resolution_time",
|
"attachment",
|
||||||
"user_resolution_time"
|
"content_type"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "subject_section",
|
"fieldname": "subject_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Subject",
|
|
||||||
"options": "fa fa-flag"
|
"options": "fa fa-flag"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -158,7 +157,7 @@
|
|||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "service_level_section",
|
"fieldname": "service_level_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Service Level"
|
"label": "Service Level Agreement Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "service_level_agreement",
|
"fieldname": "service_level_agreement",
|
||||||
@@ -191,14 +190,7 @@
|
|||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "response",
|
"fieldname": "response",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Response"
|
"label": "Response Details"
|
||||||
},
|
|
||||||
{
|
|
||||||
"bold": 1,
|
|
||||||
"fieldname": "mins_to_first_response",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"label": "Mins to First Response",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "first_responded_on",
|
"fieldname": "first_responded_on",
|
||||||
@@ -261,7 +253,7 @@
|
|||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "section_break_19",
|
"fieldname": "section_break_19",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Resolution"
|
"label": "Resolution Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.__islocal",
|
"depends_on": "eval:!doc.__islocal",
|
||||||
@@ -326,28 +318,19 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Via Customer Portal"
|
"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';",
|
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
|
||||||
"description": "in hours",
|
|
||||||
"fieldname": "response_by_variance",
|
"fieldname": "response_by_variance",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Duration",
|
||||||
|
"hide_seconds": 1,
|
||||||
"label": "Response By Variance",
|
"label": "Response By Variance",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
|
"depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';",
|
||||||
"description": "in hours",
|
|
||||||
"fieldname": "resolution_by_variance",
|
"fieldname": "resolution_by_variance",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Duration",
|
||||||
|
"hide_seconds": 1,
|
||||||
"label": "Resolution By Variance",
|
"label": "Resolution By Variance",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -406,12 +389,28 @@
|
|||||||
"fieldtype": "Duration",
|
"fieldtype": "Duration",
|
||||||
"label": "Total Hold Time",
|
"label": "Total Hold Time",
|
||||||
"read_only": 1
|
"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",
|
"icon": "fa fa-ticket",
|
||||||
"idx": 7,
|
"idx": 7,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-10 12:47:37.146914",
|
"modified": "2020-08-11 18:49:07.574769",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Support",
|
"module": "Support",
|
||||||
"name": "Issue",
|
"name": "Issue",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Issue(Document):
|
|||||||
|
|
||||||
if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
|
if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]:
|
||||||
self.resolution_date = frappe.flags.current_time or now_datetime()
|
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)
|
set_service_level_agreement_variance(issue=self.name)
|
||||||
self.update_agreement_status()
|
self.update_agreement_status()
|
||||||
set_resolution_time(issue=self)
|
set_resolution_time(issue=self)
|
||||||
@@ -72,7 +72,7 @@ class Issue(Document):
|
|||||||
self.resolution_date = None
|
self.resolution_date = None
|
||||||
self.reset_issue_metrics()
|
self.reset_issue_metrics()
|
||||||
# enable SLA and variance on Reopen
|
# enable SLA and variance on Reopen
|
||||||
self.agreement_fulfilled = "Ongoing"
|
self.agreement_status = "Ongoing"
|
||||||
set_service_level_agreement_variance(issue=self.name)
|
set_service_level_agreement_variance(issue=self.name)
|
||||||
|
|
||||||
self.handle_hold_time(status)
|
self.handle_hold_time(status)
|
||||||
@@ -113,39 +113,39 @@ class Issue(Document):
|
|||||||
if not self.first_responded_on:
|
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 = 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 = 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'] = 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 = 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 = 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'] = 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
|
update_values['on_hold_since'] = None
|
||||||
|
|
||||||
self.db_set(update_values)
|
self.db_set(update_values)
|
||||||
|
|
||||||
def update_agreement_status(self):
|
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 \
|
if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \
|
||||||
frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0:
|
frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0:
|
||||||
|
|
||||||
self.agreement_fulfilled = "Failed"
|
self.agreement_status = "Failed"
|
||||||
else:
|
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
|
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
|
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
|
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):
|
def create_communication(self):
|
||||||
communication = frappe.new_doc("Communication")
|
communication = frappe.new_doc("Communication")
|
||||||
@@ -172,7 +172,7 @@ class Issue(Document):
|
|||||||
replicated_issue = deepcopy(self)
|
replicated_issue = deepcopy(self)
|
||||||
replicated_issue.subject = subject
|
replicated_issue.subject = subject
|
||||||
replicated_issue.issue_split_from = self.name
|
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.first_responded_on = None
|
||||||
replicated_issue.creation = now_datetime()
|
replicated_issue.creation = now_datetime()
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ class Issue(Document):
|
|||||||
if replicated_issue.service_level_agreement:
|
if replicated_issue.service_level_agreement:
|
||||||
replicated_issue.service_level_agreement_creation = now_datetime()
|
replicated_issue.service_level_agreement_creation = now_datetime()
|
||||||
replicated_issue.service_level_agreement = None
|
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 = None
|
||||||
replicated_issue.response_by_variance = None
|
replicated_issue.response_by_variance = None
|
||||||
replicated_issue.resolution_by = 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.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.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.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()))
|
||||||
self.resolution_by_variance = round(time_diff_in_hours(self.resolution_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):
|
def change_service_level_agreement_and_priority(self):
|
||||||
if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \
|
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.service_level_agreement_creation = now_datetime()
|
||||||
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
|
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()
|
self.save()
|
||||||
|
|
||||||
def reset_issue_metrics(self):
|
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):
|
def set_service_level_agreement_variance(issue=None):
|
||||||
current_time = frappe.flags.current_time or now_datetime()
|
current_time = frappe.flags.current_time or now_datetime()
|
||||||
|
|
||||||
filters = {"status": "Open", "agreement_fulfilled": "Ongoing"}
|
filters = {"status": "Open", "agreement_status": "Ongoing"}
|
||||||
if issue:
|
if issue:
|
||||||
filters = {"name": 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)
|
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)
|
frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False)
|
||||||
if variance < 0:
|
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
|
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)
|
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)
|
frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False)
|
||||||
if variance < 0:
|
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):
|
def set_resolution_time(issue):
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class TestIssue(unittest.TestCase):
|
|||||||
issue.status = 'Closed'
|
issue.status = 'Closed'
|
||||||
issue.save()
|
issue.save()
|
||||||
|
|
||||||
self.assertEqual(issue.agreement_fulfilled, 'Fulfilled')
|
self.assertEqual(issue.agreement_status, 'Fulfilled')
|
||||||
|
|
||||||
def test_issue_metrics(self):
|
def test_issue_metrics(self):
|
||||||
creation = datetime.datetime(2020, 3, 4, 4, 0)
|
creation = datetime.datetime(2020, 3, 4, 4, 0)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
29
erpnext/utilities/desk_page/utilities/utilities.json
Normal file
29
erpnext/utilities/desk_page/utilities/utilities.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Video",
|
||||||
|
"links": "[\n {\n \"description\": \"Video\",\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Video settings\",\n \"label\": \"Video Settings\",\n \"name\": \"Video Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"category": "Modules",
|
||||||
|
"charts": [],
|
||||||
|
"creation": "2020-09-10 12:21:22.335307",
|
||||||
|
"developer_mode_only": 0,
|
||||||
|
"disable_user_customization": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Desk Page",
|
||||||
|
"extends_another_page": 0,
|
||||||
|
"hide_custom": 0,
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": 1,
|
||||||
|
"label": "Utilities",
|
||||||
|
"modified": "2020-09-10 12:33:30.089853",
|
||||||
|
"modified_by": "user@erpnext.com",
|
||||||
|
"module": "Utilities",
|
||||||
|
"name": "Utilities",
|
||||||
|
"owner": "user@erpnext.com",
|
||||||
|
"pin_to_bottom": 1,
|
||||||
|
"pin_to_top": 0,
|
||||||
|
"shortcuts": []
|
||||||
|
}
|
||||||
@@ -2,7 +2,16 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Video', {
|
frappe.ui.form.on('Video', {
|
||||||
// refresh: function(frm) {
|
refresh: function (frm) {
|
||||||
|
frm.events.toggle_youtube_statistics_section(frm);
|
||||||
|
frm.add_custom_button("Watch Video", () => frappe.help.show_video(frm.doc.url, frm.doc.title));
|
||||||
|
},
|
||||||
|
|
||||||
// }
|
toggle_youtube_statistics_section: (frm) => {
|
||||||
|
if (frm.doc.provider === "YouTube") {
|
||||||
|
frappe.db.get_single_value("Video Settings", "enable_youtube_tracking").then( val => {
|
||||||
|
frm.toggle_display("youtube_tracking_section", val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,11 +11,19 @@
|
|||||||
"title",
|
"title",
|
||||||
"provider",
|
"provider",
|
||||||
"url",
|
"url",
|
||||||
|
"youtube_video_id",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"publish_date",
|
"publish_date",
|
||||||
"duration",
|
"duration",
|
||||||
|
"youtube_tracking_section",
|
||||||
|
"like_count",
|
||||||
|
"view_count",
|
||||||
|
"col_break",
|
||||||
|
"dislike_count",
|
||||||
|
"comment_count",
|
||||||
"section_break_7",
|
"section_break_7",
|
||||||
"description"
|
"description",
|
||||||
|
"image"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -37,7 +45,6 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "url",
|
"fieldname": "url",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "URL",
|
"label": "URL",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
@@ -48,11 +55,12 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "publish_date",
|
"fieldname": "publish_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Publish Date"
|
"label": "Publish Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "duration",
|
"fieldname": "duration",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Duration",
|
||||||
"label": "Duration"
|
"label": "Duration"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -62,13 +70,67 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "like_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Likes",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "view_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Views",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "col_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dislike_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Dislikes",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "comment_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Comments",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Image",
|
||||||
|
"no_copy": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.provider==\"YouTube\"",
|
||||||
|
"fieldname": "youtube_tracking_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Youtube Statistics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "youtube_video_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Youtube ID",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-07-21 19:29:46.603734",
|
"modified": "2020-09-07 17:02:20.185794",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Utilities",
|
"module": "Utilities",
|
||||||
"name": "Video",
|
"name": "Video",
|
||||||
|
|||||||
@@ -3,8 +3,144 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
# import frappe
|
import frappe
|
||||||
|
import re
|
||||||
|
import pytz
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe import _
|
||||||
|
from datetime import datetime
|
||||||
|
from six import string_types
|
||||||
|
from pyyoutube import Api
|
||||||
|
|
||||||
class Video(Document):
|
class Video(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
if self.provider == "YouTube" and is_tracking_enabled():
|
||||||
|
self.set_video_id()
|
||||||
|
self.set_youtube_statistics()
|
||||||
|
|
||||||
|
def set_video_id(self):
|
||||||
|
if self.url and not self.get("youtube_video_id"):
|
||||||
|
self.youtube_video_id = get_id_from_url(self.url)
|
||||||
|
|
||||||
|
def set_youtube_statistics(self):
|
||||||
|
api_key = frappe.db.get_single_value("Video Settings", "api_key")
|
||||||
|
api = Api(api_key=api_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
video = api.get_video_by_id(video_id=self.youtube_video_id)
|
||||||
|
video_stats = video.items[0].to_dict().get('statistics')
|
||||||
|
|
||||||
|
self.like_count = video_stats.get('likeCount')
|
||||||
|
self.view_count = video_stats.get('viewCount')
|
||||||
|
self.dislike_count = video_stats.get('dislikeCount')
|
||||||
|
self.comment_count = video_stats.get('commentCount')
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name)
|
||||||
|
frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title)
|
||||||
|
|
||||||
|
def is_tracking_enabled():
|
||||||
|
return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking")
|
||||||
|
|
||||||
|
|
||||||
|
def get_frequency(value):
|
||||||
|
# Return numeric value from frequency field, return 1 as fallback default value: 1 hour
|
||||||
|
if value != "Daily":
|
||||||
|
return frappe.utils.cint(value[:2].strip())
|
||||||
|
elif value:
|
||||||
|
return 24
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def update_youtube_data():
|
||||||
|
# Called every 30 minutes via hooks
|
||||||
|
enable_youtube_tracking, frequency = frappe.db.get_value("Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"])
|
||||||
|
|
||||||
|
if not enable_youtube_tracking:
|
||||||
|
return
|
||||||
|
|
||||||
|
frequency = get_frequency(frequency)
|
||||||
|
time = datetime.now()
|
||||||
|
timezone = pytz.timezone(frappe.utils.get_time_zone())
|
||||||
|
site_time = time.astimezone(timezone)
|
||||||
|
|
||||||
|
if frequency == 30:
|
||||||
|
batch_update_youtube_data()
|
||||||
|
elif site_time.hour % frequency == 0 and site_time.minute < 15:
|
||||||
|
# make sure it runs within the first 15 mins of the hour
|
||||||
|
batch_update_youtube_data()
|
||||||
|
|
||||||
|
|
||||||
|
def get_formatted_ids(video_list):
|
||||||
|
# format ids to comma separated string for bulk request
|
||||||
|
ids = []
|
||||||
|
for video in video_list:
|
||||||
|
ids.append(video.youtube_video_id)
|
||||||
|
|
||||||
|
return ','.join(ids)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_id_from_url(url):
|
||||||
|
"""
|
||||||
|
Returns video id from url
|
||||||
|
:param youtube url: String URL
|
||||||
|
"""
|
||||||
|
if not isinstance(url, string_types):
|
||||||
|
frappe.throw(_("URL can only be a string"), title=_("Invalid URL"))
|
||||||
|
|
||||||
|
pattern = re.compile(r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?')
|
||||||
|
id = pattern.match(url)
|
||||||
|
return id.groups()[-1]
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def batch_update_youtube_data():
|
||||||
|
def get_youtube_statistics(video_ids):
|
||||||
|
api_key = frappe.db.get_single_value("Video Settings", "api_key")
|
||||||
|
api = Api(api_key=api_key)
|
||||||
|
try:
|
||||||
|
video = api.get_video_by_id(video_id=video_ids)
|
||||||
|
video_stats = video.items
|
||||||
|
return video_stats
|
||||||
|
except Exception:
|
||||||
|
title = "Failed to Update YouTube Statistics"
|
||||||
|
frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title)
|
||||||
|
|
||||||
|
def prepare_and_set_data(video_list):
|
||||||
|
video_ids = get_formatted_ids(video_list)
|
||||||
|
stats = get_youtube_statistics(video_ids)
|
||||||
|
set_youtube_data(stats)
|
||||||
|
|
||||||
|
def set_youtube_data(entries):
|
||||||
|
for entry in entries:
|
||||||
|
video_stats = entry.to_dict().get('statistics')
|
||||||
|
video_id = entry.to_dict().get('id')
|
||||||
|
stats = {
|
||||||
|
'like_count' : video_stats.get('likeCount'),
|
||||||
|
'view_count' : video_stats.get('viewCount'),
|
||||||
|
'dislike_count' : video_stats.get('dislikeCount'),
|
||||||
|
'comment_count' : video_stats.get('commentCount'),
|
||||||
|
'video_id': video_id
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.db.sql("""
|
||||||
|
UPDATE `tabVideo`
|
||||||
|
SET
|
||||||
|
like_count = %(like_count)s,
|
||||||
|
view_count = %(view_count)s,
|
||||||
|
dislike_count = %(dislike_count)s,
|
||||||
|
comment_count = %(comment_count)s
|
||||||
|
WHERE youtube_video_id = %(video_id)s""", stats)
|
||||||
|
|
||||||
|
video_list = frappe.get_all("Video", fields=["youtube_video_id"])
|
||||||
|
if len(video_list) > 50:
|
||||||
|
# Update in batches of 50
|
||||||
|
start, end = 0, 50
|
||||||
|
while start < len(video_list):
|
||||||
|
batch = video_list[start:end]
|
||||||
|
prepare_and_set_data(batch)
|
||||||
|
start += 50
|
||||||
|
end += 50
|
||||||
|
else:
|
||||||
|
prepare_and_set_data(video_list)
|
||||||
|
|||||||
7
erpnext/utilities/doctype/video/video_list.js
Normal file
7
erpnext/utilities/doctype/video/video_list.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
frappe.listview_settings["Video"] = {
|
||||||
|
onload: (listview) => {
|
||||||
|
listview.page.add_menu_item(__("Video Settings"), function() {
|
||||||
|
frappe.set_route("Form","Video Settings", "Video Settings");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestVideoSettings(unittest.TestCase):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Video Settings', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
||||||
60
erpnext/utilities/doctype/video_settings/video_settings.json
Normal file
60
erpnext/utilities/doctype/video_settings/video_settings.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-08-02 03:50:21.339609",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"enable_youtube_tracking",
|
||||||
|
"api_key",
|
||||||
|
"frequency"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_youtube_tracking",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable YouTube Tracking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.enable_youtube_tracking",
|
||||||
|
"fieldname": "api_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "API Key",
|
||||||
|
"mandatory_depends_on": "eval:doc.enable_youtube_tracking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1 hr",
|
||||||
|
"depends_on": "eval:doc.enable_youtube_tracking",
|
||||||
|
"fieldname": "frequency",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Frequency",
|
||||||
|
"mandatory_depends_on": "eval:doc.enable_youtube_tracking",
|
||||||
|
"options": "30 mins\n1 hr\n6 hrs\nDaily"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"issingle": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-09-07 16:09:00.360668",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Utilities",
|
||||||
|
"name": "Video Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
22
erpnext/utilities/doctype/video_settings/video_settings.py
Normal file
22
erpnext/utilities/doctype/video_settings/video_settings.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from apiclient.discovery import build
|
||||||
|
|
||||||
|
class VideoSettings(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_youtube_api_key()
|
||||||
|
|
||||||
|
def validate_youtube_api_key(self):
|
||||||
|
if self.enable_youtube_tracking and self.api_key:
|
||||||
|
try:
|
||||||
|
build("youtube", "v3", developerKey=self.api_key)
|
||||||
|
except Exception:
|
||||||
|
title = _("Failed to Authenticate the API key.")
|
||||||
|
frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title)
|
||||||
|
frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials"))
|
||||||
0
erpnext/utilities/report/__init__.py
Normal file
0
erpnext/utilities/report/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["YouTube Interactions"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
label: __("From Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
default: frappe.datetime.add_months(frappe.datetime.now_date(), -12),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname:"to_date",
|
||||||
|
label: __("To Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
default: frappe.datetime.now_date(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"creation": "2020-08-02 05:05:00.457093",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2020-08-02 05:05:00.457093",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Utilities",
|
||||||
|
"name": "YouTube Interactions",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Video",
|
||||||
|
"report_name": "YouTube Interactions",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "All"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "System Manager"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
if not frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") or not filters:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
chart_data, summary = get_chart_summary_data(data)
|
||||||
|
return columns, data, None, chart_data, summary
|
||||||
|
|
||||||
|
def get_columns():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"label": _("Published Date"),
|
||||||
|
"fieldname": "publish_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Title"),
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Duration"),
|
||||||
|
"fieldname": "duration",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Views"),
|
||||||
|
"fieldname": "view_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Likes"),
|
||||||
|
"fieldname": "like_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"width": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Dislikes"),
|
||||||
|
"fieldname": "dislike_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Comments"),
|
||||||
|
"fieldname": "comment_count",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"width": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_data(filters):
|
||||||
|
return frappe.db.sql("""
|
||||||
|
SELECT
|
||||||
|
publish_date, title, provider, duration,
|
||||||
|
view_count, like_count, dislike_count, comment_count
|
||||||
|
FROM `tabVideo`
|
||||||
|
WHERE view_count is not null
|
||||||
|
and publish_date between %(from_date)s and %(to_date)s
|
||||||
|
ORDER BY view_count desc""", filters, as_dict=1)
|
||||||
|
|
||||||
|
def get_chart_summary_data(data):
|
||||||
|
labels, likes, views = [], [], []
|
||||||
|
total_views = 0
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
labels.append(row.get('title'))
|
||||||
|
likes.append(row.get('like_count'))
|
||||||
|
views.append(row.get('view_count'))
|
||||||
|
total_views += flt(row.get('view_count'))
|
||||||
|
|
||||||
|
|
||||||
|
chart_data = {
|
||||||
|
"data" : {
|
||||||
|
"labels" : labels,
|
||||||
|
"datasets" : [
|
||||||
|
{
|
||||||
|
"name" : "Likes",
|
||||||
|
"values" : likes
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name" : "Views",
|
||||||
|
"values" : views
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "bar",
|
||||||
|
"barOptions": {
|
||||||
|
"stacked": 1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
summary = [
|
||||||
|
{
|
||||||
|
"value": total_views,
|
||||||
|
"indicator": "Blue",
|
||||||
|
"label": "Total Views",
|
||||||
|
"datatype": "Float",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return chart_data, summary
|
||||||
@@ -7,6 +7,7 @@ plaid-python==6.0.0
|
|||||||
pycountry==19.8.18
|
pycountry==19.8.18
|
||||||
PyGithub==1.44.1
|
PyGithub==1.44.1
|
||||||
python-stdnum==1.12
|
python-stdnum==1.12
|
||||||
|
python-youtube==0.6.0
|
||||||
taxjar==1.9.0
|
taxjar==1.9.0
|
||||||
tweepy==3.8.0
|
tweepy==3.8.0
|
||||||
Unidecode==1.1.1
|
Unidecode==1.1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user