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
commit 382676d875
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1126 additions and 391 deletions

View File

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

View File

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

View File

@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
check_plaid_status() {
const me = this;
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
if (r && r.enabled == "1") {
if (r && r.enabled === "1") {
me.plaid_status = "active"
} else {
me.plaid_status = "inactive"
@ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
}
make() {
const me = this;
const me = this;
new frappe.ui.FileUploader({
method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
allow_multiple: 0,
@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync {
init_config() {
const me = this;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration')
.then(result => {
me.plaid_env = result.plaid_env;
me.plaid_public_key = result.plaid_public_key;
me.client_name = result.client_name;
me.sync_transactions()
})
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
.then(result => {
me.plaid_env = result.plaid_env;
me.client_name = result.client_name;
me.link_token = result.link_token;
me.sync_transactions();
})
}
sync_transactions() {
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', {
bank: v['bank'],
bank: r.bank,
bank_account: me.parent.bank_account,
freeze: true
})
.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 = `
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5>
</div>`
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5>
</div>`
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',
{bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")}
{ bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
).then((result) => {
me.make_dialog(result)
})

View File

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

View File

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

View File

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

View File

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

View File

@ -1264,7 +1264,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
transitions.append(transition.as_dict())
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):
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 account_currency = %(currency)s
AND `{searchfield}` LIKE %(txt)s
{mcond}
ORDER BY idx DESC, name
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(
account_types=filters.get("account_type"),
company=filters.get("company"),
@ -359,9 +364,21 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
if filters.get("is_return"):
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'):
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,
concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date)
{search_columns}
from `tabStock Ledger Entry` sle
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where
@ -377,6 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
group by batch_no {having_clause}
order by batch.expiry_date, sle.batch_no desc
limit %(start)s, %(page_len)s""".format(
search_columns = search_columns,
cond=cond,
match_conditions=get_match_cond(doctype),
having_clause = having_clause
@ -384,7 +402,9 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
return batch_nos
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
and item = %(item_code)s
and (name like %(txt)s
@ -394,7 +414,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
{0}
{match_conditions}
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,81 +2,90 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
from frappe.utils.password import get_decrypted_password
from plaid import Client
from plaid.errors import APIError, ItemError
import plaid
import requests
from plaid.errors import APIError, ItemError, InvalidRequestError
import frappe
import requests
from frappe import _
class PlaidConnector():
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.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):
if public_token is None:
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
response = self.client.Item.public_token.exchange(public_token)
access_token = response['access_token']
access_token = response["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):
try:
self.client.Auth.get(self.access_token)
print("Authentication successful.....")
except ItemError as e:
if e.code == 'ITEM_LOGIN_REQUIRED':
pass
else:
if e.code == "ITEM_LOGIN_REQUIRED":
pass
except APIError as e:
if e.code == 'PLANNED_MAINTENANCE':
pass
else:
if e.code == "PLANNED_MAINTENANCE":
pass
except requests.Timeout:
pass
except Exception as e:
print(e)
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):
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:
self.auth()
if account_id:
account_ids = [account_id]
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(**kwargs)
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))
transactions.extend(response['transactions'])
transactions.extend(response["transactions"])
return transactions
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

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

View File

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

View File

@ -2,30 +2,36 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
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.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.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
class PlaidSettings(Document):
pass
@staticmethod
def get_link_token():
plaid = PlaidConnector()
return plaid.get_link_token()
@frappe.whitelist()
def plaid_configuration():
def get_plaid_configuration():
if frappe.db.get_single_value("Plaid Settings", "enabled"):
plaid_settings = frappe.get_single("Plaid Settings")
return {
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env,
"link_token": plaid_settings.get_link_token(),
"client_name": frappe.local.site
}
else:
return "disabled"
return "disabled"
@frappe.whitelist()
def add_institution(token, response):
@ -33,6 +39,7 @@ def add_institution(token, response):
plaid = PlaidConnector()
access_token = plaid.get_access_token(token)
bank = None
if not frappe.db.exists("Bank", response["institution"]["name"]):
try:
@ -44,7 +51,6 @@ def add_institution(token, response):
bank.insert()
except Exception:
frappe.throw(frappe.get_traceback())
else:
bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token
@ -52,6 +58,7 @@ def add_institution(token, response):
return bank
@frappe.whitelist()
def add_bank_accounts(response, bank, company):
try:
@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company):
new_account.insert()
result.append(new_account.name)
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:
frappe.throw(frappe.get_traceback())
@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company):
return result
def add_account_type(account_type):
try:
frappe.get_doc({
@ -122,10 +129,11 @@ def add_account_subtype(account_subtype):
except Exception:
frappe.throw(frappe.get_traceback())
@frappe.whitelist()
def sync_transactions(bank, bank_account):
'''Sync transactions based on the last integration date as the start date, after the sync is completed
add the transaction date of the oldest transaction as the last integration date'''
"""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."""
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
if last_transaction_date:
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))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
access_token = None
@ -168,6 +176,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
return transactions
def new_bank_transaction(transaction):
result = []
@ -182,8 +191,8 @@ def new_bank_transaction(transaction):
status = "Pending" if transaction["pending"] == "True" else "Settled"
tags = []
try:
tags = []
tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
except KeyError:
@ -216,6 +225,7 @@ def new_bank_transaction(transaction):
return result
def automatic_synchronization():
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"])
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 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# 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
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.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):
def setUp(self):
@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase):
def test_plaid_disabled(self):
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):
add_account_type("brokerage")
@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
}],
}],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
}],
}],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
@ -152,4 +155,4 @@ class TestPlaidSettings(unittest.TestCase):
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
from frappe.model.document import Document
import json
from frappe.utils import getdate, get_time
from frappe.utils import getdate, get_time, flt
from frappe.model.mapper import get_mapped_doc
from frappe import _
import datetime
@ -45,7 +45,7 @@ class PatientAppointment(Document):
def validate_overlaps(self):
end_time = datetime.datetime.combine(getdate(self.appointment_date), get_time(self.appointment_time)) \
+ datetime.timedelta(minutes=float(self.duration))
+ datetime.timedelta(minutes=flt(self.duration))
overlaps = frappe.db.sql("""
select

View File

@ -282,6 +282,11 @@ auto_cancel_exempted_doctypes= [
]
scheduler_events = {
"cron": {
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
]
},
"all": [
"erpnext.projects.doctype.project.project.project_status_update_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.v12_0.set_serial_no_status #2020-05-21
erpnext.patches.v12_0.update_price_list_currency_in_bom
execute:frappe.reload_doctype('Dashboard')
execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts')
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
@ -725,4 +726,6 @@ erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.drop_razorpay_payload_column
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports
erpnext.patches.v13_0.rename_issue_doctype_fields
erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id

View File

@ -4,17 +4,16 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings")
plaid_settings = frappe.get_single("Plaid Settings")
if plaid_settings.enabled:
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \
and frappe.conf.plaid_public_key and frappe.conf.plaid_secret):
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret):
plaid_settings.enabled = 0
else:
plaid_settings.update({
"plaid_client_id": frappe.conf.plaid_client_id,
"plaid_public_key": frappe.conf.plaid_public_key,
"plaid_env": frappe.conf.plaid_env,
"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",
"Sales Person": "Selling",
"Sales Partner": "Selling",
"Brand": "Stock"
"Brand": "Stock",
"Maintenance Schedule": "Support",
"Maintenance Visit": "Support"
});
$.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) {
if(!frm.doc.company) {
frappe.throw(__("Please select the Company first"));
frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")});
}
if(!mode_of_payment) {

View File

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

View File

@ -9,13 +9,15 @@ frappe.query_reports["Batch-Wise Balance History"] = {
"fieldtype": "Date",
"width": "80",
"default": frappe.sys_defaults.year_start_date,
"reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"width": "80",
"default": frappe.datetime.get_today()
"default": frappe.datetime.get_today(),
"reqd": 1
},
{
"fieldname": "item",

View File

@ -11,6 +11,9 @@ from frappe.utils import cint, flt, getdate
def execute(filters=None):
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
columns = get_columns(filters)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,39 @@ def get_level():
activation_level = 0
sales_data = []
min_count = 0
doctypes = {"Item": 5, "Customer": 5, "Sales Order": 2, "Sales Invoice": 2, "Purchase Order": 2, "Employee": 3, "Lead": 3, "Quotation": 3,
"Payment Entry": 2, "User": 5, "Student": 5, "Instructor": 5, "BOM": 3, "Journal Entry": 3, "Stock Entry": 3}
doctypes = {
"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):
count = frappe.db.count(doctype)
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
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",
"provider",
"url",
"youtube_video_id",
"column_break_4",
"publish_date",
"duration",
"youtube_tracking_section",
"like_count",
"view_count",
"col_break",
"dislike_count",
"comment_count",
"section_break_7",
"description"
"description",
"image"
],
"fields": [
{
@ -37,7 +45,6 @@
{
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"reqd": 1
},
@ -48,11 +55,12 @@
{
"fieldname": "publish_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Publish Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"fieldtype": "Duration",
"label": "Duration"
},
{
@ -62,13 +70,67 @@
{
"fieldname": "description",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Description",
"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": [],
"modified": "2020-07-21 19:29:46.603734",
"modified": "2020-09-07 17:02:20.185794",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Video",

View File

@ -3,8 +3,144 @@
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
import frappe
import re
import pytz
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):
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
googlemaps==3.1.1
pandas==1.0.5
plaid-python==3.4.0
plaid-python==6.0.0
pycountry==19.8.18
PyGithub==1.44.1
python-stdnum==1.12
python-youtube==0.6.0
taxjar==1.9.0
tweepy==3.8.0
Unidecode==1.1.1