+
diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json
index 177252ea22..8b1b6c4e07 100644
--- a/frappe/core/doctype/data_import/data_import.json
+++ b/frappe/core/doctype/data_import/data_import.json
@@ -119,7 +119,7 @@
{
"fieldname": "import_warnings_section",
"fieldtype": "Section Break",
- "label": "Warnings"
+ "label": "Import File Errors and Warnings"
},
{
"fieldname": "import_warnings",
@@ -127,7 +127,7 @@
"label": "Import Warnings"
},
{
- "depends_on": "reference_doctype",
+ "depends_on": "eval:!doc.__islocal",
"fieldname": "download_template",
"fieldtype": "Button",
"label": "Download Template"
@@ -159,7 +159,7 @@
"label": "Import from Google Sheets"
},
{
- "depends_on": "eval:doc.google_sheets_url",
+ "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
"fieldname": "refresh_google_sheet",
"fieldtype": "Button",
"label": "Refresh Google Sheet"
@@ -167,7 +167,7 @@
],
"hide_toolbar": 1,
"links": [],
- "modified": "2020-06-18 16:05:54.211034",
+ "modified": "2020-06-24 14:33:03.173876",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 4761652c70..14626eb5e3 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import (
read_xls_file_from_attached_file,
)
from frappe.model import no_value_fields, table_fields as table_fieldtypes
+from frappe.core.doctype.version.version import get_diff
INVALID_VALUES = ("", None)
MAX_ROWS_IN_PREVIEW = 10
@@ -58,6 +59,7 @@ class Importer:
frappe.flags.in_import = True
frappe.flags.mute_emails = self.data_import.mute_emails
+ self.data_import.db_set("status", "Pending")
self.data_import.db_set("template_warnings", "")
def import_data(self):
@@ -216,14 +218,22 @@ class Importer:
def update_record(self, doc):
id_field = get_id_field(self.doctype)
existing_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
- existing_doc.flags.updater_reference = {
- "doctype": self.data_import.doctype,
- "docname": self.data_import.name,
- "label": _("via Data Import"),
- }
- existing_doc.update(doc)
- existing_doc.save()
- return existing_doc
+
+ updated_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname))
+ updated_doc.update(doc)
+
+ if get_diff(existing_doc, updated_doc):
+ # update doc if there are changes
+ updated_doc.flags.updater_reference = {
+ "doctype": self.data_import.doctype,
+ "docname": self.data_import.name,
+ "label": _("via Data Import"),
+ }
+ updated_doc.save()
+ return updated_doc
+ else:
+ # throw if no changes
+ frappe.throw('No changes to update')
def get_eta(self, current, total, processing_time):
self.last_eta = getattr(self, "last_eta", 0)
@@ -306,8 +316,9 @@ class ImportFile:
)
self.column_to_field_map = self.template_options.column_to_field_map
self.import_type = import_type
+ self.warnings = []
- self.file_doc = self.file_path = None
+ self.file_doc = self.file_path = self.google_sheets_url = None
if isinstance(file, frappe.string_types):
if frappe.db.exists("File", {"file_url": file}):
self.file_doc = frappe.get_doc("File", {"file_url": file})
@@ -430,9 +441,8 @@ class ImportFile:
# if there are child doctypes, find the subsequent rows
if len(doctypes) > 1:
- # subsequent rows either dont have any parent value set
- # or have the same value as the parent row
- # we include a row if either of conditions match
+ # subsequent rows that have blank values in parent columns
+ # are considered as child rows
parent_column_indexes = self.header.get_column_indexes(self.doctype)
parent_row_values = first_row.get_values(parent_column_indexes)
@@ -443,11 +453,8 @@ class ImportFile:
if all([v in INVALID_VALUES for v in row_values]):
rows.append(row)
continue
- # if the row has same values as parent row, it's a child row doc
- if row_values == parent_row_values:
- rows.append(row)
- continue
- # if any of those conditions dont match, it's the next doc
+ # if we encounter a row which has values in parent columns,
+ # then it is the next doc
break
parent_doc = None
@@ -462,38 +469,46 @@ class ImportFile:
parent_doc[table_df.fieldname].append(child_doc)
doc = parent_doc
- # check if there is atleast one row for mandatory table fields
- meta = frappe.get_meta(self.doctype)
- mandatory_table_fields = [
- df
- for df in meta.fields
- if df.fieldtype in table_fieldtypes
- and df.reqd
- and len(doc.get(df.fieldname, [])) == 0
- ]
- if len(mandatory_table_fields) == 1:
- self.warnings.append(
- {
- "row": first_row.row_number,
- "message": _("There should be atleast one row for {0} table").format(
- mandatory_table_fields[0].label
- ),
- }
- )
- elif mandatory_table_fields:
- fields_string = ", ".join([df.label for df in mandatory_table_fields])
- message = _("There should be atleast one row for the following tables: {0}").format(
- fields_string
- )
- self.warnings.append({"row": first_row.row_number, "message": message})
+
+ if self.import_type == INSERT:
+ # check if there is atleast one row for mandatory table fields
+ meta = frappe.get_meta(self.doctype)
+ mandatory_table_fields = [
+ df
+ for df in meta.fields
+ if df.fieldtype in table_fieldtypes
+ and df.reqd
+ and len(doc.get(df.fieldname, [])) == 0
+ ]
+ if len(mandatory_table_fields) == 1:
+ self.warnings.append(
+ {
+ "row": first_row.row_number,
+ "message": _("There should be atleast one row for {0} table").format(
+ frappe.bold(mandatory_table_fields[0].label)
+ ),
+ }
+ )
+ elif mandatory_table_fields:
+ fields_string = ", ".join([df.label for df in mandatory_table_fields])
+ message = _("There should be atleast one row for the following tables: {0}").format(
+ fields_string
+ )
+ self.warnings.append({"row": first_row.row_number, "message": message})
return doc, rows, data[len(rows) :]
def get_warnings(self):
warnings = []
+
+ # ImportFile warnings
+ warnings += self.warnings
+
+ # Column warnings
for col in self.header.columns:
warnings += col.warnings
+ # Row warnings
for row in self.data:
warnings += row.warnings
@@ -600,14 +615,14 @@ class Row:
def validate_value(self, value, col):
df = col.df
if df.fieldtype == "Select":
- select_options = df.get_select_options()
+ select_options = [d for d in (df.options or '').split('\n') if d]
if select_options and value not in select_options:
options_string = ", ".join([frappe.bold(d) for d in select_options])
msg = _("Value must be one of {0}").format(options_string)
self.warnings.append(
{
"row": self.row_number,
- "field": df.as_dict(convert_dates_to_str=True),
+ "field": df_as_json(df),
"message": msg,
}
)
@@ -622,7 +637,7 @@ class Row:
self.warnings.append(
{
"row": self.row_number,
- "field": df.as_dict(convert_dates_to_str=True),
+ "field": df_as_json(df),
"message": msg,
}
)
@@ -635,7 +650,7 @@ class Row:
{
"row": self.row_number,
"col": col.column_number,
- "field": df.as_dict(convert_dates_to_str=True),
+ "field": df_as_json(df),
"message": _("Value {0} must in {1} format").format(
frappe.bold(value), frappe.bold(get_user_format(col.date_format))
),
@@ -646,7 +661,7 @@ class Row:
return value
def link_exists(self, value, df):
- key = df.options + "::" + value
+ key = df.options + "::" + cstr(value)
if Row.link_values_exist_map.get(key) is None:
Row.link_values_exist_map[key] = frappe.db.exists(df.options, value)
return Row.link_values_exist_map.get(key)
@@ -674,6 +689,9 @@ class Row:
return value
def get_date(self, value, column):
+ if isinstance(value, datetime):
+ return value
+
date_format = column.date_format
if date_format:
try:
@@ -755,19 +773,21 @@ class Row:
class Header(Row):
- def __init__(self, index, row, doctype, raw_data, column_to_field_map):
+ def __init__(self, index, row, doctype, raw_data, column_to_field_map=None):
self.index = index
self.row_number = index + 1
self.data = row
self.doctype = doctype
+ column_to_field_map = column_to_field_map or frappe._dict()
self.seen = []
self.columns = []
for j, header in enumerate(row):
column_values = [get_item_at_index(r, j) for r in raw_data]
+ map_to_field = column_to_field_map.get(str(j))
column = Column(
- j, header, self.doctype, column_values, column_to_field_map.get(header), self.seen
+ j, header, self.doctype, column_values, map_to_field, self.seen
)
self.seen.append(header)
self.columns.append(column)
@@ -824,7 +844,7 @@ class Column:
self.meta = frappe.get_meta(doctype)
self.parse()
- self.parse_date_format()
+ self.validate_values()
def parse(self):
header_title = self.header_title
@@ -897,10 +917,6 @@ class Column:
self.df = df
self.skip_import = skip_import
- def parse_date_format(self):
- if self.df and self.df.fieldtype in ("Date", "Time", "Datetime"):
- self.date_format = self.guess_date_format_for_column()
-
def guess_date_format_for_column(self):
""" Guesses date format for a column by parsing all the values in the column,
getting the date format and then returning the one which has the maximum frequency
@@ -935,6 +951,36 @@ class Column:
return max_occurred_date_format
+ def validate_values(self):
+ if not self.df:
+ return
+
+ if self.skip_import:
+ return
+
+ if self.df.fieldtype == 'Link':
+ # find all values that dont exist
+ values = list(set([cstr(v) for v in self.column_values[1:] if v]))
+ exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})]
+ not_exists = list(set(values) - set(exists))
+ if not_exists:
+ missing_values = ', '.join(not_exists)
+ self.warnings.append({
+ 'col': self.column_number,
+ 'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values),
+ 'type': 'warning'
+ })
+ elif self.df.fieldtype in ("Date", "Time", "Datetime"):
+ # guess date format
+ self.date_format = self.guess_date_format_for_column()
+ if not self.date_format:
+ self.date_format = '%Y-%m-%d'
+ self.warnings.append({
+ 'col': self.column_number,
+ 'message': _("Date format could not determined from the values in this column. Defaulting to yyyy-mm-dd."),
+ 'type': 'info'
+ })
+
def as_dict(self):
d = frappe._dict()
d.index = self.index
@@ -944,6 +990,9 @@ class Column:
d.map_to_field = self.map_to_field
d.date_format = self.date_format
d.df = self.df
+ if hasattr(self.df, 'is_child_table_field'):
+ d.is_child_table_field = self.df.is_child_table_field
+ d.child_table_df = self.df.child_table_df
d.skip_import = self.skip_import
d.warnings = self.warnings
return d
@@ -1021,6 +1070,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
# other fields
fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields
for df in fields:
+ label = (df.label or '').strip()
fieldtype = df.fieldtype or "Data"
parent = df.parent or parent_doctype
if fieldtype not in no_value_fields:
@@ -1029,12 +1079,12 @@ def build_fields_dict_for_column_matching(parent_doctype):
# Label
# label
# Label (label)
- if not out.get(df.label):
+ if not out.get(label):
# if Label is already set, don't set it again
# in case of duplicate column headers
- out[df.label] = df
+ out[label] = df
out[df.fieldname] = df
- label_with_fieldname = "{0} ({1})".format(df.label, df.fieldname)
+ label_with_fieldname = "{0} ({1})".format(label, df.fieldname)
out[label_with_fieldname] = df
else:
# in case there are multiple table fields with the same doctype
@@ -1045,7 +1095,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
"fields", {"fieldtype": ["in", table_fieldtypes], "options": parent}
)
for table_field in table_fields:
- by_label = "{0} ({1})".format(df.label, table_field.label)
+ by_label = "{0} ({1})".format(label, table_field.label)
by_fieldname = "{0}.{1}".format(table_field.fieldname, df.fieldname)
# create a new df object to avoid mutation problems
@@ -1113,3 +1163,13 @@ def get_user_format(date_format):
.replace("%m", "mm")
.replace("%d", "dd")
)
+
+def df_as_json(df):
+ return {
+ 'fieldname': df.fieldname,
+ 'fieldtype': df.fieldtype,
+ 'label': df.label,
+ 'options': df.options,
+ 'parent': df.parent,
+ 'default': df.default
+ }
diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json
index d9ab504db7..3008e27aa0 100644
--- a/frappe/core/doctype/file/file.json
+++ b/frappe/core/doctype/file/file.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"creation": "2012-12-12 11:19:22",
"doctype": "DocType",
@@ -63,7 +64,8 @@
"fieldname": "is_home_folder",
"fieldtype": "Check",
"hidden": 1,
- "label": "Is Home Folder"
+ "label": "Is Home Folder",
+ "search_index": 1
},
{
"default": "0",
@@ -172,7 +174,8 @@
],
"icon": "fa fa-file",
"idx": 1,
- "modified": "2019-08-30 19:46:20.796453",
+ "links": [],
+ "modified": "2020-06-28 12:21:30.772386",
"modified_by": "Administrator",
"module": "Core",
"name": "File",
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 831d2ab22d..1748c60020 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -100,26 +100,26 @@ class File(Document):
self.validate_file()
self.generate_content_hash()
- self.validate_url()
-
if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
old_file_url = self.file_url
if not self.is_folder and (self.is_private != self.db_get('is_private')):
private_files = frappe.get_site_path('private', 'files')
public_files = frappe.get_site_path('public', 'files')
+ file_name = self.file_url.split('/')[-1]
if not self.is_private:
- shutil.move(os.path.join(private_files, self.file_name),
- os.path.join(public_files, self.file_name))
+ shutil.move(os.path.join(private_files, file_name),
+ os.path.join(public_files, file_name))
- self.file_url = "/files/{0}".format(self.file_name)
+ self.file_url = "/files/{0}".format(file_name)
else:
- shutil.move(os.path.join(public_files, self.file_name),
- os.path.join(private_files, self.file_name))
+ shutil.move(os.path.join(public_files, file_name),
+ os.path.join(private_files, file_name))
- self.file_url = "/private/files/{0}".format(self.file_name)
+ self.file_url = "/private/files/{0}".format(file_name)
+ update_existing_file_docs(self)
# update documents image url with new file url
if self.attached_to_doctype and self.attached_to_name:
@@ -135,6 +135,8 @@ class File(Document):
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
self.attached_to_field, self.file_url)
+ self.validate_url()
+
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
@@ -182,13 +184,7 @@ class File(Document):
if duplicate_file:
duplicate_file_doc = frappe.get_cached_doc('File', duplicate_file.name)
if duplicate_file_doc.exists_on_disk():
- # if it is attached to a document then throw FileAlreadyAttachedException
- if self.attached_to_doctype and self.attached_to_name:
- self.duplicate_entry = duplicate_file.name
- frappe.throw(_("Same file has already been attached to the record"),
- frappe.FileAlreadyAttachedException)
- # else just use the url, to avoid uploading a duplicate
- else:
+ # just use the url, to avoid uploading a duplicate
self.file_url = duplicate_file.file_url
def set_file_name(self):
@@ -909,3 +905,20 @@ def get_files_in_folder(folder):
{ 'folder': folder },
['name', 'file_name', 'file_url', 'is_folder', 'modified']
)
+
+def update_existing_file_docs(doc):
+ # Update is private and file url of all file docs that point to the same file
+ frappe.db.sql("""
+ UPDATE `tabFile`
+ SET
+ file_url = %(file_url)s,
+ is_private = %(is_private)s
+ WHERE
+ content_hash = %(content_hash)s
+ and name != %(file_name)s
+ """, dict(
+ file_url=doc.file_url,
+ is_private=doc.is_private,
+ content_hash=doc.content_hash,
+ file_name=doc.name
+ ))
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index cc9628ed5b..ec4f97bf67 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -294,4 +294,37 @@ class TestFile(unittest.TestCase):
folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3")
self.assertRaises(frappe.ValidationError, folder.delete)
+ def test_same_file_url_update(self):
+ attached_to_doctype1, attached_to_docname1 = make_test_doc()
+ attached_to_doctype2, attached_to_docname2 = make_test_doc()
+
+ file1 = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'file1.txt',
+ "attached_to_doctype": attached_to_doctype1,
+ "attached_to_name": attached_to_docname1,
+ "is_private": 1,
+ "content": test_content1}).insert()
+
+ file2 = frappe.get_doc({
+ "doctype": "File",
+ "file_name": 'file2.txt',
+ "attached_to_doctype": attached_to_doctype2,
+ "attached_to_name": attached_to_docname2,
+ "is_private": 1,
+ "content": test_content1}).insert()
+
+ self.assertEqual(file1.is_private, file2.is_private, 1)
+ self.assertEqual(file1.file_url, file2.file_url)
+ self.assertTrue(os.path.exists(file1.get_full_path()))
+
+ file1.is_private = 0
+ file1.save()
+
+ file2 = frappe.get_doc('File', file2.name)
+
+ self.assertEqual(file1.is_private, file2.is_private, 0)
+ self.assertEqual(file1.file_url, file2.file_url)
+ self.assertTrue(os.path.exists(file2.get_full_path()))
+
diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py
index 0c0e7c4f45..755cb86dbe 100644
--- a/frappe/core/doctype/module_def/module_def.py
+++ b/frappe/core/doctype/module_def/module_def.py
@@ -42,6 +42,10 @@ class ModuleDef(Document):
def on_trash(self):
"""Delete module name from modules.txt"""
+
+ if frappe.flags.in_uninstall:
+ return
+
modules = None
if frappe.local.module_app.get(frappe.scrub(self.name)):
with open(frappe.get_app_path(self.app_name, "modules.txt"), "r") as f:
diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js
index 818c5951e6..c410e9aa1a 100644
--- a/frappe/core/doctype/report/report.js
+++ b/frappe/core/doctype/report/report.js
@@ -1,6 +1,6 @@
frappe.ui.form.on('Report', {
refresh: function(frm) {
- if(!frappe.boot.developer_mode && frappe.session.user !== 'Administrator') {
+ if (frm.doc.is_standard === "Yes" && !frappe.boot.developer_mode) {
// make the document read-only
frm.set_read_only();
}
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 3e6b7a3a98..5c12858e8a 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -52,9 +52,10 @@ class TestServerScript(unittest.TestCase):
frappe.db.commit()
- # @classmethod
- # def tearDownClass(cls):
- # frappe.db.sql('truncate `tabServer Script`')
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.commit()
+ frappe.db.sql('truncate `tabServer Script`')
def setUp(self):
frappe.cache().delete_value('server_script_map')
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 1d0cda95a4..b2cb67dbc9 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -59,6 +59,7 @@
"column_break_18",
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
+ "attach_view_link",
"chat",
"enable_chat",
"use_socketio_to_upload_file"
@@ -422,12 +423,18 @@
"fieldname": "enable_onboarding",
"fieldtype": "Check",
"label": "Enable Onboarding"
+ },
+ {
+ "default": "1",
+ "fieldname": "attach_view_link",
+ "fieldtype": "Check",
+ "label": "Send document Web View link in email"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2020-05-01 19:21:15.496065",
+ "modified": "2020-07-02 16:13:00.166382",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 7b9266ff64..64bff32189 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals, print_function
import frappe
from frappe.model.document import Document
-from frappe.utils import cint, flt, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
+from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password
from frappe.desk.notifications import clear_notifications
@@ -770,7 +770,7 @@ def sign_up(email, full_name, redirect_to):
user = frappe.get_doc({
"doctype":"User",
"email": email,
- "first_name": full_name,
+ "first_name": escape_html(full_name),
"enabled": 1,
"new_password": random_string(10),
"user_type": "Website User"
@@ -811,6 +811,7 @@ def reset_password(user):
frappe.clear_messages()
return 'not found'
+@frappe.whitelist()
def user_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 48d4fcb5d4..325c41622d 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -119,6 +119,7 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
return has_same_user_permission
+@frappe.whitelist()
def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters):
linked_doctypes_map = get_linked_doctypes(doctype, True)
diff --git a/frappe/core/doctype/video/video.js b/frappe/core/doctype/video/video.js
deleted file mode 100644
index 36ea240a36..0000000000
--- a/frappe/core/doctype/video/video.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Video', {
- // refresh: function(frm) {
-
- // }
-});
diff --git a/frappe/core/doctype/video/video.json b/frappe/core/doctype/video/video.json
deleted file mode 100644
index 26a407c05c..0000000000
--- a/frappe/core/doctype/video/video.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "actions": [],
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:title",
- "creation": "2018-10-17 05:47:13.087395",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "title",
- "provider",
- "url",
- "column_break_4",
- "publish_date",
- "duration",
- "section_break_7",
- "description"
- ],
- "fields": [
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Title",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "provider",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Provider",
- "options": "YouTube\nVimeo",
- "reqd": 1
- },
- {
- "fieldname": "url",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "URL",
- "reqd": 1
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "publish_date",
- "fieldtype": "Date",
- "label": "Publish Date"
- },
- {
- "fieldname": "duration",
- "fieldtype": "Data",
- "label": "Duration"
- },
- {
- "fieldname": "section_break_7",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "in_list_view": 1,
- "label": "Description",
- "reqd": 1
- }
- ],
- "links": [],
- "modified": "2020-04-22 12:09:49.057403",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Video",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "All",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/core/doctype/video/video.py b/frappe/core/doctype/video/video.py
deleted file mode 100644
index fdbd3a1abe..0000000000
--- a/frappe/core/doctype/video/video.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-# import frappe
-from frappe.model.document import Document
-
-class Video(Document):
- pass
diff --git a/frappe/core/page/dashboard/dashboard.css b/frappe/core/page/dashboard/dashboard.css
deleted file mode 100644
index b319cc1ed2..0000000000
--- a/frappe/core/page/dashboard/dashboard.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.restricted-button {
- cursor: default;
- position: relative;
- right: -5px;
-}
\ No newline at end of file
diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js
index f17bc1e0b5..7e45163a7e 100644
--- a/frappe/core/page/dashboard/dashboard.js
+++ b/frappe/core/page/dashboard/dashboard.js
@@ -26,13 +26,6 @@ class Dashboard {
`).appendTo(this.wrapper.find(".page-content").empty());
this.container = this.wrapper.find(".dashboard-graph");
this.page = wrapper.page;
-
- this.page.set_title_sub(
- $(`
`)
- );
}
show() {
@@ -172,19 +165,26 @@ class Dashboard {
set_dropdown() {
this.page.clear_menu();
- this.page.add_menu_item('Edit...', () => {
+ this.page.add_menu_item(__('Edit'), () => {
frappe.set_route('Form', 'Dashboard', frappe.dashboard.dashboard_name);
- }, 1);
+ });
- this.page.add_menu_item('New...', () => {
+ this.page.add_menu_item(__('New'), () => {
frappe.new_doc('Dashboard');
- }, 1);
+ });
- frappe.db.get_list("Dashboard").then(dashboards => {
+ this.page.add_menu_item(__('Refresh All'), () => {
+ this.chart_group &&
+ this.chart_group.widgets_list.forEach(chart => chart.refresh());
+ this.number_card_group &&
+ this.number_card_group.widgets_list.forEach(card => card.render_card());
+ });
+
+ frappe.db.get_list('Dashboard').then(dashboards => {
dashboards.map(dashboard => {
let name = dashboard.name;
if(name != this.dashboard_name){
- this.page.add_menu_item(name, () => frappe.set_route("dashboard", name));
+ this.page.add_menu_item(name, () => frappe.set_route("dashboard", name), 1);
}
});
});
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
index 95a04360be..5ca21b811e 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
@@ -5,23 +5,23 @@ from __future__ import unicode_literals
import frappe
from frappe import _, throw
import frappe.utils.user
-from frappe.permissions import check_admin_or_system_manager
+from frappe.permissions import check_admin_or_system_manager, rights
from frappe.model import data_fieldtypes
def execute(filters=None):
user, doctype, show_permissions = filters.get("user"), filters.get("doctype"), filters.get("show_permissions")
+
if not validate(user, doctype): return [], []
columns, fields = get_columns_and_fields(doctype)
data = frappe.get_list(doctype, fields=fields, as_list=True, user=user)
if show_permissions:
- columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email",
- "Report", "Import", "Export", "Share"]
+ columns = columns + [frappe.unscrub(right) + ':Check:80' for right in rights]
data = list(data)
- for i,item in enumerate(data):
- temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user)
- data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),)
+ for i, doc in enumerate(data):
+ permission = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, doc[0]), user)
+ data[i] = doc + tuple(permission.get(right) for right in rights)
return columns, data
@@ -41,6 +41,7 @@ def get_columns_and_fields(doctype):
return columns, fields
+@frappe.whitelist()
def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
user = filters.get("user")
user_perms = frappe.utils.user.UserPermissions(user)
diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py
index 80236b2dc2..3345fce735 100644
--- a/frappe/database/db_manager.py
+++ b/frappe/database/db_manager.py
@@ -49,7 +49,7 @@ class DbManager:
host = self.get_current_host()
if frappe.conf.get('rds_db', 0) == 1:
- self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO '%s'@'%s';" % (target, user, host))
+ self.db.sql("GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, CREATE VIEW, EVENT, TRIGGER, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, LOCK TABLES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
else:
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index e806e8e415..4bbecd2a2e 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -82,5 +82,7 @@ class MariaDBTable(DBTable):
fieldname = str(e).split("'")[-2]
frappe.throw(_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format(
fieldname, self.table_name))
+ elif e.args[0]==1067:
+ frappe.throw(str(e.args[1]))
else:
raise e
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 142c103c68..ae9d070976 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -29,31 +29,56 @@ def handle_not_exist(fn):
class Workspace:
- def __init__(self, page_name):
+ def __init__(self, page_name, minimal=False):
self.page_name = page_name
self.extended_cards = []
self.extended_charts = []
self.extended_shortcuts = []
self.user = frappe.get_user()
- self.allowed_modules = self.get_cached_value('user_allowed_modules', self.get_allowed_modules)
+ self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
+
self.doc = self.get_page_for_user()
if self.doc.module not in self.allowed_modules:
raise frappe.PermissionError
- self.can_read = self.get_cached_value('user_perm_can_read', self.get_can_read_items)
+ self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
self.allowed_pages = get_allowed_pages(cache=True)
self.allowed_reports = get_allowed_reports(cache=True)
- self.onboarding_doc = self.get_onboarding_doc()
- self.onboarding = None
-
- self.table_counts = get_table_with_counts()
+
+ if not minimal:
+ self.onboarding_doc = self.get_onboarding_doc()
+ self.onboarding = None
+
+ self.table_counts = get_table_with_counts()
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
- def get_cached_value(self, cache_key, fallback_fn):
+ def is_page_allowed(self):
+ cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module) + self.extended_cards
+ shortcuts = self.doc.shortcuts + self.extended_shortcuts
+
+ for section in cards:
+ links = loads(section.links) if isinstance(section.links, string_types) else section.links
+ for item in links:
+ if self.is_item_allowed(item.get('name'), item.get('type')):
+ return True
+
+ def _in_active_domains(item):
+ if not item.restrict_to_domain:
+ return True
+ else:
+ return item.restrict_to_domain in frappe.get_active_domains()
+
+ for item in shortcuts:
+ if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
+ return True
+
+ return False
+
+ def get_cached(self, cache_key, fallback_fn):
_cache = frappe.cache()
value = _cache.get_value(cache_key, user=frappe.session.user)
@@ -83,12 +108,12 @@ class Workspace:
'extends': self.page_name,
'for_user': frappe.session.user
}
- pages = frappe.get_list("Desk Page", filters=filters)
+ pages = frappe.get_all("Desk Page", filters=filters, limit=1)
if pages:
- return frappe.get_doc("Desk Page", pages[0])
+ return frappe.get_cached_doc("Desk Page", pages[0])
self.get_pages_to_extend()
- return frappe.get_doc("Desk Page", self.page_name)
+ return frappe.get_cached_doc("Desk Page", self.page_name)
def get_onboarding_doc(self):
# Check if onboarding is enabled
@@ -123,7 +148,7 @@ class Workspace:
'module': ['in', self.allowed_modules]
})
- pages = [frappe.get_doc("Desk Page", page['name']) for page in pages]
+ pages = [frappe.get_cached_doc("Desk Page", page['name']) for page in pages]
for page in pages:
self.extended_cards = self.extended_cards + page.cards
@@ -170,6 +195,7 @@ class Workspace:
'docs_url': self.onboarding_doc.documentation_url,
'items': self.get_onboarding_steps()
}
+
@handle_not_exist
def get_cards(self):
cards = self.doc.cards
@@ -195,6 +221,8 @@ class Workspace:
incomplete_dependencies = [d for d in item.dependencies if not _doctype_contains_a_record(d)]
if len(incomplete_dependencies):
item.incomplete_dependencies = incomplete_dependencies
+ else:
+ item.incomplete_dependencies = ""
if item.onboard:
# Mark Spotlights for initial
@@ -323,25 +351,44 @@ def get_desktop_page(page):
}
@frappe.whitelist()
-def get_desk_sidebar_items(flatten=False):
+def get_desk_sidebar_items(flatten=False, cache=True):
"""Get list of sidebar items for desk
"""
- # don't get domain restricted pages
- blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
+ pages = []
+ _cache = frappe.cache()
+ if cache:
+ pages = _cache.get_value("desk_sidebar_items", user=frappe.session.user)
+
+ if not pages or not cache:
+ # don't get domain restricted pages
+ blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
- filters = {
- 'restrict_to_domain': ['in', frappe.get_active_domains()],
- 'extends_another_page': 0,
- 'for_user': '',
- 'module': ['not in', blocked_modules]
- }
+ filters = {
+ 'restrict_to_domain': ['in', frappe.get_active_domains()],
+ 'extends_another_page': 0,
+ 'for_user': '',
+ 'module': ['not in', blocked_modules]
+ }
- if not frappe.local.conf.developer_mode:
- filters['developer_mode_only'] = '0'
+ if not frappe.local.conf.developer_mode:
+ filters['developer_mode_only'] = '0'
+
+ # pages sorted based on pinned to top and then by name
+ order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
+ all_pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
+ pages = []
+
+ # Filter Page based on Permission
+ for page in all_pages:
+ try:
+ wspace = Workspace(page.get('name'), True)
+ if wspace.is_page_allowed():
+ pages.append(page)
+ except frappe.PermissionError:
+ pass
+
+ _cache.set_value("desk_sidebar_items", pages, frappe.session.user)
- # pages sorted based on pinned to top and then by name
- order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
- pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True)
if flatten:
return pages
@@ -375,7 +422,7 @@ def get_custom_reports_and_doctypes(module):
]
def get_custom_doctype_list(module):
- doctypes = frappe.get_list("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name", ignore_permissions=True)
+ doctypes = frappe.get_all("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name")
out = []
for d in doctypes:
@@ -390,9 +437,9 @@ def get_custom_doctype_list(module):
def get_custom_report_list(module):
"""Returns list on new style reports for modules."""
- reports = frappe.get_list("Report", fields=["name", "ref_doctype", "report_type"], filters=
+ reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters=
{"is_standard": "No", "disabled": 0, "module": module},
- order_by="name", ignore_permissions=True)
+ order_by="name")
out = []
for r in reports:
diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js
index 609e943995..61300b920b 100644
--- a/frappe/desk/doctype/dashboard/dashboard.js
+++ b/frappe/desk/doctype/dashboard/dashboard.js
@@ -5,10 +5,14 @@ frappe.ui.form.on('Dashboard', {
refresh: function(frm) {
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.disable_form();
+ }
+
frm.set_query("chart", "charts", function() {
return {
filters: {
- is_public: 1
+ is_public: 1,
}
};
});
@@ -16,7 +20,7 @@ frappe.ui.form.on('Dashboard', {
frm.set_query("card", "cards", function() {
return {
filters: {
- is_public: 1
+ is_public: 1,
}
};
});
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index c0e2bddcf8..95b0846452 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_rename": 1,
"autoname": "field:dashboard_name",
"creation": "2019-01-10 12:54:40.938705",
"doctype": "DocType",
@@ -8,6 +9,8 @@
"field_order": [
"dashboard_name",
"is_default",
+ "is_standard",
+ "module",
"charts",
"chart_options",
"cards"
@@ -35,21 +38,36 @@
"reqd": 1
},
{
- "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
- "fieldname": "chart_options",
- "fieldtype": "Code",
- "label": "Chart Options",
- "options": "JSON"
+ "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
+ "fieldname": "chart_options",
+ "fieldtype": "Code",
+ "label": "Chart Options",
+ "options": "JSON"
},
{
"fieldname": "cards",
"fieldtype": "Table",
"label": "Cards",
"options": "Number Card Link"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard",
+ "read_only_depends_on": "eval: !frappe.boot.developer_mode"
+ },
+ {
+ "depends_on": "eval: doc.is_standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "mandatory_depends_on": "eval: doc.is_standard",
+ "options": "Module Def"
}
],
"links": [],
- "modified": "2020-04-29 13:26:37.362482",
+ "modified": "2020-07-23 11:05:41.890459",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index af0c48d9c6..b12bcfe27d 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+from frappe.modules.export_file import export_to_files
import frappe
from frappe import _
import json
@@ -15,7 +16,23 @@ class Dashboard(Document):
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module)
+
def validate(self):
+ if not frappe.conf.developer_mode and self.is_standard:
+ frappe.throw('Cannot edit Standard Dashboards')
+
+ if self.is_standard:
+ non_standard_docs_map = {
+ 'Dashboard Chart': get_non_standard_charts_in_dashboard(self),
+ 'Number Card': get_non_standard_cards_in_dashboard(self)
+ }
+
+ if non_standard_docs_map['Dashboard Chart'] or non_standard_docs_map['Number Card']:
+ message = get_non_standard_warning_message(non_standard_docs_map)
+ frappe.throw(message, title=_("Standard Not Set"), is_minimizable=True)
+
self.validate_custom_options()
def validate_custom_options(self):
@@ -48,3 +65,29 @@ def get_permitted_cards(dashboard_name):
if frappe.has_permission('Number Card', doc=card.card):
permitted_cards.append(card)
return permitted_cards
+
+def get_non_standard_charts_in_dashboard(dashboard):
+ non_standard_charts = [doc.name for doc in frappe.get_list('Dashboard Chart', {'is_standard': 0})]
+ return [chart_link.chart for chart_link in dashboard.charts if chart_link.chart in non_standard_charts]
+
+def get_non_standard_cards_in_dashboard(dashboard):
+ non_standard_cards = [doc.name for doc in frappe.get_list('Number Card', {'is_standard': 0})]
+ return [card_link.card for card_link in dashboard.cards if card_link.card in non_standard_cards]
+
+def get_non_standard_warning_message(non_standard_docs_map):
+ message = _('''Please set the following documents in this Dashboard as standard first.''')
+
+ def get_html(docs, doctype):
+ html = '
{}
'.format(frappe.bold(doctype))
+ for doc in docs:
+ html += '
'.format(doctype=doctype, doc=doc)
+ html += '
'
+ return html
+
+ html = message + '
'
+
+ for doctype in non_standard_docs_map:
+ if non_standard_docs_map[doctype]:
+ html += get_html(non_standard_docs_map[doctype], doctype)
+
+ return html
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index a10d3d96f2..8d89cc2f31 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -9,46 +9,41 @@ frappe.ui.form.on('Dashboard Chart', {
frm.add_fetch('source', 'timeseries', 'timeseries');
},
+ before_save: function(frm) {
+ let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
+ let static_filters = JSON.parse(frm.doc.filters_json || 'null');
+ static_filters =
+ frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
+
+ frm.set_value('filters_json', JSON.stringify(static_filters));
+ frm.trigger('show_filters');
+ },
refresh: function(frm) {
frm.chart_filters = null;
+
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.set_df_property('chart_options_section', 'hidden', 1);
+ frm.disable_form();
+ }
+
frm.add_custom_button('Add Chart to Dashboard', () => {
- const d = new frappe.ui.Dialog({
- title: __('Add to Dashboard'),
- fields: [
- {
- label: __('Select Dashboard'),
- fieldtype: 'Link',
- fieldname: 'dashboard',
- options: 'Dashboard',
- }
- ],
- primary_action: (values) => {
- values.chart_name = frm.doc.chart_name;
- frappe.xcall(
- 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard',
- {args: values}
- ).then(()=> {
- let dashboard_route_html =
- `
${values.dashboard}`;
- let message =
- __(`Dashboard Chart ${values.chart_name} add to Dashboard ` + dashboard_route_html);
-
- frappe.msgprint(message);
- });
-
- d.hide();
- }
- });
+ const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
+ frm.doc.name,
+ 'Dashboard Chart',
+ 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard'
+ );
if (!frm.doc.chart_name) {
frappe.msgprint(__('Please create chart first'));
} else {
- d.show();
+ dialog.show();
}
});
frm.set_df_property("filters_section", "hidden", 1);
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
+
frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
@@ -66,6 +61,15 @@ frappe.ui.form.on('Dashboard Chart', {
if (!frappe.boot.developer_mode) {
frm.set_df_property("custom_options", "hidden", 1);
}
+
+ },
+
+ is_standard: function(frm) {
+ if (frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.trigger('render_dynamic_filters_table');
+ } else {
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
+ }
},
source: function(frm) {
@@ -111,6 +115,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('based_on', '');
frm.set_value('value_based_on', '');
frm.set_value('filters_json', '[]');
+ frm.set_value('dynamic_filters_json', '[]');
frm.trigger('update_options');
},
@@ -119,6 +124,8 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_value('y_axis', []);
frm.set_df_property('x_field', 'options', []);
frm.set_value('filters_json', '{}');
+ frm.set_value('dynamic_filters_json', '{}');
+ frm.set_value('use_report_chart', 0);
frm.trigger('set_chart_report_filters');
},
@@ -146,7 +153,10 @@ frappe.ui.form.on('Dashboard Chart', {
},
set_chart_field_options: function(frm) {
- let filters = frm.doc.filters_json.length > 2? JSON.parse(frm.doc.filters_json): null;
+ let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
+ if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
+ filters = frappe.dashboard_utils.get_all_filters(frm.doc);
+ }
frappe.xcall(
'frappe.desk.query_report.run',
{
@@ -156,16 +166,13 @@ frappe.ui.form.on('Dashboard Chart', {
}
).then(data => {
frm.report_data = data;
- if (!data.chart) {
- frm.set_value('is_custom', 0);
- frm.set_df_property('is_custom', 'hidden', 1);
- } else {
- frm.set_df_property('is_custom', 'hidden', 0);
- }
+ let report_has_chart = Boolean(data.chart);
- if (!frm.doc.is_custom) {
+ frm.set_df_property('use_report_chart', 'hidden', !report_has_chart);
+
+ if (!frm.doc.use_report_chart) {
if (data.result.length) {
- frm.field_options = frappe.report_utils.get_possible_chart_options(data.columns, data);
+ frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields);
if (!frm.field_options.numeric_fields.length) {
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
@@ -240,11 +247,14 @@ frappe.ui.form.on('Dashboard Chart', {
show_filters: function(frm) {
frm.chart_filters = [];
frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => {
- if (filters) {
- frm.chart_filters = filters;
- }
+ if (filters) {
+ frm.chart_filters = filters;
+ }
+ frm.trigger('render_filters_table');
- frm.trigger('render_filters_table');
+ if (frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.trigger('render_dynamic_filters_table');
+ }
});
},
@@ -257,8 +267,8 @@ frappe.ui.form.on('Dashboard Chart', {
let table = $(`
- | ${__('Filter')} |
- ${__('Condition')} |
+ ${__('Filter')} |
+ ${__('Condition')} |
${__('Value')} |
@@ -281,7 +291,7 @@ frappe.ui.form.on('Dashboard Chart', {
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
}
- let fields;
+ let fields = [];
if (is_document_type) {
fields = [
{
@@ -306,7 +316,7 @@ frappe.ui.form.on('Dashboard Chart', {
} else if (frm.chart_filters.length) {
fields = frm.chart_filters.filter(f => f.fieldname);
- fields.map( f => {
+ fields.map(f => {
if (filters[f.fieldname]) {
let condition = '=';
const filter_row =
@@ -378,4 +388,102 @@ frappe.ui.form.on('Dashboard Chart', {
});
},
+ render_dynamic_filters_table(frm) {
+ frm.set_df_property("dynamic_filters_section", "hidden", 0);
+
+ let is_document_type = frm.doc.chart_type !== 'Report'
+ && frm.doc.chart_type !== 'Custom';
+
+ let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
+
+ frm.dynamic_filter_table = $(`
+
+
+ | ${__('Filter')} |
+ ${__('Condition')} |
+ ${__('Value')} |
+
+
+
+
`).appendTo(wrapper);
+
+ frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
+
+ frm.trigger('set_dynamic_filters_in_table');
+
+ let filters = JSON.parse(frm.doc.filters_json || '[]');
+
+ let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
+ is_document_type, filters, frm.dynamic_filters
+ );
+
+ frm.dynamic_filter_table.on('click', () => {
+ let dialog = new frappe.ui.Dialog({
+ title: __('Set Dynamic Filters'),
+ fields: fields,
+ primary_action: () => {
+ let values = dialog.get_values();
+ dialog.hide();
+ let dynamic_filters = [];
+ for (let key of Object.keys(values)) {
+ if (is_document_type) {
+ let [doctype, fieldname] = key.split(':');
+ dynamic_filters.push([doctype, fieldname, '=', values[key]]);
+ }
+ }
+
+ if (is_document_type) {
+ frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
+ } else {
+ frm.set_value('dynamic_filters_json', JSON.stringify(values));
+ }
+ frm.trigger('set_dynamic_filters_in_table');
+ },
+ primary_action_label: "Set"
+ });
+
+ dialog.show();
+ dialog.set_values(frm.dynamic_filters);
+ });
+ },
+
+ set_dynamic_filters_in_table: function(frm) {
+ frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
+
+ if (!frm.dynamic_filters) {
+ const filter_row = $(`|
+ ${__("Click to Set Dynamic Filters")} |
`);
+ frm.dynamic_filter_table.find('tbody').html(filter_row);
+ } else {
+ let filter_rows = '';
+ if ($.isArray(frm.dynamic_filters)) {
+ frm.dynamic_filters.forEach(filter => {
+ filter_rows +=
+ `
+ | ${filter[1]} |
+ ${filter[2] || ""} |
+ ${filter[3]} |
+
`;
+ });
+ } else {
+ let condition = '=';
+ for (let [key, val] of Object.entries(frm.dynamic_filters)) {
+ filter_rows +=
+ `
+ | ${key} |
+ ${condition} |
+ ${val || ""} |
+
`
+ ;
+ }
+ }
+
+ frm.dynamic_filter_table.find('tbody').html(filter_rows);
+ }
+ }
+
});
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index 4bab76337f..d4bba53068 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -7,10 +7,12 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "is_standard",
+ "module",
"chart_name",
"chart_type",
"report_name",
- "is_custom",
+ "use_report_chart",
"x_field",
"y_axis",
"source",
@@ -32,10 +34,12 @@
"type",
"filters_section",
"filters_json",
+ "dynamic_filters_section",
+ "dynamic_filters_json",
"chart_options_section",
- "color",
- "column_break_2",
"custom_options",
+ "column_break_2",
+ "color",
"section_break_10",
"last_synced_on"
],
@@ -67,7 +71,8 @@
"fieldname": "document_type",
"fieldtype": "Link",
"label": "Document Type",
- "options": "DocType"
+ "options": "DocType",
+ "set_only_once": 1
},
{
"depends_on": "eval: doc.timeseries && ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
@@ -189,32 +194,27 @@
"label": "To Date"
},
{
- "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
+ "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
"fieldname": "x_field",
"fieldtype": "Select",
"label": "X Field",
- "mandatory_depends_on": "eval: doc.report_name && !doc.is_custom"
+ "mandatory_depends_on": "eval: doc.report_name && !doc.use_report_chart"
},
{
"depends_on": "eval:doc.chart_type === 'Report'",
"fieldname": "report_name",
"fieldtype": "Link",
"label": "Report Name",
- "options": "Report"
+ "mandatory_depends_on": "eval:doc.chart_type === 'Report'",
+ "options": "Report",
+ "set_only_once": 1
},
{
- "default": "0",
- "depends_on": "eval: doc.report_name",
- "fieldname": "is_custom",
- "fieldtype": "Check",
- "label": "Is Custom"
- },
- {
- "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
+ "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
"fieldname": "y_axis",
"fieldtype": "Table",
"label": "Y Axis",
- "mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
+ "mandatory_depends_on": "eval:doc.report_name && !doc.use_report_chart",
"options": "Dashboard Chart Field"
},
{
@@ -235,10 +235,43 @@
"fieldname": "heatmap_year",
"fieldtype": "Select",
"label": "Year"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard",
+ "read_only_depends_on": "eval: !frappe.boot.developer_mode"
+ },
+ {
+ "depends_on": "eval: doc.is_standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "mandatory_depends_on": "eval: doc.is_standard",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "dynamic_filters_json",
+ "fieldtype": "Code",
+ "label": "Dynamic Filters JSON",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "dynamic_filters_section",
+ "fieldtype": "Section Break",
+ "label": "Dynamic Filters"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.report_name",
+ "fieldname": "use_report_chart",
+ "fieldtype": "Check",
+ "label": "Use Report Chart"
}
],
"links": [],
- "modified": "2020-05-16 15:03:02.455395",
+ "modified": "2020-07-23 11:10:33.509497",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index a5c5504db2..88c9623945 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -13,6 +13,7 @@ from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.model.document import Document
+from frappe.modules.export_file import export_to_files
def get_permission_query_conditions(user):
@@ -27,15 +28,28 @@ def get_permission_query_conditions(user):
if "System Manager" in roles:
return None
- allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()]
- allowed_reports = ['"%s"' % key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
+ doctype_condition = False
+ report_condition = False
+
+ allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
+ allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
+
+ if allowed_doctypes:
+ doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
+ allowed_doctypes=','.join(allowed_doctypes))
+ if allowed_reports:
+ report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
+ allowed_reports=','.join(allowed_reports))
return '''
- `tabDashboard Chart`.`document_type` in ({allowed_doctypes})
- or `tabDashboard Chart`.`report_name` in ({allowed_reports})
+ (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
+ and {doctype_condition})
+ or
+ (`tabDashboard Chart`.`chart_type` = 'Report'
+ and {report_condition})
'''.format(
- allowed_doctypes=','.join(allowed_doctypes),
- allowed_reports=','.join(allowed_reports)
+ doctype_condition=doctype_condition,
+ report_condition=report_condition
)
@@ -80,7 +94,9 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
to_date = get_datetime(chart.to_date)
timegrain = time_interval or chart.time_interval
- filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
+ filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
+ if not filters:
+ filters = []
# don't include cancelled documents
filters.append([chart.document_type, 'docstatus', '<', 2, False])
@@ -125,7 +141,13 @@ def add_chart_to_dashboard(args):
dashboard = frappe.get_doc('Dashboard', args.dashboard)
dashboard_link = frappe.new_doc('Dashboard Chart Link')
- dashboard_link.chart = args.chart_name
+ dashboard_link.chart = args.chart_name or args.name
+
+ if args.set_standard:
+ chart = frappe.get_doc('Dashboard Chart', dashboard_link.chart)
+ chart.is_standard = 1
+ chart.module = dashboard.module
+ chart.save()
dashboard.append('charts', dashboard_link)
dashboard.save()
@@ -260,9 +282,7 @@ def get_result(data, timegrain, from_date, to_date):
start_date = getdate(from_date)
end_date = getdate(to_date)
- result = []
- if timegrain == 'Daily':
- result.append([start_date, 0.0])
+ result = [[start_date, 0.0]]
while start_date < end_date:
next_date = get_next_expected_date(start_date, timegrain)
@@ -280,11 +300,8 @@ def get_result(data, timegrain, from_date, to_date):
def get_next_expected_date(date, timegrain):
next_date = None
- if timegrain=='Daily':
- next_date = add_to_date(date, days=1)
- else:
- # given date is always assumed to be the period ending date
- next_date = get_period_ending(add_to_date(date, days=1), timegrain)
+ # given date is always assumed to be the period ending date
+ next_date = get_period_ending(add_to_date(date, days=1), timegrain)
return getdate(next_date)
def get_period_ending(date, timegrain):
@@ -340,6 +357,7 @@ def get_year_ending(date):
# last day of this month
return add_to_date(date, days=-1)
+@frappe.whitelist()
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
or_filters = {'owner': frappe.session.user, 'is_public': 1}
return frappe.db.get_list('Dashboard Chart',
@@ -352,8 +370,13 @@ class DashboardChart(Document):
def on_update(self):
frappe.cache().delete_key('chart-data:{}'.format(self.name))
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files(record_list=[['Dashboard Chart', self.name]], record_module=self.module)
+
def validate(self):
+ if not frappe.conf.developer_mode and self.is_standard:
+ frappe.throw('Cannot edit Standard charts')
if self.chart_type != 'Custom' and self.chart_type != 'Report':
self.check_required_field()
self.check_document_type()
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 1a300e471a..5e39998e62 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -4,13 +4,12 @@
from __future__ import unicode_literals
import unittest, frappe
-from frappe.utils import getdate, formatdate
+from frappe.utils import getdate, formatdate, get_last_day
from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
get_period_ending)
from datetime import datetime
from dateutil.relativedelta import relativedelta
-import calendar
class TestDashboardChart(unittest.TestCase):
def test_period_ending(self):
@@ -53,16 +52,18 @@ class TestDashboardChart(unittest.TestCase):
cur_date = datetime.now() - relativedelta(years=1)
- result = get(chart_name ='Test Dashboard Chart', refresh = 1)
- for idx in range(13):
- month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
+ result = get(chart_name='Test Dashboard Chart', refresh=1)
+ self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
+
+ if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
+ cur_date += relativedelta(months=1)
+
+ for idx in range(1, 13):
+ month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
- # self.assertEqual(result.get('datasets')[0].get('values')[:-1],
- # [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13])
-
frappe.db.rollback()
def test_empty_dashboard_chart(self):
@@ -79,15 +80,20 @@ class TestDashboardChart(unittest.TestCase):
based_on = 'creation',
timespan = 'Last Year',
time_interval = 'Monthly',
- filters_json = '{}',
+ filters_json = '[]',
timeseries = 1
)).insert()
cur_date = datetime.now() - relativedelta(years=1)
- result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1)
- for idx in range(13):
- month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
+ result = get(chart_name ='Test Empty Dashboard Chart', refresh=1)
+ self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
+
+ if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
+ cur_date += relativedelta(months=1)
+
+ for idx in range(1, 13):
+ month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
@@ -111,15 +117,20 @@ class TestDashboardChart(unittest.TestCase):
based_on = 'creation',
timespan = 'Last Year',
time_interval = 'Monthly',
- filters_json = '{}',
+ filters_json = '[]',
timeseries = 1
)).insert()
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
- for idx in range(13):
- month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1]))
+ self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d')))
+
+ if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')):
+ cur_date += relativedelta(months=1)
+
+ for idx in range(1, 13):
+ month = get_last_day(cur_date)
month = formatdate(month.strftime('%Y-%m-%d'))
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
@@ -141,7 +152,7 @@ class TestDashboardChart(unittest.TestCase):
chart_type = 'Group By',
document_type = 'ToDo',
group_by_based_on = 'status',
- filters_json = '{}',
+ filters_json = '[]',
)).insert()
result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1)
@@ -168,7 +179,7 @@ class TestDashboardChart(unittest.TestCase):
time_interval = 'Daily',
from_date = datetime(2019, 1, 6),
to_date = datetime(2019, 1, 11),
- filters_json = '{}',
+ filters_json = '[]',
timeseries = 1
)).insert()
@@ -200,22 +211,24 @@ class TestDashboardChart(unittest.TestCase):
time_interval = 'Weekly',
from_date = datetime(2018, 12, 30),
to_date = datetime(2019, 1, 15),
- filters_json = '{}',
+ filters_json = '[]',
timeseries = 1
)).insert()
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
- self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 800.0, 0.0])
- self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
+ self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
+ self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
frappe.db.rollback()
def insert_test_records():
- create_new_communication(datetime(2019, 1, 10), 100)
+ create_new_communication(datetime(2018, 12, 30), 50)
+ create_new_communication(datetime(2019, 1, 4), 100)
create_new_communication(datetime(2019, 1, 6), 200)
create_new_communication(datetime(2019, 1, 7), 400)
create_new_communication(datetime(2019, 1, 8), 300)
+ create_new_communication(datetime(2019, 1, 10), 100)
def create_new_communication(date, rating):
communication = {
diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json
index 7f6532ce1f..fbe0ae94f0 100644
--- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json
+++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json
@@ -1,162 +1,69 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
+ "actions": [],
"autoname": "field:source_name",
- "beta": 0,
"creation": "2019-02-06 07:55:29.579840",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "source_name",
+ "module",
+ "timeseries"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "source_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Source Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "module",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Module",
- "length": 0,
- "no_copy": 0,
"options": "Module Def",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "timeseries",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Timeseries",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Timeseries"
}
],
- "has_web_view": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-04-09 14:20:51.548207",
+ "links": [],
+ "modified": "2020-06-26 18:00:37.421491",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart Source",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
+ "share": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
index de83807b4b..6685009078 100644
--- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
+++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.py
@@ -18,10 +18,6 @@ def get_config(name):
return f.read()
class DashboardChartSource(Document):
- def validate(self):
- if frappe.session.user != "Administrator":
- frappe.throw(_("Only Administrator is allowed to create Dashboard Chart Sources"))
-
def on_update(self):
export_to_files(record_list=[[self.doctype, self.name]],
record_module=self.module, create_init=True)
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 12f2c41274..c4c6077e85 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -69,7 +69,6 @@ def make_notification_logs(doc, users):
_doc = frappe.new_doc('Notification Log')
_doc.update(doc)
_doc.for_user = user
- _doc.subject = _doc.subject.replace('', '').replace('
', '')
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point' or doc.type == 'Alert':
_doc.insert(ignore_permissions=True)
diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js
index 184fe5e6cb..63b41b956e 100644
--- a/frappe/desk/doctype/number_card/number_card.js
+++ b/frappe/desk/doctype/number_card/number_card.js
@@ -3,8 +3,130 @@
frappe.ui.form.on('Number Card', {
refresh: function(frm) {
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.disable_form();
+ }
frm.set_df_property("filters_section", "hidden", 1);
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
frm.trigger('set_options');
+
+ if (!frm.doc.type) {
+ frm.set_value('type', 'Document Type');
+ }
+
+ if (frm.doc.type == 'Report' && frm.doc.report_name) {
+ frm.trigger('set_report_filters');
+ }
+
+ if (frm.doc.type == 'Custom') {
+ if (!frappe.boot.developer_mode) {
+ frm.disable_form();
+ }
+ frm.filters = eval(frm.doc.filters_config);
+ frm.trigger('set_filters_description');
+ frm.trigger('set_method_description');
+ frm.trigger('render_filters_table');
+ }
+ frm.trigger('create_add_to_dashboard_button');
+ },
+
+ create_add_to_dashboard_button: function(frm) {
+ frm.add_custom_button('Add Card to Dashboard', () => {
+ const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
+ frm.doc.name,
+ 'Number Card',
+ 'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard'
+ );
+
+ if (!frm.doc.name) {
+ frappe.msgprint(__('Please create Card first'));
+ } else {
+ dialog.show();
+ }
+ });
+ },
+
+ before_save: function(frm) {
+ let dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || 'null');
+ let static_filters = JSON.parse(frm.doc.filters_json || 'null');
+ static_filters =
+ frappe.dashboard_utils.remove_common_static_filter_values(static_filters, dynamic_filters);
+
+ frm.set_value('filters_json', JSON.stringify(static_filters));
+ frm.trigger('render_filters_table');
+ frm.trigger('render_dynamic_filters_table');
+ },
+
+ is_standard: function(frm) {
+ frm.trigger('render_dynamic_filters_table');
+ frm.set_df_property("dynamic_filters_section", "hidden", 1);
+ },
+
+ set_filters_description: function(frm) {
+ if (frm.doc.type == 'Custom') {
+ frm.fields_dict.filters_config.set_description(`
+ Set the filters here. For example:
+
+
+[{
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+},
+{
+ fieldname: "account",
+ label: __("Account"),
+ fieldtype: "Link",
+ options: "Account",
+ reqd: 1
+}]
+
`);
+ }
+ },
+
+ set_method_description: function(frm) {
+ if (frm.doc.type == 'Custom') {
+ frm.fields_dict.method.set_description(`
+ Set the path to a whitelisted function that will return the number on the card in the format:
+
+
+{
+ "value": value,
+ "fieldtype": "Currency"
+}
+
`);
+ }
+ },
+
+ type: function(frm) {
+ frm.trigger('set_filters_description');
+ if (frm.doc.type == 'Report') {
+ frm.set_query('report_name', () => {
+ return {
+ filters: {
+ 'report_type': ['!=', 'Report Builder']
+ }
+ };
+ });
+ }
+
+ },
+
+ report_name: function(frm) {
+ frm.filters = [];
+ frm.set_value('filters_json', '{}');
+ frm.set_value('dynamic_filters_json', '{}');
+ frm.set_df_property('report_field', 'options', []);
+ frm.trigger('set_report_filters');
+ },
+
+ filters_config: function(frm) {
+ frm.filters = eval(frm.doc.filters_config);
+ const filter_values = frappe.report_utils.get_filter_values(frm.filters);
+ frm.set_value('filters_json', JSON.stringify(filter_values));
frm.trigger('render_filters_table');
},
@@ -17,11 +139,16 @@ frappe.ui.form.on('Number Card', {
};
});
frm.set_value('filters_json', '[]');
+ frm.set_value('dynamic_filters_json', '[]');
frm.set_value('aggregate_function_based_on', '');
frm.trigger('set_options');
},
set_options: function(frm) {
+ if (frm.doc.type !== 'Document Type') {
+ return;
+ }
+
let aggregate_based_on_fields = [];
const doctype = frm.doc.document_type;
@@ -40,80 +167,275 @@ frappe.ui.form.on('Number Card', {
frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
});
+ frm.trigger('render_filters_table');
+ frm.trigger('render_dynamic_filters_table');
}
},
+ set_report_filters: function(frm) {
+ const report_name = frm.doc.report_name;
+ if (report_name) {
+ frappe.report_utils.get_report_filters(report_name).then(filters => {
+ if (filters) {
+ frm.filters = filters;
+ const filter_values = frappe.report_utils.get_filter_values(filters);
+ if (frm.doc.filters_json.length <= 2) {
+ frm.set_value('filters_json', JSON.stringify(filter_values));
+ }
+ }
+ frm.trigger('render_filters_table');
+ frm.trigger('set_report_field_options');
+ frm.trigger('render_dynamic_filters_table');
+ });
+ }
+ },
+
+ set_report_field_options: function(frm) {
+ let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
+ if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
+ filters = frappe.dashboard_utils.get_all_filters(frm.doc);
+ }
+ frappe.xcall(
+ 'frappe.desk.query_report.run',
+ {
+ report_name: frm.doc.report_name,
+ filters: filters,
+ ignore_prepared_report: 1
+ }
+ ).then(data => {
+ if (data.result.length) {
+ frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
+ frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields);
+ if (!frm.field_options.numeric_fields.length) {
+ frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
+ }
+ } else {
+ frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));
+ }
+ });
+ },
+
render_filters_table: function(frm) {
frm.set_df_property("filters_section", "hidden", 0);
+ let is_document_type = frm.doc.type == 'Document Type';
+ let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
- frm.filter_table = $(`
+ let table = $(`
- | ${__('Filter')} |
- ${__('Condition')} |
+ ${__('Filter')} |
+ ${__('Condition')} |
+ ${__('Value')} |
+
+
+
+
`).appendTo(wrapper);
+ $(`${__("Click table to edit")}
`).appendTo(wrapper);
+
+ let filters = JSON.parse(frm.doc.filters_json || '[]');
+ let filters_set = false;
+
+ // Set dynamic filters for reports
+ if (frm.doc.type == 'Report') {
+ let set_filters = false;
+ frm.filters.forEach(f => {
+ if (is_dynamic_filter(f)) {
+ filters[f.fieldname] = f.default;
+ set_filters = true;
+ }
+ });
+ set_filters && frm.set_value('filters_json', JSON.stringify(filters));
+ }
+
+ let fields = [];
+ if (is_document_type) {
+ fields = [
+ {
+ fieldtype: 'HTML',
+ fieldname: 'filter_area',
+ }
+ ];
+
+ if (filters.length) {
+ filters.forEach(filter => {
+ const filter_row =
+ $(`
+ | ${filter[1]} |
+ ${filter[2] || ""} |
+ ${filter[3]} |
+
`);
+
+ table.find('tbody').append(filter_row);
+ });
+ filters_set = true;
+ }
+ } else if (frm.filters.length) {
+ fields = frm.filters.filter(f => f.fieldname);
+ fields.map(f => {
+ if (filters[f.fieldname]) {
+ let condition = '=';
+ const filter_row =
+ $(`
+ | ${f.label} |
+ ${condition} |
+ ${filters[f.fieldname] || ""} |
+
`);
+ table.find('tbody').append(filter_row);
+ if (!filters_set) filters_set = true;
+ }
+ });
+ }
+
+ if (!filters_set) {
+ const filter_row = $(`|
+ ${__("Click to Set Filters")} |
`);
+ table.find('tbody').append(filter_row);
+ }
+
+ table.on('click', () => {
+ let dialog = new frappe.ui.Dialog({
+ title: __('Set Filters'),
+ fields: fields.filter(f => !is_dynamic_filter(f)),
+ primary_action: function() {
+ let values = this.get_values();
+ if (values) {
+ this.hide();
+ if (is_document_type) {
+ let filters = frm.filter_group.get_filters();
+ frm.set_value('filters_json', JSON.stringify(filters));
+ } else {
+ frm.set_value('filters_json', JSON.stringify(values));
+ }
+ frm.trigger('render_filters_table');
+ }
+ },
+ primary_action_label: "Set"
+ });
+
+ if (is_document_type) {
+ frm.filter_group = new frappe.ui.FilterGroup({
+ parent: dialog.get_field('filter_area').$wrapper,
+ doctype: frm.doc.document_type,
+ on_change: () => {},
+ });
+ filters && frm.filter_group.add_filters_to_filter_group(filters);
+ }
+
+ dialog.show();
+
+ if (frm.doc.type == 'Report') {
+ //Set query report object so that it can be used while fetching filter values in the report
+ frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
+ frappe.query_reports[frm.doc.report_name]
+ && frappe.query_reports[frm.doc.report_name].onload
+ && frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
+ }
+
+ dialog.set_values(filters);
+ });
+
+ },
+
+ render_dynamic_filters_table(frm) {
+ if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') {
+ return;
+ }
+
+ frm.set_df_property("dynamic_filters_section", "hidden", 0);
+
+ let is_document_type = frm.doc.type == 'Document Type';
+
+ let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty();
+
+ frm.dynamic_filter_table = $(`
+
+
+ | ${__('Filter')} |
+ ${__('Condition')} |
${__('Value')} |
`).appendTo(wrapper);
- frm.filters = JSON.parse(frm.doc.filters_json || '[]');
+ frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
- frm.trigger('set_filters_in_table');
+ frm.trigger('set_dynamic_filters_in_table');
- frm.filter_table.on('click', () => {
+ let filters = JSON.parse(frm.doc.filters_json || '[]');
+
+ let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
+ is_document_type, filters, frm.dynamic_filters
+ );
+
+ frm.dynamic_filter_table.on('click', () => {
let dialog = new frappe.ui.Dialog({
- title: __('Set Filters'),
- fields: [{
- fieldtype: 'HTML',
- fieldname: 'filter_area',
- }],
- primary_action: function() {
- let values = this.get_values();
- if (values) {
- this.hide();
- frm.filters = frm.filter_group.get_filters();
- frm.set_value('filters_json', JSON.stringify(frm.filters));
- frm.trigger('set_filters_in_table');
+ title: __('Set Dynamic Filters'),
+ fields: fields,
+ primary_action: () => {
+ let values = dialog.get_values();
+ dialog.hide();
+ let dynamic_filters = [];
+ for (let key of Object.keys(values)) {
+ if (is_document_type) {
+ let [doctype, fieldname] = key.split(':');
+ dynamic_filters.push([doctype, fieldname, '=', values[key]]);
+ }
}
+
+ if (is_document_type) {
+ frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters));
+ } else {
+ frm.set_value('dynamic_filters_json', JSON.stringify(values));
+ }
+ frm.trigger('set_dynamic_filters_in_table');
},
primary_action_label: "Set"
});
- frappe.dashboards.filters_dialog = dialog;
-
- frm.filter_group = new frappe.ui.FilterGroup({
- parent: dialog.get_field('filter_area').$wrapper,
- doctype: frm.doc.document_type,
- on_change: () => {},
- });
-
- frm.filter_group.add_filters_to_filter_group(frm.filters);
-
dialog.show();
- dialog.set_values(frm.filters);
+ dialog.set_values(frm.dynamic_filters);
});
-
},
- set_filters_in_table: function(frm) {
- if (!frm.filters.length) {
+ set_dynamic_filters_in_table: function(frm) {
+ frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
+ ? JSON.parse(frm.doc.dynamic_filters_json)
+ : null;
+
+ if (!frm.dynamic_filters) {
const filter_row = $(`|
- ${__("Click to Set Filters")} |
`);
- frm.filter_table.find('tbody').html(filter_row);
+ ${__("Click to Set Dynamic Filters")}`);
+ frm.dynamic_filter_table.find('tbody').html(filter_row);
} else {
let filter_rows = '';
- frm.filters.forEach(filter => {
- filter_rows +=
- `
- | ${filter[1]} |
- ${filter[2] || ""} |
- ${filter[3]} |
-
`;
+ if ($.isArray(frm.dynamic_filters)) {
+ frm.dynamic_filters.forEach(filter => {
+ filter_rows +=
+ `
+ | ${filter[1]} |
+ ${filter[2] || ""} |
+ ${filter[3]} |
+
`;
+ });
+ } else {
+ let condition = '=';
+ for (let [key, val] of Object.entries(frm.dynamic_filters)) {
+ filter_rows +=
+ `
+ | ${key} |
+ ${condition} |
+ ${val || ""} |
+
`
+ ;
+ }
+ }
- });
- frm.filter_table.find('tbody').html(filter_rows);
+ frm.dynamic_filter_table.find('tbody').html(filter_rows);
}
}
+
});
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index ec6a1e9190..d3e9598eb7 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -1,39 +1,53 @@
{
"actions": [],
+ "allow_rename": 1,
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "is_standard",
+ "module",
"label",
+ "type",
+ "report_name",
+ "method",
"function",
"aggregate_function_based_on",
"column_break_2",
"document_type",
+ "report_field",
+ "report_function",
"is_public",
+ "custom_configuration_section",
+ "filters_config",
"stats_section",
"show_percentage_stats",
"stats_time_interval",
"filters_section",
"filters_json",
+ "dynamic_filters_section",
+ "dynamic_filters_json",
+ "section_break_16",
"color"
],
"fields": [
{
+ "depends_on": "eval: doc.type == 'Document Type'",
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
- "options": "DocType",
- "reqd": 1
+ "mandatory_depends_on": "eval: doc.type == 'Document Type'",
+ "options": "DocType"
},
{
- "depends_on": "eval: doc.document_type",
+ "depends_on": "eval: doc.type == 'Document Type'",
"fieldname": "function",
"fieldtype": "Select",
"label": "Function",
- "options": "Count\nSum\nAverage\nMinimum\nMaximum",
- "reqd": 1
+ "mandatory_depends_on": "eval: doc.type == 'Document Type'",
+ "options": "Count\nSum\nAverage\nMinimum\nMaximum"
},
{
"depends_on": "eval: doc.function !== 'Count'",
@@ -92,13 +106,92 @@
"options": "Daily\nWeekly\nMonthly\nYearly"
},
{
+ "depends_on": "eval: doc.type == 'Document Type'",
"fieldname": "stats_section",
"fieldtype": "Section Break",
"label": "Stats"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard",
+ "read_only_depends_on": "eval: !frappe.boot.developer_mode"
+ },
+ {
+ "depends_on": "eval: doc.is_standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "mandatory_depends_on": "eval: doc.is_standard",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "dynamic_filters_json",
+ "fieldtype": "Code",
+ "label": "Dynamic Filters JSON",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "dynamic_filters_section",
+ "fieldtype": "Section Break",
+ "label": "Dynamic Filters Section"
+ },
+ {
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "label": "Type",
+ "options": "Document Type\nReport\nCustom"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Report'",
+ "fieldname": "report_name",
+ "fieldtype": "Link",
+ "label": "Report Name",
+ "mandatory_depends_on": "eval: doc.type == 'Report'",
+ "options": "Report"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Report'",
+ "fieldname": "report_field",
+ "fieldtype": "Select",
+ "label": "Field",
+ "mandatory_depends_on": "eval: doc.type == 'Report'"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Custom'",
+ "fieldname": "method",
+ "fieldtype": "Data",
+ "label": "Method",
+ "mandatory_depends_on": "eval: doc.type == 'Custom'"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Custom'",
+ "fieldname": "custom_configuration_section",
+ "fieldtype": "Section Break",
+ "label": "Custom Configuration"
+ },
+ {
+ "fieldname": "filters_config",
+ "fieldtype": "Code",
+ "label": "Filters Configuration",
+ "options": "JSON"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Report'",
+ "fieldname": "report_function",
+ "fieldtype": "Select",
+ "label": "Function",
+ "mandatory_depends_on": "eval: doc.type == 'Report'",
+ "options": "Sum\nAverage\nMinimum\nMaximum"
}
],
"links": [],
- "modified": "2020-05-06 19:47:57.753574",
+ "modified": "2020-07-23 11:11:03.391719",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index c4a427c4e0..68ed79e64b 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
+from frappe.modules.export_file import export_to_files
class NumberCard(Document):
def autoname(self):
@@ -16,6 +17,10 @@ class NumberCard(Document):
if frappe.db.exists("Number Card", self.name):
self.name = append_number_if_name_exists('Number Card', self.name)
+ def on_update(self):
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files(record_list=[['Number Card', self.name]], record_module=self.module)
+
def get_permission_query_conditions(user=None):
if not user:
user = frappe.session.user
@@ -27,13 +32,17 @@ def get_permission_query_conditions(user=None):
if "System Manager" in roles:
return None
- allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()]
+ doctype_condition = False
+
+ allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
+
+ if allowed_doctypes:
+ doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
+ allowed_doctypes=','.join(allowed_doctypes))
return '''
- `tabNumber Card`.`document_type` in ({allowed_doctypes})
- '''.format(
- allowed_doctypes=','.join(allowed_doctypes)
- )
+ {doctype_condition}
+ '''.format(doctype_condition=doctype_condition)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
@@ -47,7 +56,7 @@ def has_permission(doc, ptype, user):
return False
@frappe.whitelist()
-def get_result(doc, to_date=None):
+def get_result(doc, filters, to_date=None):
doc = frappe.parse_json(doc)
fields = []
sql_function_map = {
@@ -65,10 +74,13 @@ def get_result(doc, to_date=None):
else:
fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)]
- filters = frappe.parse_json(doc.filters_json)
+ filters = frappe.parse_json(filters)
+
+ if not filters:
+ filters = []
if to_date:
- filters.append([doc.document_type, 'creation', '<', to_date, False])
+ filters.append([doc.document_type, 'creation', '<', to_date])
res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters)
number = res[0]['result'] if res else 0
@@ -76,7 +88,7 @@ def get_result(doc, to_date=None):
return cint(number)
@frappe.whitelist()
-def get_percentage_difference(doc, result):
+def get_percentage_difference(doc, filters, result):
doc = frappe.parse_json(doc)
result = frappe.parse_json(result)
@@ -85,13 +97,13 @@ def get_percentage_difference(doc, result):
if not doc.get('show_percentage_stats'):
return
- previous_result = calculate_previous_result(doc)
+ previous_result = calculate_previous_result(doc, filters)
difference = (result - previous_result)/100.0
return difference
-def calculate_previous_result(doc):
+def calculate_previous_result(doc, filters):
from frappe.utils import add_to_date
current_date = frappe.utils.now()
@@ -104,7 +116,7 @@ def calculate_previous_result(doc):
else:
previous_date = add_to_date(current_date, years=-1)
- number = get_result(doc, previous_date)
+ number = get_result(doc, filters, previous_date)
return number
@frappe.whitelist()
@@ -116,11 +128,15 @@ def create_number_card(args):
doc.insert(ignore_permissions=True)
return doc
+@frappe.whitelist()
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
meta = frappe.get_meta(doctype)
searchfields = meta.get_search_fields()
search_conditions = []
+ if not frappe.db.exists('DocType', doctype):
+ return
+
if txt:
for field in searchfields:
search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt))
@@ -147,3 +163,28 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
search_conditions=search_conditions,
conditions=conditions
), values)
+
+@frappe.whitelist()
+def create_report_number_card(args):
+ card = create_number_card(args)
+ args = frappe.parse_json(args)
+ args.name = card.name
+ if args.dashboard:
+ add_card_to_dashboard(frappe.as_json(args))
+
+@frappe.whitelist()
+def add_card_to_dashboard(args):
+ args = frappe.parse_json(args)
+
+ dashboard = frappe.get_doc('Dashboard', args.dashboard)
+ dashboard_link = frappe.new_doc('Number Card Link')
+ dashboard_link.card = args.name
+
+ if args.set_standard:
+ card = frappe.get_doc('Number Card', dashboard_link.card)
+ card.is_standard = 1
+ card.module = dashboard.module
+ card.save()
+
+ dashboard.append('cards', dashboard_link)
+ dashboard.save()
\ No newline at end of file
diff --git a/frappe/desk/doctype/tag/tag.json b/frappe/desk/doctype/tag/tag.json
index 895516594e..ad9838d10f 100644
--- a/frappe/desk/doctype/tag/tag.json
+++ b/frappe/desk/doctype/tag/tag.json
@@ -1,4 +1,5 @@
{
+ "allow_rename": 1,
"autoname": "Prompt",
"creation": "2016-05-25 09:43:44.767581",
"doctype": "DocType",
@@ -46,4 +47,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index a121e71dc8..5bae49ea95 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
@frappe.whitelist()
-def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=None):
+def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
"""
Get all nested submitted linked doctype linkinfo
@@ -31,34 +31,27 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non
if not docs:
docs = []
- if not linked:
- linked = {}
-
if not visited:
- visited = []
+ visited = {}
- if name in visited:
+ if doctype not in visited:
+ visited[doctype] = []
+
+ if name in visited[doctype]:
return
linkinfo = get_linked_doctypes(doctype)
linked_docs = get_linked_docs(doctype, name, linkinfo)
link_count = 0
- visited.append(name)
-
+ visited[doctype].append(name)
+
for link_doctype, link_names in linked_docs.items():
- if link_doctype not in linked:
- linked[link_doctype] = []
for link in link_names:
if link['name'] == name:
continue
- if linked and name in linked[link_doctype]:
- continue
-
- linked[link_doctype].append(link['name'])
-
docinfo = link.update({"doctype": link_doctype})
validated_doc = validate_linked_doc(docinfo)
@@ -66,10 +59,8 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non
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, linked, visited)
+ links = get_submitted_linked_docs(link_doctype, link.name, docs, visited)
if links:
docs.append({
"doctype": link_doctype,
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index f24f33df07..cacbd3c633 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
"shared": frappe.share.get_users(doc.doctype, doc.name),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
+ "additional_timeline_content": get_additional_timeline_content(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),
@@ -277,3 +278,14 @@ def get_document_email(doctype, name):
def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")
+
+def get_additional_timeline_content(doctype, docname):
+ contents = []
+ hooks = frappe.get_hooks().get('additional_timeline_content', {})
+ methods_for_all_doctype = hooks.get('*', [])
+ methods_for_current_doctype = hooks.get(doctype, [])
+
+ for method in methods_for_all_doctype + methods_for_current_doctype:
+ contents.extend(frappe.get_attr(method)(doctype, docname) or [])
+
+ return contents
\ No newline at end of file
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index 694b44b907..cae1bf5c77 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -18,12 +18,7 @@ def savedocs(doc, action):
if doc.docstatus==1:
doc.submit()
else:
- try:
- doc.save()
- except frappe.NameError as e:
- doctype, name, original_exception = e if isinstance(e, tuple) else (doc.doctype or "", doc.name or "", None)
- frappe.msgprint(frappe._("{0} {1} already exists").format(doctype, name))
- raise
+ doc.save()
# update recent documents
run_onload(doc)
diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py
index 1ebf32febe..e5654c853f 100644
--- a/frappe/desk/leaderboard.py
+++ b/frappe/desk/leaderboard.py
@@ -14,13 +14,16 @@ def get_leaderboards():
return leaderboards
@frappe.whitelist()
-def get_energy_point_leaderboard(from_date, company = None, field = None, limit = None):
+def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None):
+ filters = [
+ ['type', '!=', 'Review'],
+ ]
+ if date_range:
+ date_range = frappe.parse_json(date_range)
+ filters.append(['creation', 'between', [date_range[0], date_range[1]]])
energy_point_users = frappe.db.get_all('Energy Point Log',
fields = ['user as name', 'sum(points) as value'],
- filters = [
- ['type', '!=', 'Review'],
- ['creation', '>', from_date]
- ],
+ filters = filters,
group_by = 'user',
order_by = 'value desc'
)
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index 4472a2978a..189949ac68 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -49,7 +49,7 @@ class Leaderboard {
this.timespans = [
"This Week", "This Month", "This Quarter", "This Year",
"Last Week", "Last Month", "Last Quarter", "Last Year",
- "All Time", "Select From Date"
+ "All Time", "Select Date Range"
];
// for saving current selected filters
@@ -113,7 +113,7 @@ class Leaderboard {
return {"label": __(d), value: d };
})
);
- this.create_from_date_field();
+ this.create_date_range_field();
this.type_select = this.page.add_select(__("Field"),
this.options.selected_filter.map(d => {
@@ -123,12 +123,12 @@ class Leaderboard {
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
- if (this.options.selected_timespan === 'Select From Date') {
- this.from_date_field.show();
+ if (this.options.selected_timespan === 'Select Date Range') {
+ this.date_range_field.show();
} else {
- this.from_date_field.hide();
- this.make_request();
+ this.date_range_field.hide();
}
+ this.make_request();
});
this.type_select.on("change", (e) => {
@@ -137,21 +137,21 @@ class Leaderboard {
});
}
- create_from_date_field() {
+ create_date_range_field() {
let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
- this.from_date_field = $(``).insertAfter(timespan_field).hide();
+ this.date_range_field = $(``).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
df: {
- fieldtype: 'Date',
- fieldname: 'selected_from_date',
- placeholder: frappe.datetime.month_start(),
- default: frappe.datetime.month_start(),
+ fieldtype: 'DateRange',
+ fieldname: 'selected_date_range',
+ placeholder: "Date Range",
+ default: [frappe.datetime.month_start(), frappe.datetime.now_date()],
input_class: 'input-sm',
reqd: 1,
change: () => {
- this.selected_from_date = date_field.get_value();
- if (this.selected_from_date) this.make_request();
+ this.selected_date_range = date_field.get_value();
+ if (this.selected_date_range) this.make_request();
}
},
parent: $(this.parent).find('.from-date-field'),
@@ -225,7 +225,7 @@ class Leaderboard {
frappe.call(
this.leaderboard_config[this.options.selected_doctype].method,
{
- 'from_date': this.get_from_date(),
+ 'date_range': this.get_date_range(),
'company': this.options.selected_company,
'field': this.options.selected_filter_item,
'limit': this.leaderboard_limit,
@@ -375,23 +375,22 @@ class Leaderboard {
`);
}
- get_from_date() {
+ get_date_range() {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
- let get_from_date = {
- "this week": frappe.datetime.week_start(),
- "this month": frappe.datetime.month_start(),
- "this quarter": frappe.datetime.quarter_start(),
- "this year": frappe.datetime.year_start(),
- "last week": frappe.datetime.add_days(current_date, -7),
- "last month": frappe.datetime.add_months(current_date, -1),
- "last quarter": frappe.datetime.add_months(current_date, -3),
- "last year": frappe.datetime.add_months(current_date, -12),
- "all time": "",
- "select from date": this.selected_from_date || frappe.datetime.month_start()
+ let date_range_map = {
+ "this week": [frappe.datetime.week_start(), current_date],
+ "this month": [frappe.datetime.month_start(), current_date],
+ "this quarter": [frappe.datetime.quarter_start(), current_date],
+ "this year": [frappe.datetime.year_start(), current_date],
+ "last week": [frappe.datetime.add_days(current_date, -7), current_date],
+ "last month": [frappe.datetime.add_months(current_date, -1), current_date],
+ "last quarter": [frappe.datetime.add_months(current_date, -3), current_date],
+ "last year": [frappe.datetime.add_months(current_date, -12), current_date],
+ "all time": null,
+ "select date range": this.selected_date_range || [frappe.datetime.month_start(), current_date]
}
-
- return get_from_date[timespan];
+ return date_range_map[timespan];
}
}
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 0edfd57d4f..d0a32ef076 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -67,8 +67,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
# Reordered columns
columns = json.loads(report.custom_columns)
- if report.report_type == 'Query Report':
- result = reorder_data_for_custom_columns(columns, query_columns, result)
+ result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type)
result = add_data_to_custom_columns(columns, result)
@@ -216,15 +215,21 @@ def add_data_to_custom_columns(columns, result):
return data
-def reorder_data_for_custom_columns(custom_columns, columns, result):
+def reorder_data_for_custom_columns(custom_columns, columns, result, report_type):
+ custom_column_labels = [col["label"] for col in custom_columns]
+
+ if report_type == 'Query Report':
+ original_column_labels = [col.split(":")[0] for col in columns]
+ else:
+ original_column_labels = [col["label"] for col in columns]
+
reordered_result = []
- columns = [col.split(":")[0] for col in columns]
for res in result:
r = []
- for col in custom_columns:
+ for col_name in custom_column_labels:
try:
- idx = columns.index(col.get("label"))
+ idx = original_column_labels.index(col_name)
r.append(res[idx])
except ValueError:
pass
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index dd5c9b7ab7..b4b54b4b6e 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe, json
from frappe.utils import cstr, unique, cint
from frappe.permissions import has_permission
+from frappe.handler import is_whitelisted
from frappe import _
from six import string_types
import re
@@ -74,8 +75,17 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
if query and query.split()[0].lower()!="select":
# by method
- frappe.response["values"] = frappe.call(query, doctype, txt,
- searchfield, start, page_length, filters, as_dict=as_dict)
+ try:
+ is_whitelisted(frappe.get_attr(query))
+ frappe.response["values"] = frappe.call(query, doctype, txt,
+ searchfield, start, page_length, filters, as_dict=as_dict)
+ except Exception as e:
+ if frappe.local.conf.developer_mode:
+ raise e
+ else:
+ frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
+ indicator_color='red', http_status_code=404)
+ return
elif not query and doctype in standard_queries:
# from standard queries
search_widget(doctype, txt, standard_queries[doctype][0],
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index d58b35040e..0b874a03bb 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -57,6 +57,7 @@ def relink(name, reference_doctype=None, reference_name=None):
communication_type = "Communication" and
name = %s""", (reference_doctype, reference_name, name))
+@frappe.whitelist()
def get_communication_doctype(doctype, txt, searchfield, start, page_len, filters):
user_perms = frappe.utils.user.UserPermissions(frappe.session.user)
user_perms.build_permissions()
diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js
index 71f9cccb0d..83896e0af7 100644
--- a/frappe/email/doctype/email_account/email_account.js
+++ b/frappe/email/doctype/email_account/email_account.js
@@ -95,6 +95,11 @@ frappe.ui.form.on("Email Account", {
enable_incoming: function(frm) {
frm.doc.no_remaining = null; //perform full sync
//frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming);
+ frm.trigger("warn_autoreply_on_incoming");
+ },
+
+ enable_auto_reply: function(frm) {
+ frm.trigger("warn_autoreply_on_incoming");
},
notify_if_unreplied: function(frm) {
@@ -184,7 +189,18 @@ frappe.ui.form.on("Email Account", {
read as well as unread message from server. This may also cause the duplication\
of Communication (emails).");
frappe.confirm(msg, null, function() {
- frm.set_value("email_sync_option", "ALL");
+ frm.set_value("email_sync_option", "UNSEEN");
+ });
+ }
+ },
+
+ warn_autoreply_on_incoming: function(frm) {
+ if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
+ var msg = __("Enabling auto reply on an incoming email account will send automated replies \
+ to all the synchronized emails. Do you wish to continue?");
+ frappe.confirm(msg, null, function() {
+ frm.set_value("enable_auto_reply", 0);
+ frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
});
}
}
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 2065f5558a..cf8c6e80c6 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -478,26 +478,38 @@ class EmailAccount(Document):
if self.append_to and self.sender_field:
if self.subject_field:
- # try and match by subject and sender
- # if sent by same sender with same subject,
- # append it to old coversation
- subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
- "", email.subject, 0, flags=re.IGNORECASE)))
+ if '#' in email.subject:
+ # try and match if ID is found
+ # document ID is appended to subject
+ # example "Re: Your email (#OPP-2020-2334343)"
+ parent_id = email.subject.rsplit('#', 1)[-1].strip(' ()')
+ if parent_id:
+ parent = frappe.db.get_all(self.append_to, filters = dict(name = parent_id),
+ fields = 'name')
- parent = frappe.db.get_all(self.append_to, filters={
- self.sender_field: email.from_email,
- self.subject_field: ("like", "%{0}%".format(subject)),
- "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
- }, fields="name")
+ if not parent:
+ # try and match by subject and sender
+ # if sent by same sender with same subject,
+ # append it to old coversation
+ subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
+ "", email.subject, 0, flags=re.IGNORECASE)))
+
+ parent = frappe.db.get_all(self.append_to, filters={
+ self.sender_field: email.from_email,
+ self.subject_field: ("like", "%{0}%".format(subject)),
+ "creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
+ }, fields = "name", limit = 1)
- # match only subject field
- # when the from_email is of a user in the system
- # and subject is atleast 10 chars long
if not parent and len(subject) > 10 and is_system_user(email.from_email):
+ # match only subject field
+ # when the from_email is of a user in the system
+ # and subject is atleast 10 chars long
parent = frappe.db.get_all(self.append_to, filters={
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
- }, fields="name")
+ }, fields = "name", limit = 1)
+
+
if parent:
parent = frappe._dict(doctype=self.append_to, name=parent[0].name)
diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py
index 29b54d7f8b..f87ee32bb1 100644
--- a/frappe/email/doctype/email_account/test_email_account.py
+++ b/frappe/email/doctype/email_account/test_email_account.py
@@ -5,7 +5,10 @@ from __future__ import unicode_literals
import frappe, os
import unittest, email
-test_records = frappe.get_test_records('Email Account')
+from frappe.test_runner import make_test_records
+
+make_test_records("User")
+make_test_records("Email Account")
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json
index ee8683af22..4529ea8211 100644
--- a/frappe/email/doctype/email_queue/email_queue.json
+++ b/frappe/email/doctype/email_queue/email_queue.json
@@ -1,640 +1,166 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
+ "actions": [],
"autoname": "hash",
- "beta": 0,
"creation": "2012-08-02 15:17:28",
- "custom": 0,
"description": "Email Queue records.",
- "docstatus": 0,
"doctype": "DocType",
"document_type": "System",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "sender",
+ "recipients",
+ "show_as_cc",
+ "message",
+ "status",
+ "error",
+ "message_id",
+ "reference_doctype",
+ "reference_name",
+ "communication",
+ "send_after",
+ "priority",
+ "add_unsubscribe_link",
+ "unsubscribe_param",
+ "unsubscribe_method",
+ "expose_recipients",
+ "attachments",
+ "retry"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "sender",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
+ "ignore_xss_filter": 1,
"label": "Sender",
- "length": 0,
- "no_copy": 0,
- "options": "Email",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Email"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "recipients",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Recipient",
- "length": 0,
- "no_copy": 0,
- "options": "Email Queue Recipient",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Email Queue Recipient"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "show_as_cc",
"fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Show as cc",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Show as cc"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "message",
"fieldtype": "Code",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Message",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Message"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Not Sent",
"fieldname": "status",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "\nNot Sent\nSending\nSent\nError\nExpired",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "\nNot Sent\nSending\nSent\nError\nExpired"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "error",
"fieldtype": "Code",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Error",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Error"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "message_id",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Message ID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Reference Document Type",
- "length": 0,
- "no_copy": 0,
"options": "DocType",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "reference_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Reference DocName",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "communication",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Communication",
- "length": 0,
- "no_copy": 0,
"options": "Communication",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "send_after",
"fieldtype": "Datetime",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Send After",
- "length": 0,
"no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "priority",
"fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Priority",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "add_unsubscribe_link",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Add Unsubscribe Link",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Add Unsubscribe Link"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "unsubscribe_param",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Unsubscribe Param",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "unsubscribe_method",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Unsubscribe Method",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Unsubscribe Method"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "expose_recipients",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expose Recipients",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Expose Recipients"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "attachments",
"fieldtype": "Code",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Attachments",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
"fieldname": "retry",
"fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Retry",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "read_only": 1
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"icon": "fa fa-envelope",
"idx": 1,
- "image_view": 0,
"in_create": 1,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-09-05 14:22:27.664645",
+ "links": [],
+ "modified": "2020-07-17 15:58:15.369419",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
"delete": 1,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "System Manager"
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
+ "sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index a82b52a663..48688afdb6 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -107,6 +107,9 @@ class Newsletter(WebsiteGenerator):
if self.get("__islocal"):
throw(_("Please save the Newsletter before sending"))
+ if not self.recipients:
+ frappe.throw(_("Newsletter should have at least one recipient"))
+
def get_context(self, context):
newsletters = get_newsletter_list("Newsletter", None, None, 0)
if newsletters:
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index 8340d81917..d545190c47 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -11,6 +11,7 @@ import email.utils
from six import iteritems, text_type, string_types
from email.mime.multipart import MIMEMultipart
from email.header import Header
+from email import policy
def get_email(recipients, sender='', msg='', subject='[No Subject]',
@@ -68,8 +69,8 @@ class EMail:
self.subject = subject
self.expose_recipients = expose_recipients
- self.msg_root = MIMEMultipart('mixed')
- self.msg_alternative = MIMEMultipart('alternative')
+ self.msg_root = MIMEMultipart('mixed', policy=policy.SMTPUTF8)
+ self.msg_alternative = MIMEMultipart('alternative', policy=policy.SMTPUTF8)
self.msg_root.attach(self.msg_alternative)
self.cc = cc or []
self.bcc = bcc or []
@@ -100,7 +101,7 @@ class EMail:
Attach message in the text portion of multipart/alternative
"""
from email.mime.text import MIMEText
- part = MIMEText(message, 'plain', 'utf-8')
+ part = MIMEText(message, 'plain', 'utf-8', policy=policy.SMTPUTF8)
self.msg_alternative.attach(part)
def set_part_html(self, message, inline_images):
@@ -113,9 +114,9 @@ class EMail:
message, _inline_images = replace_filename_with_cid(message)
# prepare parts
- msg_related = MIMEMultipart('related')
+ msg_related = MIMEMultipart('related', policy=policy.SMTPUTF8)
- html_part = MIMEText(message, 'html', 'utf-8')
+ html_part = MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8)
msg_related.attach(html_part)
for image in _inline_images:
@@ -124,7 +125,7 @@ class EMail:
self.msg_alternative.attach(msg_related)
else:
- self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8'))
+ self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8))
def set_html_as_text(self, html):
"""Set plain text from HTML"""
@@ -135,7 +136,7 @@ class EMail:
from email.mime.text import MIMEText
maintype, subtype = mime_type.split('/')
- part = MIMEText(message, _subtype = subtype)
+ part = MIMEText(message, _subtype = subtype, policy=policy.SMTPUTF8)
if as_attachment:
part.add_header('Content-Disposition', 'attachment', filename=filename)
@@ -222,7 +223,8 @@ class EMail:
# reset headers as values may be changed.
for key, val in iteritems(headers):
- self.set_header(key, val)
+ if val:
+ self.set_header(key, val)
# call hook to enable apps to modify msg_root before sending
for hook in frappe.get_hooks("make_email_body_message"):
@@ -238,7 +240,7 @@ class EMail:
"""validate, build message and convert to string"""
self.validate()
self.make()
- return self.msg_root.as_string()
+ return self.msg_root.as_string(policy=policy.SMTPUTF8)
def get_formatted_html(subject, message, footer=None, print_html=None,
email_account=None, header=None, unsubscribe_link=None, sender=None):
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index ce512de276..8bffc108b9 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -347,7 +347,7 @@ def flush(from_test=False):
if not smtpserver:
smtpserver = SMTPServer()
smtpserver_dict[email.sender] = smtpserver
-
+
if from_test:
send_one(email.name, smtpserver, auto_commit)
else:
@@ -390,12 +390,12 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
where
name=%s
for update''', email, as_dict=True)
-
+
if len(email):
email = email[0]
else:
return
-
+
recipients_list = frappe.db.sql('''select name, recipient, status from
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
@@ -417,6 +417,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
if email.communication:
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
+ email_sent_to_any_recipient = None
+
try:
message = None
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 43c4bb8333..705a853bc6 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -39,7 +39,7 @@ This is the text version of this email
subject='Test Subject',
content=email_html,
text_content=email_text
- ).as_string()
+ ).as_string().replace("\r\n", "\n")
def test_prepare_message_returns_already_encoded_string(self):
@@ -153,7 +153,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
subject='Test Subject',
content=email_html,
header=['Email Title', 'green']
- ).as_string()
+ ).as_string().replace("\r\n", "\n")
self.assertTrue('''${__('Click here')}`]));
+ }
+});
diff --git a/frappe/integrations/doctype/paytm_settings/paytm_settings.json b/frappe/integrations/doctype/paytm_settings/paytm_settings.json
new file mode 100644
index 0000000000..93fbd0df09
--- /dev/null
+++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.json
@@ -0,0 +1,89 @@
+{
+ "actions": [],
+ "creation": "2020-04-02 00:11:22.846697",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "merchant_id",
+ "merchant_key",
+ "staging",
+ "column_break_4",
+ "industry_type_id",
+ "website"
+ ],
+ "fields": [
+ {
+ "fieldname": "merchant_id",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Merchant ID",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "merchant_key",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Merchant Key",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "staging",
+ "fieldtype": "Check",
+ "label": "Staging",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "depends_on": "eval: !doc.staging",
+ "fieldname": "website",
+ "fieldtype": "Data",
+ "label": "Website",
+ "mandatory_depends_on": "eval: !doc.staging",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "depends_on": "eval: !doc.staging",
+ "fieldname": "industry_type_id",
+ "fieldtype": "Data",
+ "label": "Industry Type ID",
+ "mandatory_depends_on": "eval: !doc.staging",
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-06-08 13:36:09.703143",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "Paytm Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/paytm_settings/paytm_settings.py b/frappe/integrations/doctype/paytm_settings/paytm_settings.py
new file mode 100644
index 0000000000..616c3837d4
--- /dev/null
+++ b/frappe/integrations/doctype/paytm_settings/paytm_settings.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import json
+import requests
+from six.moves.urllib.parse import urlencode
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+from frappe.utils import get_url, call_hook_method, cint, flt, cstr
+from frappe.integrations.utils import create_request_log, create_payment_gateway
+from frappe.utils import get_request_site_address
+from paytmchecksum import generateSignature, verifySignature
+from frappe.utils.password import get_decrypted_password
+
+class PaytmSettings(Document):
+ supported_currencies = ["INR"]
+
+ def validate(self):
+ create_payment_gateway('Paytm')
+ call_hook_method('payment_gateway_enabled', gateway='Paytm')
+
+ def validate_transaction_currency(self, currency):
+ if currency not in self.supported_currencies:
+ frappe.throw(_("Please select another payment method. Paytm does not support transactions in currency '{0}'").format(currency))
+
+ def get_payment_url(self, **kwargs):
+ '''Return payment url with several params'''
+ # create unique order id by making it equal to the integration request
+ integration_request = create_request_log(kwargs, "Host", "Paytm")
+ kwargs.update(dict(order_id=integration_request.name))
+
+ return get_url("./integrations/paytm_checkout?{0}".format(urlencode(kwargs)))
+
+def get_paytm_config():
+ ''' Returns paytm config '''
+
+ paytm_config = frappe.db.get_singles_dict('Paytm Settings')
+ paytm_config.update(dict(merchant_key=get_decrypted_password('Paytm Settings', 'Paytm Settings', 'merchant_key')))
+
+ if cint(paytm_config.staging):
+ paytm_config.update(dict(
+ website="WEBSTAGING",
+ url='https://securegw-stage.paytm.in/order/process',
+ transaction_status_url='https://securegw-stage.paytm.in/order/status',
+ industry_type_id='RETAIL'
+ ))
+ else:
+ paytm_config.update(dict(
+ url='https://securegw.paytm.in/order/process',
+ transaction_status_url='https://securegw.paytm.in/order/status',
+ ))
+ return paytm_config
+
+def get_paytm_params(payment_details, order_id, paytm_config):
+
+ # initialize a dictionary
+ paytm_params = dict()
+
+ redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction"
+
+
+ paytm_params.update({
+ "MID" : paytm_config.merchant_id,
+ "WEBSITE" : paytm_config.website,
+ "INDUSTRY_TYPE_ID" : paytm_config.industry_type_id,
+ "CHANNEL_ID" : "WEB",
+ "ORDER_ID" : order_id,
+ "CUST_ID" : payment_details['payer_email'],
+ "EMAIL" : payment_details['payer_email'],
+ "TXN_AMOUNT" : cstr(flt(payment_details['amount'], 2)),
+ "CALLBACK_URL" : redirect_uri,
+ })
+
+ checksum = generateSignature(paytm_params, paytm_config.merchant_key)
+
+ paytm_params.update({
+ "CHECKSUMHASH" : checksum
+ })
+
+ return paytm_params
+
+@frappe.whitelist(allow_guest=True)
+def verify_transaction(**paytm_params):
+ '''Verify checksum for received data in the callback and then verify the transaction'''
+ paytm_config = get_paytm_config()
+ is_valid_checksum = False
+
+ paytm_params.pop('cmd', None)
+ paytm_checksum = paytm_params.pop('CHECKSUMHASH', None)
+
+ if paytm_params and paytm_config and paytm_checksum:
+ # Verify checksum
+ is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum)
+
+ if is_valid_checksum and paytm_params.get('RESPCODE') == '01':
+ verify_transaction_status(paytm_config, paytm_params['ORDERID'])
+ else:
+ frappe.respond_as_web_page("Payment Failed",
+ "Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.",
+ http_status_code=401, indicator_color='red')
+ frappe.log_error("Order unsuccessful. Failed Response:"+cstr(paytm_params), 'Paytm Payment Failed')
+
+def verify_transaction_status(paytm_config, order_id):
+ '''Verify transaction completion after checksum has been verified'''
+ paytm_params=dict(
+ MID=paytm_config.merchant_id,
+ ORDERID= order_id
+ )
+
+ checksum = generateSignature(paytm_params, paytm_config.merchant_key)
+ paytm_params["CHECKSUMHASH"] = checksum
+
+ post_data = json.dumps(paytm_params)
+ url = paytm_config.transaction_status_url
+
+ response = requests.post(url, data = post_data, headers = {"Content-type": "application/json"}).json()
+ finalize_request(order_id, response)
+
+def finalize_request(order_id, transaction_response):
+ request = frappe.get_doc('Integration Request', order_id)
+ transaction_data = frappe._dict(json.loads(request.data))
+ redirect_to = transaction_data.get('redirect_to') or None
+ redirect_message = transaction_data.get('redirect_message') or None
+
+ if transaction_response['STATUS'] == "TXN_SUCCESS":
+ if transaction_data.reference_doctype and transaction_data.reference_docname:
+ custom_redirect_to = None
+ try:
+ custom_redirect_to = frappe.get_doc(transaction_data.reference_doctype,
+ transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed')
+ request.db_set('status', 'Completed')
+ except Exception:
+ request.db_set('status', 'Failed')
+ frappe.log_error(frappe.get_traceback())
+
+ if custom_redirect_to:
+ redirect_to = custom_redirect_to
+
+ redirect_url = '/integrations/payment-success'
+ else:
+ request.db_set('status', 'Failed')
+ redirect_url = '/integrations/payment-failed'
+
+ if redirect_to:
+ redirect_url += '?' + urlencode({'redirect_to': redirect_to})
+ if redirect_message:
+ redirect_url += '&' + urlencode({'redirect_message': redirect_message})
+
+ frappe.local.response['type'] = 'redirect'
+ frappe.local.response['location'] = redirect_url
+
+def get_gateway_controller(doctype, docname):
+ reference_doc = frappe.get_doc(doctype, docname)
+ gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller")
+ return gateway_controller
\ No newline at end of file
diff --git a/frappe/core/doctype/video/test_video.py b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
similarity index 80%
rename from frappe/core/doctype/video/test_video.py
rename to frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
index 0bed1e98d6..77a16c82ae 100644
--- a/frappe/core/doctype/video/test_video.py
+++ b/frappe/integrations/doctype/paytm_settings/test_paytm_settings.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
-class TestVideo(unittest.TestCase):
+class TestPaytmSettings(unittest.TestCase):
pass
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
index 830afbae53..123bb21e88 100755
--- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
@@ -74,11 +74,11 @@
},
{
"default": "us-east-1",
- "description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.",
+ "description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.",
"fieldname": "region",
"fieldtype": "Select",
"label": "Region",
- "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1"
+ "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1"
},
{
"fieldname": "endpoint_url",
@@ -151,7 +151,7 @@
"hide_toolbar": 1,
"issingle": 1,
"links": [],
- "modified": "2020-04-13 20:57:24.432183",
+ "modified": "2020-07-27 17:27:21.400000",
"modified_by": "Administrator",
"module": "Integrations",
"name": "S3 Backup Settings",
@@ -172,4 +172,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
index 6cfd3646b2..c8b007ba7b 100755
--- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
@@ -19,6 +19,9 @@ from botocore.exceptions import ClientError
class S3BackupSettings(Document):
def validate(self):
+ if not self.enabled:
+ return
+
if not self.endpoint_url:
self.endpoint_url = 'https://s3.amazonaws.com'
conn = boto3.client(
diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py
index 887e191e16..161937a936 100644
--- a/frappe/integrations/frappe_providers/__init__.py
+++ b/frappe/integrations/frappe_providers/__init__.py
@@ -7,7 +7,7 @@ from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrato
def migrate_to(local_site, frappe_provider):
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
- return frappecloud_migrator(local_site, frappe_provider)
+ return frappecloud_migrator(local_site)
else:
print("{} is not supported yet".format(frappe_provider))
sys.exit(1)
diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py
index 16bc09d9bf..e09f09a44b 100644
--- a/frappe/integrations/frappe_providers/frappecloud.py
+++ b/frappe/integrations/frappe_providers/frappecloud.py
@@ -1,412 +1,29 @@
-# imports - standard imports
-import getpass
-import json
-import os
-import re
-import sys
-
-# imports - third party imports
import click
-from html2text import html2text
import requests
-from tenacity import retry, stop_after_attempt, wait_fixed
+from html2text import html2text
-# imports - module imports
import frappe
-import frappe.utils.backups
-from frappe.utils import get_installed_apps_info
-from frappe.utils.commands import render_table, add_line_after, add_line_before
-# TODO: check upgrade compatibility
-
-
-def render_actions_table():
- actions_table = [["#", "Action"]]
- actions = []
-
- for n, action in enumerate(migrator_actions):
- actions_table.append([n+1, action["title"]])
- actions.append(action["fn"])
-
- render_table(actions_table)
- return actions
-
-
-def render_site_table(sites_info):
- sites_table = [["#", "Site Name", "Status"]]
- available_sites = []
-
- for n, site_data in enumerate(sites_info):
- name, status = site_data["name"], site_data["status"]
- if status in ("Active", "Broken"):
- sites_table.append([n + 1, name, status])
- available_sites.append(name)
-
- render_table(sites_table)
- return available_sites
-
-
-def render_teams_table(teams):
- teams_table = [["#", "Team"]]
-
- for n, team in enumerate(teams):
- teams_table.append([n+1, team])
-
- render_table(teams_table)
-
-
-def render_plan_table(plans_list):
- plans_table = [["Plan", "CPU Time"]]
- visible_headers = ["name", "cpu_time_per_day"]
-
- for plan in plans_list:
- plan, cpu_time = [plan[header] for header in visible_headers]
- plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
-
- render_table(plans_table)
-
-
-def render_group_table(app_groups):
- # title row
- app_groups_table = [["#", "App Group", "Apps"]]
-
- # all rows
- for idx, app_group in enumerate(app_groups):
- apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
- row = [idx + 1, app_group["name"], apps_list]
- app_groups_table.append(row)
-
- render_table(app_groups_table)
-
-
-def handle_request_failure(request=None, message=None, traceback=True, exit_code=1):
- message = message or "Request failed with error code {}".format(request.status_code)
- response = html2text(request.text) if traceback else ""
-
- print("{0}{1}".format(message, "\n" + response))
- sys.exit(exit_code)
-
-
-@add_line_after
-def select_primary_action():
- actions = render_actions_table()
- idx = click.prompt("What do you want to do?", type=click.IntRange(1, len(actions))) - 1
-
- return actions[idx]
-
-
-@add_line_after
-def select_site():
- get_all_sites_request = session.post(all_site_url, headers={
- "accept": "application/json",
- "accept-encoding": "gzip, deflate, br",
- "content-type": "application/json; charset=utf-8"
- })
-
- if get_all_sites_request.ok:
- all_sites = get_all_sites_request.json()["message"]
- available_sites = render_site_table(all_sites)
-
- while True:
- selected_site = click.prompt("Name of the site you want to restore to", type=str).strip()
- if selected_site in available_sites:
- return selected_site
- else:
- print("Site {} does not exist. Try again ❌".format(selected_site))
- else:
- print("Couldn't retrive sites list...Try again later")
- sys.exit(1)
-
-
-@add_line_before
-def select_team(session):
- # get team options
- account_details_sc = session.post(account_details_url)
- if account_details_sc.ok:
- account_details = account_details_sc.json()["message"]
- available_teams = account_details["teams"]
-
- # ask if they want to select, go ahead with if only one exists
- if len(available_teams) == 1:
- team = available_teams[0]
- else:
- render_teams_table(available_teams)
- idx = click.prompt("Select Team", type=click.IntRange(1, len(available_teams))) - 1
- team = available_teams[idx]
-
- print("Team '{}' set for current session".format(team))
-
- return team
-
-
-@retry(stop=stop_after_attempt(5))
-def get_new_site_options():
- site_options_sc = session.post(options_url)
-
- if site_options_sc.ok:
- site_options = site_options_sc.json()["message"]
- return site_options
- else:
- print("Couldn't retrive New site information: {}".format(site_options_sc.status_code))
-
-
-def is_valid_subdomain(subdomain):
- if len(subdomain) < 5:
- print("Subdomain too short. Use 5 or more characters")
- return False
- matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain)
- if matched:
- return True
- print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens")
-
-
-@retry(stop=stop_after_attempt(5))
-def is_subdomain_available(subdomain):
- res = session.post(site_exists_url, {"subdomain": subdomain})
- if res.ok:
- available = not res.json()["message"]
- if not available:
- print("Subdomain already exists! Try another one")
-
- return available
-
-
-@add_line_after
-def choose_plan(plans_list):
- print("{} plans available".format(len(plans_list)))
- available_plans = [plan["name"] for plan in plans_list]
- render_plan_table(plans_list)
-
- while True:
- input_plan = click.prompt("Select Plan").strip()
- if input_plan in available_plans:
- print("{} Plan selected ✅".format(input_plan))
- return input_plan
- else:
- print("Invalid Selection ❌")
-
-
-@add_line_after
-def check_app_compat(available_group):
- is_compat = True
- incompatible_apps, filtered_apps, branch_msgs = [], [], []
- existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()]
- print("Checking availability of existing app group")
-
- for (app, branch) in existing_group:
- info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ]
- if info:
- app_title, available_branch = info[0]
-
- if branch != available_branch:
- print("⚠️ App {}:{} => {}".format(app, branch, available_branch))
- branch_msgs.append([app, branch, available_branch])
- filtered_apps.append(app_title)
- is_compat = False
-
- else:
- print("✅ App {}:{}".format(app, branch))
- filtered_apps.append(app_title)
-
- else:
- incompatible_apps.append(app)
- print("❌ App {}:{}".format(app, branch))
- is_compat = False
-
- start_msg = "\nSelecting this group will "
- incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else ""
- branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else ""
- changes = (incompatible_apps + branch_change) or "be perfect for you :)"
- warning_message = start_msg + changes
- print(warning_message)
-
- return is_compat, filtered_apps
-
-
-@add_line_after
-def filter_apps(app_groups):
- render_group_table(app_groups)
-
- while True:
- app_group_index = click.prompt("Select App Group Number", type=int) - 1
- try:
- if app_group_index == -1:
- raise IndexError
- selected_group = app_groups[app_group_index]
- except IndexError:
- print("Invalid Selection ❌")
- continue
-
- is_compat, filtered_apps = check_app_compat(selected_group)
-
- if is_compat or click.confirm("Continue anyway?"):
- print("App Group {} selected! ✅".format(selected_group["name"]))
- break
-
- return selected_group["name"], filtered_apps
-
-
-@add_line_after
-def get_subdomain(domain):
- while True:
- subdomain = click.prompt("Enter subdomain").strip()
- if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain):
- print("Site Domain: {}.{}".format(subdomain, domain))
- return subdomain
-
-
-@retry(stop=stop_after_attempt(2), wait=wait_fixed(5))
-def upload_backup_file(file_type, file_path):
- return session.post(files_url, data={}, files={
- "file": open(file_path, "rb"),
- "is_private": 1,
- "folder": "Home",
- "method": "press.api.site.upload_backup",
- "type": file_type
- })
-
-
-@add_line_after
-def upload_backup(local_site):
- # take backup
- files_session = {}
- print("Taking backup for site {}".format(local_site))
- odb = frappe.utils.backups.new_backup(ignore_files=False, force=True)
-
- # upload files
- for x, (file_type, file_path) in enumerate([
- ("database", odb.backup_path_db),
- ("public", odb.backup_path_files),
- ("private", odb.backup_path_private_files)
- ]):
- file_name = file_path.split(os.sep)[-1]
-
- print("Uploading {} file: {} ({}/3)".format(file_type, file_name, x+1))
- file_upload_response = upload_backup_file(file_type, file_path)
-
- if file_upload_response.ok:
- files_session[file_type] = file_upload_response.json()["message"]
- else:
- print("Upload failed for: {}".format(file_path))
-
- files_uploaded = { k: v["file_url"] for k, v in files_session.items() }
- print("Uploaded backup files! ✅")
-
- return files_uploaded
-
-
-def new_site(local_site):
- # get new site options
- site_options = get_new_site_options()
-
- # set preferences from site options
- subdomain = get_subdomain(site_options["domain"])
- plan = choose_plan(site_options["plans"])
-
- app_groups = site_options["groups"]
- selected_group, filtered_apps = filter_apps(app_groups)
- files_uploaded = upload_backup(local_site)
-
- # push to frappe_cloud
- payload = json.dumps({
- "site": {
- "apps": filtered_apps,
- "files": files_uploaded,
- "group": selected_group,
- "name": subdomain,
- "plan": plan
- }
- })
-
- session.headers.update({"Content-Type": "application/json; charset=utf-8"})
- site_creation_request = session.post(upload_url, payload)
-
- if site_creation_request.ok:
- site_url = site_creation_request.json()["message"]
- print("Your site {} is being migrated ✨".format(local_site))
- print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
- print("Your site URL: {}".format(site_url))
- else:
- handle_request_failure(site_creation_request)
-
-
-def restore_site(local_site):
- # get list of existing sites they can restore
- selected_site = select_site()
-
- # TODO: check if they can restore it
-
- click.confirm("This is an irreversible action. Are you sure you want to continue?", abort=True)
-
- # backup site
- files_uploaded = upload_backup(local_site)
-
- # push to frappe_cloud
- payload = json.dumps({
- "name": selected_site,
- "files": files_uploaded
- })
- headers = {"Content-Type": "application/json; charset=utf-8"}
- site_restore_request = session.post(restore_site_url, payload, headers=headers)
-
- if site_restore_request.ok:
- print("Your site {0} is being restored on {1} ✨".format(local_site, selected_site))
- print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, selected_site))
- print("Your site URL: {}".format(selected_site))
- else:
- handle_request_failure(site_restore_request)
-
-
-@add_line_after
-def create_session():
- print("Frappe Cloud credentials @ {}".format(remote_site))
-
- # take user input from STDIN
- username = click.prompt("Username").strip()
- password = getpass.unix_getpass()
-
- auth_credentials = {"usr": username, "pwd": password}
-
- session = requests.Session()
- login_sc = session.post(login_url, auth_credentials)
-
- if login_sc.ok:
- print("Authorization Successful! ✅")
- team = select_team(session)
- session.headers.update({
- "X-Press-Team": team,
- "Connection": "keep-alive"
- })
- return session
- else:
- handle_request_failure(message="Authorization Failed with Error Code {}".format(login_sc.status_code), traceback=False)
-
-
-def frappecloud_migrator(local_site, frappecloud_site):
- global login_url, upload_url, files_url, options_url, site_exists_url, restore_site_url, account_details_url, all_site_url
- global session, migrator_actions, remote_site
-
+def frappecloud_migrator(local_site):
+ print("Retreiving Site Migrator...")
remote_site = frappe.conf.frappecloud_url or "frappecloud.com"
+ request_url = "https://{}/api/method/press.api.script".format(remote_site)
+ request = requests.get(request_url)
- login_url = "https://{}/api/method/login".format(remote_site)
- upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
- files_url = "https://{}/api/method/upload_file".format(remote_site)
- options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
- site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
- account_details_url = "https://{}/api/method/press.api.account.get".format(remote_site)
- all_site_url = "https://{}/api/method/press.api.site.all".format(remote_site)
- restore_site_url = "https://{}/api/method/press.api.site.restore".format(remote_site)
+ if request.status_code / 100 != 2:
+ print("Request exitted with Status Code: {}\nPayload: {}".format(request.status_code, html2text(request.text)))
+ click.secho("Some errors occurred while recovering the migration script. Please contact us @ Frappe Cloud if this issue persists", fg="yellow")
+ return
- migrator_actions = [
- { "title": "Create a new site", "fn": new_site },
- { "title": "Restore to an existing site", "fn": restore_site }
- ]
+ script_contents = request.json()["message"]
- # get credentials + auth user + start session
- session = create_session()
+ import tempfile
+ import os
+ import sys
- # available actions defined in migrator_actions
- primary_action = select_primary_action()
-
- primary_action(local_site)
+ py = sys.executable
+ script = tempfile.NamedTemporaryFile(mode="w")
+ script.write(script_contents)
+ print("Site Migrator stored at {}".format(script.name))
+ os.execv(py, [py, script.name, local_site])
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index d7028870f4..09d303bec7 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -334,7 +334,7 @@ class BaseDocument(object):
self.db_insert()
return
- frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name))
+ frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
@@ -702,16 +702,13 @@ class BaseDocument(object):
df = self.meta.get_field(fieldname)
sanitized_value = value
- if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email":
- sanitized_value = sanitize_email(value)
+ if df and (df.get("ignore_xss_filter")
+ or (df.get("fieldtype") in ("Data", "Small Text", "Text") and df.get("options")=="Email")
+ or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code")
- elif df and (df.get("ignore_xss_filter")
- or (df.get("fieldtype")=="Code" and df.get("options")!="Email")
- or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode")
-
- # cancelled and submit but not update after submit should be ignored
- or self.docstatus==2
- or (self.docstatus==1 and not df.get("allow_on_submit"))):
+ # cancelled and submit but not update after submit should be ignored
+ or self.docstatus==2
+ or (self.docstatus==1 and not df.get("allow_on_submit"))):
continue
else:
diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py
index 2142d544fe..fcf648e718 100644
--- a/frappe/model/create_new.py
+++ b/frappe/model/create_new.py
@@ -45,7 +45,9 @@ def make_new_doc(doctype):
doc = doc.get_valid_dict(sanitize=False)
doc["doctype"] = doctype
doc["__islocal"] = 1
- doc["__unsaved"] = 1
+
+ if not frappe.model.meta.is_single(doctype):
+ doc["__unsaved"] = 1
return doc
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 19517aa4a1..ac87b1d907 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -203,7 +203,7 @@ class DatabaseQuery(object):
def sanitize_fields(self):
'''
regex : ^.*[,();].*
- purpose : The regex will look for malicious patterns like `,`, '(', ')', ';' in each
+ purpose : The regex will look for malicious patterns like `,`, '(', ')', '@', ;' in each
field which may leads to sql injection.
example :
field = "`DocType`.`issingle`, version()"
@@ -211,11 +211,11 @@ class DatabaseQuery(object):
the system will filter out this field.
'''
- sub_query_regex = re.compile("^.*[,();].*")
- blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case']
+ sub_query_regex = re.compile("^.*[,();@].*")
+ blacklisted_keywords = ['select', 'create', 'insert', 'delete', 'drop', 'update', 'case', 'show']
blacklisted_functions = ['concat', 'concat_ws', 'if', 'ifnull', 'nullif', 'coalesce',
'connection_id', 'current_user', 'database', 'last_insert_id', 'session_user',
- 'system_user', 'user', 'version']
+ 'system_user', 'user', 'version', 'global']
def _raise_exception():
frappe.throw(_('Use of sub-query or function is restricted'), frappe.DataError)
@@ -238,6 +238,10 @@ class DatabaseQuery(object):
if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions):
_raise_exception()
+ if '@' in field.lower():
+ # prevent access to global variables
+ _raise_exception()
+
if re.compile(r"[0-9a-zA-Z]+\s*'").match(field):
_raise_exception()
@@ -854,4 +858,4 @@ def get_date_range(operator, value):
timespan = period_map[operator] + ' ' + timespan_map[value] if operator != 'timespan' else value
- return get_timespan_date_range(timespan)
\ No newline at end of file
+ return get_timespan_date_range(timespan)
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 30d3442954..316c576f55 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -403,9 +403,16 @@ class Document(BaseDocument):
def set_new_name(self, force=False, set_name=None, set_child_names=True):
"""Calls `frappe.naming.set_new_name` for parent and child docs."""
+
if self.flags.name_set and not force:
return
+ # If autoname has set as Prompt (name)
+ if self.get("__newname"):
+ self.name = self.get("__newname")
+ self.flags.name_set = True
+ return
+
if set_name:
self.name = set_name
else:
@@ -830,7 +837,7 @@ class Document(BaseDocument):
def run_notifications(self, method):
"""Run notifications for this method"""
- if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install:
+ if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install:
return
if self.flags.notifications_executed==None:
@@ -1300,6 +1307,16 @@ class Document(BaseDocument):
users = set([assignment.owner for assignment in assignments])
return users
+ def add_tag(self, tag):
+ """Add a Tag to this document"""
+ from frappe.desk.doctype.tag.tag import DocTags
+ DocTags(self.doctype).add(self.name, tag)
+
+ def get_tags(self):
+ """Return a list of Tags attached to this document"""
+ from frappe.desk.doctype.tag.tag import DocTags
+ return DocTags(self.doctype).get_tags(self.name).split(",")[1:]
+
def execute_action(doctype, name, action, **kwargs):
"""Execute an action on a document (called by background worker)"""
doc = frappe.get_doc(doctype, name)
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index ea563dfc13..32919b3333 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -307,4 +307,4 @@ def set_workflow_state_on_action(doc, workflow_name, action):
for state in workflow.states:
if state.doc_status == docstatus:
doc.set(workflow_state_field, state.state)
- return
\ No newline at end of file
+ return
diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py
index b904132530..4b22c82105 100644
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -12,16 +12,17 @@ def export_doc(doc):
def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None):
"""
- Export record_list to files. record_list is a list of lists ([doctype],[docname] ) ,
+ Export record_list to files. record_list is a list of lists ([doctype, docname, folder name],) ,
"""
if frappe.flags.in_import:
return
if record_list:
for record in record_list:
- write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init)
+ folder_name = record[2] if len(record) == 3 else None
+ write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name)
-def write_document_file(doc, record_module=None, create_init=True):
+def write_document_file(doc, record_module=None, create_init=True, folder_name=None):
newdoc = doc.as_dict(no_nulls=True)
doc.run_method("before_export", newdoc)
@@ -35,7 +36,10 @@ def write_document_file(doc, record_module=None, create_init=True):
module = record_module or get_module_name(doc)
# create folder
- folder = create_folder(module, doc.doctype, doc.name, create_init)
+ if folder_name:
+ folder = create_folder(module, folder_name, doc.name, create_init)
+ else:
+ folder = create_folder(module, doc.doctype, doc.name, create_init)
# write the data file
fname = scrub(doc.name)
diff --git a/frappe/oauth.py b/frappe/oauth.py
index 4dc50366be..122c806072 100644
--- a/frappe/oauth.py
+++ b/frappe/oauth.py
@@ -4,6 +4,7 @@ import pytz
from frappe import _
from frappe.auth import LoginManager
+from http import cookies
from oauthlib.oauth2.rfc6749.tokens import BearerToken
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant
from oauthlib.oauth2 import RequestValidator
@@ -130,15 +131,12 @@ class OAuthWebRequestValidator(RequestValidator):
oac.scopes = get_url_delimiter().join(request.scopes)
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri
oac.client = client_id
- oac.user = unquote(cookie_dict['user_id'])
+ oac.user = unquote(cookie_dict['user_id'].value)
oac.authorization_code = code['code']
oac.save(ignore_permissions=True)
frappe.db.commit()
def authenticate_client(self, request, *args, **kwargs):
-
- cookie_dict = get_cookie_dict_from_headers(request)
-
#Get ClientID in URL
if request.client_id:
oc = frappe.get_doc("OAuth Client", request.client_id)
@@ -155,7 +153,9 @@ class OAuthWebRequestValidator(RequestValidator):
except Exception as e:
print("Failed body authentication: Application %s does not exist".format(cid=request.client_id))
- return frappe.session.user == unquote(cookie_dict.get('user_id', "Guest"))
+ cookie_dict = get_cookie_dict_from_headers(request)
+ user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest"
+ return frappe.session.user == user_id
def authenticate_client_id(self, client_id, request, *args, **kwargs):
cli_id = frappe.db.get_value('OAuth Client', client_id, 'name')
@@ -400,13 +400,10 @@ class OAuthWebRequestValidator(RequestValidator):
return True
def get_cookie_dict_from_headers(r):
+ cookie = cookies.BaseCookie()
if r.headers.get('Cookie'):
- cookie = r.headers.get('Cookie')
- cookie = cookie.split("; ")
- cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)}
- return cookie_dict
- else:
- return {}
+ cookie.load(r.headers.get('Cookie'))
+ return cookie
def calculate_at_hash(access_token, hash_alg):
"""Helper method for calculating an access token
diff --git a/frappe/patches.txt b/frappe/patches.txt
index a03d31918b..75750ab59c 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -19,6 +19,7 @@ execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02
+execute:frappe.reload_doc('core', 'doctype', 'has_role')
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
@@ -263,6 +264,7 @@ frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
frappe.patches.v12_0.setup_email_linking
frappe.patches.v12_0.fix_home_settings_for_all_users
frappe.patches.v12_0.change_existing_dashboard_chart_filters
+frappe.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13
execute:frappe.delete_doc("Test Runner")
execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings')
execute:frappe.db.set_default('desktop:home_page', 'workspace')
@@ -271,7 +273,9 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
+frappe.patches.v12_0.remove_example_email_thread_notify
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
+frappe.patches.v12_0.set_correct_url_in_files
frappe.patches.v13_0.website_theme_custom_scss
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
@@ -289,4 +293,7 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Field")
execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")
frappe.patches.v13_0.update_date_filters_in_user_settings
frappe.patches.v13_0.update_duration_options
-frappe.patches.v13_0.replace_old_data_import
+frappe.patches.v13_0.replace_old_data_import # 2020-06-24
+frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
+frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
+frappe.patches.v13_0.generate_theme_files_in_public_folder
diff --git a/frappe/patches/v11_0/reload_and_rename_view_log.py b/frappe/patches/v11_0/reload_and_rename_view_log.py
index 611de79a3c..12c71b746f 100644
--- a/frappe/patches/v11_0/reload_and_rename_view_log.py
+++ b/frappe/patches/v11_0/reload_and_rename_view_log.py
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe
def execute():
- if frappe.db.exists('DocType', 'View log'):
+ if frappe.db.table_exists('View log'):
# for mac users direct renaming would not work since mysql for mac saves table name in lower case
# so while renaming `tabView log` to `tabView Log` we get "Table 'tabView Log' already exists" error
# more info https://stackoverflow.com/a/44753093/5955589 ,
diff --git a/frappe/patches/v12_0/remove_example_email_thread_notify.py b/frappe/patches/v12_0/remove_example_email_thread_notify.py
new file mode 100644
index 0000000000..94959b6077
--- /dev/null
+++ b/frappe/patches/v12_0/remove_example_email_thread_notify.py
@@ -0,0 +1,8 @@
+import frappe
+
+
+def execute():
+ # remove all example.com email user accounts from notifications
+ frappe.db.sql("""UPDATE `tabUser`
+ SET thread_notify=0, send_me_a_copy=0
+ WHERE email like '%@example.com'""")
diff --git a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
new file mode 100644
index 0000000000..65a635c170
--- /dev/null
+++ b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
@@ -0,0 +1,32 @@
+import frappe
+
+def execute():
+ frappe.reload_doc('desk', 'doctype', 'todo')
+
+ query = '''
+ SELECT
+ name, reference_type, reference_name, {} as assignees
+ FROM
+ `tabToDo`
+ WHERE
+ COALESCE(reference_type, '') != '' AND
+ COALESCE(reference_name, '') != '' AND
+ status != 'Cancelled'
+ GROUP BY
+ reference_type, reference_name
+ '''
+
+ assignments = frappe.db.multisql({
+ 'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'),
+ 'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")')
+ }, as_dict=True)
+
+ for doc in assignments:
+ assignments = doc.assignees.split(',')
+ frappe.db.set_value(
+ doc.reference_type,
+ doc.reference_name,
+ '_assign',
+ frappe.as_json(assignments),
+ update_modified=False
+ )
diff --git a/frappe/patches/v12_0/set_correct_url_in_files.py b/frappe/patches/v12_0/set_correct_url_in_files.py
new file mode 100644
index 0000000000..4f820c1b24
--- /dev/null
+++ b/frappe/patches/v12_0/set_correct_url_in_files.py
@@ -0,0 +1,39 @@
+import frappe
+import os
+
+def execute():
+ files = frappe.get_all('File',
+ fields = ['name', 'file_name', 'file_url'],
+ filters = {
+ 'is_folder': 0,
+ 'file_url': ['!=', ''],
+ })
+
+ private_file_path = frappe.get_site_path('private', 'files')
+ public_file_path = frappe.get_site_path('public', 'files')
+
+ for file in files:
+ file_path = file.file_url
+ file_name = file_path.split('/')[-1]
+
+ if not file_path.startswith(('/private/', '/files/')):
+ continue
+
+ file_is_private = file_path.startswith('/private/files/')
+ full_path = frappe.utils.get_files_path(file_name, is_private=file_is_private)
+
+ if not os.path.exists(full_path):
+ if file_is_private:
+ public_file_url = os.path.join(public_file_path, file_name)
+ if os.path.exists(public_file_url):
+ frappe.db.set_value('File', file.name, {
+ 'file_url': '/files/{0}'.format(file_name),
+ 'is_private': 0
+ })
+ else:
+ private_file_url = os.path.join(private_file_path, file_name)
+ if os.path.exists(private_file_url):
+ frappe.db.set_value('File', file.name, {
+ 'file_url': '/private/files/{0}'.format(file_name),
+ 'is_private': 1
+ })
diff --git a/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py
new file mode 100644
index 0000000000..9a075a22cc
--- /dev/null
+++ b/frappe/patches/v13_0/create_custom_dashboards_cards_and_charts.py
@@ -0,0 +1,45 @@
+import frappe
+from frappe.model.naming import append_number_if_name_exists
+from frappe.utils.dashboard import get_dashboards_with_link
+
+def execute():
+ if not frappe.db.table_exists('Dashboard Chart')\
+ or not frappe.db.table_exists('Number Card')\
+ or not frappe.db.table_exists('Dashboard'):
+ return
+
+ frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
+ frappe.reload_doc('desk', 'doctype', 'number_card')
+ frappe.reload_doc('desk', 'doctype', 'dashboard')
+
+ modified_charts = get_modified_docs('Dashboard Chart')
+ modified_cards = get_modified_docs('Number Card')
+ modified_dashboards = [doc.name for doc in get_modified_docs('Dashboard')]
+
+ for chart in modified_charts:
+ modified_dashboards += get_dashboards_with_link(chart.name, 'Dashboard Chart')
+ rename_modified_doc(chart.name, 'Dashboard Chart')
+
+ for card in modified_cards:
+ modified_dashboards += get_dashboards_with_link(card.name, 'Number Card')
+ rename_modified_doc(card.name, 'Number Card')
+
+ modified_dashboards = list(set(modified_dashboards))
+
+ for dashboard in modified_dashboards:
+ rename_modified_doc(dashboard, 'Dashboard')
+
+def get_modified_docs(doctype):
+ return frappe.get_all(doctype,
+ filters = {
+ 'owner': 'Administrator',
+ 'modified_by': ['!=', 'Administrator']
+ })
+
+def rename_modified_doc(docname, doctype):
+ new_name = docname + ' Custom'
+ try:
+ frappe.rename_doc(doctype, docname, new_name)
+ except frappe.ValidationError:
+ new_name = append_number_if_name_exists(doctype, new_name)
+ frappe.rename_doc(doctype, docname, new_name)
diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
new file mode 100644
index 0000000000..c5a64780cd
--- /dev/null
+++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ themes = frappe.db.get_all(
+ "Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")}
+ )
+ for theme in themes:
+ doc = frappe.get_doc("Website Theme", theme.name)
+ doc.generate_bootstrap_theme()
+ doc.save()
diff --git a/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py b/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py
new file mode 100644
index 0000000000..4da0f8164a
--- /dev/null
+++ b/frappe/patches/v13_0/rename_is_custom_field_in_dashboard_chart.py
@@ -0,0 +1,11 @@
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ if not frappe.db.table_exists('Dashboard Chart'):
+ return
+
+ frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
+
+ if frappe.db.has_column('Dashboard Chart', 'is_custom'):
+ rename_field('Dashboard Chart', 'is_custom', 'use_report_chart')
\ No newline at end of file
diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py
index 1c00ae5f34..920ee7b553 100644
--- a/frappe/patches/v13_0/replace_old_data_import.py
+++ b/frappe/patches/v13_0/replace_old_data_import.py
@@ -6,9 +6,15 @@ import frappe
def execute():
- frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy')
+ if not frappe.db.table_exists("Data Import"): return
+
+ meta = frappe.get_meta("Data Import")
+ # if Data Import is the new one, return early
+ if meta.fields[1].fieldname == "import_type":
+ return
+
+ frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`")
+ frappe.rename_doc("DocType", "Data Import", "Data Import Legacy")
frappe.db.commit()
frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`")
- frappe.reload_doc("core", "doctype", "data_import")
- frappe.get_doc("DocType", "Data Import").on_update()
- frappe.delete_doc_if_exists("DocType", "Data Import Beta")
+ frappe.rename_doc("DocType", "Data Import Beta", "Data Import")
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index 252c706e51..e6599b2496 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -35,13 +35,20 @@ frappe.ui.form.on("Print Format", {
else if (frm.doc.custom_format && !frm.doc.raw_printing) {
frm.set_df_property("html", "reqd", 1);
}
- frm.add_custom_button(__("Make Default"), function () {
- frappe.call({
- method: "frappe.printing.doctype.print_format.print_format.make_default",
- args: {
- name: frm.doc.name
- }
- })
+ frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => {
+ if (r.default_print_format != frm.doc.name) {
+ frm.add_custom_button(__("Set as Default"), function () {
+ frappe.call({
+ method: "frappe.printing.doctype.print_format.print_format.make_default",
+ args: {
+ name: frm.doc.name
+ },
+ callback: function() {
+ frm.refresh();
+ }
+ });
+ });
+ }
});
}
},
diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json
index 397d9dda5d..f93ad0ee5a 100644
--- a/frappe/printing/doctype/print_settings/print_settings.json
+++ b/frappe/printing/doctype/print_settings/print_settings.json
@@ -1,932 +1,203 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2014-07-17 06:54:20.782907",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "System",
- "editable_grid": 0,
+ "engine": "InnoDB",
+ "field_order": [
+ "pdf_settings",
+ "send_print_as_pdf",
+ "repeat_header_footer",
+ "column_break_4",
+ "pdf_page_size",
+ "view_link_in_email",
+ "with_letterhead",
+ "allow_print_for_draft",
+ "add_draft_heading",
+ "column_break_10",
+ "allow_page_break_inside_tables",
+ "allow_print_for_cancelled",
+ "server_printer",
+ "enable_print_server",
+ "server_ip",
+ "printer_name",
+ "port",
+ "raw_printing_section",
+ "enable_raw_printing",
+ "print_style_section",
+ "print_style",
+ "print_style_preview",
+ "section_break_8",
+ "font",
+ "font_size"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "pdf_settings",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "PDF Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "PDF Settings"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"description": "Send Email Print Attachments as PDF (Recommended)",
- "fetch_if_empty": 0,
"fieldname": "send_print_as_pdf",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Send Print as PDF",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Send Print as PDF"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "fetch_if_empty": 0,
"fieldname": "repeat_header_footer",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Repeat Header and Footer in PDF",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Repeat Header and Footer in PDF"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "A4",
- "fetch_if_empty": 0,
"fieldname": "pdf_page_size",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "PDF Page Size",
- "length": 0,
- "no_copy": 0,
- "options": "A4\nLetter",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "A4\nLetter"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "view_link_in_email",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Page Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Page Settings"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "description": "",
- "fetch_if_empty": 0,
"fieldname": "with_letterhead",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Print with letterhead",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print with letterhead"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "description": "",
- "fetch_if_empty": 0,
"fieldname": "allow_print_for_draft",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allow Print for Draft",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Allow Print for Draft"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "description": "",
- "fetch_if_empty": 0,
- "fieldname": "attach_view_link",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Send document web view link in email",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
- "fetch_if_empty": 0,
"fieldname": "add_draft_heading",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Always add \"Draft\" Heading for printing draft documents",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Always add \"Draft\" Heading for printing draft documents"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "allow_page_break_inside_tables",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allow page break inside tables",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Allow page break inside tables"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "allow_print_for_cancelled",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allow Print for Cancelled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Allow Print for Cancelled"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
"fieldname": "server_printer",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Print Server",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print Server"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "enable_print_server",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Enable Print Server",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Enable Print Server"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "localhost",
"depends_on": "enable_print_server",
- "fetch_if_empty": 0,
"fieldname": "server_ip",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Server IP",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Server IP"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "enable_print_server",
- "fetch_if_empty": 0,
"fieldname": "printer_name",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Printer Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Printer Name"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "631",
"depends_on": "enable_print_server",
- "fetch_if_empty": 0,
"fieldname": "port",
"fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Port",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Port"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "raw_printing_section",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Raw Printing",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Raw Printing"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "enable_raw_printing",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Enable Raw Printing",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Enable Raw Printing"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "print_style_section",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Print Style",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print Style"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Modern",
- "fetch_if_empty": 0,
"fieldname": "print_style",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Print Style",
- "length": 0,
- "no_copy": 0,
- "options": "Print Style",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Print Style"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "print_style_preview",
"fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Print Style Preview",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Print Style Preview"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Fonts",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Fonts"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Default",
- "fetch_if_empty": 0,
"fieldname": "font",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Font",
- "length": 0,
- "no_copy": 0,
- "options": "Default\nArial\nHelvetica\nVerdana\nMonospace",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Default\nArial\nHelvetica\nVerdana\nMonospace"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "In points. Default is 9.",
- "fetch_if_empty": 0,
"fieldname": "font_size",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Font Size",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Font Size"
}
],
- "has_web_view": 0,
- "hide_toolbar": 0,
"icon": "fa fa-cog",
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
"issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2019-04-10 14:12:31.081187",
+ "links": [],
+ "modified": "2020-07-02 16:14:47.470668",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Settings",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
- "report": 0,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
"quick_entry": 1,
- "read_only": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css
index 40c6149927..5c398009ff 100644
--- a/frappe/public/css/email.css
+++ b/frappe/public/css/email.css
@@ -7,6 +7,12 @@ body {
p {
margin: 1em 0 !important;
}
+.ql-editor {
+ white-space: normal;
+}
+.ql-editor p {
+ margin: 0 !important;
+}
hr {
border-top: 1px solid #d1d8dd;
}
diff --git a/frappe/public/js/frappe/data_import/data_exporter.js b/frappe/public/js/frappe/data_import/data_exporter.js
index 735237189d..f6af338235 100644
--- a/frappe/public/js/frappe/data_import/data_exporter.js
+++ b/frappe/public/js/frappe/data_import/data_exporter.js
@@ -13,36 +13,6 @@ frappe.data_import.DataExporter = class DataExporter {
this.dialog = new frappe.ui.Dialog({
title: __('Export Data'),
fields: [
- {
- fieldtype: 'Select',
- fieldname: 'exporting_for',
- label: __('Exporting For'),
- options: [
- {
- label: __('Insert New Records'),
- value: 'Insert New Records'
- },
- {
- label: __('Update Existing Records'),
- value: 'Update Existing Records'
- }
- ],
- change: () => {
- let exporting_for = this.dialog.get_value('exporting_for');
- this.dialog.set_value(
- 'export_records',
- exporting_for === 'Insert New Records' ? 'blank_template' : 'all'
- );
-
- // Force ID field to be exported when updating existing records
- let id_field = this.dialog.get_field(this.doctype).options[0];
- if (id_field.value === 'name' && id_field.$checkbox) {
- id_field.$checkbox
- .find('input')
- .prop('disabled', exporting_for === 'Update Existing Records');
- }
- }
- },
{
fieldtype: 'Select',
fieldname: 'export_records',
@@ -65,7 +35,7 @@ frappe.data_import.DataExporter = class DataExporter {
value: 'blank_template'
}
],
- default: 'blank_template',
+ default: this.exporting_for === 'Insert New Records' ? 'blank_template' : 'all',
change: () => {
this.update_record_count_message();
}
@@ -119,10 +89,6 @@ frappe.data_import.DataExporter = class DataExporter {
on_page_show: () => this.select_mandatory()
});
- if (this.exporting_for) {
- this.dialog.set_value('exporting_for', this.exporting_for);
- }
-
this.make_filter_area();
this.make_select_all_buttons();
this.update_record_count_message();
@@ -172,15 +138,17 @@ frappe.data_import.DataExporter = class DataExporter {
}
make_select_all_buttons() {
+ let for_insert = this.exporting_for === 'Insert New Records';
+ let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update');
let $select_all_buttons = $(`
-
+
-
`: ''}
${__('Unselect All')}
@@ -285,11 +253,9 @@ frappe.data_import.DataExporter = class DataExporter {
}
get_filters() {
- return this.filter_group.get_filters().reduce((acc, filter) => {
- return Object.assign(acc, {
- [filter[1]]: [filter[2], filter[3]]
- });
- }, {});
+ return this.filter_group.get_filters().map(filter => {
+ return filter.slice(0, 4);
+ });
}
get_multicheck_options(doctype, child_fieldname = null) {
@@ -308,6 +274,9 @@ frappe.data_import.DataExporter = class DataExporter {
? this.column_map[child_fieldname]
: this.column_map[doctype];
+ let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname)
+ || (df.reqd && this.exporting_for == 'Insert New Records');
+
return fields
.filter(df => {
if (autoname_field && df.fieldname === autoname_field.fieldname) {
@@ -323,7 +292,7 @@ frappe.data_import.DataExporter = class DataExporter {
return {
label,
value: df.fieldname,
- danger: df.reqd,
+ danger: is_field_mandatory(df),
checked: false,
description: `${df.fieldname} ${df.reqd ? __('(Mandatory)') : ''}`
};
diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js
index 7cf8431456..4edcb87aeb 100644
--- a/frappe/public/js/frappe/data_import/import_preview.js
+++ b/frappe/public/js/frappe/data_import/import_preview.js
@@ -245,11 +245,12 @@ frappe.data_import.ImportPreview = class ImportPreview {
let fieldname;
if (!df) {
fieldname = null;
+ } else if (col.map_to_field) {
+ fieldname = col.map_to_field;
+ } else if (col.is_child_table_field) {
+ fieldname = `${col.child_table_df.fieldname}.${df.fieldname}`;
} else {
- fieldname =
- df.parent === this.doctype
- ? df.fieldname
- : `${df.parent}:${df.fieldname}`;
+ fieldname = df.fieldname;
}
return [
{
@@ -272,7 +273,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
label: __("Don't Import"),
value: "Don't Import"
}
- ].concat(column_picker_fields.get_fields_as_options()),
+ ].concat(get_fields_as_options(this.doctype, column_picker_fields)),
default: fieldname || "Don't Import",
change() {
changed.push(i);
@@ -328,3 +329,29 @@ frappe.data_import.ImportPreview = class ImportPreview {
});
}
};
+
+function get_fields_as_options(doctype, column_map) {
+ let keys = [doctype];
+ frappe.meta.get_table_fields(doctype).forEach(df => {
+ keys.push(df.fieldname);
+ });
+ // flatten array
+ return [].concat(
+ ...keys.map(key => {
+ return column_map[key].map(df => {
+ let label = df.label;
+ let value = df.fieldname;
+ if (doctype !== key) {
+ let table_field = frappe.meta.get_docfield(doctype, key);
+ label = `${df.label} (${table_field.label})`;
+ value = `${table_field.fieldname}.${df.fieldname}`;
+ }
+ return {
+ label,
+ value,
+ description: value
+ };
+ });
+ })
+ );
+}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js
index 1b6fb0e438..cf716c67e5 100644
--- a/frappe/public/js/frappe/db.js
+++ b/frappe/public/js/frappe/db.js
@@ -91,12 +91,26 @@ frappe.db = {
});
},
count: function(doctype, args={}) {
- return new Promise(resolve => {
- frappe.call({
- method: 'frappe.client.get_count',
- type: 'GET',
- args: Object.assign(args, { doctype })
- }).then(r => resolve(r.message));
+ let filters = args.filters || {};
+ const with_child_table_filter = Array.isArray(filters) && filters.some(filter => {
+ return filter[0] !== doctype;
+ });
+
+ const fields = [
+ // cannot break this line as it adds extra \n's and \t's which breaks the query
+ `count(${with_child_table_filter ? 'distinct': ''} ${frappe.model.get_full_column_name('name', doctype)}) AS total_count`
+ ];
+
+ return frappe.call({
+ type: 'GET',
+ method: 'frappe.desk.reportview.get',
+ args: {
+ doctype,
+ filters,
+ fields,
+ }
+ }).then(r => {
+ return r.message.values[0][0];
});
},
get_link_options(doctype, txt = '', filters={}) {
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 79a78717cb..2e80dbfd85 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -101,15 +101,6 @@ frappe.Application = Class.extend({
frappe.ui.startup_setup_dialog.show();
}
- // listen to csrf_update
- frappe.realtime.on("csrf_generated", function(data) {
- // handles the case when a user logs in again from another tab
- // and it leads to invalid request in the current tab
- if (data.csrf_token && data.sid===frappe.get_cookie("sid")) {
- frappe.csrf_token = data.csrf_token;
- }
- });
-
frappe.realtime.on("version-update", function() {
var dialog = frappe.msgprint({
message:__("The application has been updated to a new version, please refresh this page"),
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index b487be6eca..c99dfe899f 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -17,7 +17,7 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
set_description: function() {
const { description } = this.df;
const { time_zone } = frappe.sys_defaults;
- if (!frappe.datetime.is_timezone_same()) {
+ if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) {
if (!description) {
this.df.description = time_zone;
} else if (!description.includes(time_zone)) {
diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index 603506a056..56f9430238 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -332,6 +332,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
let docfield = frappe.meta.get_docfield(doctype, fieldname);
let label = docfield ? docfield.label : frappe.model.unscrub(fieldname);
+ if (docfield && docfield.fieldtype === 'Check') {
+ filter[3] = filter[3] ? __('Yes'): __('No');
+ }
+
if (filter[3] && Array.isArray(filter[3]) && filter[3].length > 5) {
filter[3] = filter[3].slice(0, 5);
filter[3].push('...');
diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js
index cd86bdd767..2a7ee5cb10 100644
--- a/frappe/public/js/frappe/form/controls/multiselect_list.js
+++ b/frappe/public/js/frappe/form/controls/multiselect_list.js
@@ -3,7 +3,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
let template = `
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index b94257106e..bbe2fa2f95 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -109,9 +109,10 @@ frappe.views.BaseList = class BaseList {
this.fields = this.fields.uniqBy(f => f[0] + f[1]);
}
- _add_field(fieldname) {
+ _add_field(fieldname, doctype) {
if (!fieldname) return;
- let doctype = this.doctype;
+
+ if (!doctype) doctype = this.doctype;
if (typeof fieldname === 'object') {
// df is passed
@@ -120,6 +121,8 @@ frappe.views.BaseList = class BaseList {
doctype = df.parent;
}
+ if (!this.fields) this.fields = [];
+
const is_valid_field = frappe.model.std_fields_list.includes(fieldname)
|| frappe.meta.has_field(doctype, fieldname)
|| fieldname === '_seen';
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 9e1ba1b9bd..4d8121ebd6 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -528,6 +528,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
get_header_html() {
+ if (!this.columns) {
+ return;
+ }
+
const subject_field = this.columns[0].df;
let subject_html = `
@@ -760,26 +764,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
let current_count = this.data.length;
let count_without_children = this.data.uniqBy(d => d.name).length;
- const filters = this.get_filters_for_args();
- const with_child_table_filter = filters.some(filter => {
- return filter[0] !== this.doctype;
- });
-
- const fields = [
- // cannot break this line as it adds extra \n's and \t's which breaks the query
- `count(${with_child_table_filter ? 'distinct': ''}${frappe.model.get_full_column_name('name', this.doctype)}) AS total_count`
- ];
-
- return frappe.call({
- type: 'GET',
- method: this.method,
- args: {
- doctype: this.doctype,
- filters,
- fields,
- }
- }).then(r => {
- this.total_count = r.message.values[0][0] || current_count;
+ return frappe.db.count(this.doctype, {
+ filters: this.get_filters_for_args()
+ }).then(total_count => {
+ this.total_count = total_count || current_count;
let str = __('{0} of {1}', [current_count, this.total_count]);
if (count_without_children !== current_count) {
str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]);
@@ -800,6 +788,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return '#Form/' + this.doctype + '/' + docname;
}
+ get_seen_class(doc) {
+ return JSON.parse(doc._seen || '[]').includes(frappe.session.user)
+ ? ''
+ : 'bold';
+ }
+
get_subject_html(doc) {
let user = frappe.session.user;
let subject_field = this.columns[0].df;
@@ -811,8 +805,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
let heart_class = liked_by.includes(user) ?
'liked-by' : 'text-extra-muted not-liked';
- const seen = JSON.parse(doc._seen || '[]')
- .includes(user) ? '' : 'bold';
+ const seen = this.get_seen_class(doc);
let subject_html = `
@@ -1162,7 +1155,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
});
this.toggle_result_area();
this.render_list();
- if (this.$checks.length) {
+ if (this.$checks && this.$checks.length) {
this.set_rows_as_checked();
}
});
diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js
index 0a5f5e7f6b..5cf50bd0a3 100644
--- a/frappe/public/js/frappe/request.js
+++ b/frappe/public/js/frappe/request.js
@@ -126,7 +126,7 @@ frappe.request.call = function(opts) {
message: __('The resource you are looking for is not available')});
},
403: function(xhr) {
- if (frappe.get_cookie('sid')==='Guest') {
+ if (frappe.session.user === 'Guest') {
// session expired
frappe.app.handle_session_expired();
}
@@ -321,7 +321,7 @@ frappe.request.cleanup = function(opts, r) {
if(r) {
// session expired? - Guest has no business here!
- if(r.session_expired || frappe.get_cookie("sid")==="Guest") {
+ if (r.session_expired || frappe.session.user === "Guest") {
frappe.app.handle_session_expired();
return;
}
diff --git a/frappe/public/js/frappe/ui/filters/edit_filter.html b/frappe/public/js/frappe/ui/filters/edit_filter.html
index 3908c63fa1..f6618a2107 100644
--- a/frappe/public/js/frappe/ui/filters/edit_filter.html
+++ b/frappe/public/js/frappe/ui/filters/edit_filter.html
@@ -10,6 +10,7 @@
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index 37eab50957..4dedfb32fe 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -13,26 +13,26 @@ frappe.ui.Filter = class {
set_conditions() {
this.conditions = [
- ["=", __("Equals")],
- ["!=", __("Not Equals")],
- ["like", __("Like")],
- ["not like", __("Not Like")],
- ["in", __("In")],
- ["not in", __("Not In")],
- ["is", __("Is")],
- [">", ">"],
- ["<", "<"],
- [">=", ">="],
- ["<=", "<="],
- ["Between", __("Between")],
- ["Timespan", __("Timespan")],
+ ['=', __('Equals')],
+ ['!=', __('Not Equals')],
+ ['like', __('Like')],
+ ['not like', __('Not Like')],
+ ['in', __('In')],
+ ['not in', __('Not In')],
+ ['is', __('Is')],
+ ['>', '>'],
+ ['<', '<'],
+ ['>=', '>='],
+ ['<=', '<='],
+ ['Between', __('Between')],
+ ['Timespan', __('Timespan')],
];
this.nested_set_conditions = [
- ["descendants of", __("Descendants Of")],
- ["not descendants of", __("Not Descendants Of")],
- ["ancestors of", __("Ancestors Of")],
- ["not ancestors of", __("Not Ancestors Of")],
+ ['descendants of', __('Descendants Of')],
+ ['not descendants of', __('Not Descendants Of')],
+ ['ancestors of', __('Ancestors Of')],
+ ['not ancestors of', __('Not Ancestors Of')],
];
this.conditions.push(...this.nested_set_conditions);
@@ -42,10 +42,10 @@ frappe.ui.Filter = class {
Datetime: ['like', 'not like'],
Data: ['Between', 'Timespan'],
Select: ['like', 'not like', 'Between', 'Timespan'],
- Link: ["Between", 'Timespan', '>', '<', '>=', '<='],
- Currency: ["Between", 'Timespan'],
- Color: ["Between", 'Timespan'],
- Check: this.conditions.map(c => c[0]).filter(c => c !== '=')
+ Link: ['Between', 'Timespan', '>', '<', '>=', '<='],
+ Currency: ['Between', 'Timespan'],
+ Color: ['Between', 'Timespan'],
+ Check: this.conditions.map((c) => c[0]).filter((c) => c !== '='),
};
}
@@ -57,7 +57,7 @@ frappe.ui.Filter = class {
this.conditions.push([key, __(`{0}`, [filter.label])]);
for (let fieldtype of Object.keys(this.invalid_condition_map)) {
if (!filter.valid_for_fieldtypes.includes(fieldtype)) {
- this.invalid_condition_map[fieldtype].push(filter.label);
+ this.invalid_condition_map[fieldtype].push(key);
}
}
}
@@ -65,10 +65,11 @@ frappe.ui.Filter = class {
}
make() {
- this.filter_edit_area = $(frappe.render_template("edit_filter", {
- conditions: this.conditions
- }))
- .appendTo(this.parent.find('.filter-edit-area'));
+ this.filter_edit_area = $(
+ frappe.render_template('edit_filter', {
+ conditions: this.conditions,
+ })
+ ).appendTo(this.parent.find('.filter-edit-area'));
this.make_select();
this.set_events();
@@ -82,41 +83,51 @@ frappe.ui.Filter = class {
filter_fields: this.filter_fields,
select: (doctype, fieldname) => {
this.set_field(doctype, fieldname);
- }
+ },
});
- if(this.fieldname) {
+ if (this.fieldname) {
this.fieldselect.set_value(this.doctype, this.fieldname);
}
}
set_events() {
- this.filter_edit_area.find("a.remove-filter").on("click", () => {
+ this.filter_edit_area.find('a.remove-filter').on('click', () => {
this.remove();
});
- this.filter_edit_area.find(".set-filter-and-run").on("click", () => {
- this.filter_edit_area.removeClass("new-filter");
+ this.filter_edit_area.find('.set-filter-and-run').on('click', () => {
+ this.filter_edit_area.removeClass('new-filter');
this.on_change();
this.update_filter_tag();
});
this.filter_edit_area.find('.condition').change(() => {
- if(!this.field) return;
+ if (!this.field) return;
let condition = this.get_condition();
let fieldtype = null;
- if(["in", "like", "not in", "not like"].includes(condition)) {
+ if (['in', 'like', 'not in', 'not like'].includes(condition)) {
fieldtype = 'Data';
this.add_condition_help(condition);
+ } else {
+ this.filter_edit_area.find('.filter-description').empty();
}
- if (['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && ["in", "not in"].includes(condition)) {
+ if (
+ ['Select', 'MultiSelect'].includes(this.field.df.fieldtype) &&
+ ['in', 'not in'].includes(condition)
+ ) {
fieldtype = 'MultiSelect';
}
- this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition);
+ this.set_field(
+ this.field.df.parent,
+ this.field.df.fieldname,
+ fieldtype,
+ condition
+ );
});
}
@@ -129,12 +140,12 @@ frappe.ui.Filter = class {
setup_state(is_new) {
let promise = Promise.resolve();
if (is_new) {
- this.filter_edit_area.addClass("new-filter");
+ this.filter_edit_area.addClass('new-filter');
} else {
promise = this.update_filter_tag();
}
- if(this.hidden) {
+ if (this.hidden) {
promise.then(() => this.$filter_tag.hide());
}
}
@@ -164,13 +175,13 @@ frappe.ui.Filter = class {
set_values(doctype, fieldname, condition, value) {
// presents given (could be via tags!)
if (this.set_field(doctype, fieldname) === false) {
- return
+ return;
}
- if(this.field.df.original_type==='Check') {
- value = (value==1) ? 'Yes' : 'No';
+ if (this.field.df.original_type === 'Check') {
+ value = value == 1 ? 'Yes' : 'No';
}
- if(condition) this.set_condition(condition, true);
+ if (condition) this.set_condition(condition, true);
// set value can be asynchronous, so update_filter_tag should happen after field is set
this._filter_value_set = Promise.resolve();
@@ -190,11 +201,13 @@ frappe.ui.Filter = class {
set_field(doctype, fieldname, fieldtype, condition) {
// set in fieldname (again)
let cur = {};
- if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k];
+ if (this.field) for (let k in this.field.df) cur[k] = this.field.df[k];
- let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname];
+ let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[
+ fieldname
+ ];
- if(!original_docfield) {
+ if (!original_docfield) {
console.warn(`Field ${fieldname} is not selectable.`);
this.remove();
return false;
@@ -214,8 +227,13 @@ frappe.ui.Filter = class {
// called when condition is changed,
// don't change if all is well
- if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
- df.parent == cur.parent && df.options == cur.options) {
+ if (
+ this.field &&
+ cur.fieldname == fieldname &&
+ df.fieldtype == cur.fieldtype &&
+ df.parent == cur.parent &&
+ df.options == cur.options
+ ) {
return;
}
@@ -223,20 +241,25 @@ frappe.ui.Filter = class {
this.fieldselect.selected_doctype = doctype;
this.fieldselect.selected_fieldname = fieldname;
- if (this.filters_config && this.filters_config[condition]
- && this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) {
+ if (
+ this.filters_config &&
+ this.filters_config[condition] &&
+ this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)
+ ) {
let args = {};
if (this.filters_config[condition].depends_on) {
const field_name = this.filters_config[condition].depends_on;
- const filter_value = this.base_list.get_filter_value(field_name);
+ const filter_value = this.filter_list.get_filter_value(fieldname);
args[field_name] = filter_value;
}
- frappe.xcall(this.filters_config[condition].get_field, args).then(field => {
- df.fieldtype = field.fieldtype;
- df.options = field.options;
- df.fieldname = fieldname;
- this.make_field(df, cur.fieldtype);
- });
+ frappe
+ .xcall(this.filters_config[condition].get_field, args)
+ .then(field => {
+ df.fieldtype = field.fieldtype;
+ df.options = field.options;
+ df.fieldname = fieldname;
+ this.make_field(df, cur.fieldtype);
+ });
} else {
this.make_field(df, cur.fieldtype);
}
@@ -255,16 +278,18 @@ frappe.ui.Filter = class {
f.refresh();
this.field = f;
- if(old_text && f.fieldtype===old_fieldtype) {
+ if (old_text && f.fieldtype === old_fieldtype) {
this.field.set_value(old_text);
}
// run on enter
- $(this.field.wrapper).find(':input').keydown(e => {
- if(e.which==13 && this.field.df.fieldtype !== 'MultiSelect') {
- this.on_change();
- }
- });
+ $(this.field.wrapper)
+ .find(':input')
+ .keydown(e => {
+ if (e.which == 13 && this.field.df.fieldtype !== 'MultiSelect') {
+ this.on_change();
+ }
+ });
}
get_value() {
@@ -273,7 +298,7 @@ frappe.ui.Filter = class {
this.field.df.fieldname,
this.get_condition(),
this.get_selected_value(),
- this.hidden
+ this.hidden,
];
}
get_selected_value() {
@@ -284,90 +309,101 @@ frappe.ui.Filter = class {
return this.filter_edit_area.find('.condition').val();
}
- set_condition(condition, trigger_change=false) {
+ set_condition(condition, trigger_change = false) {
let $condition_field = this.filter_edit_area.find('.condition');
$condition_field.val(condition);
- if(trigger_change) $condition_field.change();
-
+ if (trigger_change) $condition_field.change();
}
make_tag() {
if (!this.field) return;
- this.$filter_tag = this.get_filter_tag_element()
- .insertAfter(this.parent.find(".active-tag-filters .clear-filters"));
+ this.$filter_tag = this.get_filter_tag_element().insertAfter(
+ this.parent.find('.active-tag-filters .clear-filters')
+ );
this.set_filter_button_text();
this.bind_tag();
}
bind_tag() {
- this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this));
+ this.$filter_tag.find('.remove-filter').on('click', this.remove.bind(this));
- let filter_button = this.$filter_tag.find(".toggle-filter");
- filter_button.on("click", () => {
- filter_button.closest('.tag-filters-area').find('.filter-edit-area').show();
+ let filter_button = this.$filter_tag.find('.toggle-filter');
+ filter_button.on('click', () => {
+ filter_button
+ .closest('.tag-filters-area')
+ .find('.filter-edit-area')
+ .show();
this.filter_edit_area.toggle();
});
}
set_filter_button_text() {
- this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text());
+ this.$filter_tag.find('.toggle-filter').html(this.get_filter_button_text());
}
get_filter_button_text() {
- let value = this.utils.get_formatted_value(this.field, this.get_selected_value());
- return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`;
+ let value = this.utils.get_formatted_value(
+ this.field,
+ this.get_selected_value()
+ );
+ return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
+ value
+ )}`;
}
get_filter_tag_element() {
return $(`
+ title="${__('Edit Filter')}">
+ title="${__('Remove Filter')}">
`);
}
add_condition_help(condition) {
- let $desc = this.field.desc_area;
- if(!$desc) {
- $desc = $('
').appendTo(this.field.wrapper);
- }
- // set description
- $desc.html((in_list(["in", "not in"], condition)==="in"
- ? __("values separated by commas")
- : __("use % as wildcard"))+'
');
+ const description = ['in', 'not in'].includes(condition)
+ ? __('values separated by commas')
+ : __('use % as wildcard');
+
+ this.filter_edit_area.find('.filter-description').html(description);
}
hide_invalid_conditions(fieldtype, original_type) {
- let invalid_conditions = this.invalid_condition_map[original_type]
- || this.invalid_condition_map[fieldtype] || [];
+ let invalid_conditions =
+ this.invalid_condition_map[original_type] ||
+ this.invalid_condition_map[fieldtype] ||
+ [];
for (let condition of this.conditions) {
- this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(
- !invalid_conditions.includes(condition[0])
- );
+ this.filter_edit_area
+ .find(`.condition option[value="${condition[0]}"]`)
+ .toggle(!invalid_conditions.includes(condition[0]));
}
}
toggle_nested_set_conditions(df) {
- let show_condition = df.fieldtype === "Link" && frappe.boot.nested_set_doctypes.includes(df.options);
- this.nested_set_conditions.forEach(condition => {
- this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(show_condition);
+ let show_condition =
+ df.fieldtype === 'Link' &&
+ frappe.boot.nested_set_doctypes.includes(df.options);
+ this.nested_set_conditions.forEach((condition) => {
+ this.filter_edit_area
+ .find(`.condition option[value="${condition[0]}"]`)
+ .toggle(show_condition);
});
}
};
frappe.ui.filter_utils = {
get_formatted_value(field, value) {
- if(field.df.fieldname==="docstatus") {
- value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
- } else if(field.df.original_type==="Check") {
- value = {0:"No", 1:"Yes"}[cint(value)];
+ if (field.df.fieldname === 'docstatus') {
+ value = { 0: 'Draft', 1: 'Submitted', 2: 'Cancelled' }[value] || value;
+ } else if (field.df.original_type === 'Check') {
+ value = { 0: 'No', 1: 'Yes' }[cint(value)];
}
- return frappe.format(value, field.df, {only_value: 1});
+ return frappe.format(value, field.df, { only_value: 1 });
},
get_selected_value(field, condition) {
@@ -382,7 +418,7 @@ frappe.ui.filter_utils = {
}
if (field.df.original_type == 'Check') {
- val = (val=='Yes' ? 1 :0);
+ val = val == 'Yes' ? 1 : 0;
}
if (condition.indexOf('like', 'not like') !== -1) {
@@ -390,12 +426,13 @@ frappe.ui.filter_utils = {
if (val && !(val.startsWith('%') || val.endsWith('%'))) {
val = '%' + val + '%';
}
- } else if (in_list(["in", "not in"], condition)) {
+ } else if (in_list(['in', 'not in'], condition)) {
if (val) {
- val = val.split(',').map(v => strip(v));
+ val = val.split(',').map((v) => strip(v));
}
- } if (val === '%') {
- val = "";
+ }
+ if (val === '%') {
+ val = '';
}
return val;
@@ -404,7 +441,7 @@ frappe.ui.filter_utils = {
get_default_condition(df) {
if (df.fieldtype == 'Data') {
return 'like';
- } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){
+ } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') {
return 'Between';
} else {
return '=';
@@ -413,44 +450,73 @@ frappe.ui.filter_utils = {
set_fieldtype(df, fieldtype, condition) {
// reset
- if(df.original_type)
- df.fieldtype = df.original_type;
- else
- df.original_type = df.fieldtype;
+ if (df.original_type) df.fieldtype = df.original_type;
+ else df.original_type = df.fieldtype;
- df.description = ''; df.reqd = 0;
+ df.description = '';
+ df.reqd = 0;
df.ignore_link_validation = true;
// given
- if(fieldtype) {
+ if (fieldtype) {
df.fieldtype = fieldtype;
return;
}
// scrub
- if(df.fieldname=="docstatus") {
- df.fieldtype="Select",
- df.options=[
- {value:0, label:__("Draft")},
- {value:1, label:__("Submitted")},
- {value:2, label:__("Cancelled")}
+ if (df.fieldname == 'docstatus') {
+ df.fieldtype = 'Select',
+ df.options = [
+ { value: 0, label: __('Draft') },
+ { value: 1, label: __('Submitted') },
+ { value: 2, label: __('Cancelled') },
];
- } else if(df.fieldtype=='Check') {
- df.fieldtype='Select';
- df.options='No\nYes';
- } else if(['Text','Small Text','Text Editor','Code','Tag','Comments',
- 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) {
+ } else if (df.fieldtype == 'Check') {
+ df.fieldtype = 'Select';
+ df.options = 'No\nYes';
+ } else if (
+ [
+ 'Text',
+ 'Small Text',
+ 'Text Editor',
+ 'Code',
+ 'Tag',
+ 'Comments',
+ 'Dynamic Link',
+ 'Read Only',
+ 'Assign',
+ ].indexOf(df.fieldtype) != -1
+ ) {
df.fieldtype = 'Data';
- } else if(df.fieldtype=='Link' && ['=', '!=', 'descendants of', 'ancestors of', 'not descendants of', 'not ancestors of'].indexOf(condition)==-1) {
+ } else if (
+ df.fieldtype == 'Link' &&
+ [
+ '=',
+ '!=',
+ 'descendants of',
+ 'ancestors of',
+ 'not descendants of',
+ 'not ancestors of',
+ ].indexOf(condition) == -1
+ ) {
df.fieldtype = 'Data';
}
- if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") {
+ if (
+ df.fieldtype === 'Data' &&
+ (df.options || '').toLowerCase() === 'email'
+ ) {
df.options = null;
}
- if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
+ if (
+ condition == 'Between' &&
+ (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')
+ ) {
df.fieldtype = 'DateRange';
}
- if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) {
+ if (
+ condition == 'Timespan' &&
+ ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)
+ ) {
df.fieldtype = 'Select';
df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
}
@@ -466,15 +532,15 @@ frappe.ui.filter_utils = {
get_timespan_options(periods) {
const period_map = {
- 'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'],
- 'Today': null,
- 'This': ['Week', 'Month', 'Quarter', 'Year'],
- 'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year']
+ Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
+ Today: null,
+ This: ['Week', 'Month', 'Quarter', 'Year'],
+ Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
};
let options = [];
- periods.forEach(period => {
+ periods.forEach((period) => {
if (period_map[period]) {
- period_map[period].forEach(p => {
+ period_map[period].forEach((p) => {
options.push({
label: __(`{0} {1}`, [period, p]),
value: `${period.toLowerCase()} ${p.toLowerCase()}`,
@@ -488,5 +554,5 @@ frappe.ui.filter_utils = {
}
});
return options;
- }
+ },
};
diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js
index ed9ddefe64..6c577aa0bc 100644
--- a/frappe/public/js/frappe/ui/filters/filter_list.js
+++ b/frappe/public/js/frappe/ui/filters/filter_list.js
@@ -104,13 +104,18 @@ frappe.ui.FilterGroup = class {
filter_items: (doctype, fieldname) => {
return !this.filter_exists([doctype, fieldname]);
},
- base_list: this.base_list
+ filter_list: this.base_list || this,
};
let filter = new frappe.ui.Filter(args);
this.filters.push(filter);
return filter;
}
+ get_filter_value(fieldname) {
+ let filter_obj = this.filters.find(f => f.fieldname == fieldname) || {};
+ return filter_obj.value;
+ }
+
filter_exists(filter_value) {
// filter_value of form: [doctype, fieldname, condition, value]
let exists = false;
diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js
deleted file mode 100644
index a775413d39..0000000000
--- a/frappe/public/js/frappe/ui/filters/filters.js
+++ /dev/null
@@ -1,684 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// MIT License. See license.txt
-
-frappe.ui.FilterList = Class.extend({
- init: function(opts) {
- $.extend(this, opts);
- this.filters = [];
- this.wrapper = this.parent;
- this.stats = [];
- this.make();
- this.set_events();
- },
- make: function() {
- this.wrapper.find('.show_filters, .filter_area').remove();
- this.wrapper.append(`
-
-
-
- ${__("Add Filter")}
-
-
-
`);
- },
- set_events: function() {
- var me = this;
- // show filters
- this.wrapper.find('.new-filter').bind('click', function() {
- me.add_filter();
- });
-
- this.wrapper.find('.clear-filters').bind('click', function() {
- me.clear_filters();
- $('.date-range-picker').val('')
- me.base_list.run();
- $(this).addClass("hide");
- });
- },
-
- show_filters: function() {
- this.wrapper.find('.show_filters').toggle();
- if(!this.filters.length) {
- this.add_filter(this.doctype, 'name');
- this.filters[0].wrapper.find(".filter_field input").focus();
- }
- },
-
- clear_filters: function() {
- $.each(this.filters, function(i, f) { f.remove(true); });
- if(this.base_list.page.fields_dict) {
- $.each(this.base_list.page.fields_dict, (key, value) => {
- value.set_input('');
- });
- }
- this.filters = [];
- },
-
- add_filter: function(doctype, fieldname, condition, value, hidden) {
- // adds a new filter, returns true if filter has been added
-
- // allow equal to be used as like
- let base_filter = this.base_list.page.fields_dict[fieldname];
- if (base_filter
- && (base_filter.df.condition==condition
- || (condition==='=' && base_filter.df.condition==='like'))) {
- // if filter exists in base_list, then exit
- this.base_list.page.fields_dict[fieldname].set_input(value);
-
- return true;
- }
-
- if(doctype && fieldname
- && !frappe.meta.has_field(doctype, fieldname)
- && !in_list(frappe.model.std_fields_list, fieldname)) {
- frappe.msgprint({
- message: __('Filter {0} missing', [fieldname.bold()]),
- title: 'Invalid Filter',
- indicator: 'red'
- });
- return false;
- }
-
- this.wrapper.find('.show_filters').toggle(true);
- var is_new_filter = arguments.length===0;
-
- if (is_new_filter && this.wrapper.find(".is-new-filter:visible").length) {
- // only allow 1 new filter at a time!
- return false;
- }
-
- var filter = this.push_new_filter(doctype, fieldname, condition, value);
- if (!filter) return;
-
- if(this.wrapper.find('.clear-filters').hasClass("hide")) {
- this.wrapper.find('.clear-filters').removeClass("hide");
- }
-
- if (filter && is_new_filter) {
- filter.wrapper.addClass("is-new-filter");
- } else {
- filter.freeze();
- }
-
- if (hidden) {
- filter.$btn_group.addClass("hide");
- }
-
- return true;
- },
- push_new_filter: function(doctype, fieldname, condition, value) {
- if(this.filter_exists(doctype, fieldname, condition, value)) {
- return;
- }
-
- // if standard filter exists, then clear it.
- if(this.base_list.page.fields_dict[fieldname]) {
- this.base_list.page.fields_dict[fieldname].set_input('');
- }
-
- var filter = new frappe.ui.Filter({
- flist: this,
- _doctype: doctype,
- fieldname: fieldname,
- condition: condition,
- value: value
- });
-
- this.filters.push(filter);
-
- return filter;
- },
-
- remove: function(filter) {
- // remove `filter` from flist
- for (var i in this.filters) {
- if (this.filters[i] === filter) {
- break;
- }
- }
- if (i!==undefined) {
- // remove index
- this.filters.splice(i, 1);
- }
- },
-
- filter_exists: function(doctype, fieldname, condition, value) {
- var flag = false;
- for(var i in this.filters) {
- if(this.filters[i].field) {
- var f = this.filters[i].get_value();
-
- if(f[0]==doctype && f[1]==fieldname && f[2]==condition && f[3]==value) {
- flag = true;
- } else if($.isArray(value) && frappe.utils.arrays_equal(value, f[3])) {
- flag = true;
- }
- }
- }
- return flag;
- },
-
- get_filters: function() {
- // get filter values as dict
- var values = [];
- $.each(this.filters, function(i, filter) {
- if(filter.field) {
- filter.freeze();
- values.push(filter.get_value());
- }
- });
- this.base_list.update_standard_filters(values);
-
- return values;
- },
-
- // remove hidden filters
- update_filters: function() {
- var fl = [];
- $.each(this.filters, function(i, f) {
- if(f.field) fl.push(f);
- })
- this.filters = fl;
- if(this.filters.length === 0) {
- this.wrapper.find('.clear-filters').addClass("hide");
- }
- },
-
- get_filter: function(fieldname) {
- for(var i in this.filters) {
- if(this.filters[i].field && this.filters[i].field.df.fieldname==fieldname)
- return this.filters[i];
- }
- },
-
- get_formatted_value: function(field, val){
- var value = val;
-
- if(field.df.fieldname==="docstatus") {
- value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
- } else if(field.df.original_type==="Check") {
- value = {0:"No", 1:"Yes"}[cint(value)];
- } else if (field.df.original_type === "Duration") {
- let duration_options = {
- hide_days: field.df.hide_days,
- hide_seconds: field.df.hide_seconds
- };
- value = frappe.utils.get_formatted_duration(value, duration_options);
- }
-
- value = frappe.format(value, field.df, {only_value: 1});
- return value;
- }
-});
-
-frappe.ui.Filter = Class.extend({
- init: function(opts) {
- $.extend(this, opts);
-
- this.doctype = this.flist.doctype;
- this.make();
- this.make_select();
- this.set_events();
- },
- make: function() {
- this.wrapper = $(frappe.render_template("edit_filter", {}))
- .appendTo(this.flist.wrapper.find('.filter_area'));
- },
- make_select: function() {
- var me = this;
- this.fieldselect = new frappe.ui.FieldSelect({
- parent: this.wrapper.find('.fieldname_select_area'),
- doctype: this.doctype,
- filter_fields: this.filter_fields,
- select: function(doctype, fieldname) {
- me.set_field(doctype, fieldname);
- }
- });
- if(this.fieldname) {
- this.fieldselect.set_value(this._doctype || this.doctype, this.fieldname);
- }
- },
- set_events: function() {
- var me = this;
-
- this.wrapper.find("a.remove-filter").on("click", function() {
- me.remove();
- });
-
- this.wrapper.find(".set-filter-and-run").on("click", function() {
- me.wrapper.removeClass("is-new-filter");
- me.flist.base_list.run();
- me.apply();
- });
-
- // add help for "in" codition
- me.wrapper.find('.condition').change(function() {
- if(!me.field) return;
- var condition = $(this).val();
- if(in_list(["in", "like", "not in", "not like"], condition)) {
- me.set_field(me.field.df.parent, me.field.df.fieldname, 'Data', condition);
- if(!me.field.desc_area) {
- me.field.desc_area = $('
').appendTo(me.field.wrapper);
- }
- // set description
- me.field.desc_area.html((in_list(["in", "not in"], condition)==="in"
- ? __("values separated by commas")
- : __("use % as wildcard"))+'
');
- } else {
- //if condition selected after refresh
- me.set_field(me.field.df.parent, me.field.df.fieldname, null, condition);
- }
- });
-
- // set the field
- if(me.fieldname) {
- // pre-sets given (could be via tags!)
- return this.set_values(me._doctype, me.fieldname, me.condition, me.value);
- } else {
- me.set_field(me.doctype, 'name');
- }
- },
-
- apply: function() {
- var f = this.get_value();
-
- this.flist.remove(this);
- this.flist.push_new_filter(f[0], f[1], f[2], f[3]);
- this.remove();
- },
-
- remove: function(dont_run) {
- this.wrapper.remove();
- this.$btn_group && this.$btn_group.remove();
- this.field = null;
- this.flist.update_filters();
-
- if(!dont_run) {
- this.flist.base_list.refresh(true);
- }
- },
-
- set_values: function(doctype, fieldname, condition, value) {
- // presents given (could be via tags!)
- this.set_field(doctype, fieldname);
-
- // change 0,1 to Yes, No for check field type
- if(this.field.df.original_type==='Check') {
- if(value==0) value = 'No';
- else if(value==1) value = 'Yes';
- }
-
- if(condition) {
- this.wrapper.find('.condition').val(condition).change();
- }
- if(value!=null) {
- return this.field.set_value(value);
- }
- },
-
- set_field: function(doctype, fieldname, fieldtype, condition) {
- var me = this;
-
- // set in fieldname (again)
- var cur = me.field ? {
- fieldname: me.field.df.fieldname,
- fieldtype: me.field.df.fieldtype,
- parent: me.field.df.parent,
- } : {};
-
- var original_docfield = me.fieldselect.fields_by_name[doctype][fieldname];
- if(!original_docfield) {
- frappe.msgprint(__("Field {0} is not selectable.", [fieldname]));
- return;
- }
-
- var df = copy_dict(me.fieldselect.fields_by_name[doctype][fieldname]);
-
- // filter field shouldn't be read only or hidden
- df.read_only = 0;
- df.hidden = 0;
-
- if(!condition) this.set_default_condition(df, fieldtype);
- this.set_fieldtype(df, fieldtype);
-
- // called when condition is changed,
- // don't change if all is well
- if(me.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
- df.parent == cur.parent) {
- return;
- }
-
- // clear field area and make field
- me.fieldselect.selected_doctype = doctype;
- me.fieldselect.selected_fieldname = fieldname;
-
- // save old text
- var old_text = null;
- if(me.field) {
- old_text = me.field.get_value();
- }
-
- var field_area = me.wrapper.find('.filter_field').empty().get(0);
- var f = frappe.ui.form.make_control({
- df: df,
- parent: field_area,
- only_input: true,
- })
- f.refresh();
-
- me.field = f;
- if(old_text && me.field.df.fieldtype===cur.fieldtype) {
- me.field.set_value(old_text);
- }
-
- // run on enter
- $(me.field.wrapper).find(':input').keydown(function(ev) {
- if(ev.which==13) {
- me.flist.base_list.run();
- }
- })
- },
-
- set_fieldtype: function(df, fieldtype) {
- // reset
- if(df.original_type)
- df.fieldtype = df.original_type;
- else
- df.original_type = df.fieldtype;
-
- df.description = ''; df.reqd = 0;
- df.ignore_link_validation = true;
-
- // given
- if(fieldtype) {
- df.fieldtype = fieldtype;
- return;
- }
-
- // scrub
- if(df.fieldname=="docstatus") {
- df.fieldtype="Select",
- df.options=[
- {value:0, label:__("Draft")},
- {value:1, label:__("Submitted")},
- {value:2, label:__("Cancelled")}
- ]
- } else if(df.fieldtype=='Check') {
- df.fieldtype='Select';
- df.options='No\nYes';
- } else if(['Text','Small Text','Text Editor','Code','Tag','Comments',
- 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) {
- df.fieldtype = 'Data';
- } else if(df.fieldtype=='Link' && ['=', '!='].indexOf(this.wrapper.find('.condition').val())==-1) {
- df.fieldtype = 'Data';
- }
- if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") {
- df.options = null;
- }
- if(this.wrapper.find('.condition').val()== "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
- df.fieldtype = 'DateRange';
- }
- },
-
- set_default_condition: function(df, fieldtype) {
- if(!fieldtype) {
- // set as "like" for data fields
- if (df.fieldtype == 'Data') {
- this.wrapper.find('.condition').val('like');
- } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){
- this.wrapper.find('.condition').val('Between');
- }else{
- this.wrapper.find('.condition').val('=');
- }
- }
- },
-
- get_value: function() {
- return [this.fieldselect.selected_doctype,
- this.field.df.fieldname, this.get_condition(), this.get_selected_value()];
- },
-
- get_selected_value: function() {
- var val = this.field.get_value();
-
- if(typeof val==='string') {
- val = strip(val);
- }
-
- if(this.field.df.original_type == 'Check') {
- val = (val=='Yes' ? 1 :0);
- }
-
- if(this.get_condition().indexOf('like', 'not like')!==-1) {
- // automatically append wildcards
- if(val) {
- if(val.slice(0,1) !== "%") {
- val = "%" + val;
- }
- if(val.slice(-1) !== "%") {
- val = val + "%";
- }
- }
- } else if(in_list(["in", "not in"], this.get_condition())) {
- if(val) {
- val = $.map(val.split(","), function(v) { return strip(v); });
- }
- } if(val === '%') {
- val = "";
- }
-
- return val;
- },
-
- get_condition: function() {
- return this.wrapper.find('.condition').val();
- },
-
- freeze: function() {
- if(this.$btn_group) {
- // already made, just hide the condition setter
- this.set_filter_button_text();
- this.wrapper.toggle(false);
- return;
- }
-
- var me = this;
-
- // add a button for new filter if missing
- this.$btn_group = $(`
-
-
-
-
-
`)
- .insertAfter(this.flist.wrapper.find(".set-filters .new-filter"));
-
- this.set_filter_button_text();
-
- this.$btn_group.find(".remove-filter").on("click", function() {
- me.remove();
- });
-
- this.$btn_group.find(".toggle-filter").on("click", function() {
- $(this).closest('.show_filters').find('.filter_area').show();
- me.wrapper.toggle();
- })
- this.wrapper.toggle(false);
- },
-
- set_filter_button_text: function() {
- var value = this.get_selected_value();
- value = this.flist.get_formatted_value(this.field, value);
-
- // for translations
- // __("like"), __("not like"), __("in")
-
- this.$btn_group.find(".toggle-filter")
- .html(repl('%(label)s %(condition)s "%(value)s"', {
- label: __(this.field.df.label),
- condition: __(this.get_condition()),
- value: __(value),
- }));
- }
-
-});
-
-//
`);
};
-function go_to_list_with_filters(doctype, filters) {
- const route = `List/${doctype}/List`;
- frappe.set_route(route).then(()=> {
- let list_view = frappe.views.list_view[route];
- let filter_area = list_view.filter_area;
- filter_area.clear();
- filter_area.filter_list.add_filters_to_filter_group(filters);
- });
-}
-
function shorten_number(number, country) {
country = (country == 'India') ? country : '';
const number_system = get_number_system(country);
@@ -167,4 +157,4 @@ function get_number_system(country) {
return number_system_map[country];
}
-export { generate_route, generate_grid, build_summary_item, go_to_list_with_filters, shorten_number };
\ No newline at end of file
+export { generate_route, generate_grid, build_summary_item, shorten_number };
\ No newline at end of file
diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js
index 054159116f..2ca26d59e1 100644
--- a/frappe/public/js/frappe/widgets/widget_dialog.js
+++ b/frappe/public/js/frappe/widgets/widget_dialog.js
@@ -174,18 +174,12 @@ class ShortcutDialog extends WidgetDialog {
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
let doctype = this.dialog.get_value("link_to");
-
- doctype &&
- frappe.db
- .get_value("DocType", doctype, "issingle")
- .then((res) => {
- if (res.message && res.message.issingle) {
- this.hide_filters();
- } else {
- this.setup_filter(doctype);
- this.show_filters();
- }
- });
+ if (doctype && frappe.boot.single_types.includes(doctype)) {
+ this.hide_filters();
+ } else if (doctype) {
+ this.setup_filter(doctype);
+ this.show_filters();
+ }
} else {
this.hide_filters();
}
diff --git a/frappe/public/less/dashboard_view.less b/frappe/public/less/dashboard_view.less
index ab78fa5b2a..17fa52cc0f 100644
--- a/frappe/public/less/dashboard_view.less
+++ b/frappe/public/less/dashboard_view.less
@@ -33,13 +33,6 @@
line-height: 1.5em;
vertical-align: text-bottom;
}
-
- .restricted-button {
- cursor: default;
- position: relative;
- right: 5px;
- top: -3px;
- }
}
.customize-dashboard {
diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less
index b0fb60b6a3..36ceed9bfa 100644
--- a/frappe/public/less/desk.less
+++ b/frappe/public/less/desk.less
@@ -787,13 +787,14 @@ li.user-progress {
margin-right: 5px;
margin-left: 0px;
position: relative;
- height: 12px;
}
+@checkbox-height: 14px;
+
// custom font awesome checkbox
input[type="checkbox"] {
position: relative;
- visibility: hidden;
+ height: 0px !important;
&:before {
position: absolute;
@@ -804,9 +805,10 @@ input[type="checkbox"] {
font-weight: normal;
font-variant: normal;
text-transform: none;
- line-height: 14px;
- display: inline-block;
- font-size: 14px;
+ line-height: @checkbox-height;
+ margin-top: -10px;
+ display: inline-block !important;
+ font-size: @checkbox-height;
color: @text-extra-muted;
.transition(150ms color);
left: 0px;
@@ -818,10 +820,17 @@ input[type="checkbox"] {
&:checked:before {
content: '\f14a';
- font-size: 13px;
+ font-size: @checkbox-height - 1px;
color: @checkbox-color;
}
+ &:checked:focus:before {
+ box-shadow: inset 0 0 1px 2px @text-muted;
+ padding: 0px 1px;
+ border-radius: 3px;
+ height: @checkbox-height - 1px;
+ }
+
&:focus {
outline: none;
}
@@ -836,28 +845,11 @@ input[type="checkbox"] {
height: 100%;
}
-// mozilla doesn't support
+// Firefox doesn't support
// pseudo elements on checkbox
-@-moz-document url-prefix() {
+@supports (-moz-appearance: none) or (-ms-ime-align:auto) {
input[type="checkbox"] {
- visibility: visible;
- left: 0;
- }
-}
-
-@supports (-moz-appearance: none) {
- input[type="checkbox"] {
- visibility: visible;
- left: 0;
- }
-}
-
-// edge doesn't support pseudo elements on checkbox
-//Microsoft Edge Browser 12+ (All)
-@supports (-ms-ime-align:auto) {
- input[type="checkbox"] {
- visibility: visible;
- left: 0;
+ height: @checkbox-height !important
}
}
diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less
index 3e3b59ddf8..a738679cc8 100644
--- a/frappe/public/less/desktop.less
+++ b/frappe/public/less/desktop.less
@@ -143,6 +143,13 @@
}
}
+.frappe-rtl {
+ .desk-body {
+ padding-left: 0px;
+ padding-right: calc(20rem + 15px);
+ }
+}
+
.widget-group {
margin-bottom: 25px;
// -webkit-animation-name: slideInUp;
diff --git a/frappe/public/less/email.less b/frappe/public/less/email.less
index b6d9540586..bf0507138b 100644
--- a/frappe/public/less/email.less
+++ b/frappe/public/less/email.less
@@ -10,6 +10,13 @@ p {
margin: 1em 0 !important;
}
+.ql-editor {
+ white-space: normal;
+ p {
+ margin: 0 !important;
+ }
+}
+
hr {
border-top: 1px solid @border-color;
}
@@ -210,4 +217,4 @@ hr {
.report-title {
margin-bottom: 20px;
}
-/* csslint ignore:end */
+/* csslint ignore:end */
\ No newline at end of file
diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less
index cd391c1f10..8d01cd6dd7 100644
--- a/frappe/public/less/form.less
+++ b/frappe/public/less/form.less
@@ -349,6 +349,9 @@ h6.uppercase, .h6.uppercase {
.form-section {
padding: 15px 7px;
}
+ .hide-border {
+ padding-top: 0;
+ }
}
.help ol {
@@ -573,7 +576,13 @@ h6.uppercase, .h6.uppercase {
margin-left: 5px;
}
- .media-body:after, .media-body:before {
+ .media-body {
+ .left-arrow;
+ }
+}
+
+.left-arrow {
+ &::after, &::before {
right: 100%;
top: 15px;
border: solid transparent;
@@ -584,13 +593,13 @@ h6.uppercase, .h6.uppercase {
pointer-events: none;
}
- .media-body:after {
+ &::after {
border-color: rgba(136, 183, 213, 0);
border-right-color: #fafbfc;
border-width: 6px;
margin-top: -6px;
}
- .media-body:before {
+ &::before {
border-color: rgba(194, 225, 245, 0);
border-right-color: @border-color;
border-width: 7px;
@@ -638,6 +647,18 @@ h6.uppercase, .h6.uppercase {
top: 5px;
}
+.timeline-item.user-content.show-indicator {
+ position: relative;
+ .media-body {
+ margin-left: 50px;
+ }
+ &::before {
+ .timeline-indicator();
+ left: 13px;
+ top: 13px;
+ }
+}
+
.timeline-item.notification-content::before {
.timeline-indicator();
}
diff --git a/frappe/public/less/form_grid.less b/frappe/public/less/form_grid.less
index 5cb04a252c..d9e7d8bceb 100644
--- a/frappe/public/less/form_grid.less
+++ b/frappe/public/less/form_grid.less
@@ -98,6 +98,10 @@
text-align: right;
}
+.grid-row .grid-row-check {
+ margin-top: 12px;
+}
+
.grid-row > .row {
.col:last-child {
margin-right: -10px;
diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less
index 4e066f86e4..4c7d04406d 100644
--- a/frappe/public/less/list.less
+++ b/frappe/public/less/list.less
@@ -197,7 +197,7 @@ body.no-list-sidebar {
}
input.list-check-all, input.list-row-checkbox {
- margin-top: 0px;
+ display: inline-block;
}
.filterable {
diff --git a/frappe/public/less/quill.less b/frappe/public/less/quill.less
index a72602697a..514d8993cd 100644
--- a/frappe/public/less/quill.less
+++ b/frappe/public/less/quill.less
@@ -9,10 +9,6 @@
font-family: inherit;
}
-.ql-editor {
- white-space: normal;
-}
-
.ql-editor {
font-family: @font-stack;
line-height: 1.6;
@@ -82,7 +78,7 @@
}
}
-.ql-editor .mention {
+.ql-editor:not(.read-mode) .mention {
height: auto;
width: auto;
border-radius: 10px;
@@ -137,3 +133,7 @@
margin-top: 0px;
margin-bottom: 0px;
}
+
+.ql-editor.read-mode {
+ padding: 0;
+}
\ No newline at end of file
diff --git a/frappe/public/scss/portal.scss b/frappe/public/scss/portal.scss
new file mode 100644
index 0000000000..33371d5693
--- /dev/null
+++ b/frappe/public/scss/portal.scss
@@ -0,0 +1,7 @@
+.portal-row {
+ padding: 1rem 0;
+
+ a {
+ color: $body-color;
+ }
+}
\ No newline at end of file
diff --git a/frappe/public/scss/sidebar.scss b/frappe/public/scss/sidebar.scss
index d3442c2344..b13eaf2a74 100644
--- a/frappe/public/scss/sidebar.scss
+++ b/frappe/public/scss/sidebar.scss
@@ -20,6 +20,11 @@
}
}
+// Remove top margin from frist child
+.sidebar-item:first-child a {
+ margin-top: 0rem;
+}
+
.sidebar-item a.active {
color: $primary;
background-color: $primary-light;
diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss
index e03c502784..3c11d23252 100644
--- a/frappe/public/scss/website.scss
+++ b/frappe/public/scss/website.scss
@@ -1,3 +1,4 @@
+@import '~quill/dist/quill.core';
@import 'variables';
@import 'frappe/public/css/font-awesome';
@import '~bootstrap/scss/bootstrap';
@@ -8,8 +9,23 @@
@import 'blog';
@import 'markdown';
@import 'sidebar';
+@import 'portal';
@import 'doc';
+.ql-editor.read-mode {
+ padding: 0;
+ line-height: 1.6;
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5 {
+ margin-top: 0.5em;
+ margin-bottom: 0.25em;
+ }
+}
+
.container {
padding-left: 1.25rem;
padding-right: 1.25rem;
@@ -110,8 +126,13 @@
color: $light;
}
+.page-content-wrapper {
+ margin: 2rem 0;
+}
+
.breadcrumb-container {
margin-top: 1rem;
+ padding-top: 0.25rem;
}
.breadcrumb {
@@ -326,4 +347,10 @@ h5.modal-title {
left: 0;
width: 100%;
height: 100%;
-}
\ No newline at end of file
+}
+
+.ellipsis {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
diff --git a/frappe/sessions.py b/frappe/sessions.py
index d317d6caf3..7a018bb0aa 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -172,13 +172,6 @@ def generate_csrf_token():
frappe.local.session.data.csrf_token = frappe.generate_hash()
frappe.local.session_obj.update(force=True)
- # send sid and csrf token to the user
- # handles the case when a user logs in again from another tab
- # and it leads to invalid request in the current tab
- frappe.publish_realtime(event="csrf_generated",
- message={"sid": frappe.local.session.sid, "csrf_token": frappe.local.session.data.csrf_token},
- user=frappe.session.user, after_commit=True)
-
class Session:
def __init__(self, user, resume=False, full_name=None, user_type=None):
self.sid = cstr(frappe.form_dict.get('sid') or
diff --git a/frappe/templates/base.html b/frappe/templates/base.html
index 0b82b3dac2..8c843a44a4 100644
--- a/frappe/templates/base.html
+++ b/frappe/templates/base.html
@@ -42,7 +42,11 @@
{{ head_include or "" }}
{% endblock -%}
- {%- block style %}{%- endblock -%}
+ {%- block style %}
+ {% if colocated_css -%}
+
+ {%- endif %}
+ {%- endblock -%}
{%- endfor -%}
- {%- block script %}{%- endblock %}
+ {%- block script %}
+ {% if colocated_js -%}
+
+ {%- endif %}
+ {%- endblock %}
{%- block body_include %}{{ body_include or "" }}{% endblock -%}