From 22d3824fee3ac7e4253edc677ba4c98834e88d37 Mon Sep 17 00:00:00 2001 From: Steffen Brennscheidt Date: Thu, 5 Nov 2020 11:32:06 +0000 Subject: [PATCH 01/49] fix: No redis dependency during tests and install Adding a user during after_install hook caused error during install of an app --- frappe/core/doctype/user/user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 2c5865fb69..0cec7a511c 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -98,15 +98,16 @@ class User(Document): self.share_with_self() clear_notifications(user=self.name) frappe.clear_cache(user=self.name) + now=frappe.flags.in_test or frappe.flags.in_install self.send_password_notification(self.__new_password) frappe.enqueue( 'frappe.core.doctype.user.user.create_contact', user=self, ignore_mandatory=True, - now=frappe.flags.in_test or frappe.flags.in_install + now=now ) if self.name not in ('Administrator', 'Guest') and not self.user_image: - frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) + frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now) def has_website_permission(self, ptype, user, verbose=False): """Returns true if current user is the session user""" From a9bf8023c6862310c294cc810e3c518a49b79ba0 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sat, 7 Nov 2020 19:22:00 +0530 Subject: [PATCH 02/49] Update standard_macros.html --- frappe/templates/print_formats/standard_macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 3681a87f53..24967b9525 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -137,7 +137,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {% elif df.fieldtype=="HTML" %} {{ frappe.render_template(df.options, {"doc":doc}) }} {% elif df.fieldtype=="Currency" %} - {{ doc.get_formatted(df.fieldname, doc, translated=df.translatable) }} + {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} {% else %} {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} {% endif %} From 19a14d09e0dc872594fc5ae0f0be9505ee6e76d3 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 8 Nov 2020 17:58:10 +0000 Subject: [PATCH 03/49] fix(quick entry): make sure init_callback is always called Prior to this PR, as noted in issue #7638, it is not possible with frappe.new_doc to initialize certain fields of the new document, such as the description of a Task or the posting_date of a Journal Entry (in ERPNext). The reason this occurs is that currently the route_options which can be set in the second argument to frappe.new_doc() are only allowed to set certain field types of a document (namely, Link, Select, Data, and Dynamic Link). Although it turns out that it would not work to allow any field type to be set in the route options (in particular, attempting to allow one to set Table field types in this way is non-functional), it would be reasonable simply to try setting other fields that cannot be set in the route_options via the callback allowed as the third argument of frappe.new_doc. And indeed, this approach works for those DocTypes that have a Quick Entry Form. For those DocTypes that do not, however, the callback is never called. This PR modifies frappe.ui.form.make_quick_entry() -- which frappe.new_doc calls to do most of its work -- so that the callback is called regardless of whether the DocType has a Quick Entry Form or not. The only slight awkwardness in this is that if there is a Quick Entry, the callback is passed the dialog object of that Quick Entry, whereas if there is no Quick Entry, the callback is only passed the doc object that is about to be edited in the standard Form interface for a new document. Nevertheless, in any case, it is now possible to write a callback which will initialize any field in the new document being created. Resolves #7638. --- frappe/public/js/frappe/form/quick_entry.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 2da7b8f236..0a489e26d6 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -35,7 +35,11 @@ frappe.ui.form.QuickEntryForm = Class.extend({ if (this.is_quick_entry() || this.force) { this.render_dialog(); resolve(this); - } else { + } else { // No quick entry, use full Form + // but still give callback a shot at the doc + if (this.init_callback) { + this.init_callback(this.doc); + } frappe.quick_entry = null; frappe.set_route('Form', this.doctype, this.doc.name) .then(() => resolve(this)); From 4d8ef4690eb9e364e244f296f935dab45733d5f6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 12 Nov 2020 17:42:25 +0530 Subject: [PATCH 04/49] feat: Ability to set weekdays for Auto Repeat with weekly frequency --- .../doctype/auto_repeat/auto_repeat.json | 18 ++++++- .../doctype/auto_repeat/auto_repeat.py | 54 +++++++++++++------ .../doctype/auto_repeat_day/__init__.py | 0 .../auto_repeat_day/auto_repeat_day.json | 33 ++++++++++++ .../auto_repeat_day/auto_repeat_day.py | 10 ++++ 5 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 frappe/automation/doctype/auto_repeat_day/__init__.py create mode 100644 frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json create mode 100644 frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 8ee6ca1d45..87cf423e2c 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "format:AUT-AR-{#####}", @@ -21,6 +22,8 @@ "repeat_on_last_day", "column_break_12", "next_schedule_date", + "section_break_12", + "repeat_on_days", "notification", "notify_by_email", "recipients", @@ -186,9 +189,22 @@ "fieldname": "repeat_on_last_day", "fieldtype": "Check", "label": "Repeat on Last Day of the Month" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "repeat_on_days", + "fieldtype": "Table", + "label": "Repeat on Days", + "options": "Auto Repeat Day" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "section_break_12", + "fieldtype": "Section Break" } ], - "modified": "2019-07-17 11:30:51.412317", + "links": [], + "modified": "2020-11-10 22:44:51.815740", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fcf24bf1a9..dc6ec554d5 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from datetime import timedelta from frappe.desk.form import assign_to from frappe.utils.jinja import validate_template from dateutil.relativedelta import relativedelta @@ -15,6 +16,7 @@ from frappe.core.doctype.communication.email import make from frappe.utils.background_jobs import get_jobs 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} class AutoRepeat(Document): @@ -48,7 +50,7 @@ class AutoRepeat(Document): if self.disabled: self.next_schedule_date = None else: - self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date) + self.next_schedule_date = get_next_schedule_date(schedule_date=self.start_date, auto_repeat_doc=self) def unlink_if_applicable(self): if self.status == 'Completed' or self.disabled: @@ -107,7 +109,7 @@ class AutoRepeat(Document): end_date = getdate(self.end_date) if not self.end_date: - next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day) + next_date = get_next_schedule_date(schedule_date=start_date, auto_repeat_doc=self) row = { "reference_document": self.reference_document, "frequency": self.frequency, @@ -117,7 +119,7 @@ class AutoRepeat(Document): if self.end_date: next_date = get_next_schedule_date( - start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True) + schedule_date=start_date, auto_repeat_doc=self, for_full_schedule=True) while (getdate(next_date) < getdate(end_date)): row = { @@ -127,7 +129,7 @@ class AutoRepeat(Document): } schedule_details.append(row) next_date = get_next_schedule_date( - next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) + schedule_date=next_date, auto_repeat_doc=self, for_full_schedule=True) return schedule_details @@ -282,31 +284,34 @@ class AutoRepeat(Document): ) -def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False): - if month_map.get(frequency): - month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1 +def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedule=False): + if month_map.get(auto_repeat_doc.frequency): + month_count = month_map.get(auto_repeat_doc.frequency) + month_diff(schedule_date, auto_repeat_doc.start_date) - 1 else: month_count = 0 day_count = 0 - if month_count and repeat_on_last_day: + if month_count and auto_repeat_doc.repeat_on_last_day: day_count = 31 - next_date = get_next_date(start_date, month_count, day_count) + next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif month_count and repeat_on_day: day_count = repeat_on_day - next_date = get_next_date(start_date, month_count, day_count) + next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif month_count: - next_date = get_next_date(start_date, month_count) + next_date = get_next_date(auto_repeat_doc.start_date, month_count) else: - days = 7 if frequency == 'Weekly' else 1 + if auto_repeat_doc.frequency == "Weekly": + days = get_offset_for_weekly_frequency(auto_repeat_doc) + else: + days = 1 next_date = add_days(schedule_date, days) # next schedule date should be after or on current date if not for_full_schedule: while getdate(next_date) < getdate(today()): if month_count: - month_count += month_map.get(frequency) - next_date = get_next_date(start_date, month_count, day_count) + month_count += month_map.get(auto_repeat_doc.frequency) + next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif days: next_date = add_days(next_date, days) @@ -318,6 +323,25 @@ def get_next_date(dt, mcount, day=None): dt += relativedelta(months=mcount, day=day) return dt + +def get_offset_for_weekly_frequency(auto_repeat_doc): + if not auto_repeat_doc.repeat_on_days: + return 7 + + repeat_on_days = [entry.day for entry in auto_repeat_doc.repeat_on_days] + current_day = getdate().weekday() + weekday = get_next_weekday(current_day, repeat_on_days) + return timedelta((7 + week_map.get(weekday) - current_day) % 7).days + + +def get_next_weekday(current_day, weekdays): + days = list(week_map.keys()) + days = days[current_day:] + days[:current_day] + for entry in days: + if entry in weekdays: + return entry + + #called through hooks def make_auto_repeat_entry(): enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' @@ -337,7 +361,7 @@ def create_repeated_entries(data): if schedule_date == current_date and not doc.disabled: doc.create_documents() - schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date) + schedule_date = get_next_schedule_date(schedule_date=schedule_date, auto_repeat_doc=doc) if schedule_date and not doc.disabled: frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) diff --git a/frappe/automation/doctype/auto_repeat_day/__init__.py b/frappe/automation/doctype/auto_repeat_day/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json new file mode 100644 index 0000000000..6f5c3060cd --- /dev/null +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-11-10 22:30:53.690228", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day" + ], + "fields": [ + { + "fieldname": "day", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-10 22:30:53.690228", + "modified_by": "Administrator", + "module": "Automation", + "name": "Auto Repeat Day", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py new file mode 100644 index 0000000000..3a7ced1370 --- /dev/null +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class AutoRepeatDay(Document): + pass From 5e7e7cc922fe0cad987f32ede3d39ea9ef3f02c8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 14:13:27 +0530 Subject: [PATCH 05/49] fix: full schedule calculation for weekdays --- .../doctype/auto_repeat/auto_repeat.js | 4 +- .../doctype/auto_repeat/auto_repeat.py | 37 +++++++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index a11de1d881..2b1102f681 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -86,10 +86,10 @@ frappe.ui.form.on('Auto Repeat', { frappe.auto_repeat.render_schedule = function(frm) { if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { - frappe.call({ + frm.call({ method: "get_auto_repeat_schedule", doc: frm.doc - }).done((r) => { + }).then((r) => { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index dc6ec554d5..d0f01f4ad5 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -14,6 +14,7 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_ 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 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} @@ -25,6 +26,7 @@ class AutoRepeat(Document): self.validate_reference_doctype() self.validate_dates() self.validate_email_id() + self.validate_auto_repeat_days() self.set_dates() self.update_auto_repeat_id() self.unlink_if_applicable() @@ -84,6 +86,12 @@ class AutoRepeat(Document): else: frappe.throw(_("'Recipients' not specified")) + def validate_auto_repeat_days(self): + auto_repeat_days = get_auto_repeat_days(self) + if not len(set(auto_repeat_days)) == len(auto_repeat_days): + repeated_days = get_repeated(auto_repeat_days) + frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days))) + def update_auto_repeat_id(self): #check if document is already on auto repeat auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") @@ -301,7 +309,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul next_date = get_next_date(auto_repeat_doc.start_date, month_count) else: if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(auto_repeat_doc) + days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) else: days = 1 next_date = add_days(schedule_date, days) @@ -324,24 +332,37 @@ def get_next_date(dt, mcount, day=None): return dt -def get_offset_for_weekly_frequency(auto_repeat_doc): +def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): if not auto_repeat_doc.repeat_on_days: return 7 - repeat_on_days = [entry.day for entry in auto_repeat_doc.repeat_on_days] - current_day = getdate().weekday() - weekday = get_next_weekday(current_day, repeat_on_days) - return timedelta((7 + week_map.get(weekday) - current_day) % 7).days + repeat_on_days = get_auto_repeat_days(auto_repeat_doc) + current_schedule_day = getdate(schedule_date).weekday() + + if len(repeat_on_days) > 1 or list(week_map.keys())[current_schedule_day] not in repeat_on_days: + weekday = get_next_weekday(current_schedule_day, repeat_on_days) + next_weekday_number = week_map.get(weekday) + return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days + else: + return 7 -def get_next_weekday(current_day, weekdays): +def get_next_weekday(current_schedule_day, weekdays): days = list(week_map.keys()) - days = days[current_day:] + days[:current_day] + if current_schedule_day > 0: + days = days[(current_schedule_day + 1):] + days[:current_schedule_day] + else: + days = days[(current_schedule_day + 1):] + for entry in days: if entry in weekdays: return entry +def get_auto_repeat_days(doc): + return [d.day for d in doc.get('repeat_on_days', [])] + + #called through hooks def make_auto_repeat_entry(): enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' From 6f6a20d6b5e880a7dde0f23976088ae8626978e9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 14:35:30 +0530 Subject: [PATCH 06/49] fix: handle schedule for a past start date --- frappe/automation/doctype/auto_repeat/auto_repeat.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index d0f01f4ad5..fe2fccf99d 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -311,6 +311,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul if auto_repeat_doc.frequency == "Weekly": days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) else: + # daily frequency days = 1 next_date = add_days(schedule_date, days) @@ -321,6 +322,11 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul month_count += month_map.get(auto_repeat_doc.frequency) next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif days: + if auto_repeat_doc.frequency == "Weekly": + days = get_offset_for_weekly_frequency(next_date, auto_repeat_doc) + else: + # daily frequency + days = 1 next_date = add_days(next_date, days) return next_date @@ -333,15 +339,20 @@ def get_next_date(dt, mcount, day=None): def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): + # if weekdays are not set, offset is 7 from current schedule date if not auto_repeat_doc.repeat_on_days: return 7 repeat_on_days = get_auto_repeat_days(auto_repeat_doc) current_schedule_day = getdate(schedule_date).weekday() + # if repeats on more than 1 day or + # start date's weekday is not in repeat days, then get next weekday + # else offset is 7 if len(repeat_on_days) > 1 or list(week_map.keys())[current_schedule_day] not in repeat_on_days: weekday = get_next_weekday(current_schedule_day, repeat_on_days) next_weekday_number = week_map.get(weekday) + # offset for upcoming weekday return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days else: return 7 From 4b7120c5c4dd2d6286666d9a04f676d6c00c21da Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 15:30:07 +0530 Subject: [PATCH 07/49] test: Auto Repeat with weekly frequency --- .../doctype/auto_repeat/auto_repeat.py | 7 +-- .../doctype/auto_repeat/test_auto_repeat.py | 50 ++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fe2fccf99d..a41fef1f6b 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -302,8 +302,8 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul if month_count and auto_repeat_doc.repeat_on_last_day: day_count = 31 next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif month_count and repeat_on_day: - day_count = repeat_on_day + elif month_count and auto_repeat_doc.repeat_on_day: + day_count = auto_repeat_doc.repeat_on_day next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif month_count: next_date = get_next_date(auto_repeat_doc.start_date, month_count) @@ -345,11 +345,12 @@ def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): repeat_on_days = get_auto_repeat_days(auto_repeat_doc) current_schedule_day = getdate(schedule_date).weekday() + weekdays = list(week_map.keys()) # if repeats on more than 1 day or # start date's weekday is not in repeat days, then get next weekday # else offset is 7 - if len(repeat_on_days) > 1 or list(week_map.keys())[current_schedule_day] not in repeat_on_days: + if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: weekday = get_next_weekday(current_schedule_day, repeat_on_days) next_weekday_number = week_map.get(weekday) # offset for upcoming weekday diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 60fa9cb59e..69e9d98c0a 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -10,6 +10,7 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries from frappe.utils import today, add_days, getdate, add_months +week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} def add_custom_fields(): df = dict( @@ -42,6 +43,52 @@ class TestAutoRepeat(unittest.TestCase): self.assertEqual(todo.get('description'), new_todo.get('description')) + def test_weekly_auto_repeat(self): + todo = frappe.get_doc( + dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert() + + doc = make_auto_repeat(reference_doctype='ToDo', + frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7)) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + frappe.db.commit() + + todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + new_todo = frappe.db.get_value('ToDo', + {'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') + + new_todo = frappe.get_doc('ToDo', new_todo) + + self.assertEqual(todo.get('description'), new_todo.get('description')) + + def test_weekly_auto_repeat_with_weekdays(self): + todo = frappe.get_doc( + dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert() + + weekdays = list(week_map.keys()) + current_weekday = getdate().weekday() + days = [ + {'day': weekdays[current_weekday]}, + {'day': weekdays[current_weekday + 2]} + ] + doc = make_auto_repeat(reference_doctype='ToDo', + frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + frappe.db.commit() + + todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + doc.reload() + self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2)) + def test_monthly_auto_repeat(self): start_date = today() end_date = add_months(start_date, 12) @@ -124,7 +171,8 @@ def make_auto_repeat(**args): 'notify_by_email': args.notify or 0, 'recipients': args.recipients or "", 'subject': args.subject or "", - 'message': args.message or "" + 'message': args.message or "", + 'repeat_on_days': args.days or [] }).insert(ignore_permissions=True) return doc From 4ebc7be1a3ad256aa2d8be9f633bf8063b526df3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 16:25:12 +0530 Subject: [PATCH 08/49] chore: code clean-up --- .../doctype/auto_repeat/auto_repeat.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index a41fef1f6b..c47b672595 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -308,11 +308,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul elif month_count: next_date = get_next_date(auto_repeat_doc.start_date, month_count) else: - if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) - else: - # daily frequency - days = 1 + days = get_days(schedule_date, auto_repeat_doc) next_date = add_days(schedule_date, days) # next schedule date should be after or on current date @@ -322,11 +318,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul month_count += month_map.get(auto_repeat_doc.frequency) next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif days: - if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(next_date, auto_repeat_doc) - else: - # daily frequency - days = 1 + days = get_days(next_date, auto_repeat_doc) next_date = add_days(next_date, days) return next_date @@ -338,6 +330,16 @@ def get_next_date(dt, mcount, day=None): return dt +def get_days(schedule_date, auto_repeat_doc): + if auto_repeat_doc.frequency == "Weekly": + days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) + else: + # daily frequency + days = 1 + + return days + + def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): # if weekdays are not set, offset is 7 from current schedule date if not auto_repeat_doc.repeat_on_days: @@ -355,8 +357,7 @@ def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): next_weekday_number = week_map.get(weekday) # offset for upcoming weekday return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days - else: - return 7 + return 7 def get_next_weekday(current_schedule_day, weekdays): @@ -385,6 +386,7 @@ def make_auto_repeat_entry(): data = get_auto_repeat_entries(date) frappe.enqueue(enqueued_method, data=data) + def create_repeated_entries(data): for d in data: doc = frappe.get_doc('Auto Repeat', d.name) @@ -398,6 +400,7 @@ def create_repeated_entries(data): if schedule_date and not doc.disabled: frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) + def get_auto_repeat_entries(date=None): if not date: date = getdate(today()) @@ -406,6 +409,7 @@ def get_auto_repeat_entries(date=None): ['status', '=', 'Active'] ]) + #called through hooks def set_auto_repeat_as_completed(): auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) @@ -415,6 +419,7 @@ def set_auto_repeat_as_completed(): doc.status = 'Completed' doc.save() + @frappe.whitelist() def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): if not start_date: From 17ec45b92a11ac7f11c547f06240a4987b284342 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 16:41:17 +0530 Subject: [PATCH 09/49] fix: test case for edge case scenario --- frappe/automation/doctype/auto_repeat/test_auto_repeat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 69e9d98c0a..3cd10e8a61 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -73,7 +73,7 @@ class TestAutoRepeat(unittest.TestCase): current_weekday = getdate().weekday() days = [ {'day': weekdays[current_weekday]}, - {'day': weekdays[current_weekday + 2]} + {'day': weekdays[(current_weekday + 2) % 7]} ] doc = make_auto_repeat(reference_doctype='ToDo', frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) From f71a8e0afcffae02dee34fd20642ee439c6b6017 Mon Sep 17 00:00:00 2001 From: everyx Date: Tue, 24 Nov 2020 14:36:53 +0800 Subject: [PATCH 10/49] fix: `PRIMARY KEY must be NOT NULL` error when install with MySQL 5.7+ --- frappe/database/mariadb/framework_mariadb.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 15b0bed699..a52efd01e3 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` ( DROP TABLE IF EXISTS `tabSeries`; CREATE TABLE `tabSeries` ( - `name` varchar(100) DEFAULT NULL, + `name` varchar(100), `current` int(10) NOT NULL DEFAULT 0, PRIMARY KEY(`name`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; From 926d7e78fd75dcfc5713de7c684900d54d84a843 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 26 Nov 2020 15:33:55 +0530 Subject: [PATCH 11/49] fix: clear localstorage if quota exceeds --- frappe/public/js/frappe/model/model.js | 33 +++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 308d9bd5f8..1d302215dd 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -103,6 +103,31 @@ $.extend(frappe.model, { return docfield[0]; }, + get_from_localstorage: function(doctype) { + if (localStorage["_doctype:" + doctype]) { + return JSON.parse(localStorage["_doctype:" + doctype]); + } + }, + + set_in_localstorage: function(doctype, docs) { + try { + localStorage["_doctype:" + doctype] = JSON.stringify(docs); + } catch(e) { + // if quota is exceeded, clear local storage and set item + console.warn("localStorage quota exceeded, clearing doctype cache") + frappe.model.clear_local_storage(); + localStorage["_doctype:" + doctype] = JSON.stringify(docs); + } + }, + + clear_local_storage: function() { + for(var key in localStorage) { + if (key.startsWith("_doctype:")) { + localStorage.removeItem(key); + } + } + }, + with_doctype: function(doctype, callback, async) { if(locals.DocType[doctype]) { callback && callback(); @@ -110,13 +135,15 @@ $.extend(frappe.model, { let cached_timestamp = null; let cached_doc = null; - if(localStorage["_doctype:" + doctype]) { - let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]); + let cached_docs = frappe.model.get_from_localstorage(doctype) + + if (cached_docs) { cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; if(cached_doc) { cached_timestamp = cached_doc.modified; } } + return frappe.call({ method:'frappe.desk.form.load.getdoctype', type: "GET", @@ -134,7 +161,7 @@ $.extend(frappe.model, { if(r.message=="use_cache") { frappe.model.sync(cached_doc); } else { - localStorage["_doctype:" + doctype] = JSON.stringify(r.docs); + frappe.model.set_in_localstorage(doctype, r.docs) } frappe.model.init_doctype(doctype); From 8192db7382a02fd4334c227ad15339c10f7bede5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 26 Nov 2020 16:22:40 +0530 Subject: [PATCH 12/49] chore: remove stray console --- frappe/public/js/frappe/desk.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index c8ed29fb76..5fa7a9dbcb 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -148,7 +148,6 @@ frappe.Application = Class.extend({ user: frappe.session.user }, callback: function(r) { - console.log(r); if(r.message.show_alert){ frappe.show_alert({ indicator: 'red', From 729ee6ea9ea1138043a501632891cfd03fc68396 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 27 Nov 2020 19:03:23 +0530 Subject: [PATCH 13/49] fix: changing percent field precision to global default --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 3f422d0a9b..c456739add 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,7 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, 2) + "%", options) + return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options) }, Rating: function(value) { return ` From 8aeb1716642a1e275ddbc83c77501a24529dd1c5 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 27 Nov 2020 19:58:41 +0530 Subject: [PATCH 14/49] fix: sider issue fixed --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index c456739add..7f5c3bf472 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,7 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options) + return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options); }, Rating: function(value) { return ` From be6724c7c8288589d616b73a641a2440ef580f26 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 30 Nov 2020 11:15:18 +0530 Subject: [PATCH 15/49] fix: check if print settings has landscape --- frappe/public/js/frappe/microtemplate.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js index d233a47893..7b45db952e 100644 --- a/frappe/public/js/frappe/microtemplate.js +++ b/frappe/public/js/frappe/microtemplate.js @@ -89,11 +89,19 @@ frappe.render_template = function(name, data) { } frappe.render_grid = function(opts) { // build context - if(opts.grid) { + if (opts.grid) { opts.columns = opts.grid.getColumns(); opts.data = opts.grid.getData().getItems(); } + if ( + opts.print_settings && + opts.print_settings.orientation && + opts.print_settings.orientation.toLowerCase() === "landscape" + ) { + opts.landscape = true; + } + // show landscape view if columns more than 10 if (opts.landscape == null) { if(opts.columns && opts.columns.length > 10) { From 9808c868d70ce334dadda790ca55b3819d557ca7 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 30 Nov 2020 14:14:19 +0530 Subject: [PATCH 16/49] feat: allow html in email templates --- .../email_template/email_template.json | 22 +++++++++++++-- .../doctype/email_template/email_template.py | 27 ++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json index 0d0922f16f..dc73acacc1 100644 --- a/frappe/email/doctype/email_template/email_template.json +++ b/frappe/email/doctype/email_template/email_template.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "Prompt", @@ -8,6 +9,8 @@ "engine": "InnoDB", "field_order": [ "subject", + "use_html", + "response_html", "response", "owner", "section_break_4", @@ -22,11 +25,12 @@ "reqd": 1 }, { + "depends_on": "eval:!doc.use_html", "fieldname": "response", "fieldtype": "Text Editor", "in_list_view": 1, "label": "Response", - "reqd": 1 + "mandatory_depends_on": "eval:!doc.use_html" }, { "default": "user", @@ -45,10 +49,24 @@ "fieldtype": "HTML", "label": "Email Reply Help", "options": "

