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..1ee5db416b 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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/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/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..87bfc1ca17 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) @@ -64,13 +65,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 +113,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.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/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/patches.txt b/frappe/patches.txt index d4aaec5bfc..9473323059 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -253,7 +253,7 @@ 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 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/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..0ea9a801fb 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'; } @@ -625,17 +625,13 @@ class FilterArea { options = options.join("\n"); } } - let default_value = (fieldtype === 'Link') ? frappe.defaults.get_user_default(options) : null; - if (['__default', '__global'].includes(default_value)) { - default_value = null; - } + return { fieldtype: fieldtype, label: __(df.label), options: options, fieldname: df.fieldname, condition: condition, - default: default_value, onchange: () => this.refresh_list_view(), ignore_link_validation: fieldtype === 'Dynamic Link' }; @@ -650,6 +646,13 @@ class FilterArea { for (let key in fields_dict) { let field = fields_dict[key]; let value = field.get_value(); + let default_value = (field.df.fieldtype === 'Link') ? + frappe.defaults.get_user_default(field.df.options) : null; + + if (['__default', '__global'].includes(default_value)) { + default_value = null; + } + if (value) { if (field.df.condition === 'like' && !value.includes('%')) { value = '%' + value + '%'; @@ -660,6 +663,13 @@ class FilterArea { field.df.condition || '=', value ]); + } else if (default_value) { + filters.push([ + this.list_view.doctype, + field.df.fieldname, + field.df.condition, + default_value + ]); } } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 6912065e1c..b980ae7684 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1077,10 +1077,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/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/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..a4cefd1d02 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; } 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": " { +