Merge branch 'develop' into mr-stock-uom-v13

This commit is contained in:
Sagar Vora
2020-09-27 20:09:00 +05:30
committed by GitHub
62 changed files with 1126 additions and 391 deletions

View File

@@ -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);

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['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")

View File

@@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
check_plaid_status() { check_plaid_status() {
const me = this; const me = this;
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
if (r && r.enabled == "1") { if (r && r.enabled === "1") {
me.plaid_status = "active" me.plaid_status = "active"
} else { } else {
me.plaid_status = "inactive" me.plaid_status = "inactive"
@@ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
} }
make() { make() {
const me = this; const me = this;
new frappe.ui.FileUploader({ new frappe.ui.FileUploader({
method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
allow_multiple: 0, allow_multiple: 0,
@@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync {
init_config() { init_config() {
const me = this; const me = this;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
.then(result => { .then(result => {
me.plaid_env = result.plaid_env; me.plaid_env = result.plaid_env;
me.plaid_public_key = result.plaid_public_key; me.client_name = result.client_name;
me.client_name = result.client_name; me.link_token = result.link_token;
me.sync_transactions() me.sync_transactions();
}) })
} }
sync_transactions() { sync_transactions() {
const me = this; const me = this;
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => { frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
bank: v['bank'], bank: r.bank,
bank_account: me.parent.bank_account, bank_account: me.parent.bank_account,
freeze: true freeze: true
}) })
.then((result) => { .then((result) => {
let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized") let result_title = (result && result.length > 0)
? __("{0} bank transaction(s) created", [result.length])
: __("This bank account is already synchronized");
let result_msg = ` let result_msg = `
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;"> <div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5> <h5 class="text-muted">${result_title}</h5>
</div>` </div>`
this.parent.$main_section.append(result_msg) this.parent.$main_section.append(result_msg)
frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'}); frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
}) })
}) })
} }
@@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
}) })
frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
{bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")} { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
).then((result) => { ).then((result) => {
me.make_dialog(result) me.make_dialog(result)
}) })

View File

@@ -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)

View File

@@ -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()

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}
}
} }
} }
} };

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

@@ -2,81 +2,90 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals import plaid
from frappe import _ import requests
from frappe.utils.password import get_decrypted_password from plaid.errors import APIError, ItemError, InvalidRequestError
from plaid import Client
from plaid.errors import APIError, ItemError
import frappe import frappe
import requests from frappe import _
class PlaidConnector(): class PlaidConnector():
def __init__(self, access_token=None): def __init__(self, access_token=None):
plaid_settings = frappe.get_single("Plaid Settings")
self.config = {
"plaid_client_id": plaid_settings.plaid_client_id,
"plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'),
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env
}
self.client = Client(client_id=self.config.get("plaid_client_id"),
secret=self.config.get("plaid_secret"),
public_key=self.config.get("plaid_public_key"),
environment=self.config.get("plaid_env")
)
self.access_token = access_token self.access_token = access_token
self.settings = frappe.get_single("Plaid Settings")
self.products = ["auth", "transactions"]
self.client_name = frappe.local.site
self.client = plaid.Client(
client_id=self.settings.plaid_client_id,
secret=self.settings.get_password("plaid_secret"),
environment=self.settings.plaid_env,
api_version="2019-05-29"
)
def get_access_token(self, public_token): def get_access_token(self, public_token):
if public_token is None: if public_token is None:
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error")) frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
response = self.client.Item.public_token.exchange(public_token) response = self.client.Item.public_token.exchange(public_token)
access_token = response['access_token'] access_token = response["access_token"]
return access_token return access_token
def get_link_token(self):
token_request = {
"client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
"user": {
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
}
}
try:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:
frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error"))
frappe.msgprint(_("Please check your Plaid client ID and secret values"))
except APIError as e:
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.throw(_(str(e)), title=_("Authentication Failed"))
else:
return response["link_token"]
def auth(self): def auth(self):
try: try:
self.client.Auth.get(self.access_token) self.client.Auth.get(self.access_token)
print("Authentication successful.....")
except ItemError as e: except ItemError as e:
if e.code == 'ITEM_LOGIN_REQUIRED': if e.code == "ITEM_LOGIN_REQUIRED":
pass
else:
pass pass
except APIError as e: except APIError as e:
if e.code == 'PLANNED_MAINTENANCE': if e.code == "PLANNED_MAINTENANCE":
pass
else:
pass pass
except requests.Timeout: except requests.Timeout:
pass pass
except Exception as e: except Exception as e:
print(e)
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'}) frappe.throw(_(str(e)), title=_("Authentication Failed"))
def get_transactions(self, start_date, end_date, account_id=None): def get_transactions(self, start_date, end_date, account_id=None):
self.auth()
kwargs = dict(
access_token=self.access_token,
start_date=start_date,
end_date=end_date
)
if account_id:
kwargs.update(dict(account_ids=[account_id]))
try: try:
self.auth() response = self.client.Transactions.get(**kwargs)
if account_id: transactions = response["transactions"]
account_ids = [account_id] while len(transactions) < response["total_transactions"]:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids)
else:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date)
transactions = response['transactions']
while len(transactions) < response['total_transactions']:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response['transactions']) transactions.extend(response["transactions"])
return transactions return transactions
except Exception: except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

