diff --git a/.github/frappe-framework-logo.png b/.github/frappe-framework-logo.png deleted file mode 100644 index 5049078a46..0000000000 Binary files a/.github/frappe-framework-logo.png and /dev/null differ diff --git a/.github/frappe-framework-logo.svg b/.github/frappe-framework-logo.svg new file mode 100644 index 0000000000..ba04ebf264 --- /dev/null +++ b/.github/frappe-framework-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/README.md b/README.md index 7545249610..1f59376f48 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@
-

+
- frappe +

- a web framework with "batteries included" + a web framework with "batteries included"

it's pronounced - fra-pay diff --git a/frappe/auth.py b/frappe/auth.py index 1353acf10f..64fea36748 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -333,12 +333,20 @@ class CookieManager: # sid expires in 3 days expires = datetime.datetime.now() + datetime.timedelta(days=3) if frappe.session.sid: - self.cookies["sid"] = {"value": frappe.session.sid, "expires": expires} + self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True) if frappe.session.session_country: - self.cookies["country"] = {"value": frappe.session.get("session_country")} + self.set_cookie("country", frappe.session.session_country) - def set_cookie(self, key, value, expires=None): - self.cookies[key] = {"value": value, "expires": expires} + def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): + if not secure: + secure = frappe.local.request.scheme == "https" + self.cookies[key] = { + "value": value, + "expires": expires, + "secure": secure, + "httponly": httponly, + "samesite": samesite + } def delete_cookie(self, to_delete): if not isinstance(to_delete, (list, tuple)): @@ -349,7 +357,10 @@ class CookieManager: def flush_cookies(self, response): for key, opts in self.cookies.items(): response.set_cookie(key, quote((opts.get("value") or "").encode('utf-8')), - expires=opts.get("expires")) + expires=opts.get("expires"), + secure=opts.get("secure"), + httponly=opts.get("httponly"), + samesite=opts.get("samesite")) # expires yesterday! expires = datetime.datetime.now() + datetime.timedelta(days=-1) diff --git a/frappe/boot.py b/frappe/boot.py index 8862ce3c61..b552d7d703 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -107,7 +107,7 @@ def load_desktop_data(bootinfo): from frappe.config import get_modules_from_all_apps_for_user from frappe.desk.desktop import get_desk_sidebar_items bootinfo.allowed_modules = get_modules_from_all_apps_for_user() - bootinfo.allowed_workspaces = get_desk_sidebar_items(True) + bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False) bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 92d12289c6..97b6c235b5 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -21,7 +21,7 @@ global_cache_keys = ("app_hooks", "installed_apps", user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", "desktop_icons", 'portal_menu_items', 'user_perm_can_read', - "has_role:Page", "has_role:Report") + "has_role:Page", "has_role:Report", "desk_sidebar_items") doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified", "linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map', 'data_import_column_header_map') diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 0f51f21104..26eb455338 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -201,16 +201,31 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro def install_app(context, apps): "Install a new app to site, supports multiple apps" from frappe.installer import install_app as _install_app + exit_code = 0 + + if not context.sites: + raise SiteNotSpecifiedError + for site in context.sites: frappe.init(site=site) frappe.connect() - try: - for app in apps: + + for app in apps: + try: _install_app(app, verbose=context.verbose) - finally: - frappe.destroy() - if not context.sites: - raise SiteNotSpecifiedError + except frappe.IncompatibleApp as err: + err_msg = ":\n{}".format(err) if str(err) else "" + print("App {} is Incompatible with Site {}{}".format(app, site, err_msg)) + exit_code = 1 + except Exception as err: + err_msg = ":\n{}".format(err if str(err) else frappe.get_traceback()) + print("An error occurred while installing {}{}".format(app, err_msg)) + exit_code = 1 + + frappe.destroy() + + sys.exit(exit_code) + @click.command('list-apps') @pass_context @@ -422,15 +437,16 @@ def remove_from_installed_apps(context, app): @click.option('--yes', '-y', help='To bypass confirmation prompt for uninstalling the app', is_flag=True, default=False, multiple=True) @click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) @click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False) +@click.option('--force', help='Force remove app from site', is_flag=True, default=False) @pass_context -def uninstall(context, app, dry_run=False, yes=False, no_backup=False): +def uninstall(context, app, dry_run, yes, no_backup, force): "Remove app and linked modules from site" from frappe.installer import remove_app for site in context.sites: try: frappe.init(site=site) frappe.connect() - remove_app(app, dry_run, yes, no_backup) + remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force) finally: frappe.destroy() if not context.sites: @@ -615,6 +631,29 @@ def stop_recording(context): if not context.sites: raise SiteNotSpecifiedError +@click.command('ngrok') +@pass_context +def start_ngrok(context): + from pyngrok import ngrok + + site = get_site(context) + frappe.init(site=site) + + port = frappe.conf.http_port or frappe.conf.webserver_port + public_url = ngrok.connect(port=port, options={ + 'host_header': site + }) + print(f'Public URL: {public_url}') + print('Inspect logs at http://localhost:4040') + + ngrok_process = ngrok.get_ngrok_process() + try: + # Block until CTRL-C or some other terminating event + ngrok_process.proc.wait() + except KeyboardInterrupt: + print("Shutting down server...") + frappe.destroy() + ngrok.kill() commands = [ add_system_manager, @@ -640,5 +679,6 @@ commands = [ browse, start_recording, stop_recording, - add_to_hosts + add_to_hosts, + start_ngrok ] diff --git a/frappe/config/settings.py b/frappe/config/settings.py index 848ef2e1aa..e43abd9fcb 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -16,6 +16,13 @@ def get_data(): "description": _("Language, Date and Time settings"), "hide_count": True }, + { + "type": "doctype", + "name": "Global Defaults", + "label": _("Global Defaults"), + "description": _("Company, Fiscal Year and Currency defaults"), + "hide_count": True + }, { "type": "doctype", "name": "Error Log", diff --git a/frappe/core/desk_page/settings/settings.json b/frappe/core/desk_page/settings/settings.json index 90c32adbb0..862230f463 100644 --- a/frappe/core/desk_page/settings/settings.json +++ b/frappe/core/desk_page/settings/settings.json @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Core", - "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -44,7 +44,7 @@ "idx": 0, "is_standard": 1, "label": "Settings", - "modified": "2020-07-08 12:46:52.264510", + "modified": "2020-07-14 10:09:09.520557", "modified_by": "Administrator", "module": "Core", "name": "Settings", diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index daf64d4b8b..e9db865ade 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -221,7 +221,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None) :param print_html: Send given value as HTML attachment. :param print_format: Attach print format of parent document.""" - view_link = frappe.utils.cint(frappe.db.get_value("Print Settings", "Print Settings", "attach_view_link")) + view_link = frappe.utils.cint(frappe.db.get_value("System Settings", "System Settings", "attach_view_link")) if print_format and view_link: doc.content += get_attach_link(doc, print_format) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index ec3cccc1b1..910e42af1a 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -59,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): @@ -440,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) @@ -453,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 @@ -618,7 +615,7 @@ 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) @@ -692,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: @@ -957,7 +957,7 @@ class Column: if self.df.fieldtype == 'Link': # find all values that dont exist - values = list(set([v for v in self.column_values[1:] if v])) + 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: @@ -970,6 +970,13 @@ class Column: 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() @@ -1060,6 +1067,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: @@ -1068,12 +1076,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 @@ -1084,7 +1092,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 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/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 fc58f66bfc..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" 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 18a321e55f..80e30eb55e 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 @@ -323,25 +349,48 @@ 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) - filters = { - 'restrict_to_domain': ['in', frappe.get_active_domains()], - 'extends_another_page': 0, - 'for_user': '', - 'module': ['not in', blocked_modules] - } + if not pages or not cache: + # don't get domain restricted pages + blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() - if not frappe.local.conf.developer_mode: - filters['developer_mode_only'] = '0' + 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' + + # 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", "icon"], filters=filters, order_by=order_by, ignore_permissions=True) + if flatten: return pages @@ -375,7 +424,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 +439,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..237b549433 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.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..c7128823fe 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,35 @@ "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" + }, + { + "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-10 17:48:19.468813", "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 += '
{doc}
'.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..738d77ae27 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -9,9 +9,24 @@ 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'), @@ -49,6 +64,8 @@ frappe.ui.form.on('Dashboard Chart', { }); 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 +83,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 +137,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 +146,7 @@ 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.trigger('set_chart_report_filters'); }, @@ -146,7 +174,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.length > 2) { + filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)}; + } frappe.xcall( 'frappe.desk.query_report.run', { @@ -165,7 +196,7 @@ frappe.ui.form.on('Dashboard Chart', { if (!frm.doc.is_custom) { 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 +271,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 +291,8 @@ frappe.ui.form.on('Dashboard Chart', { let table = $(` - - + + @@ -378,4 +412,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')}${__('Filter')}${__('Condition')} ${__('Value')}
+ + + + + + + + +
${__('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..d67e725eb9 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -7,6 +7,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "is_standard", + "module", "chart_name", "chart_type", "report_name", @@ -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)", @@ -200,7 +205,8 @@ "fieldname": "report_name", "fieldtype": "Link", "label": "Report Name", - "options": "Report" + "options": "Report", + "set_only_once": 1 }, { "default": "0", @@ -235,10 +241,43 @@ "fieldname": "heatmap_year", "fieldtype": "Select", "label": "Year" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: doc.is_standard", + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "mandatory_depends_on": "eval: doc.is_standard", + "options": "Module Def", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "dynamic_filters_json", + "fieldtype": "Code", + "label": "Dynamic Filters JSON", + "options": "JSON", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "dynamic_filters_section", + "fieldtype": "Section Break", + "label": "Dynamic Filters", + "show_days": 1, + "show_seconds": 1 } ], "links": [], - "modified": "2020-05-16 15:03:02.455395", + "modified": "2020-07-10 16:09:47.102062", "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 4ad6943e0b..70aece3ee7 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): @@ -80,7 +81,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]) @@ -347,8 +350,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/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..d5a743818a 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -3,8 +3,153 @@ 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 d = new frappe.ui.Dialog({ + title: __('Add to Dashboard'), + fields: [ + { + label: __('Select Dashboard'), + fieldtype: 'Link', + fieldname: 'dashboard', + options: 'Dashboard', + } + ], + primary_action: (values) => { + values.name = frm.doc.name; + frappe.xcall( + 'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard', + { + args: values + } + ).then(()=> { + let dashboard_route_html = + `${values.dashboard}`; + let message = + __(`Number Card ${values.name} add to Dashboard ` + dashboard_route_html); + + frappe.msgprint(message); + }); + + d.hide(); + } + }); + + if (!frm.doc.name) { + frappe.msgprint(__('Please create Card first')); + } else { + d.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.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 +162,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 +190,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.length > 2) { + filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)}; + } + 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..e94a06dab8 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -1,39 +1,54 @@ { "actions": [], + "allow_rename": 1, + "allow_workflow": 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 +107,91 @@ "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" + }, + { + "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-17 18:04:00.814756", "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..5b52b60474 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 @@ -47,7 +52,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 +70,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 +84,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 +93,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 +112,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() @@ -147,3 +155,22 @@ 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 + + dashboard.append('cards', dashboard_link) + dashboard.save() \ 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/installer.py b/frappe/installer.py index 40fdc057d6..4baf0929f0 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -9,6 +9,7 @@ from __future__ import unicode_literals, print_function from six.moves import input import os, json, subprocess, shutil +import click import frappe import frappe.database import importlib @@ -118,12 +119,20 @@ def remove_from_installed_apps(app_name): if frappe.flags.in_install: post_install() -def remove_app(app_name, dry_run=False, yes=False, no_backup=False): +def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False): """Remove app and all linked to the app's module with the app from a site.""" + # dont allow uninstall app if not installed unless forced + if not force: + if app_name not in frappe.get_installed_apps(): + click.secho("App {0} not installed on Site {1}".format(app_name, frappe.local.site), fg="yellow") + return + + print("Uninstalling App {0} from Site {1}...".format(app_name, frappe.local.site)) + if not dry_run and not yes: - confirm = input("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue (y/n) ? ") - if confirm!="y": + confirm = click.confirm("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue?") + if not confirm: return if not no_backup: @@ -146,8 +155,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False): if not doctype.issingle: drop_doctypes.append(doctype.name) - # remove reports, pages and web forms - for doctype in ("Report", "Page", "Web Form"): + + linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent']) + ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"] + doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes] + + for doctype in doctypes_with_linked_modules: for record in frappe.get_list(doctype, filters={"module": module_name}): print("removing {0} {1}...".format(doctype, record.name)) if not dry_run: @@ -166,6 +179,8 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False): for doctype in set(drop_doctypes): frappe.db.sql("drop table `tab{0}`".format(doctype)) + click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green") + frappe.flags.in_uninstall = False def post_install(rebuild_website=False): diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index 0c28e95a24..c110694dff 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -189,10 +189,10 @@ def upload_system_backup_to_google_drive(): if frappe.flags.create_new_backup: set_progress(1, "Backing up Data.") backup = new_backup() - fileurl_backup = os.path.basename(backup.backup_path_db) - fileurl_site_config = os.path.basename(backup.site_config_backup_path) - fileurl_public_files = os.path.basename(backup.backup_path_files) - fileurl_private_files = os.path.basename(backup.backup_path_private_files) + fileurl_backup = backup.backup_path_db + fileurl_site_config = backup.site_config_backup_path + fileurl_public_files = backup.backup_path_files + fileurl_private_files = backup.backup_path_private_files else: fileurl_backup, fileurl_site_config, fileurl_public_files, fileurl_private_files = get_latest_backup_file(with_files=True) @@ -208,7 +208,7 @@ def upload_system_backup_to_google_drive(): try: media = MediaFileUpload(get_absolute_path(filename=fileurl), mimetype="application/gzip", resumable=True) except IOError as e: - frappe.throw(_("Google Drive - Could not locate locate - {0}").format(e)) + frappe.throw(_("Google Drive - Could not locate - {0}").format(e)) try: set_progress(2, "Uploading backup to Google Drive.") @@ -232,7 +232,7 @@ def weekly_backup(): upload_system_backup_to_google_drive() def get_absolute_path(filename): - file_path = os.path.join(get_backups_path()[2:], filename) + file_path = os.path.join(get_backups_path()[2:], os.path.basename(filename)) return "{0}/sites/{1}".format(get_bench_path(), file_path) def set_progress(progress, message): 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..c7ef7890b4 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): 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 ea693167f8..69a781d6d1 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: 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/patches.txt b/frappe/patches.txt index 6c4c75aca9..f8c767f5a3 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -264,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') @@ -272,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 @@ -291,3 +294,4 @@ 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 # 2020-06-24 +frappe.patches.v13_0.create_custom_dashboards_cards_and_charts 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/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py index f3eed6253c..920ee7b553 100644 --- a/frappe/patches/v13_0/replace_old_data_import.py +++ b/frappe/patches/v13_0/replace_old_data_import.py @@ -6,11 +6,15 @@ import frappe def execute(): - if not frappe.db.exists("DocType", "Data Import Beta"): + 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.rename_doc("DocType", "Data Import", "Data Import Legacy") frappe.db.commit() frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`") - frappe.rename_doc('DocType', 'Data Import Beta', 'Data Import') + frappe.rename_doc("DocType", "Data Import Beta", "Data Import") 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/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index aab7d7ddba..feb62fb196 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/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 3c0f7d5110..e367989b81 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -199,6 +199,8 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ get_input_value() { let value = this.quill ? this.quill.root.innerHTML : ''; + // hack to retain space sequence. + value = value.replace(/(\s)(\s)/g, '  '); return value; }, diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index ebe94b4cdb..f7d1b3e873 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -842,6 +842,15 @@ frappe.ui.form.Form = class FrappeForm { this.page.clear_primary_action(); } + disable_form() { + this.set_read_only(); + this.fields + .forEach((field) => { + this.set_df_property(field.df.fieldname, "read_only", "1"); + }); + this.disable_save(); + } + handle_save_fail(btn, on_error) { $(btn).prop('disabled', false); if (on_error) { @@ -1604,6 +1613,7 @@ frappe.ui.form.Form = class FrappeForm { }); driver.defineSteps(steps); + frappe.route.on('change', () => driver.reset()); driver.start(); } }; diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 68444c8a3b..2da7b8f236 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -240,13 +240,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({ var me = this; var data = this.dialog.get_values(true); $.each(data, function(key, value) { - if(key==='__newname') { - me.dialog.doc.name = value; - } - else { - if(!is_null(value)) { - me.dialog.doc[key] = value; - } + if (!is_null(value)) { + me.dialog.doc[key] = value; } }); return this.dialog.doc; @@ -282,7 +277,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ field.doctype = me.doc.doctype; field.docname = me.doc.name; - if(!is_null(me.doc[fieldname])) { + if (!is_null(me.doc[fieldname])) { field.set_input(me.doc[fieldname]); } }); diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index bd83c0428b..69b8f9e7f8 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -111,9 +111,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 @@ -122,6 +123,7 @@ 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) || diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 57c17caa02..79a94acbc2 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -878,6 +878,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; @@ -890,7 +896,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ? "liked-by" : "not-liked"; - const seen = JSON.parse(doc._seen || "[]").includes(user) ? "" : "bold"; + const seen = this.get_seen_class(doc); let subject_html = ` (.*?)<\/b>/); - message = title ? message.replace(title[1], frappe.ellipsis(title[1], 100)) : message; + message = title ? message.replace(title[1], frappe.ellipsis(strip_html(title[1]), 100)) : message; let timestamp = frappe.datetime.comment_when(field.creation); let message_html = `
@@ -359,6 +359,7 @@ class NotificationsView extends BaseNotificaitonsView { order_by: 'creation desc' }); } + } get_item_link(notification_doc) { const link_doctype = diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index 3e59986928..e11adcfb66 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -228,7 +228,7 @@ frappe.search.AwesomeBar = Class.extend({ } this.options.push({ - label: __("Search for '{0}'", [txt.bold()]), + label: __("Search for '{0}'", [frappe.utils.xss_sanitise(txt).bold()]), value: __("Search for '{0}'", [txt]), match: txt, index: 100, diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index 0ed4b6cebc..9dcea5f812 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -60,7 +60,7 @@ frappe.dashboard_utils = { frappe.dom.eval(config); return frappe.dashboards.chart_sources[chart.source].filters; }); - } else if (chart.chart_type === 'Report') { + } else if (chart.chart_type === 'Report' && chart.report_name) { return frappe.report_utils.get_report_filters(chart.report_name).then(filters => { return filters; }); @@ -110,6 +110,108 @@ frappe.dashboard_utils = { get_year(date_str) { return date_str.substring(0, date_str.indexOf('-')); + }, + + remove_common_static_filter_values(static_filters, dynamic_filters) { + if (dynamic_filters) { + if ($.isArray(static_filters)) { + static_filters = static_filters.filter(static_filter => { + for (let dynamic_filter of dynamic_filters) { + if (static_filter[0] == dynamic_filter[0] + && static_filter[1] == dynamic_filter[1]) { + return false; + } + } + return true; + }); + } else { + for (let key of Object.keys(dynamic_filters)) { + delete static_filters[key]; + } + } + } + + return static_filters; + }, + + get_fields_for_dynamic_filter_dialog(is_document_type, filters, dynamic_filters) { + let fields = [ + { + fieldtype: 'HTML', + fieldname: 'description', + options: + `
+

Set dynamic filter values in JavaScript for the required fields here. +

+

Ex: + frappe.defaults.get_user_default("Company") +

+
` + } + ]; + + if (is_document_type) { + if (dynamic_filters) { + filters = [...filters, ...dynamic_filters]; + } + filters.forEach(f => { + for (let field of fields) { + if (field.fieldname == f[0] + ':' + f[1]) { + return; + } + } + if (f[2] == '=') { + fields.push({ + label: `${f[1]} (${f[0]})`, + fieldname: f[0] + ':' + f[1], + fieldtype: 'Data', + }); + } + }); + } else { + filters = {...dynamic_filters, ...filters}; + for (let key of Object.keys(filters)) { + fields.push({ + label: key, + fieldname: key, + fieldtype: 'Data', + }); + } + } + + return fields; + }, + + get_all_filters(doc) { + let filters = JSON.parse(doc.filters_json || "null"); + let dynamic_filters = JSON.parse(doc.dynamic_filters_json || "null"); + + if (!dynamic_filters) { + return filters; + } + + if ($.isArray(dynamic_filters)) { + dynamic_filters.forEach(f => { + try { + f[3] = eval(f[3]); + } catch (e) { + frappe.throw(__(`Invalid expression set in filter ${f[1]} (${f[0]})`)); + } + }); + filters = [...filters, ...dynamic_filters]; + } else { + for (let key of Object.keys(dynamic_filters)) { + try { + const val = eval(dynamic_filters[key]); + dynamic_filters[key] = val; + } catch (e) { + frappe.throw(__(`Invalid expression set in filter ${key}`)); + } + } + Object.assign(filters, dynamic_filters); + } + + return filters; } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js index 764768f87f..5b5613cbf7 100644 --- a/frappe/public/js/frappe/views/desktop/desktop.js +++ b/frappe/public/js/frappe/views/desktop/desktop.js @@ -53,7 +53,7 @@ export default class Desktop { .call("frappe.desk.desktop.get_desk_sidebar_items") .then(response => { if (response.message) { - this.desktop_settings = response.message; + this.sidebar_configuration = response.message; } else { frappe.throw({ title: __("Couldn't Load Desk"), @@ -71,10 +71,11 @@ export default class Desktop { make_sidebar() { const get_sidebar_item = function(item) { - return $(` + return $(` ${item.label || item.name}
`); }; @@ -105,9 +106,9 @@ export default class Desktop { }; this.sidebar_categories.forEach(category => { - if (this.desktop_settings.hasOwnProperty(category)) { + if (this.sidebar_configuration.hasOwnProperty(category)) { make_category_title(category); - this.desktop_settings[category].forEach(item => { + this.sidebar_configuration[category].forEach(item => { make_sidebar_category_item(item); }); } @@ -139,8 +140,8 @@ export default class Desktop { } get_page_to_show() { - const default_page = this.desktop_settings - ? this.desktop_settings["Modules"][0].name + const default_page = this.sidebar_configuration + ? this.sidebar_configuration["Modules"][0].name : frappe.boot.allowed_workspaces[0].name; let page = diff --git a/frappe/public/js/frappe/views/inbox/inbox_view.js b/frappe/public/js/frappe/views/inbox/inbox_view.js index f80b07751e..1085e93e6c 100644 --- a/frappe/public/js/frappe/views/inbox/inbox_view.js +++ b/frappe/public/js/frappe/views/inbox/inbox_view.js @@ -69,6 +69,14 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView { }); } + get_seen_class(doc) { + const seen = + Boolean(doc.seen) || JSON.parse(doc._seen || '[]').includes(frappe.session.user) + ? '' + : 'bold'; + return seen; + } + get is_sent_emails() { const f = this.filter_area.get() .find(filter => filter[1] === 'sent_or_received'); @@ -77,7 +85,7 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView { render_header() { this.$result.find('.list-row-head').remove(); - this.$result.prepend(this.get_header_html()); + this.$result.prepend(this.get_header_html()); } render() { diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index a35891731a..4ad39634ef 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -136,10 +136,17 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { () => this.setup_progress_bar(), () => this.setup_page_head(), () => this.refresh_report(), - () => this.add_chart_buttons_to_toolbar(true) + () => this.add_chart_buttons_to_toolbar(true), + () => this.add_card_button_to_toolbar(true), ]); } + add_card_button_to_toolbar() { + this.page.add_inner_button(__("Create Card"), () => { + this.add_card_to_dashboard(); + }); + } + add_chart_buttons_to_toolbar(show) { if (show) { this.create_chart_button && this.create_chart_button.remove() @@ -159,6 +166,62 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } + add_card_to_dashboard() { + let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data); + const dialog = new frappe.ui.Dialog({ + title: __('Create Card'), + fields: [ + { + fieldname: 'report_field', + label: __('Field'), + fieldtype: 'Select', + options: field_options.numeric_fields, + }, + { + fieldname: 'cb_1', + fieldtype: 'Column Break' + }, + { + fieldname: 'report_function', + label: __('Function'), + options: ['Sum', 'Average', 'Minimum', 'Maximum'], + fieldtype: 'Select' + }, + { + fieldname: 'sb_1', + label: __('Add to Dashboard'), + fieldtype: 'Section Break' + }, + { + fieldname: 'dashboard', + label: __('Choose Dashboard'), + fieldtype: 'Link', + options: 'Dashboard', + }, + { + fieldname: 'cb_2', + fieldtype: 'Column Break' + }, + { + fieldname: 'label', + label: __('Card Label'), + fieldtype: 'Data', + } + ], + primary_action_label: __('Add'), + primary_action: (values) => { + if (!values.label) { + values.label = `${values.report_function} of ${toTitle(values.report_field)}`; + } + this.create_number_card(values, values.dashboard, values.label); + dialog.hide(); + } + }); + + dialog.show(); + + } + add_chart_to_dashboard() { if (this.chart_fields || this.chart_options) { const dialog = new frappe.ui.Dialog({ @@ -193,6 +256,24 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } + create_number_card(values, dashboard_name, card_name) { + let args = { + 'dashboard': dashboard_name || null, + 'type': 'Report', + 'report_name': this.report_name, + 'filters_json': JSON.stringify(this.get_filter_values()), + }; + Object.assign(args, values); + + this.add_to_dashboard( + 'frappe.desk.doctype.number_card.number_card.create_report_number_card', + args, + dashboard_name, + card_name, + 'Number Card' + ); + } + create_dashboard_chart(chart_args, dashboard_name, chart_name) { let args = { 'dashboard': dashboard_name || null, @@ -235,19 +316,29 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ); } - frappe.xcall( + this.add_to_dashboard( 'frappe.desk.doctype.dashboard_chart.dashboard_chart.create_report_chart', + args, + dashboard_name, + chart_name, + 'Dashboard Chart' + ); + } + + add_to_dashboard(method, args, dashboard_name, name, doctype) { + frappe.xcall( + method, {args: args} - ).then( () => { + ).then(() => { let message; if (dashboard_name) { let dashboard_route_html = `${dashboard_name}`; - message = __(`New Dashboard Chart ${chart_name} added to Dashboard ` + dashboard_route_html); + message = __(`New {0} {1} added to Dashboard ` + dashboard_route_html, [doctype, name]); } else { - message = __(`New chart ${chart_name} created`); + message = __(`New {0} {1} created`, [doctype, name]); } - frappe.msgprint(message, __('New Chart Created')); + frappe.msgprint(message, __(`New {0} Created`, [doctype])); }); } @@ -533,6 +624,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } this.render_datatable(); this.add_chart_buttons_to_toolbar(true); + this.add_card_button_to_toolbar(); } else { this.data = []; this.toggle_nothing_to_show(true); @@ -716,7 +808,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { open_create_chart_dialog() { const me = this; - let field_options = frappe.report_utils.get_possible_chart_options(this.columns, this.raw_data); + let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data); function set_chart_values(values) { values.y_fields = []; diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 876ebfe6e4..d573718ab4 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -42,7 +42,7 @@ frappe.report_utils = { } }, - get_possible_chart_options: function(columns, data) { + get_field_options_from_report: function(columns, data) { const rows = data.result.filter(value => Object.keys(value).length); const first_row = Array.isArray(rows[0]) ? rows[0] : columns.map(col => rows[0][col.fieldname]); @@ -139,4 +139,14 @@ frappe.report_utils = { return filter_values; }, + get_result_of_fn(fn, values) { + const get_result = { + 'Minimum': values => values.reduce((min, val) => Math.min(min, val), values[0]), + 'Maximum': values => values.reduce((min, val) => Math.max(min, val), values[0]), + 'Average': values => values.reduce((a, b) => a + b, 0) / values.length, + 'Sum': values => values.reduce((a, b) => a + b, 0) + }; + return get_result[fn](values); + }, + }; diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index d634cdf1f4..ab05abb670 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -645,11 +645,13 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { set_fields() { if (this.report_name && this.report_doc.json.fields) { - this.fields = this.report_doc.json.fields.slice(); + let fields = this.report_doc.json.fields.slice(); + fields.forEach(f => this._add_field(f[0], f[1])); return; } else if (this.view_user_settings.fields) { // get from user_settings - this.fields = this.view_user_settings.fields; + let fields = this.view_user_settings.fields; + fields.forEach(f => this._add_field(f[0], f[1])); return; } diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index 401b2f2a69..2c21716f6e 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -421,10 +421,13 @@ export default class ChartWidget extends Widget { }); dialog.show(); - //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[this.chart_doc.report_name].onload - && frappe.query_reports[this.chart_doc.report_name].onload(frappe.query_report); + + if (this.chart_doc.chart_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[this.chart_doc.report_name].onload + && frappe.query_reports[this.chart_doc.report_name].onload(frappe.query_report); + } dialog.set_values(this.filters); } @@ -659,7 +662,7 @@ export default class ChartWidget extends Widget { set_chart_filters() { let user_saved_filters = this.chart_settings.filters || null; - let chart_saved_filters = JSON.parse(this.chart_doc.filters_json || "null"); + let chart_saved_filters = frappe.dashboard_utils.get_all_filters(this.chart_doc); if (this.chart_doc.chart_type == 'Report') { return frappe.dashboard_utils diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 713f4f9bdc..575926f45c 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -1,5 +1,5 @@ import Widget from "./base_widget.js"; -import { go_to_list_with_filters, shorten_number } from "./utils"; +import { generate_route, shorten_number } from "./utils"; export default class NumberCardWidget extends Widget { constructor(opts) { @@ -60,20 +60,39 @@ export default class NumberCardWidget extends Widget { } ).then(doc => { this.name = doc.name; - this.card_doc.stats_time_interval = doc.stats_time_interval; - this.card_doc.name = this.name; + this.card_doc = doc; this.widget.attr('data-widget-name', this.name); }); } set_events() { $(this.body).click(() => { - if (this.in_customize_mode) return; - let filters = JSON.parse(this.card_doc.filters_json); - go_to_list_with_filters(this.card_doc.document_type, filters); + if (this.in_customize_mode || this.card_doc.type == 'Custom') return; + this.set_route(); }); } + set_route() { + const is_document_type = this.card_doc.type !== 'Report'; + const name = is_document_type ? this.card_doc.document_type : this.card_doc.report_name; + const route = generate_route({ + name: name, + type: is_document_type ? 'doctype' : 'report', + is_query_report: !is_document_type, + }); + + if (is_document_type) { + const filters = JSON.parse(this.card_doc.filters_json); + frappe.route_options = filters.reduce((acc, filter) => { + return Object.assign(acc, { + [`${filter[0]}.${filter[1]}`]: [filter[2], filter[3]] + }); + }, {}); + } + + frappe.set_route(route); + } + set_doc_args() { this.card_doc = Object.assign({}, { document_type: this.document_type, @@ -85,11 +104,53 @@ export default class NumberCardWidget extends Widget { }); } + get_settings(type) { + this.filters = this.get_filters(); + const settings_map = { + 'Custom': { + method: this.card_doc.method, + args: { + filters: this.filters + }, + get_number: res => this.get_number_for_custom_card(res), + }, + 'Report': { + method: 'frappe.desk.query_report.run', + args: { + report_name: this.card_doc.report_name, + filters: this.filters, + ignore_prepared_report: 1 + }, + get_number: res => this.get_number_for_report_card(res), + }, + 'Document Type': { + method: 'frappe.desk.doctype.number_card.number_card.get_result', + args: { + doc: this.card_doc, + filters: this.filters, + }, + get_number: res => this.get_number_for_doctype_card(res), + } + }; + return settings_map[type]; + } + + get_filters() { + const filters = frappe.dashboard_utils.get_all_filters(this.card_doc); + return filters; + } + render_card() { this.prepare_actions(); this.set_title(); this.set_loading_state(); + if (!this.card_doc.type) { + this.card_doc.type = 'Document Type'; + } + + this.settings = this.get_settings(this.card_doc.type); + frappe.run_serially([ () => this.render_number(), () => this.render_stats(), @@ -103,43 +164,69 @@ export default class NumberCardWidget extends Widget { } get_number() { - return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_result', { - doc: this.card_doc - }).then(res => { - this.number = res; - if (this.card_doc.function !== 'Count') { - return frappe.model.with_doctype(this.card_doc.document_type, () => { - this.get_formatted_number(); - }); - } else { - this.number_html = res; - } + return frappe.xcall(this.settings.method, this.settings.args).then(res => { + this.settings.get_number(res); }); } - get_formatted_number() { - const based_on_df = - frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); + get_number_for_custom_card(res) { + if (typeof res === 'object') { + this.number = res.value; + this.get_formatted_number(res); + } else { + this.formatted_number = res; + } + } + + get_number_for_doctype_card(res) { + this.number = res; + if (this.card_doc.function !== 'Count') { + return frappe.model.with_doctype(this.card_doc.document_type, () => { + const based_on_df = + frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); + this.get_formatted_number(based_on_df); + }); + } else { + this.formatted_number = res; + } + } + + get_number_for_report_card(res) { + const field = this.card_doc.report_field; + const vals = res.result.reduce((acc, col) => { + col[field] && acc.push(col[field]); + return acc; + }, []); + const col = res.columns.find(col => col.fieldname == field); + this.number = frappe.report_utils.get_result_of_fn(this.card_doc.report_function, vals); + this.get_formatted_number(col); + } + + get_formatted_number(df) { const default_country = frappe.sys_defaults.country; const shortened_number = shorten_number(this.number, default_country); let number_parts = shortened_number.split(' '); const symbol = number_parts[1] || ''; - const formatted_number = $(frappe.format(number_parts[0], based_on_df)).text(); + const formatted_number = $(frappe.format(number_parts[0], df)).text(); - this.number_html = formatted_number + ' ' + symbol; + this.formatted_number = formatted_number + ' ' + symbol; } render_number() { return this.get_number().then(() => { $(this.body).html(`
-
${this.number_html}
+
${this.formatted_number}
`); }); } render_stats() { - let caret_html =''; + if (this.card_doc.type !== 'Document Type') { + return; + } + + let caret_html = ''; let color_class = ''; return this.get_percentage_stats().then(() => { @@ -186,6 +273,7 @@ export default class NumberCardWidget extends Widget { get_percentage_stats() { return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_percentage_difference', { doc: this.card_doc, + filters: this.filters, result: this.number }).then(res => { if (res !== undefined) { diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index a1b5a6216d..a5916086b0 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -136,7 +136,7 @@ export default class OnboardingWidget extends Widget { if (step.is_single) { route = `Form/${step.reference_document}`; } else { - route = `Form/${step.reference_document}/__('New')+ ' ' + ${step.reference_document}`; + route = `Form/${step.reference_document}/${__('New')} ${__(step.reference_document)}`; } let current_route = frappe.get_route(); @@ -262,7 +262,7 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks.after_save = callback; } - frappe.set_route(`Form/${step.reference_document}/__('New')+ ' ' + ${step.reference_document}`); + frappe.set_route(`Form/${step.reference_document}/${__('New')} ${__(step.reference_document)}`); } show_quick_entry(step) { diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index 1fcb3b10dd..233daa7a7d 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -126,16 +126,6 @@ const build_summary_item = (summary) => {
`); }; -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); @@ -176,4 +166,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/less/quill.less b/frappe/public/less/quill.less index a72602697a..98c762bb4a 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; diff --git a/frappe/public/scss/desktop.scss b/frappe/public/scss/desktop.scss index fba868d9e5..4109beece6 100644 --- a/frappe/public/scss/desktop.scss +++ b/frappe/public/scss/desktop.scss @@ -47,6 +47,13 @@ $panel-bg: $gray-50; padding: 15px; } +.frappe-rtl { + .desk-body { + padding-left: 0px; + padding-right: calc(20rem + 15px); + } +} + .widget-group { margin-bottom: 40px; 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/includes/comments/comments.html b/frappe/templates/includes/comments/comments.html index ef53effe53..ffd09523af 100644 --- a/frappe/templates/includes/comments/comments.html +++ b/frappe/templates/includes/comments/comments.html @@ -9,10 +9,10 @@ {% endif %}
- {% for comment in comment_list %} -
- {% include "templates/includes/comments/comment.html" %} -
+ {% for comment in comment_list %} +
+ {% include "templates/includes/comments/comment.html" %} +
{% endfor %}
@@ -25,26 +25,22 @@
{{ _("Add Comment") }} -