Email Reply Example

\n\n
Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n
\n\n

How to get fieldnames

\n\n

The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

\n\n

Templating

\n\n

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

\n" + }, + { + "default": "0", + "fieldname": "use_html", + "fieldtype": "Check", + "label": "Use HTML" + }, + { + "depends_on": "eval:doc.use_html", + "fieldname": "response_html", + "fieldtype": "Code", + "label": "Response ", + "options": "HTML" } ], "icon": "fa fa-comment", - "modified": "2019-10-30 14:15:00.956347", + "links": [], + "modified": "2020-11-30 14:12:50.321633", "modified_by": "Administrator", "module": "Email", "name": "Email Template", diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py index 2743032331..6708e9dd3f 100644 --- a/frappe/email/doctype/email_template/email_template.py +++ b/frappe/email/doctype/email_template/email_template.py @@ -9,7 +9,29 @@ from six import string_types class EmailTemplate(Document): def validate(self): - validate_template(self.response) + if self.use_html: + validate_template(self.response_html) + else: + validate_template(self.response) + + def get_formatted_subject(self, doc): + return frappe.render_template(self.subject, doc) + + def get_formatted_response(self, doc): + if self.use_html: + return frappe.render_template(self.response_html, doc) + + return frappe.render_template(self.response, doc) + + def get_formatted_email(self, doc): + if isinstance(doc, string_types): + doc = json.loads(doc) + + return { + "subject" : self.get_formatted_subject(doc), + "message" : self.get_formatted_response(doc) + } + @frappe.whitelist() def get_email_template(template_name, doc): @@ -18,5 +40,4 @@ def get_email_template(template_name, doc): doc = json.loads(doc) email_template = frappe.get_doc("Email Template", template_name) - return {"subject" : frappe.render_template(email_template.subject, doc), - "message" : frappe.render_template(email_template.response, doc)} \ No newline at end of file + return email_template.get_formatted_email(doc) \ No newline at end of file From 3296097df606df2998f01dde927541836504d6bc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 30 Nov 2020 15:46:29 +0530 Subject: [PATCH 17/49] feat: show absolute value in print format --- frappe/model/base_document.py | 6 +++--- frappe/printing/doctype/print_format/print_format.json | 9 ++++++++- frappe/templates/print_formats/standard_macros.html | 5 ++--- frappe/www/printview.py | 1 + 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 0a219b4253..5d86b3bac8 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -802,12 +802,12 @@ class BaseDocument(object): if translated: val = _(val) - if absolute_value and isinstance(val, (int, float)): - val = abs(self.get(fieldname)) - if not doc: doc = getattr(self, "parent_doc", None) or self + if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)): + val = abs(self.get(fieldname)) + return format_value(val, df=df, doc=doc, currency=currency) def is_print_hide(self, fieldname, df=None, for_print=True): diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 63448ccc39..3867ce4502 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -22,6 +22,7 @@ "align_labels_right", "show_section_headings", "line_breaks", + "absolute_value", "column_break_11", "font", "css_section", @@ -196,13 +197,19 @@ "fieldtype": "Check", "hidden": 1, "label": "Print Format Builder" + }, + { + "default": "0", + "fieldname": "absolute_value", + "fieldtype": "Check", + "label": "Show absolute values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-27 18:27:58.307070", + "modified": "2020-11-30 15:26:35.605213", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 3681a87f53..168547798b 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -136,10 +136,9 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> {% elif df.fieldtype=="HTML" %} {{ frappe.render_template(df.options, {"doc":doc}) }} - {% elif df.fieldtype=="Currency" %} - {{ doc.get_formatted(df.fieldname, doc, translated=df.translatable) }} {% else %} - {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} + {%- set parent = parent_doc or doc -%} + {{ doc.get_formatted(df.fieldname, parent, translated=df.translatable, absolute_value=parent.absolute_value) }} {% endif %} {%- endmacro %} diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 545e5d581d..71316dc48c 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -100,6 +100,7 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None, doc.print_section_headings = print_format.show_section_headings doc.print_line_breaks = print_format.line_breaks doc.align_labels_right = print_format.align_labels_right + doc.absolute_value = print_format.absolute_value def get_template_from_string(): return jenv.from_string(get_print_format(doc.doctype, From a19932835a8e8ab66042e19c8ae52f87a19af946 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 3 Dec 2020 12:30:10 +0530 Subject: [PATCH 18/49] feat: provision to open child table in customize form --- frappe/custom/doctype/customize_form/customize_form.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 2d220b864c..17343573ed 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", { } else { f._sortable = false; } + if (f.fieldtype == "Table") { + frm.add_custom_button(f.options, function() { + frm.set_value('doc_type', f.options); + }, __('Customize Child Table')); + } }); frm.fields_dict.fields.grid.refresh(); }, From ee3fa3e4e0ebf414a9a7e777a509f6a7535be0a0 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 3 Dec 2020 22:39:38 +0530 Subject: [PATCH 19/49] fix: Remove unreferenced variable base_path --- frappe/commands/site.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index bc65aa178c..35f5b13582 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -100,12 +100,10 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas # Extract public and/or private files to the restored site, if user has given the path if with_public_files: - with_public_files = os.path.join(base_path, with_public_files) public = extract_files(site, with_public_files, 'public') os.remove(public) if with_private_files: - with_private_files = os.path.join(base_path, with_private_files) private = extract_files(site, with_private_files, 'private') os.remove(private) From 9ac14fb5abf6930234e4da41217dd74718c858da Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 3 Dec 2020 23:26:34 +0530 Subject: [PATCH 20/49] feat: Permission Query script type Add dynamic conditions in where clause of get_list query --- .../doctype/server_script/server_script.js | 41 ++++++++++++++----- .../doctype/server_script/server_script.json | 6 +-- .../doctype/server_script/server_script.py | 6 +++ .../server_script/server_script_utils.py | 11 ++++- .../server_script/test_server_script.py | 12 ++++++ frappe/model/db_query.py | 13 +++++- 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 78ef2d0509..a317d69166 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', { setup_help(frm) { frm.get_field('help_html').html(` -