@@ -4,14 +4,14 @@
frappe.provide("erpnext.integrations"); frappe.provide("erpnext.integrations");
frappe.ui.form.on('Plaid Settings', { frappe.ui.form.on('Plaid Settings', {
enabled: function(frm) { enabled: function (frm) {
frm.toggle_reqd('plaid_client_id', frm.doc.enabled); frm.toggle_reqd('plaid_client_id', frm.doc.enabled);
frm.toggle_reqd('plaid_secret', frm.doc.enabled); frm.toggle_reqd('plaid_secret', frm.doc.enabled);
frm.toggle_reqd('plaid_public_key', frm.doc.enabled);
frm.toggle_reqd('plaid_env', frm.doc.enabled); frm.toggle_reqd('plaid_env', frm.doc.enabled);
}, },
refresh: function(frm) {
if(frm.doc.enabled) { refresh: function (frm) {
if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => { frm.add_custom_button('Link a new bank account', () => {
new erpnext.integrations.plaidLink(frm); new erpnext.integrations.plaidLink(frm);
}); });
@@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', {
erpnext.integrations.plaidLink = class plaidLink { erpnext.integrations.plaidLink = class plaidLink {
constructor(parent) { constructor(parent) {
this.frm = parent; this.frm = parent;
this.product = ["transactions", "auth"];
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config(); this.init_config();
} }
init_config() { async init_config() {
const me = this; this.product = ["auth", "transactions"];
me.plaid_env = me.frm.doc.plaid_env; this.plaid_env = this.frm.doc.plaid_env;
me.plaid_public_key = me.frm.doc.plaid_public_key; this.client_name = frappe.boot.sitename;
me.client_name = frappe.boot.sitename; this.token = await this.frm.call("get_link_token").then(resp => resp.message);
me.init_plaid(); this.init_plaid();
} }
init_plaid() { init_plaid() {
@@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink {
} }
onScriptLoaded(me) { onScriptLoaded(me) {
me.linkHandler = window.Plaid.create({ me.linkHandler = Plaid.create({
clientName: me.client_name, clientName: me.client_name,
product: me.product,
env: me.plaid_env, env: me.plaid_env,
key: me.plaid_public_key, token: me.token,
onSuccess: me.plaid_success, onSuccess: me.plaid_success
product: me.product
}); });
} }
onScriptError(error) { onScriptError(error) {
frappe.msgprint('There was an issue loading the link-initialize.js script'); frappe.msgprint("There was an issue connecting to Plaid's authentication server");
frappe.msgprint(error); frappe.msgprint(error);
} }
@@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink {
const me = this; const me = this;
frappe.prompt({ frappe.prompt({
fieldtype:"Link", fieldtype: "Link",
options: "Company", options: "Company",
label:__("Company"), label: __("Company"),
fieldname:"company", fieldname: "company",
reqd:1 reqd: 1
}, (data) => { }, (data) => {
me.company = data.company; me.company = data.company;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response}) frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {
.then((result) => { token: token,
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response, response: response
bank: result, company: me.company}); }).then((result) => {
}) frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {
.then(() => { response: response,
frappe.show_alert({message:__("Bank accounts added"), indicator:'green'}); bank: result,
company: me.company
}); });
}).then(() => {
frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' });
});
}, __("Select a company"), __("Continue")); }, __("Select a company"), __("Continue"));
} }
}; };

View File

