From 4d8ef4690eb9e364e244f296f935dab45733d5f6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 12 Nov 2020 17:42:25 +0530 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 6398ca7db7ea9565f5fef78cab584ac771c59f51 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Dec 2020 12:11:07 +0530 Subject: [PATCH 7/9] 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 4c1756b3d633568500179c39066aeaa6da50962c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 11:29:05 +0530 Subject: [PATCH 8/9] 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 9/9] 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: