- | {{ j.queue.split(".").slice(-1)[0] }} |
+ {{ j.queue.split(".").slice(-1)[0] }} |
{{ frappe.utils.encode_tags(j.job_name) }}
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index f1eadaaf2e..089f2a733b 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -17,6 +17,14 @@ frappe.ui.form.on("Customize Form", {
};
});
+ frm.set_query("default_print_format", function() {
+ return {
+ filters: {
+ 'print_format_type': ['!=', 'JS']
+ }
+ }
+ });
+
$(frm.wrapper).on("grid-row-render", function(e, grid_row) {
if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
$(grid_row.row).css({"font-weight": "bold"});
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index a330f7e97e..d51231097e 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -114,9 +114,8 @@ frappe.ui.form.on('Dashboard Chart', {
} else {
// standard filters
if (frm.doc.document_type) {
- // allow all link and select fields as filters
- frm.chart_filters = [];
frappe.model.with_doctype(frm.doc.document_type, () => {
+ frm.chart_filters = [];
frappe.get_meta(frm.doc.document_type).fields.map(df => {
if (['Link', 'Select'].includes(df.fieldtype)) {
let _df = copy_dict(df);
@@ -131,8 +130,8 @@ frappe.ui.form.on('Dashboard Chart', {
frm.chart_filters.push(_df);
}
- frm.trigger('render_filters_table');
});
+ frm.trigger('render_filters_table');
});
}
}
@@ -158,7 +157,7 @@ frappe.ui.form.on('Dashboard Chart', {
let filters = JSON.parse(frm.doc.filters_json || '{}');
var filters_set = false;
- fields.map( f => {
+ fields.map(f => {
if (filters[f.fieldname]) {
const filter_row = $(` | ${f.label} | ${filters[f.fieldname] || ""} | `);
table.find('tbody').append(filter_row);
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index 637904b35c..45dc5d86c7 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -73,6 +73,8 @@ class Event(Document):
communication.subject = self.subject
communication.content = self.description if self.description else self.subject
communication.communication_date = self.starts_on
+ communication.sender = self.owner
+ communication.sender_full_name = frappe.utils.get_fullname(self.owner)
communication.reference_doctype = self.doctype
communication.reference_name = self.name
communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else ""
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 734b99a003..6c679bf312 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -1,14 +1,119 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
-
-import frappe, json
+import json
+from collections import defaultdict
+from six import string_types
+import frappe
+import frappe.desk.form.load
+import frappe.desk.form.meta
+from frappe import _
from frappe.model.meta import is_single
from frappe.modules import load_doctype_module
-import frappe.desk.form.meta
-import frappe.desk.form.load
-from six import string_types
-from collections import defaultdict
+
+
+@frappe.whitelist()
+def get_submitted_linked_docs(doctype, name, docs=None):
+ """
+ Get all nested submitted linked doctype linkinfo
+
+ Arguments:
+ doctype (str) - The doctype for which get all linked doctypes
+ name (str) - The docname for which get all linked doctypes
+
+ Keyword Arguments:
+ docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
+
+ Returns:
+ dict - Return list of documents and link count
+ """
+
+ if not docs:
+ docs = []
+
+ linkinfo = get_linked_doctypes(doctype)
+ linked_docs = get_linked_docs(doctype, name, linkinfo)
+
+ link_count = 0
+ for link_doctype, link_names in linked_docs.items():
+ for link in link_names:
+ docinfo = link.update({"doctype": link_doctype})
+ validated_doc = validate_linked_doc(docinfo)
+
+ if not validated_doc:
+ continue
+
+ link_count += 1
+ if link.name in [doc.get("name") for doc in docs]:
+ continue
+
+ links = get_submitted_linked_docs(link_doctype, link.name, docs)
+ docs.append({
+ "doctype": link_doctype,
+ "name": link.name,
+ "docstatus": link.docstatus,
+ "link_count": links.get("count")
+ })
+
+ # sort linked documents by ascending number of links
+ docs.sort(key=lambda doc: doc.get("link_count"))
+ return {
+ "docs": docs,
+ "count": link_count
+ }
+
+
+@frappe.whitelist()
+def cancel_all_linked_docs(docs):
+ """
+ Cancel all linked doctype
+
+ Arguments:
+ docs (str) - It contains all list of dictionaries of a linked documents.
+ """
+
+ docs = json.loads(docs)
+ for i, doc in enumerate(docs, 1):
+ if validate_linked_doc(doc) is True:
+ frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
+ linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
+ linked_doc.cancel()
+
+
+def validate_linked_doc(docinfo):
+ """
+ Validate a document to be submitted and non-exempted from auto-cancel.
+
+ Args:
+ docs (dict): The document to check for submitted and non-exempt from auto-cancel
+
+ Returns:
+ bool: True if linked document passes all validations, else False
+ """
+
+ # skip non-submittable doctypes since they don't need to be cancelled
+ if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
+ return False
+
+ # skip draft or cancelled documents
+ if docinfo.get('docstatus') != 1:
+ return False
+
+ # skip other doctypes since they don't need to be cancelled
+ auto_cancel_exempt_doctypes = get_exempted_doctypes()
+ if docinfo.get('doctype') in auto_cancel_exempt_doctypes:
+ return False
+
+ return True
+
+
+def get_exempted_doctypes():
+ """ Get list of doctypes exempted from being auto-cancelled """
+
+ auto_cancel_exempt_doctypes = []
+ for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
+ auto_cancel_exempt_doctypes.append(doctypes)
+ return auto_cancel_exempt_doctypes
@frappe.whitelist()
@@ -184,8 +289,8 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
if is_single(df.doctype): continue
# optimized to get both link exists and parenttype
- possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype
- from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True)
+ possible_link = frappe.get_all(df.doctype, filters={df.doctype_fieldname: doctype},
+ fields=['parenttype'], distinct=True)
if not possible_link: continue
@@ -203,4 +308,4 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
"doctype_fieldname": df.doctype_fieldname
}
- return ret
\ No newline at end of file
+ return ret
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index 4044a3dcfc..f24f33df07 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -11,6 +11,7 @@ from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed
from frappe import _
+from six.moves.urllib.parse import quote
@frappe.whitelist()
def getdoc(doctype, name, user=None):
@@ -101,7 +102,8 @@ def get_docinfo(doc=None, doctype=None, name=None):
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"milestones": get_milestones(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
- "tags": get_tags(doc.doctype, doc.name)
+ "tags": get_tags(doc.doctype, doc.name),
+ "document_email": get_document_email(doc.doctype, doc.name)
}
def get_milestones(doctype, name):
@@ -263,4 +265,15 @@ def get_tags(doctype, name):
"document_name": name
}, fields=["tag"])]
- return ",".join([tag for tag in tags])
\ No newline at end of file
+ return ",".join(tags)
+
+def get_document_email(doctype, name):
+ email = get_automatic_email_link()
+ if not email:
+ return None
+
+ email = email.split("@")
+ return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
+
+def get_automatic_email_link():
+ return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index c64d2dcb4f..3d480eb00e 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -13,7 +13,7 @@ class Leaderboard {
constructor(parent) {
frappe.ui.make_app_page({
parent: parent,
- title: "Leaderboard",
+ title: __("Leaderboard"),
single_column: false
});
this.parent = parent;
@@ -187,7 +187,7 @@ class Leaderboard {
render_search_box() {
this.$search_box =
- $(`
+ $(`
`);
@@ -363,7 +363,7 @@ class Leaderboard {
const link = `#Form/${this.options.selected_doctype}/${item.name}`;
const name_html = item.formatted_name ?
- ` ${item.formatted_name}`
+ ` ${item.formatted_name}`
: ` ${item.name} `;
const html =
`
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index dca1a99802..99e3c2e96f 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -158,7 +158,7 @@ def get_script(report_name):
@frappe.whitelist()
@frappe.read_only()
-def run(report_name, filters=None, user=None):
+def run(report_name, filters=None, user=None, ignore_prepared_report=False):
report = get_report_doc(report_name)
if not user:
@@ -169,7 +169,7 @@ def run(report_name, filters=None, user=None):
result = None
- if report.prepared_report and not report.disable_prepared_report:
+ if report.prepared_report and not report.disable_prepared_report and not ignore_prepared_report:
if filters:
if isinstance(filters, string_types):
filters = json.loads(filters)
@@ -229,7 +229,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
"status": "Completed",
"filters": json.dumps(filters),
"owner": user,
- "report_name": report.custom_report or report.report_name
+ "report_name": report.get('custom_report') or report.get('report_name')
},
order_by = 'creation desc'
)
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index b06d3f7dfe..d31c3a67e4 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -66,7 +66,7 @@ class AutoEmailReport(Document):
self.prepare_dynamic_filters()
columns, data = report.get_data(limit=self.no_of_rows or 100, user = self.user,
- filters = self.filters, as_dict=True)
+ filters = self.filters, as_dict=True, ignore_prepared_report=True)
# add serial numbers
columns.insert(0, frappe._dict(fieldname='idx', label='', width='30px'))
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index a108225a48..41b47a1fa2 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -765,7 +765,3 @@ def get_max_email_uid(email_account):
else:
max_uid = cint(result[0].get("uid", 0)) + 1
return max_uid
-
-@frappe.whitelist()
-def get_automatic_email_link():
- return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 0256a3d2d2..a132eb69d5 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -139,6 +139,7 @@ doc_events = {
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
],
+ "after_insert": "frappe.cache_manager.build_table_count_cache",
},
"Event": {
"after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar",
diff --git a/frappe/integrations/doctype/stripe_settings/stripe_settings.py b/frappe/integrations/doctype/stripe_settings/stripe_settings.py
index 35beef7faa..70ca6002e4 100644
--- a/frappe/integrations/doctype/stripe_settings/stripe_settings.py
+++ b/frappe/integrations/doctype/stripe_settings/stripe_settings.py
@@ -76,7 +76,7 @@ class StripeSettings(Document):
def create_charge_on_stripe(self):
import stripe
try:
- charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description)
+ charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description, receipt_email=self.data.payer_email)
if charge.captured == True:
self.integration_request.db_set('status', 'Completed', update_modified=False)
diff --git a/frappe/integrations/doctype/webhook/webhook.js b/frappe/integrations/doctype/webhook/webhook.js
index 90b5b12dc6..09c296113a 100644
--- a/frappe/integrations/doctype/webhook/webhook.js
+++ b/frappe/integrations/doctype/webhook/webhook.js
@@ -66,6 +66,10 @@ frappe.ui.form.on('Webhook', {
webhook_doctype: (frm) => {
frappe.webhook.set_fieldname_select(frm);
+ },
+
+ enable_security: (frm) => {
+ frm.toggle_reqd('webhook_secret', frm.doc.enable_security);
}
});
diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json
index 8aa96e6859..9f979099c9 100644
--- a/frappe/integrations/doctype/webhook/webhook.json
+++ b/frappe/integrations/doctype/webhook/webhook.json
@@ -19,6 +19,9 @@
"request_url",
"cb_webhook",
"request_structure",
+ "sb_security",
+ "enable_security",
+ "webhook_secret",
"sb_webhook_headers",
"webhook_headers",
"sb_webhook_data",
@@ -127,10 +130,27 @@
"fieldtype": "Select",
"label": "Naming Series",
"options": "\nHOOK-.####"
+ },
+ {
+ "fieldname": "sb_security",
+ "fieldtype": "Section Break",
+ "label": "Webhook Security"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_security",
+ "fieldtype": "Check",
+ "label": "Enable Security"
+ },
+ {
+ "depends_on": "eval:doc.enable_security == 1",
+ "fieldname": "webhook_secret",
+ "fieldtype": "Password",
+ "label": "Webhook Secret"
}
],
"links": [],
- "modified": "2020-01-06 02:51:07.997566",
+ "modified": "2020-01-13 01:53:04.459968",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index 0a97022f66..5cbe7c0a02 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -4,7 +4,10 @@
from __future__ import unicode_literals
+import base64
import datetime
+import hashlib
+import hmac
import json
from time import sleep
@@ -16,6 +19,8 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils.jinja import validate_template
+WEBHOOK_SECRET_HEADER = "X-Frappe-Webhook-Signature"
+
class Webhook(Document):
def validate(self):
@@ -94,10 +99,23 @@ def enqueue_webhook(doc, webhook):
def get_webhook_headers(doc, webhook):
headers = {}
+
+ if webhook.enable_security:
+ data = get_webhook_data(doc, webhook)
+ signature = base64.b64encode(
+ hmac.new(
+ webhook.get_password("webhook_secret").encode("utf8"),
+ json.dumps(data).encode("utf8"),
+ hashlib.sha256
+ ).digest()
+ )
+ headers[WEBHOOK_SECRET_HEADER] = signature
+
if webhook.webhook_headers:
for h in webhook.webhook_headers:
if h.get("key") and h.get("value"):
headers[h.get("key")] = h.get("value")
+
return headers
diff --git a/frappe/integrations/oauth2_logins.py b/frappe/integrations/oauth2_logins.py
index a3ee98ad4e..14a6bcc417 100644
--- a/frappe/integrations/oauth2_logins.py
+++ b/frappe/integrations/oauth2_logins.py
@@ -31,6 +31,10 @@ def login_via_office365(code, state):
def login_via_salesforce(code, state):
login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
+@frappe.whitelist(allow_guest=True)
+def login_via_fairlogin(code, state):
+ login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
+
@frappe.whitelist(allow_guest=True)
def custom(code, state):
"""
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 2d534eb0a8..adb951a7fb 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -584,14 +584,17 @@ class Document(BaseDocument):
def get_permlevel_access(self, permission_type='write'):
if not hasattr(self, "_has_access_to"):
+ self._has_access_to = {}
+
+ if not self._has_access_to.get(permission_type):
+ self._has_access_to[permission_type] = []
roles = frappe.get_roles()
- self._has_access_to = []
for perm in self.get_permissions():
if perm.role in roles and perm.permlevel > 0 and perm.get(permission_type):
- if perm.permlevel not in self._has_access_to:
- self._has_access_to.append(perm.permlevel)
+ if perm.permlevel not in self._has_access_to[permission_type]:
+ self._has_access_to[permission_type].append(perm.permlevel)
- return self._has_access_to
+ return self._has_access_to[permission_type]
def has_permlevel_access_to(self, fieldname, df=None, permission_type='read'):
if not df:
diff --git a/frappe/patches/v11_0/delete_all_prepared_reports.py b/frappe/patches/v11_0/delete_all_prepared_reports.py
index de36db66af..1d722da7e6 100644
--- a/frappe/patches/v11_0/delete_all_prepared_reports.py
+++ b/frappe/patches/v11_0/delete_all_prepared_reports.py
@@ -3,7 +3,7 @@ import frappe
def execute():
if frappe.db.table_exists('Prepared Report'):
- frappe.reload_doc("core", "doctype", "prepared_doctype")
+ frappe.reload_doc("core", "doctype", "prepared_report")
prepared_reports = frappe.get_all("Prepared Report")
for report in prepared_reports:
frappe.delete_doc("Prepared Report", report.name)
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 23d7bffec2..5e642f907b 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -481,7 +481,7 @@ frappe.Application = Class.extend({
// Iterate over changelog
var change_log_dialog = frappe.msgprint({
message: frappe.render_template("change_log", {"change_log": change_log}),
- title: __("Updated To New Version 🎉"),
+ title: __("Updated To A New Version 🎉"),
wide: true,
scroll: true
});
diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js
index 08bb7b763f..859cbbb22a 100644
--- a/frappe/public/js/frappe/form/controls/barcode.js
+++ b/frappe/public/js/frappe/form/controls/barcode.js
@@ -27,7 +27,11 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
set_formatted_input(value) {
// Set values to display
let svg = value;
- const barcode_value = $(svg).attr('data-barcode-value');
+ let barcode_value = '';
+
+ if (value && value.startsWith(' |