@@ -1,5 +1,4 @@
{ {
"actions": [],
"creation": "2018-10-25 10:02:48.656165", "creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -12,7 +11,6 @@
"plaid_client_id", "plaid_client_id",
"plaid_secret", "plaid_secret",
"column_break_7", "column_break_7",
"plaid_public_key",
"plaid_env" "plaid_env"
], ],
"fields": [ "fields": [
@@ -41,12 +39,6 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Plaid Secret" "label": "Plaid Secret"
}, },
{
"fieldname": "plaid_public_key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Plaid Public Key"
},
{ {
"fieldname": "plaid_env", "fieldname": "plaid_env",
"fieldtype": "Select", "fieldtype": "Select",
@@ -69,8 +61,7 @@
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "modified": "2020-09-12 02:31:44.542385",
"modified": "2020-02-07 15:21:11.616231",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Plaid Settings", "name": "Plaid Settings",

View File

@@ -2,30 +2,36 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json import json
from frappe import _
from frappe.model.document import Document import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
from frappe.utils import getdate, formatdate, today, add_months from frappe import _
from frappe.desk.doctype.tag.tag import add_tag from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
class PlaidSettings(Document): class PlaidSettings(Document):
pass @staticmethod
def get_link_token():
plaid = PlaidConnector()
return plaid.get_link_token()
@frappe.whitelist() @frappe.whitelist()
def plaid_configuration(): def get_plaid_configuration():
if frappe.db.get_single_value("Plaid Settings", "enabled"): if frappe.db.get_single_value("Plaid Settings", "enabled"):
plaid_settings = frappe.get_single("Plaid Settings") plaid_settings = frappe.get_single("Plaid Settings")
return { return {
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env, "plaid_env": plaid_settings.plaid_env,
"link_token": plaid_settings.get_link_token(),
"client_name": frappe.local.site "client_name": frappe.local.site
} }
else:
return "disabled" return "disabled"
@frappe.whitelist() @frappe.whitelist()
def add_institution(token, response): def add_institution(token, response):
@@ -33,6 +39,7 @@ def add_institution(token, response):
plaid = PlaidConnector() plaid = PlaidConnector()
access_token = plaid.get_access_token(token) access_token = plaid.get_access_token(token)
bank = None
if not frappe.db.exists("Bank", response["institution"]["name"]): if not frappe.db.exists("Bank", response["institution"]["name"]):
try: try:
@@ -44,7 +51,6 @@ def add_institution(token, response):
bank.insert() bank.insert()
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
else: else:
bank = frappe.get_doc("Bank", response["institution"]["name"]) bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token bank.plaid_access_token = access_token
@@ -52,6 +58,7 @@ def add_institution(token, response):
return bank return bank
@frappe.whitelist() @frappe.whitelist()
def add_bank_accounts(response, bank, company): def add_bank_accounts(response, bank, company):
try: try:
@@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company):
new_account.insert() new_account.insert()
result.append(new_account.name) result.append(new_account.name)
except frappe.UniqueValidationError: except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name)) frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
@@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company):
return result return result
def add_account_type(account_type): def add_account_type(account_type):
try: try:
frappe.get_doc({ frappe.get_doc({
@@ -122,10 +129,11 @@ def add_account_subtype(account_subtype):
except Exception: except Exception:
frappe.throw(frappe.get_traceback()) frappe.throw(frappe.get_traceback())
@frappe.whitelist() @frappe.whitelist()
def sync_transactions(bank, bank_account): def sync_transactions(bank, bank_account):
'''Sync transactions based on the last integration date as the start date, after the sync is completed """Sync transactions based on the last integration date as the start date, after sync is completed
add the transaction date of the oldest transaction as the last integration date''' add the transaction date of the oldest transaction as the last integration date."""
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
if last_transaction_date: if last_transaction_date:
start_date = formatdate(last_transaction_date, "YYYY-MM-dd") start_date = formatdate(last_transaction_date, "YYYY-MM-dd")
@@ -147,10 +155,10 @@ def sync_transactions(bank, bank_account):
len(result), bank_account, start_date, end_date)) len(result), bank_account, start_date, end_date))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date) frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
except Exception: except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
def get_transactions(bank, bank_account=None, start_date=None, end_date=None): def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
access_token = None access_token = None
@@ -168,6 +176,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
return transactions return transactions
def new_bank_transaction(transaction): def new_bank_transaction(transaction):
result = [] result = []
@@ -182,8 +191,8 @@ def new_bank_transaction(transaction):
status = "Pending" if transaction["pending"] == "True" else "Settled" status = "Pending" if transaction["pending"] == "True" else "Settled"
tags = []
try: try:
tags = []
tags += transaction["category"] tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])] tags += ["Plaid Cat. {}".format(transaction["category_id"])]
except KeyError: except KeyError:
@@ -216,6 +225,7 @@ def new_bank_transaction(transaction):
return result return result
def automatic_synchronization(): def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings") settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
@@ -223,4 +233,8 @@ def automatic_synchronization():
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
for plaid_account in plaid_accounts: for plaid_account in plaid_accounts:
frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name) frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)

View File

@@ -1,14 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts
import json import json
from frappe.utils.response import json_handler import unittest
import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
add_account_subtype, add_account_type, add_bank_accounts,
new_bank_transaction, get_plaid_configuration)
from frappe.utils.response import json_handler
class TestPlaidSettings(unittest.TestCase): class TestPlaidSettings(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase):
def test_plaid_disabled(self): def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0) frappe.db.set_value("Plaid Settings", None, "enabled", 0)
self.assertTrue(plaid_configuration() == "disabled") self.assertTrue(get_plaid_configuration() == "disabled")
def test_add_account_type(self): def test_add_account_type(self):
add_account_type("brokerage") add_account_type("brokerage")
@@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000', 'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking' 'name': 'Plaid Checking'
}], }],
'institution': { 'institution': {
'institution_id': 'ins_6', 'institution_id': 'ins_6',
'name': 'Citi' 'name': 'Citi'
@@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000', 'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking' 'name': 'Plaid Checking'
}], }],
'institution': { 'institution': {
'institution_id': 'ins_6', 'institution_id': 'ins_6',
'name': 'Citi' 'name': 'Citi'
@@ -152,4 +155,4 @@ class TestPlaidSettings(unittest.TestCase):
new_bank_transaction(transactions) new_bank_transaction(transactions)
self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1) self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1)

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
import json 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.model.mapper import get_mapped_doc
from frappe import _ from frappe import _
import datetime import datetime
@@ -45,7 +45,7 @@ class PatientAppointment(Document):
def validate_overlaps(self): def validate_overlaps(self):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \ 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(""" overlaps = frappe.db.sql("""
select select

View File

@@ -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",

View File

@@ -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

View File

@@ -4,17 +4,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
def execute(): def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings") frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings")
plaid_settings = frappe.get_single("Plaid Settings") plaid_settings = frappe.get_single("Plaid Settings")
if plaid_settings.enabled: if plaid_settings.enabled:
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \ if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret):
and frappe.conf.plaid_public_key and frappe.conf.plaid_secret):
plaid_settings.enabled = 0 plaid_settings.enabled = 0
else: else:
plaid_settings.update({ plaid_settings.update({
"plaid_client_id": frappe.conf.plaid_client_id, "plaid_client_id": frappe.conf.plaid_client_id,
"plaid_public_key": frappe.conf.plaid_public_key,
"plaid_env": frappe.conf.plaid_env, "plaid_env": frappe.conf.plaid_env,
"plaid_secret": frappe.conf.plaid_secret "plaid_secret": frappe.conf.plaid_secret
}) })

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

@@ -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))

View File

@@ -11,7 +11,9 @@ $.extend(frappe.breadcrumbs.preferred, {
"Territory": "Selling", "Territory": "Selling",
"Sales Person": "Selling", "Sales Person": "Selling",
"Sales Partner": "Selling", "Sales Partner": "Selling",
"Brand": "Stock" "Brand": "Stock",
"Maintenance Schedule": "Support",
"Maintenance Visit": "Support"
}); });
$.extend(frappe.breadcrumbs.module_map, { $.extend(frappe.breadcrumbs.module_map, {

View File

@@ -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) {

View File

@@ -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
} }

View File

@@ -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",

View File

@@ -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)

View File

@@ -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",

View File

@@ -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};
} }

View File

@@ -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",

View File

@@ -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):

View File

@@ -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)

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

View File

@@ -11,8 +11,39 @@ def get_level():
activation_level = 0 activation_level = 0
sales_data = [] sales_data = []
min_count = 0 min_count = 0
doctypes = {"Item": 5, "Customer": 5, "Sales Order": 2, "Sales Invoice": 2, "Purchase Order": 2, "Employee": 3, "Lead": 3, "Quotation": 3, doctypes = {
"Payment Entry": 2, "User": 5, "Student": 5, "Instructor": 5, "BOM": 3, "Journal Entry": 3, "Stock Entry": 3} "Asset": 5,
"BOM": 3,
"Customer": 5,
"Delivery Note": 5,
"Employee": 3,
"Instructor": 5,
"Instructor": 5,
"Issue": 5,
"Item": 5,
"Journal Entry": 3,
"Lead": 3,
"Leave Application": 5,
"Material Request": 5,
"Opportunity": 5,
"Payment Entry": 2,
"Project": 5,
"Purchase Order": 2,
"Purchase Invoice": 5,
"Purchase Receipt": 5,
"Quotation": 3,
"Salary Slip": 5,
"Salary Structure": 5,
"Sales Order": 2,
"Sales Invoice": 2,
"Stock Entry": 3,
"Student": 5,
"Supplier": 5,
"Task": 5,
"User": 5,
"Work Order": 5
}
for doctype, min_count in iteritems(doctypes): for doctype, min_count in iteritems(doctypes):
count = frappe.db.count(doctype) count = frappe.db.count(doctype)
if count > min_count: if count > min_count:

View 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": []
}

View File

@@ -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);
});
}
}
}); });

View File

@@ -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",

View File

@@ -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)

View 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");
});
}
}

View File

@@ -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

View File

@@ -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) {
// }
});

View 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
}

View 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"))

View File

View 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(),
}
]
};

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -3,10 +3,11 @@ frappe
gocardless-pro==1.11.0 gocardless-pro==1.11.0
googlemaps==3.1.1 googlemaps==3.1.1
pandas==1.0.5 pandas==1.0.5
plaid-python==3.4.0 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