diff --git a/frappe/__init__.py b/frappe/__init__.py index 95d9c782a4..9b3ffc4662 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1,8 +1,14 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt """ -globals attached to frappe module -+ some utility functions that should probably be moved +Frappe - Low Code Open Source Framework in Python and JS + +Frappe, pronounced fra-pay, is a full stack, batteries-included, web +framework written in Python and Javascript with MariaDB as the database. +It is the framework which powers ERPNext. It is pretty generic and can +be used to build database driven apps. + +Read the documentation: https://frappeframework.com/docs """ from __future__ import unicode_literals, print_function diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 830af68de7..281e699640 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -15,6 +15,8 @@ from frappe.model.document import Document from frappe.core.doctype.communication.email import make from frappe.utils.background_jobs import get_jobs from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated +from frappe.contacts.doctype.contact.contact import get_contacts_linked_from +from frappe.contacts.doctype.contact.contact import get_contacts_linking_to month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} @@ -328,13 +330,8 @@ class AutoRepeat(Document): def fetch_linked_contacts(self): if self.reference_doctype and self.reference_document: - res = frappe.db.get_all('Contact', - fields=['email_id'], - filters=[ - ['Dynamic Link', 'link_doctype', '=', self.reference_doctype], - ['Dynamic Link', 'link_name', '=', self.reference_document] - ]) - + res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) + res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id']) email_ids = list(set([d.email_id for d in res])) if not email_ids: frappe.msgprint(_('No contacts linked to document'), alert=True) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 987ba7d3d6..42fa039f74 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -256,3 +256,27 @@ def get_contact_with_phone_number(number): def get_contact_name(email_id): contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) return contact[0].parent if contact else None + +def get_contacts_linking_to(doctype, docname, fields=None): + """Return a list of contacts containing a link to the given document.""" + return frappe.get_list('Contact', fields=fields, filters=[ + ['Dynamic Link', 'link_doctype', '=', doctype], + ['Dynamic Link', 'link_name', '=', docname] + ]) + +def get_contacts_linked_from(doctype, docname, fields=None): + """Return a list of contacts that are contained in (linked from) the given document.""" + link_fields = frappe.get_meta(doctype).get('fields', { + 'fieldtype': 'Link', + 'options': 'Contact' + }) + if not link_fields: + return [] + + contact_names = frappe.get_value(doctype, docname, fieldname=[f.fieldname for f in link_fields]) + if not contact_names: + return [] + + return frappe.get_list('Contact', fields=fields, filters={ + 'name': ('in', contact_names) + }) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 8a9090bc37..cbcfa350f5 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -656,7 +656,7 @@ class DocType(Document): flags = {"flags": re.ASCII} if six.PY3 else {} # a DocType name should not start or end with an empty space - if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags): + if re.search("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags): frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError) # a DocType's name should not start with a number or underscore diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 99d1deeb03..747ace5de6 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -302,7 +302,7 @@ "no_copy": 1 }, { - "default": "0", + "default": "1", "fieldname": "logout_all_sessions", "fieldtype": "Check", "label": "Logout From All Devices After Changing Password" @@ -669,7 +669,7 @@ } ], "max_attachments": 5, - "modified": "2021-01-02 11:21:50.507786", + "modified": "2021-02-01 16:11:06.037543", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5a35907ccf..142cc1ee26 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -562,6 +562,10 @@ def get_perm_info(role): @frappe.whitelist(allow_guest=True) def update_password(new_password, logout_all_sessions=0, key=None, old_password=None): + #validate key to avoid key input like ['like', '%'], '', ['in', ['']] + if key and not isinstance(key, str): + frappe.throw(_('Invalid key type')) + result = test_password_strength(new_password, key, old_password) feedback = result.get("feedback", None) diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 6bddd09fc7..7d1a697f6b 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -86,7 +86,7 @@ def get_result(doc, filters, to_date=None): filters = frappe.parse_json(filters) if not filters: - filters = [] + filters = [] if to_date: filters.append([doc.document_type, 'creation', '<', to_date]) @@ -107,9 +107,13 @@ def get_percentage_difference(doc, filters, result): return previous_result = calculate_previous_result(doc, filters) - difference = (result - previous_result)/100.0 - - return difference + if previous_result == 0: + return None + else: + if result == previous_result: + return 0 + else: + return ((result/previous_result)-1)*100.0 def calculate_previous_result(doc, filters): @@ -197,4 +201,4 @@ def add_card_to_dashboard(args): card.save() dashboard.append('cards', dashboard_link) - dashboard.save() \ No newline at end of file + dashboard.save() diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 7aa70830e7..3fb1dfa0da 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -297,8 +297,9 @@ def inline_style_in_html(html): for app in apps: path = 'assets/{0}/css/email.css'.format(app) - if os.path.exists(os.path.abspath(path)): - css_files.append(path) + css_files.append(path) + + css_files = [css_file for css_file in css_files if os.path.exists(os.path.abspath(css_file))] p = Premailer(html=html, external_styles=css_files, strip_important=False) diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index e4c4e278b0..1e0ae161bc 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -2729,11 +2729,11 @@ }, "Zimbabwe": { "code": "zw", - "currency": "ZWD", - "currency_fraction": "Thebe", + "currency": "ZWL", + "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_name": "Zimbabwe Dollar", - "currency_symbol": "P", + "currency_symbol": "ZWL$", "number_format": "# ###.##", "timezones": [ "Africa/Harare" diff --git a/frappe/hooks.py b/frappe/hooks.py index 97a8b70953..3e206f0ad3 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -58,6 +58,11 @@ website_route_rules = [ {"from_route": "/kb/", "to_route": "Help Article"}, {"from_route": "/newsletters", "to_route": "Newsletter"}, {"from_route": "/profile", "to_route": "me"}, + {"from_route": "/app/", "to_route": "app"}, +] + +website_redirects = [ + {"source": r"/desk(.*)", "target": r"/app\1"}, ] base_template = "templates/base.html" diff --git a/frappe/model/sync.py b/frappe/model/sync.py index e04d3d56b9..61983d322c 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -61,7 +61,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe for module_name in frappe.local.app_modules.get(app_name) or []: folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__) - get_doc_files(files, folder, force, sync_everything, verbose=verbose) + get_doc_files(files, folder) l = len(files) if l: @@ -77,7 +77,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe # print each progress bar on new line print() -def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=False): +def get_doc_files(files, start_path): """walk and sync all doctypes and pages""" # load in sequence - warning for devs diff --git a/frappe/patches.txt b/frappe/patches.txt index f076d5bd9c..0f37946398 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -209,7 +209,7 @@ frappe.patches.v9_1.resave_domain_settings frappe.patches.v9_1.revert_domain_settings frappe.patches.v9_1.move_feed_to_activity_log execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) -frappe.patches.v10_0.reload_countries_and_currencies # 14-10-2020 +frappe.patches.v10_0.reload_countries_and_currencies # 2021-02-03 frappe.patches.v10_0.refactor_social_login_keys frappe.patches.v10_0.enable_chat_by_default_within_system_settings frappe.patches.v10_0.remove_custom_field_for_disabled_domain diff --git a/frappe/patches/v13_0/rename_desk_page_to_workspace.py b/frappe/patches/v13_0/rename_desk_page_to_workspace.py index 1801c4cf61..6483fc380c 100644 --- a/frappe/patches/v13_0/rename_desk_page_to_workspace.py +++ b/frappe/patches/v13_0/rename_desk_page_to_workspace.py @@ -15,7 +15,7 @@ def execute(): rename_doc('DocType', 'Desk Shortcut', 'Workspace Shortcut', ignore_if_exists=True) rename_doc('DocType', 'Desk Link', 'Workspace Link', ignore_if_exists=True) - frappe.reload_doc('desk', 'doctype', 'workspace') - frappe.reload_doc('desk', 'doctype', 'workspace_link') - frappe.reload_doc('desk', 'doctype', 'workspace_chart') - frappe.reload_doc('desk', 'doctype', 'workspace_shortcut') + frappe.reload_doc('desk', 'doctype', 'workspace', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_link', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_chart', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_shortcut', force=True) diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index f63005278d..813205ecd0 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -1305,8 +1305,6 @@ class { this.set_wrapper(selector ? selector : "body") this.set_options(options) - // Load Emojis. - frappe.chat.emoji() } /** diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 3c5faf4a9a..46ab62b717 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -127,13 +127,6 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ let display_value = frappe.format(value, this.df, { no_icon: true, inline: true }, doc); this.disp_area && $(this.disp_area).html(display_value); }, - - bind_change_event: function() { - var me = this; - this.$input && this.$input.on("change", this.change || function(e) { - me.parse_validate_and_set_in_model(me.get_input_value(), e); - }); - }, set_label: function(label) { if(label) this.df.label = label; diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 401de2ed5d..48b4d9da35 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -3,6 +3,7 @@ frappe.provide('frappe.phone_call'); frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ html_element: "input", input_type: "text", + trigger_change_on_input_event: true, make_input: function() { if(this.$input) return; @@ -22,8 +23,20 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.has_input = true; this.bind_change_event(); this.setup_autoname_check(); - // somehow this event does not bubble up to document - // after v7, if you can debug, remove this + }, + bind_change_event: function() { + const change_handler = e => { + if (this.change) this.change(e); + else { + let value = this.get_input_value(); + this.parse_validate_and_set_in_model(value, e); + } + }; + this.$input.on("change", change_handler); + if (this.trigger_change_on_input_event) { + // debounce to avoid repeated validations on value change + this.$input.on("input", frappe.utils.debounce(change_handler, 500)); + } }, setup_autoname_check: function() { if (!this.df.parent) return; diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index da214029be..ca214ca0fa 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -1,5 +1,5 @@ - frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({ + trigger_change_on_input_event: false, make_input: function() { this._super(); this.make_picker(); diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index b9c3dc80ec..4ed0c40d33 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -9,6 +9,7 @@ import Awesomplete from 'awesomplete'; frappe.ui.form.recent_link_validations = {}; frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ + trigger_change_on_input_event: false, make_input: function() { var me = this; // line-height: 1 is for Mozilla 51, shows extra padding otherwise diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 987b94c0f9..a70797e295 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -564,13 +564,8 @@ frappe.ui.form.Form = class FrappeForm { let me = this; return new Promise((resolve, reject) => { btn && $(btn).prop("disabled", true); - $(document.activeElement).blur(); - frappe.ui.form.close_grid_form(); - // let any pending js process finish - setTimeout(function() { - me.validate_and_save(save_action, callback, btn, on_error, resolve, reject); - }, 100); + me.validate_and_save(save_action, callback, btn, on_error, resolve, reject); }).then(() => { me.show_success_action(); }).catch((e) => { diff --git a/frappe/public/js/frappe/form/link_selector.js b/frappe/public/js/frappe/form/link_selector.js index c72e74cafc..07cd6864c5 100644 --- a/frappe/public/js/frappe/form/link_selector.js +++ b/frappe/public/js/frappe/form/link_selector.js @@ -152,9 +152,13 @@ frappe.ui.form.LinkSelector = Class.extend({ d = me.target.add_new_row(); }, () => frappe.timeout(0.1), - () => frappe.model.set_value(d.doctype, d.name, me.fieldname, value), - () => frappe.timeout(0.5), - () => frappe.model.set_value(d.doctype, d.name, me.qty_fieldname, data.qty), + () => { + let args = {}; + args[me.fieldname] = value; + args[me.qty_fieldname] = data.qty; + + return frappe.model.set_value(d.doctype, d.name, args); + }, () => frappe.show_alert(__("Added {0} ({1})", [value, data.qty])) ]); } diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 396d6f59b3..dc6ee56fca 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -362,9 +362,7 @@ $.extend(frappe.model, { ); } else if (!opts.source_name && opts.frm) { opts.source_name = opts.frm.doc.name; - - // Allow opening a mapped doc without a source document name - } else if (!opts.frm) { + } else if (!opts.frm && !opts.source_name) { opts.source_name = null; } diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index 113ba3ee60..cd3f35d830 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -191,9 +191,9 @@ window.strip = function (s, chars) { window.lstrip = function lstrip(s, chars) { if (!chars) chars = ['\n', '\t', ' ']; // strip left - var first_char = s.substr(0, 1); + let first_char = s.substr(0, 1); while (in_list(chars, first_char)) { - var s = s.substr(1); + s = s.substr(1); first_char = s.substr(0, 1); } return s; @@ -201,9 +201,9 @@ window.lstrip = function lstrip(s, chars) { window.rstrip = function (s, chars) { if (!chars) chars = ['\n', '\t', ' ']; - var last_char = s.substr(s.length - 1); + let last_char = s.substr(s.length - 1); while (in_list(chars, last_char)) { - let s = s.substr(0, s.length - 1); + s = s.substr(0, s.length - 1); last_char = s.substr(s.length - 1); } return s; diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 93409a15a4..82d056bb31 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -231,9 +231,7 @@ export default class NumberCardWidget extends Widget { let color_class = ''; return this.get_percentage_stats().then(() => { - if (this.percentage_stat == undefined) return; - - if (this.percentage_stat == 0) { + if (this.percentage_stat == 0 || this.percentage_stat == undefined) { color_class = 'grey-stat'; } else if (this.percentage_stat > 0) { caret_html = @@ -258,6 +256,7 @@ export default class NumberCardWidget extends Widget { const stats_qualifier = stats_qualifier_map[this.card_doc.stats_time_interval]; let get_stat = () => { + if (this.percentage_stat == undefined) return NaN; const parts = this.percentage_stat.split(' '); const symbol = parts[1] || ''; return Math.abs(parts[0]) + ' ' + symbol; diff --git a/frappe/website/render.py b/frappe/website/render.py index 5c19a3eb25..af3b18b233 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -252,13 +252,6 @@ def resolve_path(path): if path != "index": path = resolve_from_map(path) - if path.startswith("app"): - path = "app" - - # to keep backward compatibility - if path.startswith("desk"): - path = "app" - return path def resolve_from_map(path): diff --git a/requirements.txt b/requirements.txt index d99469daf6..194853dcfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ Babel==2.6.0 beautifulsoup4==4.8.2 bleach-whitelist==0.0.10 -bleach==3.1.4 +bleach==3.3.0 boto3==1.10.18 braintree==3.57.1 chardet==3.0.4 @@ -27,7 +27,7 @@ html5lib==1.0.1 ipython==7.14.0 Jinja2==2.11.3 ldap3==2.7 -markdown2==2.3.9 +markdown2==2.4.0 maxminddb-geolite2==2018.703 ndg-httpsclient==0.5.1 num2words==0.5.10