diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js new file mode 100644 index 0000000000..ac2a687bae --- /dev/null +++ b/cypress/integration/control_barcode.js @@ -0,0 +1,55 @@ +context('Control Barcode', () => { + beforeEach(() => { + cy.login(); + cy.visit('/desk'); + }); + + function get_dialog_with_barcode() { + return cy.dialog({ + title: 'Barcode', + fields: [ + { + label: 'Barcode', + fieldname: 'barcode', + fieldtype: 'Barcode' + } + ] + }); + } + + it('should generate barcode on setting a value', () => { + get_dialog_with_barcode().as('dialog'); + + cy.get('.frappe-control[data-fieldname=barcode] input') + .focus() + .type('123456789') + .blur(); + cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') + .should('exist'); + + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('barcode'); + expect(value).to.contain(' { + get_dialog_with_barcode().as('dialog'); + + cy.get('.frappe-control[data-fieldname=barcode] input') + .focus() + .type('123456789') + .blur(); + cy.get('.frappe-control[data-fieldname=barcode] input') + .clear() + .blur(); + cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]') + .should('not.exist'); + + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('barcode'); + expect(value).to.equal(''); + }); + }); +}); diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index a934132c89..63c99c4d1b 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -61,12 +61,18 @@ context('Control Link', () => { cy.server(); cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); + cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); cy.get('@todos').then(todos => { - cy.get('.frappe-control[data-fieldname=link] input').type(todos[0]).blur(); + cy.get('.frappe-control[data-fieldname=link] input').as('input'); + cy.get('@input').focus(); + cy.wait('@search_link'); + cy.get('@input').type(todos[0]).blur(); cy.wait('@validate_link'); - cy.get('.frappe-control[data-fieldname=link] input').focus(); - cy.get('.frappe-control[data-fieldname=link] .link-btn').click(); + cy.get('@input').focus(); + cy.get('.frappe-control[data-fieldname=link] .link-btn') + .should('be.visible') + .click(); cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`); }); }); diff --git a/frappe/__init__.py b/frappe/__init__.py index 6424dcd9b5..18ec5c0ee7 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '12.0.17' +__version__ = '12.0.20' __title__ = "Frappe Framework" local = Local() @@ -290,7 +290,7 @@ def log(msg): debug_log.append(as_unicode(msg)) -def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False): +def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None): """Print a message to the user (via HTTP response). Messages are sent in the `__server_messages` property in the response JSON and shown in a pop-up / modal. @@ -299,6 +299,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, :param title: [optional] Message title. :param raise_exception: [optional] Raise given exception and show message. :param as_table: [optional] If `msg` is a list of lists, render as HTML table. + :param primary_action: [optional] Bind a primary server/client side action. """ from frappe.utils import encode @@ -338,6 +339,9 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, if alert: out.alert = 1 + if primary_action: + out.primary_action = primary_action + message_log.append(json.dumps(out)) if raise_exception and hasattr(raise_exception, '__name__'): diff --git a/frappe/chat/doctype/chat_profile/chat_profile.json b/frappe/chat/doctype/chat_profile/chat_profile.json index f585924930..eb36f803fe 100644 --- a/frappe/chat/doctype/chat_profile/chat_profile.json +++ b/frappe/chat/doctype/chat_profile/chat_profile.json @@ -22,8 +22,7 @@ "fieldtype": "Link", "label": "User", "options": "User", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "default": "Online", diff --git a/frappe/chat/doctype/chat_token/chat_token.json b/frappe/chat/doctype/chat_token/chat_token.json index 40b85c5c6e..b73505ac2c 100644 --- a/frappe/chat/doctype/chat_token/chat_token.json +++ b/frappe/chat/doctype/chat_token/chat_token.json @@ -16,8 +16,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Token", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "fieldname": "ip_address", diff --git a/frappe/config/settings.py b/frappe/config/settings.py index 2422f2fae2..a0a7dcd65f 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import frappe from frappe import _ from frappe.desk.moduleview import add_setup_section @@ -88,7 +89,7 @@ def get_data(): ] }, { - "label": _("Email"), + "label": _("Email / Notifications"), "icon": "fa fa-envelope", "items": [ { @@ -120,6 +121,12 @@ def get_data(): "type": "doctype", "name": "Newsletter", "description": _("Create and manage newsletter") + }, + { + "type": "doctype", + "route": "Form/Notification Settings/{}".format(frappe.session.user), + "name": "Notification Settings", + "description": _("Configure notifications for mentions, assignments, energy points and more.") } ] }, diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 6795011745..21ed88addb 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -145,30 +145,10 @@ def get_list_context(context=None): def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None): from frappe.www.list import get_list user = frappe.session.user - ignore_permissions = False - if is_website_user(): - if not filters: filters = [] - add_name = [] - contact = frappe.db.sql(""" - select - address.name - from - `tabDynamic Link` as link - join - `tabAddress` as address on link.parent = address.name - where - link.parenttype = 'Address' and - link_name in( - select - link.link_name from `tabContact` as contact - join - `tabDynamic Link` as link on contact.name = link.parent - where - contact.user = %s)""",(user)) - for c in contact: - add_name.append(c[0]) - filters.append(("Address", "name", "in", add_name)) - ignore_permissions = True + ignore_permissions = True + + if not filters: filters = [] + filters.append(("Address", "owner", "=", user)) return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) diff --git a/frappe/core/doctype/activity_log/feed.py b/frappe/core/doctype/activity_log/feed.py index 22a7a3ee14..f51692fe9f 100644 --- a/frappe/core/doctype/activity_log/feed.py +++ b/frappe/core/doctype/activity_log/feed.py @@ -81,9 +81,9 @@ def get_feed_match_conditions(user=None, doctype='Comment'): if user_permissions: can_read_docs = [] - for doctype, obj in user_permissions.items(): + for dt, obj in user_permissions.items(): for n in obj: - can_read_docs.append('{}|{}'.format(doctype, frappe.db.escape(n.get('doc', '')))) + can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', '')))) if can_read_docs: conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 924c29bee2..1c51319790 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -18,6 +18,10 @@ frappe.ui.form.on("Communication", { frm.convert_to_click && frm.set_convert_button(); frm.subject_field = "subject"; + // content field contains weird table html that does not render well in Quill + // this field is not to be edited directly anyway, so setting it as read only + frm.set_df_property('content', 'read_only', 1); + if(frm.doc.reference_doctype && frm.doc.reference_name) { frm.add_custom_button(__(frm.doc.reference_name), function() { frappe.set_route("Form", frm.doc.reference_doctype, frm.doc.reference_name); diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index c5bd4e99c9..9391b262d7 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -3,19 +3,20 @@ frappe.ui.form.on('Data Import', { onload: function(frm) { - if(frm.doc.__islocal) { + if (frm.doc.__islocal) { frm.set_value("action", ""); } frappe.call({ - method: "frappe.core.doctype.data_import.data_import.get_importable_doc", + method: "frappe.core.doctype.data_import.data_import.get_importable_doctypes", callback: function (r) { + let importable_doctypes = r.message; frm.set_query("reference_doctype", function () { return { "filters": { "issingle": 0, "istable": 0, - "name": ['in', r.message] + "name": ['in', importable_doctypes] } }; }); diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index 80f8553121..ecf34d24b0 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -30,9 +30,8 @@ class DataImport(Document): @frappe.whitelist() -def get_importable_doc(): - import_lst = frappe.cache().hget("can_import", frappe.session.user) - return import_lst +def get_importable_doctypes(): + return frappe.cache().hget("can_import", frappe.session.user) @frappe.whitelist() def import_data(data_import): diff --git a/frappe/core/doctype/data_import/importer_new.py b/frappe/core/doctype/data_import/importer_new.py index 6fccbc89ef..f14f38c56d 100644 --- a/frappe/core/doctype/data_import/importer_new.py +++ b/frappe/core/doctype/data_import/importer_new.py @@ -54,8 +54,10 @@ class Importer: extension = None if self.data_import and self.data_import.import_file: file_doc = frappe.get_doc("File", {"file_url": self.data_import.import_file}) + parts = file_doc.get_extension() + extension = parts[1] content = file_doc.get_content() - extension = file_doc.file_name.split(".")[1] + extension = extension.lstrip(".") if file_path: content, extension = self.read_file(file_path) @@ -79,6 +81,12 @@ class Importer: return file_content, extn def read_content(self, content, extension): + error_title = _("Template Error") + if extension not in ("csv", "xlsx", "xls"): + frappe.throw( + _("Import template should be of type .csv, .xlsx or .xls"), title=error_title + ) + if extension == "csv": data = read_csv_content(content) elif extension == "xlsx": @@ -86,6 +94,11 @@ class Importer: elif extension == "xls": data = read_xls_file_from_attached_file(content) + if len(data) <= 1: + frappe.throw( + _("Import template should contain a Header and atleast one row."), title=error_title + ) + self.header_row = data[0] self.data = data[1:] @@ -862,15 +875,15 @@ class Importer: if failed_records: print("Failed to import {0} records".format(len(failed_records))) - file_name = '{0}_import_on_{1}.txt'.format(self.doctype, frappe.utils.now()) - print('Check {0} for errors'.format(os.path.join('sites', file_name))) + file_name = "{0}_import_on_{1}.txt".format(self.doctype, frappe.utils.now()) + print("Check {0} for errors".format(os.path.join("sites", file_name))) text = "" for w in failed_records: - text += "Row Indexes: {0}\n".format(str(w.get('row_indexes', []))) - text += "Messages:\n{0}\n".format('\n'.join(w.get('messages', []))) - text += "Traceback:\n{0}\n\n".format(w.get('exception')) + text += "Row Indexes: {0}\n".format(str(w.get("row_indexes", []))) + text += "Messages:\n{0}\n".format("\n".join(w.get("messages", []))) + text += "Traceback:\n{0}\n\n".format(w.get("exception")) - with open(file_name, 'w') as f: + with open(file_name, "w") as f: f.write(text) diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 071bb4afc0..bc9a1fcdcd 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -25,7 +25,7 @@ class PreparedReport(Document): enqueue(run_background, prepared_report=self.name, timeout=6000) def on_trash(self): - remove_all("PreparedReport", self.name, from_delete=True) + remove_all("Prepared Report", self.name) def run_background(prepared_report): @@ -85,7 +85,8 @@ def create_json_gz_file(data, dt, dn): "file_name": json_filename, "attached_to_doctype": dt, "attached_to_name": dn, - "content": compressed_content + "content": compressed_content, + "is_private": 1 }) _file.save(ignore_permissions=True) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index f71179d388..099c279dab 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -30,7 +30,7 @@ class Report(Document): if self.is_standard == "No": # allow only script manager to edit scripts - if frappe.session.user!="Administrator": + if self.report_type != 'Report Builder': frappe.only_for('Script Manager', True) if frappe.db.get_value("Report", self.name, "is_standard") == "Yes": diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 0399dea106..878810f459 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -56,8 +56,10 @@ def get_server_script_map(): script_map = frappe.cache().get_value('server_script_map') if script_map is None: script_map = {} - for script in frappe.get_all('Server Script', ('name', 'reference_doctype', 'doctype_event', - 'api_method', 'script_type')): + enabled_server_scripts = frappe.get_all('Server Script', + fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'), + filters={'disabled': 0}) + for script in enabled_server_scripts: if script.script_type == 'DocType Event': script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) else: diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 0bd7437ae4..7de2bb20e5 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -8,6 +8,7 @@ from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_ from frappe import throw, msgprint, _ from frappe.utils.password import update_password as _update_password from frappe.desk.notifications import clear_notifications +from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings from frappe.utils.user import get_system_managers from bs4 import BeautifulSoup import frappe.permissions @@ -46,6 +47,9 @@ class User(Document): self.flags.in_insert = True throttle_user_creation() + def after_insert(self): + create_notification_settings(self.name) + def validate(self): self.check_demo() @@ -364,6 +368,9 @@ class User(Document): if frappe.db.exists("Chat Profile", old_name): frappe.rename_doc("Chat Profile", old_name, new_name, force=True) + if frappe.db.exists("Notification Settings", old_name): + frappe.rename_doc("Notification Settings", old_name, new_name, force=True) + # set email frappe.db.sql("""UPDATE `tabUser` SET email = %s diff --git a/frappe/core/doctype/version/version.css b/frappe/core/doctype/version/version.css index 987204ed9b..769b352585 100644 --- a/frappe/core/doctype/version/version.css +++ b/frappe/core/doctype/version/version.css @@ -1,3 +1,7 @@ +.version-info { + overflow: auto; +} + .version-info pre { border: 0px; margin: 0px; @@ -14,4 +18,4 @@ .version-info .danger { background-color: #f2dede !important; -} \ No newline at end of file +} diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json index 32b66ef1ea..ecb746df64 100644 --- a/frappe/desk/doctype/notification_log/notification_log.json +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -10,7 +10,7 @@ "email_content", "column_break_4", "document_type", - "seen", + "read", "document_name", "from_user" ], @@ -57,14 +57,6 @@ "read_only": 1, "search_index": 1 }, - { - "default": "0", - "fieldname": "seen", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 1, - "label": "Seen" - }, { "fieldname": "document_name", "fieldtype": "Data", @@ -79,11 +71,19 @@ "options": "User", "read_only": 1, "search_index": 1 + }, + { + "default": "0", + "fieldname": "read", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Read" } ], "in_create": 1, - "modified": "2019-10-23 12:48:01.119356", - "modified_by": "Administrator", + "modified": "2019-11-12 15:22:35.283678", + "modified_by": "umair@erpnext.com", "module": "Desk", "name": "Notification Log", "owner": "Administrator", diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index f58c14d363..398a3de351 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -7,11 +7,12 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled, - is_email_notifications_enabled, is_email_notifications_enabled_for_type) + is_email_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value) class NotificationLog(Document): def after_insert(self): frappe.publish_realtime('notification', after_commit=True, user=self.for_user) + set_notifications_as_unseen(self.for_user) if is_email_notifications_enabled(self.for_user): send_notification_email(self) @@ -41,7 +42,6 @@ def enqueue_create_notification(users, doc): This breaks new site creation if Redis server is not running. We do not need any notifications in fresh installation ''' - if frappe.flags.in_install: return @@ -64,13 +64,13 @@ def make_notification_logs(doc, users): if is_notifications_enabled(user): if doc.type == 'Energy Point' and not is_energy_point_enabled(): return - else: - _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': - _doc.insert(ignore_permissions=True) + + _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': + _doc.insert(ignore_permissions=True) def send_notification_email(doc): is_type_enabled = is_email_notifications_enabled_for_type(doc.for_user, doc.type) @@ -112,11 +112,25 @@ def get_email_header(doc): @frappe.whitelist() -def mark_as_seen(docname): - if docname: - frappe.db.set_value('Notification Log', docname, 'seen', 1, update_modified=False) +def mark_all_as_read(): + unread_docs_list = frappe.db.get_all('Notification Log', filters = {'read': 0, 'for_user': frappe.session.user}) + unread_docnames = [doc.name for doc in unread_docs_list] + if unread_docnames: + filters = {'name': ['in', unread_docnames]} + frappe.db.set_value('Notification Log', filters, 'read', 1, update_modified=False) +@frappe.whitelist() +def mark_as_read(docname): + if docname: + frappe.db.set_value('Notification Log', docname, 'read', 1, update_modified=False) + @frappe.whitelist() def trigger_indicator_hide(): frappe.publish_realtime('indicator_hide', user=frappe.session.user) + +def set_notifications_as_unseen(user): + try: + frappe.db.set_value('Notification Settings', user, 'seen', 0) + except frappe.DoesNotExistError: + return diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js new file mode 100644 index 0000000000..d4e3b08def --- /dev/null +++ b/frappe/desk/doctype/notification_settings/notification_settings.js @@ -0,0 +1,12 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Notification Settings', { + onload: () => { + frappe.breadcrumbs.add({ + label: __('Settings'), + route: '#modules/Settings', + type: 'Custom' + }); + } +}); diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index 68eec92125..6af325507b 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -13,7 +13,8 @@ "enable_email_assignment", "enable_email_energy_point", "enable_email_share", - "user" + "user", + "seen" ], "fields": [ { @@ -72,14 +73,20 @@ "fieldname": "user", "fieldtype": "Link", "hidden": 1, - "in_list_view": 1, "label": "User", "options": "User", "read_only": 1 + }, + { + "default": "0", + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 1, + "label": "Seen" } ], "in_create": 1, - "modified": "2019-10-23 12:42:56.175928", + "modified": "2019-11-19 12:57:59.356786", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 3bb3cf9320..295b4c8afd 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -31,12 +31,12 @@ def is_email_notifications_enabled_for_type(user, notification_type): return True return enabled -@frappe.whitelist() -def create_notification_settings(): - _doc = frappe.new_doc('Notification Settings') - _doc.name = frappe.session.user - _doc.insert(ignore_permissions=True) - frappe.db.commit() +def create_notification_settings(user): + if not frappe.db.exists("Notification Settings", user): + _doc = frappe.new_doc('Notification Settings') + _doc.name = user + _doc.insert(ignore_permissions=True) + frappe.db.commit() @frappe.whitelist() @@ -60,3 +60,7 @@ def get_permission_query_conditions(user): if not user: user = frappe.session.user return '''(`tabNotification Settings`.user = '{user}')'''.format(user=user) + +@frappe.whitelist() +def set_seen_value(value, user): + frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False) \ No newline at end of file diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index c5e5ea7c2b..0bad171b04 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -234,8 +234,11 @@ def get_config(app, module): for item in section["items"]: if item["type"]=="report" and item["name"] in disabled_reports: continue + # some module links might not have name + if not item.get("name"): + item["name"] = item.get("label") if not item.get("label"): - item["label"] = _(item["name"]) + item["label"] = _(item.get("name")) items.append(item) section['items'] = items @@ -297,7 +300,7 @@ def get_onboard_items(app, module): @frappe.whitelist() def get_links_for_module(app, module): - return [l.get('label') for l in get_links(app, module)] + return [{'value': l.get('name'), 'label': l.get('label')} for l in get_links(app, module)] def get_links(app, module): try: @@ -330,13 +333,13 @@ def get_desktop_settings(): def apply_user_saved_links(module): module = frappe._dict(module) all_links = get_links(module.app, module.module_name) - module_links_by_label = {} + module_links_by_name = {} for link in all_links: - module_links_by_label[link['label']] = link + module_links_by_name[link['name']] = link if module.module_name in user_saved_links_by_module: user_links = frappe.parse_json(user_saved_links_by_module[module.module_name]) - module.links = [module_links_by_label[l] for l in user_links if l in module_links_by_label] + module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name] return module diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d5b43807a8..937285206e 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -261,17 +261,24 @@ def delete_bulk(doctype, items): @frappe.whitelist() @frappe.read_only() def get_sidebar_stats(stats, doctype, filters=[]): + _user_tags = [] + data = frappe._dict(frappe.local.form_dict) + filters = json.loads(data["filters"]) - if not frappe.cache().hget("tags_count", doctype): - tags = [tag.name for tag in frappe.get_list("Tag")] - _user_tags = [] - for tag in tags: - count = frappe.db.count("Tag Link", filters={"document_type": doctype, "tag": tag}) - if count > 0: - _user_tags.append([tag, count]) - frappe.cache().hset("tags_count", doctype, _user_tags) + if not frappe.cache().hget("Tags", doctype): + tags = set([tag.tag for tag in frappe.get_list("Tag Link", filters={"document_type": doctype}, fields=["tag"])]) + frappe.cache().hset("Tags", doctype, tags) - return {"stats": {"_user_tags": frappe.cache().hget("tags_count", doctype)}} + for tag in list(frappe.cache().hget("Tags", doctype)): + tag_filters = [] + tag_filters.extend(filters) + tag_filters.extend([['Tag Link', 'tag', '=', tag]]) + + count = frappe.get_all(doctype, filters=tag_filters, fields=["count(*)"]) + if count[0].get("count(*)") > 0: + _user_tags.append([tag, count[0].get("count(*)")]) + + return {"stats": {"_user_tags": _user_tags}} @frappe.whitelist() @frappe.read_only() diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js index 7a175e7192..71f9cccb0d 100644 --- a/frappe/email/doctype/email_account/email_account.js +++ b/frappe/email/doctype/email_account/email_account.js @@ -184,7 +184,7 @@ frappe.ui.form.on("Email Account", { read as well as unread message from server. This may also cause the duplication\ of Communication (emails)."); frappe.confirm(msg, null, function() { - frm.set_value("email_sync_option", "UNSEEN"); + frm.set_value("email_sync_option", "ALL"); }); } } diff --git a/frappe/email/receive.py b/frappe/email/receive.py index ee7075b570..b8fde57a43 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -298,7 +298,7 @@ class EmailServer: "Connection timed out", ) for message in messages: - if message in strip(cstr(e.message)) or message in strip(cstr(getattr(e, 'strerror', ''))): + if message in strip(cstr(e)) or message in strip(cstr(getattr(e, 'strerror', ''))): return True return False diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py index 26e0de35b5..5e464d4882 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -180,6 +180,33 @@ class RazorpaySettings(Document): integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) + def create_order(self, **kwargs): + # Creating Orders https://razorpay.com/docs/api/orders/ + + # convert rupees to paisa + kwargs['amount'] *= 100 + + # Create integration log + integration_request = create_request_log(kwargs, "Host", "Razorpay") + + # Setup payment options + payment_options = { + "amount": kwargs.get('amount'), + "currency": kwargs.get('currency', 'INR'), + "receipt": kwargs.get('receipt'), + "payment_capture": kwargs.get('payment_capture') + } + if self.api_key and self.api_secret: + try: + order = make_post_request("https://api.razorpay.com/v1/orders", + auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), + data=payment_options) + order['integration_request'] = integration_request.name + return order # Order returned to be consumed by razorpay.js + except Exception: + frappe.log(frappe.get_traceback()) + frappe.throw(_("Could not create razorpay order")) + def create_request(self, data): self.data = frappe._dict(data) @@ -213,6 +240,10 @@ class RazorpaySettings(Document): self.integration_request.update_status(data, 'Authorized') self.flags.status_changed_to = "Authorized" + if resp.get("status") == "captured": + self.integration_request.update_status(data, 'Completed') + self.flags.status_changed_to = "Completed" + elif data.get('subscription_id'): if resp.get("status") == "refunded": # if subscription start date is in future then @@ -222,14 +253,6 @@ class RazorpaySettings(Document): self.integration_request.update_status(data, 'Completed') self.flags.status_changed_to = "Verified" - if resp.get("status") == "captured": - # if subscription starts immediately then - # razorpay charge the actual amount - # thus changing status to Completed - - self.integration_request.update_status(data, 'Completed') - self.flags.status_changed_to = "Completed" - else: frappe.log_error(str(resp), 'Razorpay Payment not authorized') @@ -242,7 +265,6 @@ class RazorpaySettings(Document): redirect_to = data.get('redirect_to') or None redirect_message = data.get('redirect_message') or None - if self.flags.status_changed_to in ("Authorized", "Verified", "Completed"): if self.data.reference_doctype and self.data.reference_docname: custom_redirect_to = None @@ -330,6 +352,63 @@ def capture_payment(is_sandbox=False, sanbox_response=None): doc.error = frappe.get_traceback() frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) + +@frappe.whitelist(allow_guest=True) +def get_api_key(): + controller = frappe.get_doc("Razorpay Settings") + return controller.api_key + +@frappe.whitelist(allow_guest=True) +def get_order(doctype, docname): + # Order returned to be consumed by razorpay.js + doc = frappe.get_doc(doctype, docname) + try: + # Do not use run_method here as it fails silently + return doc.get_razorpay_order() + except AttributeError: + frappe.log_error(frappe.get_traceback(), _("Controller method get_razorpay_order missing")) + frappe.throw(_("Could not create Razorpay order. Please contact Administrator")) + +@frappe.whitelist(allow_guest=True) +def order_payment_success(integration_request, params): + """Called by razorpay.js on order payment success, the params + contains razorpay_payment_id, razorpay_order_id, razorpay_signature + that is updated in the data field of integration request + + Args: + integration_request (string): Name for integration request doc + params (string): Params to be updated for integration request. + """ + params = json.loads(params) + integration = frappe.get_doc("Integration Request", integration_request) + + # Update integration request + integration.update_status(params, integration.status) + integration.reload() + + data = json.loads(integration.data) + controller = frappe.get_doc("Razorpay Settings") + + # Update payment and integration data for payment controller object + controller.integration_request = integration + controller.data = frappe._dict(data) + + # Authorize payment + controller.authorize_payment() + +@frappe.whitelist(allow_guest=True) +def order_payment_failure(integration_request, params): + """Called by razorpay.js on failure + + Args: + integration_request (TYPE): Description + params (TYPE): error data to be updated + """ + frappe.log_error(params, 'Razorpay Payment Failure') + params = json.loads(params) + integration = frappe.get_doc("Integration Request", integration_request) + integration.update_status(params, integration.status) + def convert_rupee_to_paisa(**kwargs): for addon in kwargs.get('addons'): addon['item']['amount'] *= 100 @@ -383,4 +462,4 @@ def validate_payment_callback(data): _throw() def handle_subscription_notification(doctype, docname): - call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) \ No newline at end of file + call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index af67350ab6..8ea12f2cec 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -332,8 +332,8 @@ def clear_references(doctype, reference_doctype, reference_name, (reference_doctype, reference_name)) def clear_timeline_references(link_doctype, link_name): - frappe.db.sql("""delete from `tabCommunication Link` - where `tabCommunication Link`.link_doctype='{0}' and `tabCommunication Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec + frappe.db.sql("""DELETE FROM `tabCommunication Link` + WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name)) def insert_feed(doc): from frappe.utils import get_fullname diff --git a/frappe/patches.txt b/frappe/patches.txt index d4aaec5bfc..5dbde7ed40 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -253,7 +253,8 @@ frappe.patches.v12_0.move_email_and_phone_to_child_table frappe.patches.v12_0.delete_duplicate_indexes frappe.patches.v12_0.set_default_incoming_email_port frappe.patches.v12_0.update_global_search -execute:frappe.reload_doc('desk', 'doctype', 'notification_settings') frappe.patches.v12_0.setup_tags frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable frappe.patches.v12_0.copy_to_parent_for_tags +frappe.patches.v12_0.create_notification_settings_for_user +frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 diff --git a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py new file mode 100644 index 0000000000..dd212d157e --- /dev/null +++ b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals +import frappe + + +def execute(): + files = frappe.get_all("File", fields=["name", "attached_to_name"], filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) + for file_dict in files: + # For some reason Prepared Report doc might not exist, check if it exists first + if frappe.db.exists("Prepared Report", file_dict.attached_to_name): + try: + file_doc = frappe.get_doc("File", file_dict.name) + file_doc.is_private = 1 + file_doc.save() + except Exception: + # File might not exist on the file system in that case delete both Prepared Report and File doc + frappe.delete_doc("Prepared Report", file_dict.attached_to_name) + else: + # If Prepared Report doc doesn't exist then the file doc is useless. Delete it. + frappe.delete_doc("File", file_dict.name) diff --git a/frappe/patches/v12_0/create_notification_settings_for_user.py b/frappe/patches/v12_0/create_notification_settings_for_user.py new file mode 100644 index 0000000000..63eeccc07a --- /dev/null +++ b/frappe/patches/v12_0/create_notification_settings_for_user.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +import frappe +from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings + +def execute(): + frappe.reload_doc('desk', 'doctype', 'notification_settings') + frappe.reload_doc('desk', 'doctype', 'notification_subscribed_document') + + users = frappe.db.get_all('User', fields=['name']) + for user in users: + create_notification_settings(user.name) \ No newline at end of file diff --git a/frappe/public/build.json b/frappe/public/build.json index ddca0bbc7e..32a2cfd223 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -18,6 +18,9 @@ "js/frappe-recorder.min.js": [ "public/js/frappe/recorder/recorder.js" ], + "js/checkout.min.js": [ + "public/js/integrations/razorpay.js" + ], "js/frappe-web.min.js": [ "public/js/frappe/class.js", "public/js/frappe/polyfill.js", diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 3deeb02ae4..4fbea6684f 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -136,11 +136,7 @@ frappe.Application = Class.extend({ method: 'frappe.core.page.background_jobs.background_jobs.get_scheduler_status', callback: function(r) { if (r.message[0] == __("Inactive")) { - frappe.msgprint({ - title: __("Scheduler Inactive"), - indicator: "red", - message: __("Background jobs are not running. Please contact Administrator") - }); + frappe.call('frappe.utils.scheduler.activate_scheduler'); } } }); diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index dbbde40f2a..845fbf92b4 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -367,6 +367,13 @@ export default { if (this.on_success) { this.on_success(file_doc, r); } + } else if (xhr.status === 403) { + let response = JSON.parse(xhr.responseText); + frappe.msgprint({ + title: __('Not permitted'), + indicator: 'red', + message: response._error_message + }); } else { file.failed = true; let error = null; diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js index 1cd1411b49..08bb7b763f 100644 --- a/frappe/public/js/frappe/form/controls/barcode.js +++ b/frappe/public/js/frappe/form/controls/barcode.js @@ -1,18 +1,27 @@ -import JsBarcode from "jsbarcode"; +import JsBarcode from 'jsbarcode'; frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({ make_wrapper() { // Create the elements for barcode area this._super(); + this.default_svg = ''; let $input_wrapper = this.$wrapper.find('.control-input-wrapper'); - this.barcode_area = $(`
`); + this.barcode_area = $( + `
${this.default_svg}
` + ); this.barcode_area.appendTo($input_wrapper); }, parse(value) { // Parse raw value - return value ? this.get_barcode_html(value) : ""; + if (value) { + if (value.startsWith(' { + if ($(e.target).is('input')) { + return; + } if (e.key === 'Backspace') { this.set_value([]); } diff --git a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js index 26485adcc0..1c5787f854 100644 --- a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js +++ b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js @@ -35,4 +35,4 @@ MentionBlot.blotName = 'mention'; MentionBlot.tagName = 'span'; MentionBlot.className = 'mention'; -Quill.register(MentionBlot); +Quill.register(MentionBlot, true); diff --git a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js index 0b50ffd758..80ee5a6c6d 100644 --- a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js +++ b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js @@ -361,6 +361,6 @@ class Mention { } } -Quill.register('modules/mention', Mention); +Quill.register('modules/mention', Mention, true); export default Mention; diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 1b9078ae03..b35c92c1ae 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -36,7 +36,7 @@ class MyLink extends Link { } } -Quill.register(MyLink); +Quill.register(MyLink, true); // image uploader const Uploader = Quill.import('modules/uploader'); diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 90e0f90be7..3de62daf64 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -52,7 +52,7 @@ export default class Grid { let template = `
- +
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 648a3d6d55..2c5e1df1df 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -614,7 +614,7 @@ class FilterArea { let options = df.options; let condition = '='; let fieldtype = df.fieldtype; - if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) { + if (['Text', 'Small Text', 'Text Editor', 'Data', 'Code'].includes(fieldtype)) { fieldtype = 'Data'; condition = 'like'; } diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 0000a3dc73..4767e02f43 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -285,7 +285,8 @@ frappe.views.ListSidebar = class ListSidebar { args: { stats: me.stats, doctype: me.doctype, - filters: me.default_filters || [] + // wait for list filter area to be generated before getting filters, or fallback to default filters + filters: (me.list_view.filter_area ? me.list_filter.get_current_filters() : me.default_filters) || [] }, callback: function(r) { me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); @@ -385,7 +386,8 @@ frappe.views.ListSidebar = class ListSidebar { } reload_stats() { - this.sidebar.find(".sidebar-stat").remove(); + this.sidebar.find(".stat-link").remove(); + this.sidebar.find(".stat-no-records").remove(); this.get_stats(); } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 6912065e1c..724f6829c2 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -390,6 +390,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); } + after_render() { + this.list_sidebar.reload_stats(); + } + render() { this.render_list(); this.on_row_checked(); @@ -577,7 +581,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { data-filter="${fieldname},=,${value}"> ${_value} `; - } else if (['Text Editor', 'Text', 'Small Text'].includes(df.fieldtype)) { + } else if (['Text Editor', 'Text', 'Small Text', 'HTML Editor'].includes(df.fieldtype)) { html = ` ${_value} `; @@ -589,7 +593,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } return ` + title="${__(label)}: ${escape(_value)}"> ${html} `; }; @@ -1077,10 +1081,21 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); this.toggle_result_area(); this.render_list(); + if (this.$checks.length) { + this.set_rows_as_checked(); + } }); }); } + set_rows_as_checked() { + $.each(this.$checks, (i, el) => { + let docname = $(el).attr('data-name'); + this.$result.find(`.list-row-checkbox[data-name='${docname}']`).prop('checked', true); + }); + this.on_row_checked(); + } + on_row_checked() { this.$list_head_subject = this.$list_head_subject || this.$result.find('header .list-header-subject'); this.$checkbox_actions = this.$checkbox_actions || this.$result.find('header .checkbox-actions'); diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 0c6388c681..48d0d14037 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -525,7 +525,13 @@ $.extend(frappe.model, { }, delete_doc: function(doctype, docname, callback) { - frappe.confirm(__("Permanently delete {0}?", [docname]), function() { + var title = docname; + var title_field = frappe.get_meta(doctype).title_field; + if (frappe.get_meta(doctype).autoname == "hash" && title_field) { + var title = frappe.model.get_value(doctype, docname, title_field); + title += " (" + docname + ")"; + } + frappe.confirm(__("Permanently delete {0}?", [title]), function() { return frappe.call({ method: 'frappe.client.delete', args: { diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 5fda43375d..4737f18bc0 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -367,24 +367,28 @@ frappe.ui.filter_utils = { get_selected_value(field, condition) { let val = field.get_value(); - if(typeof val==='string') { + if (typeof val === 'string') { val = strip(val); } - if(field.df.original_type == 'Check') { + if (condition == 'is' && !val) { + val = field.df.options[0].value; + } + + if (field.df.original_type == 'Check') { val = (val=='Yes' ? 1 :0); } - if(condition.indexOf('like', 'not like')!==-1) { + if (condition.indexOf('like', 'not like') !== -1) { // automatically append wildcards - if(val && !(val.startsWith('%') || val.endsWith('%'))) { + if (val && !(val.startsWith('%') || val.endsWith('%'))) { val = '%' + val + '%'; } - } else if(in_list(["in", "not in"], condition)) { - if(val) { + } else if (in_list(["in", "not in"], condition)) { + if (val) { val = val.split(',').map(v => strip(v)); } - } if(val === '%') { + } if (val === '%') { val = ""; } diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index f804648834..08711a8237 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -120,12 +120,6 @@ frappe.msgprint = function(msg, title) { } }); - // setup and bind an action to the primary button - if (data.primary_action) { - frappe.msg_dialog.set_primary_action(__(data.primary_action.label || "Done"), - data.primary_action.action); - } - // class "msgprint" is used in tests frappe.msg_dialog.msg_area = $('
') .appendTo(frappe.msg_dialog.body); @@ -137,6 +131,43 @@ frappe.msgprint = function(msg, title) { frappe.msg_dialog.indicator = frappe.msg_dialog.header.find('.indicator'); } + // setup and bind an action to the primary button + if (data.primary_action) { + if (data.primary_action.server_action && typeof data.primary_action.server_action === 'string') { + data.primary_action.action = () => { + frappe.call({ + method: data.primary_action.server_action, + args: { + args: data.primary_action.args + } + }); + } + } + + if (data.primary_action.client_action && typeof data.primary_action.client_action === 'string') { + let parts = data.primary_action.client_action.split('.'); + let obj = window; + for (let part of parts) { + obj = obj[part]; + } + data.primary_action.action = () => { + if (typeof obj === 'function') { + obj(data.primary_action.args); + } + } + } + + frappe.msg_dialog.set_primary_action( + __(data.primary_action.label || "Done"), + data.primary_action.action + ); + } else { + if (frappe.msg_dialog.has_primary_action) { + frappe.msg_dialog.get_primary_btn().addClass('hide'); + frappe.msg_dialog.has_primary_action = false; + } + } + if(data.message==null) { data.message = ''; } diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 36d2891928..2420d6772e 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -1,3 +1,5 @@ +frappe.provide('frappe.search'); + frappe.ui.Notifications = class Notifications { constructor() { frappe.model @@ -29,16 +31,25 @@ frappe.ui.Notifications = class Notifications { ); frappe.utils.bind_actions_with_object(this.$dropdown_list, this); + let me = this; + frappe.search.utils.make_function_searchable( + me.route_to_settings, + __('Notification Settings'), + ); + this.setup_notifications(); this.bind_events(); } + route_to_settings() { + frappe.set_route(`#Form/Notification Settings/${frappe.session.user}`); + } + setup_notifications() { this.get_notifications_list(this.max_length).then(list => { this.dropdown_items = list; this.render_notifications_dropdown(); - - if (this.$notifications.find('.unseen').length) { + if (this.notifications_settings.seen == 0) { this.$notification_indicator.show(); } }); @@ -204,7 +215,7 @@ frappe.ui.Notifications = class Notifications { change_activity_status() { if (this.$dropdown_list.find('.activity-status')) { this.$dropdown_list.find('.activity-status').replaceWith( - `
${__('View Full Log')}
` @@ -212,26 +223,44 @@ frappe.ui.Notifications = class Notifications { } } - set_field_as_seen(docname, $el) { + set_field_as_read(docname, $el) { frappe.call( - 'frappe.desk.doctype.notification_log.notification_log.mark_as_seen', + 'frappe.desk.doctype.notification_log.notification_log.mark_as_read', { docname: docname } ).then(()=> { - $el.removeClass('unseen'); + $el.removeClass('unread'); }); } - explicitly_mark_as_seen(e, $target) { + explicitly_mark_as_read(e, $target) { e.preventDefault(); e.stopImmediatePropagation(); - let docname = $target.parents('.unseen').attr('data-name'); - this.set_field_as_seen(docname, $target.parents('.unseen')); + let docname = $target.parents('.unread').attr('data-name'); + this.set_field_as_read(docname, $target.parents('.unread')); } - mark_as_seen(e, $target) { + mark_as_read(e, $target) { let docname = $target.attr('data-name'); let df = this.dropdown_items.filter(f => docname.includes(f.name))[0]; - this.set_field_as_seen(df.name, $target); + this.set_field_as_read(df.name, $target); + } + + mark_all_as_read(e) { + e.stopImmediatePropagation(); + this.$dropdown_list.find('.unread').removeClass('unread'); + frappe.call( + 'frappe.desk.doctype.notification_log.notification_log.mark_all_as_read', + ); + } + + toggle_seen(flag) { + frappe.call( + 'frappe.desk.doctype.notification_settings.notification_settings.set_seen_value', + { + value: cint(flag), + user: frappe.session.user + } + ); } get_notifications_list(limit) { @@ -279,8 +308,8 @@ frappe.ui.Notifications = class Notifications { field.document_type, field.document_name ); - let seen_class = field.seen ? '' : 'unseen'; - let mark_seen_action = field.seen ? '': 'data-action="mark_as_seen"'; + let read_class = field.read ? '' : 'unread'; + let mark_read_action = field.read ? '': 'data-action="mark_as_read"'; let message = field.subject; let title = message.match(/(.*?)<\/b>/); message = title ? message.replace(title[1], frappe.ellipsis(title[1], 100)): message; @@ -288,18 +317,18 @@ frappe.ui.Notifications = class Notifications { let user = field.from_user; let user_avatar = frappe.avatar(user, 'avatar-small user-avatar'); let timestamp = frappe.datetime.comment_when(field.creation, true); - let item_html = - ` ${user_avatar} ${message_html}
${timestamp}
-
`; @@ -329,18 +358,25 @@ frappe.ui.Notifications = class Notifications { let category_id = frappe.dom.get_unique_id(); let settings_html = category.value === 'Notifications' - ? ` + ? ` ${__('Settings')} ` : ''; + let mark_all_read_html = + category.value === 'Notifications' + ? ` + ${__('Mark all as Read')} + ` + : ''; let html = `
  • ${category.label} ${settings_html} + ${mark_all_read_html}
  • @@ -364,20 +400,11 @@ frappe.ui.Notifications = class Notifications { ); } - make_and_route_to_settings(e) { + go_to_settings(e) { e.stopImmediatePropagation(); this.$dropdown.removeClass('open'); this.$dropdown.trigger('hide.bs.dropdown'); - let method = - 'frappe.desk.doctype.notification_settings.notification_settings.create_notification_settings'; - - return Promise.resolve() - .then(() => { - if (!this.notifications_settings) return frappe.call(method); - }) - .then(() => { - frappe.set_route(`#Form/Notification Settings/${frappe.session.user}`); - }); + this.route_to_settings(); } bind_events() { @@ -418,22 +445,14 @@ frappe.ui.Notifications = class Notifications { }); this.$dropdown.on('hide.bs.dropdown', e => { let hide = $(e.currentTarget).data('closable'); - if (hide) { - this.$dropdown_list - .find('[data-category="Notifications"]') - .collapse('show'); - this.$dropdown_list - .find( - '[data-category="Todays Events"], [data-category="Open Documents"]' - ) - .collapse('hide'); - } $(e.currentTarget).data('closable', true); return hide; }); this.$dropdown.on('show.bs.dropdown', () => { + this.toggle_seen(true); if (this.$notification_indicator.is(':visible')) { + this.$notification_indicator.hide(); frappe.call( 'frappe.desk.doctype.notification_log.notification_log.trigger_indicator_hide' ); @@ -490,4 +509,4 @@ frappe.ui.notifications = { } frappe.set_route('List', doctype); } -}; \ No newline at end of file +}; diff --git a/frappe/public/js/frappe/ui/toolbar/search_utils.js b/frappe/public/js/frappe/ui/toolbar/search_utils.js index c67c4b0b13..153a4dfa67 100644 --- a/frappe/public/js/frappe/ui/toolbar/search_utils.js +++ b/frappe/public/js/frappe/ui/toolbar/search_utils.js @@ -622,20 +622,21 @@ frappe.search.utils = { value: this.bolden_match_part(__(item.label), txt), index: this.fuzzy_search(txt, target), match: item.label, - onclick: item.action, + onclick: () => item.action.apply(this, item.args) }); } }); return results; }, - make_function_searchable(_function, label=null) { + make_function_searchable(_function, label=null, args=null) { if (typeof _function !== 'function') { throw new Error('First argument should be a function'); } this.searchable_functions.push({ 'label': label || _function.name, - 'action': _function + 'action': _function, + 'args': args, }); }, searchable_functions: [], diff --git a/frappe/public/js/frappe/views/components/DeskModuleBox.vue b/frappe/public/js/frappe/views/components/DeskModuleBox.vue index 355a447475..0d809508ee 100644 --- a/frappe/public/js/frappe/views/components/DeskModuleBox.vue +++ b/frappe/public/js/frappe/views/components/DeskModuleBox.vue @@ -63,7 +63,7 @@ export default { } }, dropdown_links() { - return this.links.length > 0 ? this.links + return this.type === 'module' ? this.links .filter(link => !link.hidden) .concat([ { label: __('Customize'), action: () => this.$emit('customize'), class: 'border-top' } diff --git a/frappe/public/js/frappe/views/components/DeskSection.vue b/frappe/public/js/frappe/views/components/DeskSection.vue index 9b24f75294..83670a3e82 100644 --- a/frappe/public/js/frappe/views/components/DeskSection.vue +++ b/frappe/public/js/frappe/views/components/DeskSection.vue @@ -26,7 +26,8 @@ export default { }, data() { return { - dragging: false + dragging: false, + fetched_module_links: {} } }, mounted() { @@ -53,6 +54,7 @@ export default { }) }, show_module_card_customize_dialog(module) { + const me = this; const d = new frappe.ui.Dialog({ title: __('Customize Shortcuts'), fields: [ @@ -60,11 +62,19 @@ export default { label: __('Shortcuts'), fieldname: 'links', fieldtype: 'MultiSelectPills', - get_data() { - return frappe.call('frappe.desk.moduleview.get_links_for_module', { - app: module.app, - module: module.module_name, - }).then(r => r.message); + get_data: () => { + const module_links = me.fetched_module_links[module.module_name]; + if (!module_links) { + return frappe.xcall('frappe.desk.moduleview.get_links_for_module', { + app: module.app, + module: module.module_name, + }).then(links => { + me.fetched_module_links[module.module_name] = links; + return links; + }); + } else { + return module_links; + } }, default: module.links.filter(l => !l.hidden).map(l => l.name) } @@ -73,7 +83,7 @@ export default { primary_action: ({ links }) => { frappe.call('frappe.desk.moduleview.update_links_for_module', { module_name: module.module_name, - links + links: links || [] }).then(r => { this.$emit('update-desktop-settings', r.message); }); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 4b72a6b7b5..560cb3d17b 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -975,12 +975,15 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return this.data[index]; } }).filter(Boolean); - let totalRow = this.datatable.bodyRenderer.getTotalRow().reduce((row, cell) => { - row[cell.column.id] = cell.content; - return row; - }, {}); - rows.push(totalRow); + if (this.raw_data.add_total_row) { + let totalRow = this.datatable.bodyRenderer.getTotalRow().reduce((row, cell) => { + row[cell.column.id] = cell.content; + return row; + }, {}); + + rows.push(totalRow); + } return rows; } diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 6503f1c7ac..94c8e122c8 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -500,10 +500,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { axisOptions: { shortenYAxisNumbers: 1 }, - - format_tooltip_x: value => value.doc.name, - format_tooltip_y: - value => frappe.format(value, get_df(value.field), { always_show_decimals: true, inline: true }, get_doc(value.doc)) + tooltipOptions: { + formatTooltipY: value => frappe.format(value, get_df(this.chart_args.y_axes[0]), { always_show_decimals: true, inline: true }, get_doc(value.doc)) + } }); } @@ -997,7 +996,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { content: d[cdt_field(col.field)], editable: Boolean(name && this.is_editable(col.docfield, d)), format: value => { - return frappe.format(value, col.docfield, { always_show_decimals: true }); + return frappe.format(value, col.docfield, { always_show_decimals: true }, d); } }; } diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index ca44dd65d0..f1d1d5076e 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -40,7 +40,7 @@ export default class WebForm extends frappe.ui.FieldGroup { } set_field_values() { - if (this.doc_name) this.set_values(this.doc); + if (this.doc.name) this.set_values(this.doc); else return; } @@ -101,7 +101,9 @@ export default class WebForm extends frappe.ui.FieldGroup { this.validate && this.validate(); // validation hack: get_values will check for missing data - super.get_values(this.allow_incomplete); + let isvalid = super.get_values(this.allow_incomplete); + + if (!isvalid) return; if (window.saving) return; let for_payment = Boolean(this.accept_payment && !this.doc.paid); diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index faae88fce6..7bf7162101 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -52,12 +52,13 @@ frappe.ready(function() { const data = setup_fields(r.message); let web_form_doc = data.web_form; - if (web_form_doc.doc_name && web_form_doc.allow_edit === 0) { - window.location.replace(window.location.pathname + "?new=1"); - return; + if (web_form_doc.name && web_form_doc.allow_edit === 0) { + if (!window.location.href.includes("?new=1")) { + window.location.replace(window.location.pathname + "?new=1"); + } } let doc = r.message.doc || build_doc(r.message); - web_form.prepare(web_form_doc, doc); + web_form.prepare(web_form_doc, r.message.doc && web_form_doc.allow_edit === 1 ? r.message.doc : {}); web_form.make(); web_form.set_default_values(); }) diff --git a/frappe/public/js/integrations/razorpay.js b/frappe/public/js/integrations/razorpay.js index 0885826e5c..e1186427d8 100644 --- a/frappe/public/js/integrations/razorpay.js +++ b/frappe/public/js/integrations/razorpay.js @@ -1,26 +1,148 @@ -frappe.provide("frappe.integration_service") +/* HOW-TO -frappe.integration_service.razorpay = { - load: function(frm) { - new frappe.integration_service.Razorpay(frm) - }, - scheduler_job_helper: function(){ - return { - "Every few minutes": "Check and capture new payments" +Razorpay Payment + +1. Include checkout script in your code + + +2. Create the Order controller in your backend + def get_razorpay_order(self): + controller = get_payment_gateway_controller("Razorpay") + + payment_details = { + "amount": 300, + ... + "reference_doctype": "Conference Participant", + "reference_docname": self.name, + ... + "receipt": self.name + } + + return controller.create_order(**payment_details) + +3. Inititate the payment in client using checkout API + function make_payment(ticket) { + var options = { + "name": "", + "description": "", + "image": "", + "prefill": { + "name": "", + "email": "", + "contact": "" + }, + "theme": { + "color": "" + }, + "doctype": "", + "docname": " { +