Examples

DocType Event

-

+

Add logic for standard doctype events like Before Insert, After Submit, etc.

+
+	
 # set property
 if "test" in doc.description:
-    doc.status = 'Closed'
+	doc.status = 'Closed'
 
 
 # validate
 if "validate" in doc.description:
-    raise frappe.ValidationError
+	raise frappe.ValidationError
 
 # auto create another document
-if doc.allocted_to:
-    frappe.get_doc(dict(
-        doctype = 'ToDo'
-        owner = doc.allocated_to,
-        description = doc.subject
-    )).insert()
-
+if doc.allocated_to: + frappe.get_doc(dict( + doctype = 'ToDo' + owner = doc.allocated_to, + description = doc.subject + )).insert() +
+
+

API Call

+

Respond to /api/method/<method-name> calls, just like whitelisted methods


 # respond to API
 
@@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
 else:
 	frappe.response['message'] = "ok"
 
+ +
+ +

Permission Query

+

Add conditions to the where clause of list queries.

+

+# generate dynamic conditions and set it in the conditions variable
+tenant_id = frappe.db.get_value(...)
+conditions = 'tenant_id = {}'.format(tenant_id)
+
+# resulting select query
+select name from \`tabPerson\`
+where tenant_id = 2
+order by creation desc
+
`); } diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 420f96ec2f..94a48f196c 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -24,7 +24,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Script Type", - "options": "DocType Event\nScheduler Event\nAPI", + "options": "DocType Event\nScheduler Event\nPermission Query\nAPI", "reqd": 1 }, { @@ -35,7 +35,7 @@ "reqd": 1 }, { - "depends_on": "eval:doc.script_type==='DocType Event'", + "depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)", "fieldname": "reference_doctype", "fieldtype": "Link", "in_list_view": 1, @@ -88,7 +88,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-11 12:39:41.391052", + "modified": "2020-12-03 22:42:02.708148", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 839b784651..b1bf79dc52 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -41,6 +41,12 @@ class ServerScript(Document): # wrong report type! raise frappe.DoesNotExistError + def get_permission_query_conditions(self, user): + locals = {"user": user, "conditions": ""} + safe_exec(self.script, None, locals) + if locals["conditions"]: + return locals["conditions"] + @frappe.whitelist() def setup_scheduler_events(script_name, frequency): method = frappe.scrub('{0}-{1}'.format(script_name, frequency)) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index e03504f30b..4dc4f12b34 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -50,6 +50,9 @@ def get_server_script_map(): # }, # '_api': { # '[path]': '[server script]' + # }, + # 'permission_query': { + # 'DocType': '[server script]' # } # } if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'): @@ -57,16 +60,20 @@ def get_server_script_map(): script_map = frappe.cache().get_value('server_script_map') if script_map is None: - script_map = {} + script_map = { + 'permission_query': {} + } 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) + elif script.script_type == 'Permission Query': + script_map['permission_query'][script.reference_doctype] = script.name else: script_map.setdefault('_api', {})[script.api_method] = script.name frappe.cache().set_value('server_script_map', script_map) - return script_map \ No newline at end of file + return script_map diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 3356e584af..565436d9f2 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -45,6 +45,14 @@ frappe.response['message'] = 'hello' allow_guest = 1, script = ''' frappe.flags = 'hello' +''' + ), + dict( + name='test_permission_query', + script_type = 'Permission Query', + reference_doctype = 'ToDo', + script = ''' +conditions = '1 = 1' ''' ) ] @@ -85,3 +93,7 @@ class TestServerScript(unittest.TestCase): def test_api_return(self): self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello') + + def test_permission_query(self): + self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1)) + self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list)) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index ace9b04cec..b936251b50 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -18,6 +18,7 @@ from frappe.client import check_parent_permission from frappe.model.utils.user_settings import get_user_settings, update_user_settings from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range from frappe.model.meta import get_table_columns +from frappe.core.doctype.server_script.server_script_utils import get_server_script_map class DatabaseQuery(object): def __init__(self, doctype, user=None): @@ -683,15 +684,23 @@ class DatabaseQuery(object): self.match_filters.append(match_filters) def get_permission_query_conditions(self): + conditions = [] condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, []) if condition_methods: - conditions = [] for method in condition_methods: c = frappe.call(frappe.get_attr(method), self.user) if c: conditions.append(c) - return " and ".join(conditions) if conditions else None + permision_script_name = get_server_script_map().get("permission_query").get(self.doctype) + if permision_script_name: + script = frappe.get_doc("Server Script", permision_script_name) + condition = script.get_permission_query_conditions(self.user) + if condition: + conditions.append(condition) + + return " and ".join(conditions) if conditions else "" + def run_custom_query(self, query): if '%(key)s' in query: From e338f2f7b6cb5e446dacd9a7527d12e1c4806405 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 3 Dec 2020 23:56:26 +0530 Subject: [PATCH 21/49] fix(newsletter): Render template for HTML content type --- frappe/email/doctype/newsletter/newsletter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a4d60706eb..2791ebb75b 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator): self.db_set("scheduled_to_send", len(self.recipients)) def get_message(self): - + if self.content_type == "HTML": + return frappe.render_template(self.message_html, {"doc": self.as_dict()}) return { 'Rich Text': self.message, - 'Markdown': markdown(self.message_md), - 'HTML': self.message_html + 'Markdown': markdown(self.message_md) }[self.content_type or 'Rich Text'] def get_recipients(self): From 69befda08efa8d42f6c9ea2bb0fd84936d124f99 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 4 Dec 2020 16:12:22 +0530 Subject: [PATCH 22/49] fix: label for _assign field --- frappe/model/meta.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 8c17a5b19b..c740d495c1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -209,7 +209,8 @@ class Meta(Document): 'owner': _('Created By'), 'modified_by': _('Modified By'), 'creation': _('Created On'), - 'modified': _('Last Modified On') + 'modified': _('Last Modified On'), + '_assign': _('Assigned To') }.get(fieldname) or _('No Label') return label From 1b609af8e5de0b0cc75fdd9d60e8fe9748371180 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 4 Dec 2020 18:15:17 +0530 Subject: [PATCH 23/49] fix: Handle paths relative to bench root and sites folder --- frappe/commands/site.py | 4 ++-- frappe/installer.py | 20 +++++++------------- frappe/utils/__init__.py | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 35f5b13582..4a631be3ac 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -100,11 +100,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas # Extract public and/or private files to the restored site, if user has given the path if with_public_files: - public = extract_files(site, with_public_files, 'public') + public = extract_files(site, with_public_files) os.remove(public) if with_private_files: - private = extract_files(site, with_private_files, 'private') + private = extract_files(site, with_private_files) os.remove(private) # Removing temporarily created file diff --git a/frappe/installer.py b/frappe/installer.py index 1245a08cb7..a11c8dfbfa 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -440,20 +440,11 @@ def extract_sql_from_archive(sql_file_path): Returns: str: Path of the decompressed SQL file """ + from frappe.utils import get_bench_relative_path + sql_file_path = get_bench_relative_path(sql_file_path) # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file - if not os.path.exists(sql_file_path): - base_path = '..' - sql_file_path = os.path.join(base_path, sql_file_path) - if not os.path.exists(sql_file_path): - print('Invalid path {0}'.format(sql_file_path[3:])) - sys.exit(1) - elif sql_file_path.startswith(os.sep): - base_path = os.sep - else: - base_path = '.' - if sql_file_path.endswith('sql.gz'): - decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path)) + decompressed_file_name = extract_sql_gzip(sql_file_path) else: decompressed_file_name = sql_file_path @@ -475,9 +466,12 @@ def extract_sql_gzip(sql_gz_path): return decompressed_file -def extract_files(site_name, file_path, folder_name): +def extract_files(site_name, file_path): import shutil import subprocess + from frappe.utils import get_bench_relative_path + + file_path = get_bench_relative_path(file_path) # Need to do frappe.init to maintain the site locals frappe.init(site=site_name) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index c209ee13c9..cc5b42acb7 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -729,3 +729,27 @@ def get_build_version(): # .build can sometimes not exist # this is not a major problem so send fallback return frappe.utils.random_string(8) + +def get_bench_relative_path(file_path): + """Fixes paths relative to the bench root directory if exists and returns the absolute path + + Args: + file_path (str, Path): Path of a file that exists on the file system + + Returns: + str: Absolute path of the file_path + """ + if not os.path.exists(file_path): + base_path = '..' + elif file_path.startswith(os.sep): + base_path = os.sep + else: + base_path = '.' + + file_path = os.path.join(base_path, file_path) + + if not os.path.exists(file_path): + print('Invalid path {0}'.format(file_path[3:])) + sys.exit(1) + + return os.path.abspath(file_path) From b1cab9a9c1788ee654fe6d08d49a71c67eb6e163 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 5 Dec 2020 19:26:52 +0530 Subject: [PATCH 24/49] fix: Query report filter --- frappe/core/doctype/report_filter/report_filter.json | 4 ++-- frappe/public/js/frappe/views/reports/query_report.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/report_filter/report_filter.json b/frappe/core/doctype/report_filter/report_filter.json index 9d277db11d..964294b96e 100644 --- a/frappe/core/doctype/report_filter/report_filter.json +++ b/frappe/core/doctype/report_filter/report_filter.json @@ -44,7 +44,7 @@ }, { "fieldname": "options", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Options" }, { @@ -58,7 +58,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-17 16:15:46.937267", + "modified": "2020-12-05 19:20:00.503097", "modified_by": "Administrator", "module": "Core", "name": "Report Filter", diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 60abb187ae..eccfa9c089 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1112,7 +1112,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } get_filter_values(raise) { - const mandatory = this.filters.filter(f => f.df.reqd); + + // check for mandatory property for filters added via UI + const mandatory = this.filters.filter(f => (f.df.reqd || f.df.mandatory)); const missing_mandatory = mandatory.filter(f => !f.get_value()); if (raise && missing_mandatory.length > 0) { let message = __('Please set filters'); From c5677b52d0af3dd31812625db8b22a2fab501983 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 7 Dec 2020 14:42:54 +0530 Subject: [PATCH 25/49] refactor: shorten_number function - pass min_length and max_no_of_decimals --- frappe/public/js/frappe/utils/utils.js | 41 +++++++++++++++---- .../js/frappe/widgets/number_card_widget.js | 2 +- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 4bf9c5bbd8..f8f25293b3 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -979,17 +979,42 @@ Object.assign(frappe.utils, { return route; }, - shorten_number: function (number, country) { - country = (country == 'India') ? country : ''; + shorten_number: function (number, country, min_length=4, max_no_of_decimals=2) { + /* returns the number as an abbreviated string + * PARAMS + * number - number to be shortened + * country - country that determines the numnber system to be used + * min_length - length below which the number will not be shortened + * max_no_of_decimals - max number of decimals of the shortened number + */ + + // return number if total digits is lesser than min_length + const len = String(number).match(/\d/g).length; + if (len < min_length) return number.toString(); + const number_system = this.get_number_system(country); let x = Math.abs(Math.round(number)); for (const map of number_system) { - const condition = map.condition ? map.condition(x) : x >= map.divisor; - if (condition) { - return (number/map.divisor).toFixed(2) + ' ' + map.symbol; + if (x >= map.divisor) { + let result = number/map.divisor; + const no_of_decimals = this.get_number_of_decimals(result); + /* + If no_of_decimals is greater than max_no_of_decimals, + round the number to max_no_of_decimals + */ + result = no_of_decimals > max_no_of_decimals + ? result.toFixed(max_no_of_decimals) + : result; + return result + ' ' + map.symbol; } } - return number.toFixed(); + + return number.toFixed(max_no_of_decimals); + }, + + get_number_of_decimals: function (number) { + if (Math.floor(number) === number) return 0; + return number.toString().split(".")[1].length || 0; }, get_number_system: function (country) { @@ -1019,9 +1044,11 @@ Object.assign(frappe.utils, { { divisor: 1.0e+3, symbol: 'K', - condition: (num) => num.toFixed().length > 5 }] }; + + if (!Object.keys(number_system_map).includes(country)) country = ''; + return number_system_map[country]; }, }); diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 8855de435b..c41f9bc6e7 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -204,7 +204,7 @@ export default class NumberCardWidget extends Widget { get_formatted_number(df) { const default_country = frappe.sys_defaults.country; - const shortened_number = frappe.utils.shorten_number(this.number, default_country); + const shortened_number = frappe.utils.shorten_number(this.number, default_country, 5); let number_parts = shortened_number.split(' '); const symbol = number_parts[1] || ''; From fc97e080c67ddc241af9548fb08d9672fb3c0470 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 7 Dec 2020 16:32:19 +0100 Subject: [PATCH 26/49] feat: remove useless functionality --- .../s3_backup_settings.json | 32 ++-------- .../s3_backup_settings/s3_backup_settings.py | 58 +++++-------------- 2 files changed, 19 insertions(+), 71 deletions(-) diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index 123bb21e88..2ca1723cb2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -18,12 +18,9 @@ "bucket", "endpoint_url", "column_break_13", - "region", "backup_details_section", "frequency", - "backup_files", - "column_break_18", - "backup_limit" + "backup_files" ], "fields": [ { @@ -42,7 +39,7 @@ }, { "default": "1", - "description": "Note: By default emails for failed backups are sent.", + "description": "By default, emails are only sent for failed backups.", "fieldname": "send_email_for_successful_backup", "fieldtype": "Check", "label": "Send Email for Successful Backup" @@ -73,14 +70,7 @@ "reqd": 1 }, { - "default": "us-east-1", - "description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.", - "fieldname": "region", - "fieldtype": "Select", - "label": "Region", - "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1" - }, - { + "default": "https://s3.amazonaws.com", "fieldname": "endpoint_url", "fieldtype": "Data", "label": "Endpoint URL" @@ -92,14 +82,6 @@ "mandatory_depends_on": "enabled", "reqd": 1 }, - { - "description": "Set to 0 for no limit on the number of backups taken", - "fieldname": "backup_limit", - "fieldtype": "Int", - "label": "Backup Limit", - "mandatory_depends_on": "enabled", - "reqd": 1 - }, { "depends_on": "enabled", "fieldname": "api_access_section", @@ -142,16 +124,12 @@ "fieldname": "backup_files", "fieldtype": "Check", "label": "Backup Files" - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" } ], "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2020-07-27 17:27:21.400000", + "modified": "2020-12-07 15:30:55.047689", "modified_by": "Administrator", "module": "Integrations", "name": "S3 Backup Settings", @@ -172,4 +150,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 7c90d37f82..308d34c5c2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -24,6 +24,7 @@ class S3BackupSettings(Document): if not self.endpoint_url: self.endpoint_url = 'https://s3.amazonaws.com' + conn = boto3.client( 's3', aws_access_key_id=self.access_key_id, @@ -31,25 +32,21 @@ class S3BackupSettings(Document): endpoint_url=self.endpoint_url ) - bucket_lower = str(self.bucket) - - try: - conn.list_buckets() - - except ClientError: - frappe.throw(_("Invalid Access Key ID or Secret Access Key.")) - try: # Head_bucket returns a 200 OK if the bucket exists and have access to it. - conn.head_bucket(Bucket=bucket_lower) + # Requires ListBucket permission + conn.head_bucket(Bucket=self.bucket) except ClientError as e: error_code = e.response['Error']['Code'] + bucket_name = frappe.bold(self.bucket) if error_code == '403': - frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower)) - else: # '400'-Bad request or '404'-Not Found return - # try to create bucket - conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={ - 'LocationConstraint': self.region}) + msg = _("Do not have permission to access bucket {0}.").format(bucket_name) + elif error_code == '404': + msg = _("Bucket {0} not found.").format(bucket_name) + else: + msg = e.args[0] + + frappe.throw(msg) @frappe.whitelist() @@ -70,11 +67,13 @@ def take_backups_weekly(): def take_backups_monthly(): take_backups_if("Monthly") + def take_backups_if(freq): if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")): if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq: take_backups_s3() + @frappe.whitelist() def take_backups_s3(retry_count=0): try: @@ -146,42 +145,13 @@ def backup_to_s3(): if files_filename: upload_file_to_s3(files_filename, folder, conn, bucket) - delete_old_backups(doc.backup_limit, bucket) - def upload_file_to_s3(filename, folder, conn, bucket): destpath = os.path.join(folder, os.path.basename(filename)) try: print("Uploading file:", filename) - conn.upload_file(filename, bucket, destpath) + conn.upload_file(filename, bucket, destpath) # Requires PutObject permission except Exception as e: frappe.log_error() print("Error uploading: %s" % (e)) - - -def delete_old_backups(limit, bucket): - all_backups = [] - doc = frappe.get_single("S3 Backup Settings") - backup_limit = int(limit) - - s3 = boto3.resource( - 's3', - aws_access_key_id=doc.access_key_id, - aws_secret_access_key=doc.get_password('secret_access_key'), - endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com' - ) - - bucket = s3.Bucket(bucket) - objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/') - if objects: - for obj in objects.get('CommonPrefixes'): - all_backups.append(obj.get('Prefix')) - - oldest_backup = sorted(all_backups)[0] if all_backups else '' - - if len(all_backups) > backup_limit: - print("Deleting Backup: {0}".format(oldest_backup)) - for obj in bucket.objects.filter(Prefix=oldest_backup): - # delete all keys that are inside the oldest_backup - s3.Object(bucket.name, obj.key).delete() From 3a11ef3365ed4838f1bd82929af5d4f5589110e0 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 8 Dec 2020 12:46:16 +0530 Subject: [PATCH 27/49] fix: filter dashboards, dashboard charts, number cards by modules --- frappe/desk/doctype/dashboard/dashboard.py | 19 ++++++++++++ .../dashboard_chart/dashboard_chart.py | 30 ++++++++++++------- .../desk/doctype/number_card/number_card.py | 13 ++++++-- frappe/hooks.py | 1 + 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index b12bcfe27d..1c04b6a2fe 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from frappe.model.document import Document from frappe.modules.export_file import export_to_files +from frappe.config import get_modules_from_all_apps_for_user import frappe from frappe import _ import json @@ -42,6 +43,24 @@ class Dashboard(Document): except ValueError as error: frappe.throw(_("Invalid json added in the custom options: {0}").format(error)) + +def get_permission_query_conditions(user): + if not user: + user = frappe.session.user + + if user == 'Administrator': + return + + roles = frappe.get_roles(user) + if "System Manager" in roles: + return None + + allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] + module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format( + allowed_modules=','.join(allowed_modules)) + + return '{module_condition}'.format(module_condition=module_condition) + @frappe.whitelist() def get_permitted_charts(dashboard_name): permitted_charts = [] diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 3f8d7c3c79..2fa36b5514 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -13,12 +13,12 @@ from frappe.utils.dateutils import\ get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain from frappe.model.naming import append_number_if_name_exists from frappe.boot import get_allowed_reports +from frappe.config import get_modules_from_all_apps_for_user from frappe.model.document import Document from frappe.modules.export_file import export_to_files def get_permission_query_conditions(user): - if not user: user = frappe.session.user @@ -31,9 +31,11 @@ def get_permission_query_conditions(user): doctype_condition = False report_condition = False + module_condition = False allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()] + allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] if allowed_doctypes: doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format( @@ -41,18 +43,24 @@ def get_permission_query_conditions(user): if allowed_reports: report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format( allowed_reports=','.join(allowed_reports)) + if allowed_modules: + module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules}) + or `tabDashboard Chart`.`module` is NULL'''.format( + allowed_modules=','.join(allowed_modules)) return ''' - (`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') - and {doctype_condition}) - or - (`tabDashboard Chart`.`chart_type` = 'Report' - and {report_condition}) - '''.format( - doctype_condition=doctype_condition, - report_condition=report_condition - ) - + ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') + and {doctype_condition}) + or + (`tabDashboard Chart`.`chart_type` = 'Report' + and {report_condition})) + and + ({module_condition}) + '''.format( + doctype_condition=doctype_condition, + report_condition=report_condition, + module_condition=module_condition + ) def has_permission(doc, ptype, user): roles = frappe.get_roles(user) diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index d4a2b00c57..6bddd09fc7 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -8,6 +8,7 @@ from frappe.model.document import Document from frappe.utils import cint from frappe.model.naming import append_number_if_name_exists from frappe.modules.export_file import export_to_files +from frappe.config import get_modules_from_all_apps_for_user class NumberCard(Document): def autoname(self): @@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None): return None doctype_condition = False + module_condition = False allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] + allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] if allowed_doctypes: doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format( allowed_doctypes=','.join(allowed_doctypes)) + if allowed_modules: + module_condition = '''`tabNumber Card`.`module` in ({allowed_modules}) + or `tabNumber Card`.`module` is NULL'''.format( + allowed_modules=','.join(allowed_modules)) return ''' - {doctype_condition} - '''.format(doctype_condition=doctype_condition) + {doctype_condition} + and + {module_condition} + '''.format(doctype_condition=doctype_condition, module_condition=module_condition) def has_permission(doc, ptype, user): roles = frappe.get_roles(user) diff --git a/frappe/hooks.py b/frappe/hooks.py index d8c8cd841c..3d7ae0abb4 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -94,6 +94,7 @@ permission_query_conditions = { "User": "frappe.core.doctype.user.user.get_permission_query_conditions", "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", + "Dashboard": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions", "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", "Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", From e197a6fd7fbecfca9eadd8c2d92577eb20ba12b7 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 8 Dec 2020 11:35:41 +0530 Subject: [PATCH 28/49] feat: added data format support DD-Mon-YY --- frappe/public/js/frappe/data_import/import_preview.js | 1 + frappe/utils/data.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 6c17cb4351..477cfb0786 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -101,6 +101,7 @@ frappe.data_import.ImportPreview = class ImportPreview { .replace('%H', 'HH') .replace('%M', 'mm') .replace('%S', 'ss') + .replace('%b', 'Mon') : null; let column_title = ` diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 41f247da45..cef4243913 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1300,12 +1300,14 @@ def generate_hash(*args, **kwargs): def guess_date_format(date_string): DATE_FORMATS = [ + r"%d/%b/%y", r"%d-%m-%Y", r"%m-%d-%Y", r"%Y-%m-%d", r"%d-%m-%y", r"%m-%d-%y", r"%y-%m-%d", + r"%y-%b-%d", r"%d/%m/%Y", r"%m/%d/%Y", r"%Y/%m/%d", From 309f483bb3c158fe0b3186e3d6c4762f66b2049c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 9 Dec 2020 16:54:40 +0530 Subject: [PATCH 29/49] refactor: update return statement --- frappe/desk/doctype/dashboard/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 1c04b6a2fe..fa03bf8f80 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -59,7 +59,7 @@ def get_permission_query_conditions(user): module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format( allowed_modules=','.join(allowed_modules)) - return '{module_condition}'.format(module_condition=module_condition) + return module_condition @frappe.whitelist() def get_permitted_charts(dashboard_name): From c5d08305536c77162ba7821bd4ac9ec9a0cee13b Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 10 Dec 2020 10:37:29 +0530 Subject: [PATCH 30/49] fix: cint seconds before operations --- frappe/utils/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 34659e1cac..75e8dedbe9 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -369,6 +369,8 @@ def format_duration(seconds, hide_days=False): example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float """ + + seconds = cint(seconds) total_duration = { 'days': math.floor(seconds / (3600 * 24)), From c805851ebe20ea60719a0d9929fbfb900265e28b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Dec 2020 10:52:50 +0530 Subject: [PATCH 31/49] feat(Auto Repeat): Submit on Creation configuration --- .../automation/doctype/auto_repeat/auto_repeat.js | 14 ++++++++++++++ .../doctype/auto_repeat/auto_repeat.json | 11 ++++++++++- .../automation/doctype/auto_repeat/auto_repeat.py | 5 ++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index a11de1d881..121b4bd2f0 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', { // auto repeat schedule frappe.auto_repeat.render_schedule(frm); + + frm.trigger('toggle_submit_on_creation'); + }, + + reference_doctype: function(frm) { + frm.trigger('toggle_submit_on_creation'); + }, + + toggle_submit_on_creation: function(frm) { + // submit on creation checkbox + frappe.model.with_doctype(frm.doc.reference_doctype, () => { + let meta = frappe.get_meta(frm.doc.reference_doctype); + frm.toggle_display('submit_on_creation', meta.is_submittable); + }); }, template: function(frm) { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 8ee6ca1d45..80975dd4f5 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "format:AUT-AR-{#####}", @@ -12,6 +13,7 @@ "section_break_3", "reference_doctype", "reference_document", + "submit_on_creation", "column_break_5", "start_date", "end_date", @@ -186,9 +188,16 @@ "fieldname": "repeat_on_last_day", "fieldtype": "Check", "label": "Repeat on Last Day of the Month" + }, + { + "default": "0", + "fieldname": "submit_on_creation", + "fieldtype": "Check", + "label": "Submit on Creation" } ], - "modified": "2019-07-17 11:30:51.412317", + "links": [], + "modified": "2020-12-10 10:43:13.449172", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fcf24bf1a9..019692f136 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -150,6 +150,9 @@ class AutoRepeat(Document): self.update_doc(new_doc, reference_doc) new_doc.insert(ignore_permissions = True) + if self.submit_on_creation: + new_doc.submit(gnore_permissions = True) + return new_doc def update_doc(self, new_doc, reference_doc): @@ -160,7 +163,7 @@ class AutoRepeat(Document): if new_doc.meta.get_field('auto_repeat'): new_doc.set('auto_repeat', self.name) - for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']: + for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: if new_doc.meta.get_field(fieldname): new_doc.set(fieldname, reference_doc.get(fieldname)) From 30fbf37f25530cec068d39a98066684cfccd8848 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Dec 2020 11:48:12 +0530 Subject: [PATCH 32/49] test: submit on creation --- .../doctype/auto_repeat/auto_repeat.py | 2 +- .../doctype/auto_repeat/test_auto_repeat.py | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 019692f136..7eb533ce44 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -151,7 +151,7 @@ class AutoRepeat(Document): new_doc.insert(ignore_permissions = True) if self.submit_on_creation: - new_doc.submit(gnore_permissions = True) + new_doc.submit() return new_doc diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 60fa9cb59e..e40b12e3b9 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -111,6 +111,25 @@ class TestAutoRepeat(unittest.TestCase): doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) self.assertEqual(getdate(doc.next_schedule_date), current_date) + def test_submit_on_creation(self): + doctype = 'Test Submittable DocType' + create_submittable_doctype(doctype) + + current_date = getdate() + submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() + submittable_doc.submit() + doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, + start_date=add_days(current_date, -1), submit_on_creation=1) + + data = get_auto_repeat_entries(current_date) + create_repeated_entries(data) + docnames = frappe.db.get_all(doc.reference_doctype, + filters={'auto_repeat': doc.name}, + fields=['docstatus'], + limit=1 + ) + self.assertEquals(docnames[0].docstatus, 1) + def make_auto_repeat(**args): args = frappe._dict(args) @@ -118,6 +137,7 @@ def make_auto_repeat(**args): 'doctype': 'Auto Repeat', 'reference_doctype': args.reference_doctype or 'ToDo', 'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), + 'submit_on_creation': args.submit_on_creation or 0, 'frequency': args.frequency or 'Daily', 'start_date': args.start_date or add_days(today(), -1), 'end_date': args.end_date or "", @@ -128,3 +148,34 @@ def make_auto_repeat(**args): }).insert(ignore_permissions=True) return doc + + +def create_submittable_doctype(doctype): + if frappe.db.exists('DocType', doctype): + return + else: + doc = frappe.get_doc({ + 'doctype': 'DocType', + '__newname': doctype, + 'module': 'Custom', + 'custom': 1, + 'is_submittable': 1, + 'fields': [{ + 'fieldname': 'test', + 'label': 'Test', + 'fieldtype': 'Data' + }], + 'permissions': [{ + 'role': 'System Manager', + 'read': 1, + 'write': 1, + 'create': 1, + 'delete': 1, + 'submit': 1, + 'cancel': 1, + 'amend': 1 + }] + }).insert() + + doc.allow_auto_repeat = 1 + doc.save() \ No newline at end of file From 5a60048a0a9e57a5227796a3585b402c02435348 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Dec 2020 12:31:01 +0530 Subject: [PATCH 33/49] test: for get_bench_relative_path --- frappe/tests/test_commands.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index 8c76ce2f48..2da08718a4 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -14,7 +14,7 @@ import glob import frappe import frappe.recorder from frappe.installer import add_to_installed_apps -from frappe.utils import add_to_date, now +from frappe.utils import add_to_date, get_bench_relative_path, now from frappe.utils.backups import fetch_latest_backups @@ -364,3 +364,21 @@ class TestCommands(BaseTestCommands): else: installed_apps = set(frappe.get_installed_apps()) self.assertSetEqual(list_apps, installed_apps) + + def test_get_bench_relative_path(self): + bench_path = frappe.utils.get_bench_path() + test1_path = os.path.join(bench_path, 'test1.txt') + test2_path = os.path.join(bench_path, 'sites/test2.txt') + + with open(test1_path, 'w+') as test1: + test1.write('asdf') + with open(test2_path, 'w+') as test2: + test2.write('asdf') + + self.assertTrue('test1.txt' in get_bench_relative_path('test1.txt')) + self.assertTrue('sites/test2.txt' in get_bench_relative_path('test2.txt')) + with self.assertRaises(SystemExit): + get_bench_relative_path('test3.txt') + + os.remove(test1_path) + os.remove(test2_path) From 9e954a737257b3fb11821ae56255d3bf07082cb6 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 10 Dec 2020 13:52:21 +0530 Subject: [PATCH 34/49] style: quotes --- frappe/tests/test_commands.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index 2da08718a4..0786a0e14f 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -367,18 +367,18 @@ class TestCommands(BaseTestCommands): def test_get_bench_relative_path(self): bench_path = frappe.utils.get_bench_path() - test1_path = os.path.join(bench_path, 'test1.txt') - test2_path = os.path.join(bench_path, 'sites/test2.txt') + test1_path = os.path.join(bench_path, "test1.txt") + test2_path = os.path.join(bench_path, "sites", "test2.txt") - with open(test1_path, 'w+') as test1: - test1.write('asdf') - with open(test2_path, 'w+') as test2: - test2.write('asdf') + with open(test1_path, "w+") as test1: + test1.write("asdf") + with open(test2_path, "w+") as test2: + test2.write("asdf") - self.assertTrue('test1.txt' in get_bench_relative_path('test1.txt')) - self.assertTrue('sites/test2.txt' in get_bench_relative_path('test2.txt')) + self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt")) + self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt")) with self.assertRaises(SystemExit): - get_bench_relative_path('test3.txt') + get_bench_relative_path("test3.txt") os.remove(test1_path) os.remove(test2_path) From ec0d1dd63f727ca7d4713fbf2f739e1c49151a30 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Dec 2020 17:18:13 +0530 Subject: [PATCH 35/49] fix: added server side validation for submit on creation --- frappe/automation/doctype/auto_repeat/auto_repeat.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 7eb533ce44..31d6539e61 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -21,6 +21,7 @@ class AutoRepeat(Document): def validate(self): self.update_status() self.validate_reference_doctype() + self.validate_submit_on_creation() self.validate_dates() self.validate_email_id() self.set_dates() @@ -60,6 +61,11 @@ class AutoRepeat(Document): if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) + def validate_submit_on_creation(self): + if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: + frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( + frappe.bold('Submit on Creation'))) + def validate_dates(self): if frappe.flags.in_patch: return From 0651341bf8535af743d0fda128008ee180f3d533 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Dec 2020 18:53:21 +0530 Subject: [PATCH 36/49] feat: show absolute value checkbox if doctype has appropriate fields --- .../doctype/print_format/print_format.js | 18 +++++++++++++++++- .../doctype/print_format/print_format.json | 4 +++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index e6599b2496..9ef5652dda 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", { } frm.trigger('render_buttons'); frm.toggle_display('standard', frappe.boot.developer_mode); + frm.trigger('hide_absolute_value_field'); }, render_buttons: function (frm) { frm.page.clear_inner_toolbar(); @@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", { frm.set_value('show_section_headings', value); frm.set_value('line_breaks', value); frm.trigger('render_buttons'); + }, + doc_type: function (frm) { + frm.trigger('hide_absolute_value_field'); + }, + hide_absolute_value_field: function (frm) { + // TODO: make it work with frm.doc.doc_type + // Problem: frm isn't updated in some random cases + const doctype = locals[frm.doc.doctype][frm.doc.name]; + if (doctype) { + frappe.model.with_doctype(doctype, () => { + const meta = frappe.get_meta(doctype); + const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype)); + frm.toggle_display('absolute_value', has_int_float_currency_field.length); + }); + } } -}) +}); diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 3867ce4502..6e64e802c9 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -200,6 +200,8 @@ }, { "default": "0", + "depends_on": "doc_type", + "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", "fieldname": "absolute_value", "fieldtype": "Check", "label": "Show absolute values" @@ -209,7 +211,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-11-30 15:26:35.605213", + "modified": "2020-12-10 18:58:55.598269", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", From f0ae807df83009fc2fe9f7e82111b3404a395f60 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 10:31:36 +0530 Subject: [PATCH 37/49] fix: fallback for global default precision --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 7f5c3bf472..a03a4d8405 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,7 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options); + return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision") || 3) + "%", options); }, Rating: function(value) { return ` From e7417ebe486bd5d60a73056735743c079fc44597 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 11:31:56 +0530 Subject: [PATCH 38/49] fix: fallback for percent field precision to 2 --- frappe/public/js/frappe/form/formatters.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index a03a4d8405..f163f59171 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,8 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision") || 3) + "%", options); + var precision = docfield.precision || cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) || 2; + return frappe.form.formatters._right(flt(value, precision) + "%", options); }, Rating: function(value) { return ` From 6398ca7db7ea9565f5fef78cab584ac771c59f51 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Dec 2020 12:11:07 +0530 Subject: [PATCH 39/49] refactor: move common functions as methods to the Auto Repeat class --- .../doctype/auto_repeat/auto_repeat.js | 5 +- .../doctype/auto_repeat/auto_repeat.py | 143 +++++++++--------- .../doctype/auto_repeat/test_auto_repeat.py | 4 +- 3 files changed, 70 insertions(+), 82 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index afee9b98bb..e914ff27b0 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -100,10 +100,7 @@ frappe.ui.form.on('Auto Repeat', { frappe.auto_repeat.render_schedule = function(frm) { if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { - frm.call({ - method: "get_auto_repeat_schedule", - doc: frm.doc - }).then((r) => { + frm.call("get_auto_repeat_schedule").then((r) => { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 205d49df56..1fd2cdf1b3 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -19,7 +19,6 @@ from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeat 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} - class AutoRepeat(Document): def validate(self): self.update_status() @@ -53,7 +52,7 @@ class AutoRepeat(Document): if self.disabled: self.next_schedule_date = None else: - self.next_schedule_date = get_next_schedule_date(schedule_date=self.start_date, auto_repeat_doc=self) + self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) def unlink_if_applicable(self): if self.status == 'Completed' or self.disabled: @@ -93,7 +92,7 @@ class AutoRepeat(Document): frappe.throw(_("'Recipients' not specified")) def validate_auto_repeat_days(self): - auto_repeat_days = get_auto_repeat_days(self) + auto_repeat_days = self.get_auto_repeat_days() if not len(set(auto_repeat_days)) == len(auto_repeat_days): repeated_days = get_repeated(auto_repeat_days) frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days))) @@ -123,7 +122,7 @@ class AutoRepeat(Document): end_date = getdate(self.end_date) if not self.end_date: - next_date = get_next_schedule_date(schedule_date=start_date, auto_repeat_doc=self) + next_date = self.get_next_schedule_date(schedule_date=start_date) row = { "reference_document": self.reference_document, "frequency": self.frequency, @@ -132,8 +131,7 @@ class AutoRepeat(Document): schedule_details.append(row) if self.end_date: - next_date = get_next_schedule_date( - schedule_date=start_date, auto_repeat_doc=self, for_full_schedule=True) + next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) while (getdate(next_date) < getdate(end_date)): row = { @@ -142,8 +140,7 @@ class AutoRepeat(Document): "next_scheduled_date" : next_date } schedule_details.append(row) - next_date = get_next_schedule_date( - schedule_date=next_date, auto_repeat_doc=self, for_full_schedule=True) + next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) return schedule_details @@ -221,6 +218,68 @@ class AutoRepeat(Document): new_doc.set('from_date', from_date) new_doc.set('to_date', to_date) + def get_next_schedule_date(self, schedule_date, for_full_schedule=False): + if month_map.get(self.frequency): + month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 + else: + month_count = 0 + + day_count = 0 + if month_count and self.repeat_on_last_day: + day_count = 31 + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count and self.repeat_on_day: + day_count = self.repeat_on_day + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count: + next_date = get_next_date(self.start_date, month_count) + else: + days = self.get_days(schedule_date) + next_date = add_days(schedule_date, days) + + # next schedule date should be after or on current date + if not for_full_schedule: + while getdate(next_date) < getdate(today()): + if month_count: + month_count += month_map.get(self.frequency) + next_date = get_next_date(self.start_date, month_count, day_count) + elif days: + days = self.get_days(next_date) + next_date = add_days(next_date, days) + + return next_date + + def get_days(self, schedule_date): + if self.frequency == "Weekly": + days = self.get_offset_for_weekly_frequency(schedule_date) + else: + # daily frequency + days = 1 + + return days + + def get_offset_for_weekly_frequency(self, schedule_date): + # if weekdays are not set, offset is 7 from current schedule date + if not self.repeat_on_days: + return 7 + + repeat_on_days = self.get_auto_repeat_days() + current_schedule_day = getdate(schedule_date).weekday() + weekdays = list(week_map.keys()) + + # if repeats on more than 1 day or + # start date's weekday is not in repeat days, then get next weekday + # else offset is 7 + if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: + weekday = get_next_weekday(current_schedule_day, repeat_on_days) + next_weekday_number = week_map.get(weekday) + # offset for upcoming weekday + return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days + return 7 + + def get_auto_repeat_days(self): + return [d.day for d in self.get('repeat_on_days', [])] + def send_notification(self, new_doc): """Notify concerned people about recurring document generation""" subject = self.subject or '' @@ -301,74 +360,12 @@ class AutoRepeat(Document): ) -def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedule=False): - if month_map.get(auto_repeat_doc.frequency): - month_count = month_map.get(auto_repeat_doc.frequency) + month_diff(schedule_date, auto_repeat_doc.start_date) - 1 - else: - month_count = 0 - - day_count = 0 - if month_count and auto_repeat_doc.repeat_on_last_day: - day_count = 31 - next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif month_count and auto_repeat_doc.repeat_on_day: - day_count = auto_repeat_doc.repeat_on_day - next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif month_count: - next_date = get_next_date(auto_repeat_doc.start_date, month_count) - else: - days = get_days(schedule_date, auto_repeat_doc) - next_date = add_days(schedule_date, days) - - # next schedule date should be after or on current date - if not for_full_schedule: - while getdate(next_date) < getdate(today()): - if month_count: - month_count += month_map.get(auto_repeat_doc.frequency) - next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif days: - days = get_days(next_date, auto_repeat_doc) - next_date = add_days(next_date, days) - - return next_date - - def get_next_date(dt, mcount, day=None): dt = getdate(dt) dt += relativedelta(months=mcount, day=day) return dt -def get_days(schedule_date, auto_repeat_doc): - if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) - else: - # daily frequency - days = 1 - - return days - - -def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): - # if weekdays are not set, offset is 7 from current schedule date - if not auto_repeat_doc.repeat_on_days: - return 7 - - repeat_on_days = get_auto_repeat_days(auto_repeat_doc) - current_schedule_day = getdate(schedule_date).weekday() - weekdays = list(week_map.keys()) - - # if repeats on more than 1 day or - # start date's weekday is not in repeat days, then get next weekday - # else offset is 7 - if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: - weekday = get_next_weekday(current_schedule_day, repeat_on_days) - next_weekday_number = week_map.get(weekday) - # offset for upcoming weekday - return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days - return 7 - - def get_next_weekday(current_schedule_day, weekdays): days = list(week_map.keys()) if current_schedule_day > 0: @@ -381,10 +378,6 @@ def get_next_weekday(current_schedule_day, weekdays): return entry -def get_auto_repeat_days(doc): - return [d.day for d in doc.get('repeat_on_days', [])] - - #called through hooks def make_auto_repeat_entry(): enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' @@ -405,7 +398,7 @@ def create_repeated_entries(data): if schedule_date == current_date and not doc.disabled: doc.create_documents() - schedule_date = get_next_schedule_date(schedule_date=schedule_date, auto_repeat_doc=doc) + schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) if schedule_date and not doc.disabled: frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index a9bfbb1cf8..0d6229cd9e 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -7,11 +7,9 @@ import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field -from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries +from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map from frappe.utils import today, add_days, getdate, add_months -week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} - def add_custom_fields(): df = dict( fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', From 56302986036a367c86d38445a59116928f826624 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 11 Dec 2020 12:34:09 +0530 Subject: [PATCH 40/49] fix: Reload server script via patch (#12076) --- frappe/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/patches.txt b/frappe/patches.txt index 0daf29e001..b459019dd7 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -21,6 +21,7 @@ execute:frappe.reload_doc('email', 'doctype', 'document_follow') execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02 execute:frappe.reload_doc('core', 'doctype', 'has_role') execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02 +execute:frappe.reload_doc('core', 'doctype', 'server_script') frappe.patches.v11_0.replicate_old_user_permissions frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 frappe.patches.v7_1.rename_scheduler_log_to_error_log From 5f708fa160ef9df5553aca0eb7292539ccb07cac Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 11 Dec 2020 12:47:16 +0530 Subject: [PATCH 41/49] feat: cache bench apps --- frappe/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index c5f13f2295..2bab1cd294 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -348,7 +348,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, if as_table and type(msg) in (list, tuple): out.as_table = 1 - + if as_list and type(msg) in (list, tuple) and len(msg) > 1: out.as_list = 1 @@ -939,7 +939,11 @@ def get_installed_apps(sort=False, frappe_last=False): connect() if not local.all_apps: - local.all_apps = get_all_apps(True) + local.all_apps = cache().get_value('all_apps', get_all_apps) + + #cache bench apps + if not cache().get_value('all_apps'): + cache().set_value('all_apps', local.all_apps) installed = json.loads(db.get_global("installed_apps") or "[]") From 881c42629650837dc05940fb45167b399d8fb6a1 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 11:39:21 +0530 Subject: [PATCH 42/49] fix: typo --- frappe/printing/doctype/print_format/print_format.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 6e64e802c9..3a47fb554f 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -201,17 +201,17 @@ { "default": "0", "depends_on": "doc_type", - "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", + "description": "If checked, negative numeric values of Currency, Quantity or Count would be shown as positive", "fieldname": "absolute_value", "fieldtype": "Check", - "label": "Show absolute values" + "label": "Show Absolute Values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-10 18:58:55.598269", + "modified": "2020-12-14 11:38:49.132061", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", From 9e4e36041ae2ec5e0c85a3ae929a716626f09312 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 11:48:10 +0530 Subject: [PATCH 43/49] fix: toggling of show absolute values checkbox --- frappe/printing/doctype/print_format/print_format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 9ef5652dda..786f8f97ab 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -66,7 +66,7 @@ frappe.ui.form.on("Print Format", { hide_absolute_value_field: function (frm) { // TODO: make it work with frm.doc.doc_type // Problem: frm isn't updated in some random cases - const doctype = locals[frm.doc.doctype][frm.doc.name]; + const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type; if (doctype) { frappe.model.with_doctype(doctype, () => { const meta = frappe.get_meta(doctype); From 8c539743cf5ad77cc8cb2dd42034b7d5ef4cc5ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 08:18:58 +0000 Subject: [PATCH 44/49] chore(deps): bump ini from 1.3.5 to 1.3.8 Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 26797675c6..e0383afccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2918,9 +2918,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inquirer@^7.3.3: version "7.3.3" From 06fcb822054d364089646eb947b431180852a46f Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 15 Dec 2020 12:35:30 +0530 Subject: [PATCH 45/49] fix: call init_callback after set_route --- frappe/public/js/frappe/form/quick_entry.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 0a489e26d6..eed49e070b 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -35,14 +35,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({ if (this.is_quick_entry() || this.force) { this.render_dialog(); resolve(this); - } else { // No quick entry, use full Form - // but still give callback a shot at the doc - if (this.init_callback) { - this.init_callback(this.doc); - } + } else { + // no quick entry, open full form frappe.quick_entry = null; frappe.set_route('Form', this.doctype, this.doc.name) .then(() => resolve(this)); + // call init_callback for consistency + if (this.init_callback) { + this.init_callback(this.doc); + } } }); }); From 8ef2f7ed1896f7a055e42b456bba8152a69ac1a7 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 15 Dec 2020 13:54:27 +0530 Subject: [PATCH 46/49] style: Re-format and use const instead of var --- frappe/public/js/frappe/form/formatters.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index f163f59171..f9a1d0b643 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,14 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - var precision = docfield.precision || cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) || 2; + const precision = ( + docfield.precision + || cint( + frappe.boot.sysdefaults + && frappe.boot.sysdefaults.float_precision + ) + || 2 + ); return frappe.form.formatters._right(flt(value, precision) + "%", options); }, Rating: function(value) { From f4f7e8e798940684cfbdb76d05ac7b88d6f4dce0 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Wed, 16 Dec 2020 07:04:07 +0200 Subject: [PATCH 47/49] chore(Snyk): Security upgrade frappe-charts from 1.5.4 to 1.5.5 (#12086) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d1a94d0e35..114de53384 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "driver.js": "^0.9.8", "express": "^4.17.1", "fast-deep-equal": "^2.0.1", - "frappe-charts": "^1.5.1", + "frappe-charts": "^1.5.5", "frappe-datatable": "^1.15.3", "frappe-gantt": "^0.5.0", "fuse.js": "^3.4.6", diff --git a/yarn.lock b/yarn.lock index e0383afccb..3f8eaa3d9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2299,10 +2299,10 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -frappe-charts@^1.5.1: - version "1.5.4" - resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.4.tgz#5870f77ac6ffc8ea4dab32adda1d4e5e4fbda64b" - integrity sha512-hBr7cRLmsCC5VBj/HwKOCgdwyXnkeAO5CAvOd5H4IYFbk84VD9jOjx9fSaqAE0MygVVbY1nCN+5nb08WThW4Xw== +frappe-charts@^1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.5.tgz#5f44a3639aecc6f8fc7d15752abc80bb68e26734" + integrity sha512-L9pJTsrSuRobS/EaBKT8i1x+DVOjkXyUwT85cteZAPqynU/7K+uqjQOy4tMSTv5zsTWJNWFJ37ax68T73YdR3g== frappe-datatable@^1.15.3: version "1.15.3" From 4c1756b3d633568500179c39066aeaa6da50962c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 11:29:05 +0530 Subject: [PATCH 48/49] fix: code clean-up and fallbacks for get method --- frappe/automation/doctype/auto_repeat/auto_repeat.js | 2 +- frappe/automation/doctype/auto_repeat/auto_repeat.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index e914ff27b0..c2c84692d8 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -100,7 +100,7 @@ frappe.ui.form.on('Auto Repeat', { frappe.auto_repeat.render_schedule = function(frm) { if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { - frm.call("get_auto_repeat_schedule").then((r) => { + frm.call("get_auto_repeat_schedule").then(r => { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 1fd2cdf1b3..7dbbcdd05d 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -241,9 +241,9 @@ class AutoRepeat(Document): if not for_full_schedule: while getdate(next_date) < getdate(today()): if month_count: - month_count += month_map.get(self.frequency) + month_count += month_map.get(self.frequency, 0) next_date = get_next_date(self.start_date, month_count, day_count) - elif days: + else: days = self.get_days(next_date) next_date = add_days(next_date, days) @@ -272,7 +272,7 @@ class AutoRepeat(Document): # else offset is 7 if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: weekday = get_next_weekday(current_schedule_day, repeat_on_days) - next_weekday_number = week_map.get(weekday) + next_weekday_number = week_map.get(weekday, 0) # offset for upcoming weekday return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days return 7 From 7d70e552aaa7bde72bc88a324b7c09d097f9718f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 12:09:10 +0530 Subject: [PATCH 49/49] chore: added a docstring for the get_next_schedule_date method --- frappe/automation/doctype/auto_repeat/auto_repeat.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 7dbbcdd05d..830af68de7 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -219,6 +219,13 @@ class AutoRepeat(Document): new_doc.set('to_date', to_date) def get_next_schedule_date(self, schedule_date, for_full_schedule=False): + """ + Returns the next schedule date for auto repeat after a recurring document has been created. + Adds required offset to the schedule_date param and returns the next schedule date. + + :param schedule_date: The date when the last recurring document was created. + :param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. + """ if month_map.get(self.frequency): month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 else: