feat: Employee reminders (#25735) (#27115)

* feat: Add reminders section to HR Settings

* refactor: Extract generic function for getting Employees

* feat: Employee Work Anniversary Reminder

* feat: Daily Holiday Reminder

* fix: Unnecessary params and replace [] with .get()

* test: Daily Holiday Reminders

* test: is_holiday basic tests

* refactor: Move employee reminders code to separate module

* feat: Add advance reminder to HR settings

* feat: Advance Holiday Reminders

* refactor: get_holidays_for_employee

* feat: Email holiday reminders in advance + tests

* fix: Remove unused import

* refactor: HR Setting Reminder Section

* refactor: Remove Daily Holiday Reminders feat

* feat: Reminder miss warning

* fix: Failing test and function name change

* chore: Add patch for field rename

* chore: Rename frequency label

* fix: Failing patch test

* fix: sider and removed description of fields

* fix: email alignment

Co-authored-by: pateljannat <pateljannat2308@gmail.com>
Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com>
(cherry picked from commit 24b2a315818d08ad4cb03347ccf5297df916a5ac)

Co-authored-by: Mohammad Hussain Nagaria <34810212+NagariaHussain@users.noreply.github.com>
Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com>
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 74977cd..4854bfd 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -355,7 +355,8 @@
 		"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
 		"erpnext.controllers.accounts_controller.update_invoice_status",
 		"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
-		"erpnext.hr.doctype.employee.employee.send_birthday_reminders",
+		"erpnext.hr.doctype.employee.employee_reminders.send_work_anniversary_reminders",
+		"erpnext.hr.doctype.employee.employee_reminders.send_birthday_reminders",
 		"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
 		"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
 		"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary",
@@ -387,6 +388,12 @@
 		"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
 		"erpnext.crm.doctype.lead.lead.daily_open_lead"
 	],
+	"weekly": [
+		"erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"
+	],
+	"monthly": [
+		"erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"
+	],
 	"monthly_long": [
 		"erpnext.accounts.deferred_revenue.process_deferred_accounting",
 		"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
index 0d7fded..3db8165 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
@@ -8,7 +8,7 @@
 from frappe.utils import date_diff, add_days, getdate, cint, format_date
 from frappe.model.document import Document
 from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
-	get_holidays_for_employee, create_additional_leave_ledger_entry
+	create_additional_leave_ledger_entry, get_holiday_dates_for_employee
 
 class CompensatoryLeaveRequest(Document):
 
@@ -39,7 +39,7 @@
 			frappe.throw(_("You are not present all day(s) between compensatory leave request days"))
 
 	def validate_holidays(self):
-		holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date)
+		holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date)
 		if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
 			if date_diff(self.work_end_date, self.work_from_date):
 				msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index f428015..643f3da 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -1,7 +1,5 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
 import frappe
 
 from frappe.utils import getdate, validate_email_address, today, add_years, cstr
@@ -9,7 +7,6 @@
 from frappe import throw, _, scrub
 from frappe.permissions import add_user_permission, remove_user_permission, \
 	set_user_permission_if_allowed, has_permission, get_doc_permissions
-from frappe.model.document import Document
 from erpnext.utilities.transaction_base import delete_events
 from frappe.utils.nestedset import NestedSet
 
@@ -286,94 +283,8 @@
 		employee = frappe.get_doc("Employee", {"user_id": doc.name})
 		employee.update_user_permissions()
 
-def send_birthday_reminders():
-	"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
-	if int(frappe.db.get_single_value("HR Settings", "stop_birthday_reminders") or 0):
-		return
-
-	employees_born_today = get_employees_who_are_born_today()
-
-	for company, birthday_persons in employees_born_today.items():
-		employee_emails = get_all_employee_emails(company)
-		birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons]
-		recipients = list(set(employee_emails) - set(birthday_person_emails))
-
-		reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons)
-		send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
-
-		if len(birthday_persons) > 1:
-			# special email for people sharing birthdays
-			for person in birthday_persons:
-				person_email = person["user_id"] or person["personal_email"] or person["company_email"]
-				others = [d for d in birthday_persons if d != person]
-				reminder_text, message = get_birthday_reminder_text_and_message(others)
-				send_birthday_reminder(person_email, reminder_text, others, message)
-
 def get_employee_email(employee_doc):
-	return employee_doc["user_id"] or employee_doc["personal_email"] or employee_doc["company_email"]
-
-def get_birthday_reminder_text_and_message(birthday_persons):
-	if len(birthday_persons) == 1:
-		birthday_person_text = birthday_persons[0]['name']
-	else:
-		# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
-		person_names = [d['name'] for d in birthday_persons]
-		last_person = person_names[-1]
-		birthday_person_text = ", ".join(person_names[:-1])
-		birthday_person_text = _("{} & {}").format(birthday_person_text, last_person)
-
-	reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
-	message = _("A friendly reminder of an important date for our team.")
-	message += "<br>"
-	message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text)
-
-	return reminder_text, message
-
-def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
-	frappe.sendmail(
-		recipients=recipients,
-		subject=_("Birthday Reminder"),
-		template="birthday_reminder",
-		args=dict(
-			reminder_text=reminder_text,
-			birthday_persons=birthday_persons,
-			message=message,
-		),
-		header=_("Birthday Reminder 🎂")
-	)
-
-def get_employees_who_are_born_today():
-	"""Get all employee born today & group them based on their company"""
-	from collections import defaultdict
-	employees_born_today = frappe.db.multisql({
-		"mariadb": """
-			SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`
-			FROM `tabEmployee`
-			WHERE
-				DAY(date_of_birth) = DAY(%(today)s)
-			AND
-				MONTH(date_of_birth) = MONTH(%(today)s)
-			AND
-				`status` = 'Active'
-		""",
-		"postgres": """
-			SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
-			FROM "tabEmployee"
-			WHERE
-				DATE_PART('day', "date_of_birth") = date_part('day', %(today)s)
-			AND
-				DATE_PART('month', "date_of_birth") = date_part('month', %(today)s)
-			AND
-				"status" = 'Active'
-		""",
-	}, dict(today=today()), as_dict=1)
-
-	grouped_employees = defaultdict(lambda: [])
-
-	for employee_doc in employees_born_today:
-		grouped_employees[employee_doc.get('company')].append(employee_doc)
-
-	return grouped_employees
+	return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email")
 
 def get_holiday_list_for_employee(employee, raise_exception=True):
 	if employee:
@@ -390,17 +301,40 @@
 
 	return holiday_list
 
-def is_holiday(employee, date=None, raise_exception=True):
-	'''Returns True if given Employee has an holiday on the given date
-	:param employee: Employee `name`
-	:param date: Date to check. Will check for today if None'''
+def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False):
+	'''
+	Returns True if given Employee has an holiday on the given date
+		:param employee: Employee `name`
+		:param date: Date to check. Will check for today if None
+		:param raise_exception: Raise an exception if no holiday list found, default is True
+		:param only_non_weekly: Check only non-weekly holidays, default is False
+	'''
 
 	holiday_list = get_holiday_list_for_employee(employee, raise_exception)
 	if not date:
 		date = today()
 
-	if holiday_list:
-		return frappe.get_all('Holiday List', dict(name=holiday_list, holiday_date=date)) and True or False
+	if not holiday_list:
+		return False
+
+	filters = {
+		'parent': holiday_list,
+		'holiday_date': date
+	}
+	if only_non_weekly:
+		filters['weekly_off'] = False
+
+	holidays = frappe.get_all(
+		'Holiday',
+		fields=['description'],
+		filters=filters,
+		pluck='description'
+	)
+
+	if with_description:
+		return len(holidays) > 0, holidays
+
+	return len(holidays) > 0
 
 @frappe.whitelist()
 def deactivate_sales_person(status = None, employee = None):
@@ -503,7 +437,6 @@
 
 	return employees
 
-
 def on_doctype_update():
 	frappe.db.add_index("Employee", ["lft", "rgt"])
 
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
new file mode 100644
index 0000000..2155c02
--- /dev/null
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -0,0 +1,247 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import comma_sep, getdate, today, add_months, add_days
+from erpnext.hr.doctype.employee.employee import get_all_employee_emails, get_employee_email
+from erpnext.hr.utils import get_holidays_for_employee
+
+# -----------------
+# HOLIDAY REMINDERS
+# -----------------
+def send_reminders_in_advance_weekly():
+	to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
+	frequency = frappe.db.get_single_value("HR Settings", "frequency")
+	if not (to_send_in_advance and frequency == "Weekly"):
+		return
+
+	send_advance_holiday_reminders("Weekly")
+
+def send_reminders_in_advance_monthly():
+	to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
+	frequency = frappe.db.get_single_value("HR Settings", "frequency")
+	if not (to_send_in_advance and frequency == "Monthly"):
+		return
+
+	send_advance_holiday_reminders("Monthly")
+
+def send_advance_holiday_reminders(frequency):
+	"""Send Holiday Reminders in Advance to Employees
+	`frequency` (str): 'Weekly' or 'Monthly'
+	"""
+	if frequency == "Weekly":
+		start_date = getdate()
+		end_date = add_days(getdate(), 7)
+	elif frequency == "Monthly":
+		# Sent on 1st of every month
+		start_date = getdate()
+		end_date = add_months(getdate(), 1)
+	else:
+		return
+
+	employees = frappe.db.get_all('Employee', pluck='name')
+	for employee in employees:
+		holidays = get_holidays_for_employee(
+			employee,
+			start_date, end_date,
+			only_non_weekly=True,
+			raise_exception=False
+		)
+
+		if not (holidays is None):
+			send_holidays_reminder_in_advance(employee, holidays)
+
+def send_holidays_reminder_in_advance(employee, holidays):
+	employee_doc = frappe.get_doc('Employee', employee)
+	employee_email = get_employee_email(employee_doc)
+	frequency = frappe.db.get_single_value("HR Settings", "frequency")
+
+	email_header = _("Holidays this Month.") if frequency == "Monthly" else _("Holidays this Week.")
+	frappe.sendmail(
+		recipients=[employee_email],
+		subject=_("Upcoming Holidays Reminder"),
+		template="holiday_reminder",
+		args=dict(
+			reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')),
+			message=_("Below is the list of upcoming holidays for you:"),
+			advance_holiday_reminder=True,
+			holidays=holidays,
+			frequency=frequency[:-2]
+		),
+		header=email_header
+	)
+
+# ------------------
+# BIRTHDAY REMINDERS
+# ------------------
+def send_birthday_reminders():
+	"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
+	to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1)
+	if not to_send:
+		return
+
+	employees_born_today = get_employees_who_are_born_today()
+
+	for company, birthday_persons in employees_born_today.items():
+		employee_emails = get_all_employee_emails(company)
+		birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons]
+		recipients = list(set(employee_emails) - set(birthday_person_emails))
+
+		reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons)
+		send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
+
+		if len(birthday_persons) > 1:
+			# special email for people sharing birthdays
+			for person in birthday_persons:
+				person_email = person["user_id"] or person["personal_email"] or person["company_email"]
+				others = [d for d in birthday_persons if d != person]
+				reminder_text, message = get_birthday_reminder_text_and_message(others)
+				send_birthday_reminder(person_email, reminder_text, others, message)
+
+def get_birthday_reminder_text_and_message(birthday_persons):
+	if len(birthday_persons) == 1:
+		birthday_person_text = birthday_persons[0]['name']
+	else:
+		# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
+		person_names = [d['name'] for d in birthday_persons]
+		birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False)
+
+	reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
+	message = _("A friendly reminder of an important date for our team.")
+	message += "<br>"
+	message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text)
+
+	return reminder_text, message
+
+def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
+	frappe.sendmail(
+		recipients=recipients,
+		subject=_("Birthday Reminder"),
+		template="birthday_reminder",
+		args=dict(
+			reminder_text=reminder_text,
+			birthday_persons=birthday_persons,
+			message=message,
+		),
+		header=_("Birthday Reminder 🎂")
+	)
+
+def get_employees_who_are_born_today():
+	"""Get all employee born today & group them based on their company"""
+	return get_employees_having_an_event_today("birthday")
+
+def get_employees_having_an_event_today(event_type):
+	"""Get all employee who have `event_type` today
+	& group them based on their company. `event_type`
+	can be `birthday` or `work_anniversary`"""
+
+	from collections import defaultdict
+
+	# Set column based on event type
+	if event_type == 'birthday':
+		condition_column = 'date_of_birth'
+	elif event_type == 'work_anniversary':
+		condition_column = 'date_of_joining'
+	else:
+		return
+
+	employees_born_today = frappe.db.multisql({
+		"mariadb": f"""
+			SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining`
+			FROM `tabEmployee`
+			WHERE
+				DAY({condition_column}) = DAY(%(today)s)
+			AND
+				MONTH({condition_column}) = MONTH(%(today)s)
+			AND
+				`status` = 'Active'
+		""",
+		"postgres": f"""
+			SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
+			FROM "tabEmployee"
+			WHERE
+				DATE_PART('day', {condition_column}) = date_part('day', %(today)s)
+			AND
+				DATE_PART('month', {condition_column}) = date_part('month', %(today)s)
+			AND
+				"status" = 'Active'
+		""",
+	}, dict(today=today(), condition_column=condition_column), as_dict=1)
+
+	grouped_employees = defaultdict(lambda: [])
+
+	for employee_doc in employees_born_today:
+		grouped_employees[employee_doc.get('company')].append(employee_doc)
+
+	return grouped_employees
+
+
+# --------------------------
+# WORK ANNIVERSARY REMINDERS
+# --------------------------
+def send_work_anniversary_reminders():
+	"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
+	to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1)
+	if not to_send:
+		return
+
+	employees_joined_today = get_employees_having_an_event_today("work_anniversary")
+
+	for company, anniversary_persons in employees_joined_today.items():
+		employee_emails = get_all_employee_emails(company)
+		anniversary_person_emails = [get_employee_email(doc) for doc in anniversary_persons]
+		recipients = list(set(employee_emails) - set(anniversary_person_emails))
+
+		reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons)
+		send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message)
+
+		if len(anniversary_persons) > 1:
+			# email for people sharing work anniversaries
+			for person in anniversary_persons:
+				person_email = person["user_id"] or person["personal_email"] or person["company_email"]
+				others = [d for d in anniversary_persons if d != person]
+				reminder_text, message = get_work_anniversary_reminder_text_and_message(others)
+				send_work_anniversary_reminder(person_email, reminder_text, others, message)
+
+def get_work_anniversary_reminder_text_and_message(anniversary_persons):
+	if len(anniversary_persons) == 1:
+		anniversary_person = anniversary_persons[0]['name']
+		persons_name = anniversary_person
+		# Number of years completed at the company
+		completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
+		anniversary_person += f" completed {completed_years} years"
+	else:
+		person_names_with_years = []
+		names = []
+		for person in anniversary_persons:
+			person_text = person['name']
+			names.append(person_text)
+			# Number of years completed at the company
+			completed_years = getdate().year - person['date_of_joining'].year
+			person_text += f" completed {completed_years} years"
+			person_names_with_years.append(person_text)
+
+		# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
+		anniversary_person = comma_sep(person_names_with_years, frappe._("{0} & {1}"), False)
+		persons_name = comma_sep(names, frappe._("{0} & {1}"), False)
+
+	reminder_text = _("Today {0} at our Company! 🎉").format(anniversary_person)
+	message = _("A friendly reminder of an important date for our team.")
+	message += "<br>"
+	message += _("Everyone, let’s congratulate {0} on their work anniversary!").format(persons_name)
+
+	return reminder_text, message
+
+def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
+	frappe.sendmail(
+		recipients=recipients,
+		subject=_("Work Anniversary Reminder"),
+		template="anniversary_reminder",
+		args=dict(
+			reminder_text=reminder_text,
+			anniversary_persons=anniversary_persons,
+			message=message,
+		),
+		header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️")
+	)
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 8fc7cf1..5feb6de 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -1,7 +1,5 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
 
 import frappe
 import erpnext
@@ -12,29 +10,6 @@
 test_records = frappe.get_test_records('Employee')
 
 class TestEmployee(unittest.TestCase):
-	def test_birthday_reminders(self):
-		employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
-		employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
-		employee.company_email = "test@example.com"
-		employee.company = "_Test Company"
-		employee.save()
-
-		from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders
-
-		employees_born_today = get_employees_who_are_born_today()
-		self.assertTrue(employees_born_today.get("_Test Company"))
-
-		frappe.db.sql("delete from `tabEmail Queue`")
-
-		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
-		hr_settings.stop_birthday_reminders = 0
-		hr_settings.save()
-
-		send_birthday_reminders()
-
-		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
-		self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
-
 	def test_employee_status_left(self):
 		employee1 = make_employee("test_employee_1@company.com")
 		employee2 = make_employee("test_employee_2@company.com")
diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py
new file mode 100644
index 0000000..7e560f5
--- /dev/null
+++ b/erpnext/hr/doctype/employee/test_employee_reminders.py
@@ -0,0 +1,173 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+import unittest
+
+from frappe.utils import getdate
+from datetime import timedelta
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change
+
+
+class TestEmployeeReminders(unittest.TestCase):
+	@classmethod
+	def setUpClass(cls):
+		from erpnext.hr.doctype.holiday_list.test_holiday_list import make_holiday_list
+
+		# Create a test holiday list
+		test_holiday_dates = cls.get_test_holiday_dates()
+		test_holiday_list = make_holiday_list(
+			'TestHolidayRemindersList', 
+			holiday_dates=[
+				{'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'},
+				{'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'},
+				{'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1},
+				{'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'},
+				{'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'},
+				{'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'},
+			],
+			from_date=getdate()-timedelta(days=10),
+			to_date=getdate()+timedelta(weeks=5)
+		)
+
+		# Create a test employee
+		test_employee = frappe.get_doc(
+			'Employee',
+			make_employee('test@gopher.io', company="_Test Company")
+		)
+
+		# Attach the holiday list to employee
+		test_employee.holiday_list = test_holiday_list.name
+		test_employee.save()
+
+		# Attach to class
+		cls.test_employee = test_employee
+		cls.test_holiday_dates = test_holiday_dates
+
+	@classmethod
+	def get_test_holiday_dates(cls):
+		today_date = getdate()
+		return [
+			today_date, 
+			today_date-timedelta(days=4), 
+			today_date-timedelta(days=3),
+			today_date+timedelta(days=1),
+			today_date+timedelta(days=3),
+			today_date+timedelta(weeks=3)
+		]
+
+	def setUp(self):
+		# Clear Email Queue
+		frappe.db.sql("delete from `tabEmail Queue`")
+
+	def test_is_holiday(self):
+		from erpnext.hr.doctype.employee.employee import is_holiday
+		
+		self.assertTrue(is_holiday(self.test_employee.name))
+		self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1]))
+		self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1)))
+
+		# Test weekly_off holidays
+		self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2]))
+		self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True))
+
+		# Test with descriptions
+		has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True)
+		self.assertTrue(has_holiday)
+		self.assertTrue('test holiday1' in descriptions)
+
+	def test_birthday_reminders(self):
+		employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
+		employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
+		employee.company_email = "test@example.com"
+		employee.company = "_Test Company"
+		employee.save()
+
+		from erpnext.hr.doctype.employee.employee_reminders import get_employees_who_are_born_today, send_birthday_reminders
+
+		employees_born_today = get_employees_who_are_born_today()
+		self.assertTrue(employees_born_today.get("_Test Company"))
+
+		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
+		hr_settings.send_birthday_reminders = 1
+		hr_settings.save()
+
+		send_birthday_reminders()
+
+		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+		self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
+
+	def test_work_anniversary_reminders(self):
+		employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
+		employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:]
+		employee.company_email = "test@example.com"
+		employee.company = "_Test Company"
+		employee.save()
+
+		from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today, send_work_anniversary_reminders
+
+		employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
+		self.assertTrue(employees_having_work_anniversary.get("_Test Company"))
+
+		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
+		hr_settings.send_work_anniversary_reminders = 1
+		hr_settings.save()
+
+		send_work_anniversary_reminders()
+
+		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+		self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
+	
+	def test_send_holidays_reminder_in_advance(self):
+		from erpnext.hr.utils import get_holidays_for_employee
+		from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
+
+		# Get HR settings and enable advance holiday reminders
+		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
+		hr_settings.send_holiday_reminders = 1
+		set_proceed_with_frequency_change()
+		hr_settings.frequency = 'Weekly'
+		hr_settings.save()
+
+		holidays = get_holidays_for_employee(
+					self.test_employee.get('name'),
+					getdate(), getdate() + timedelta(days=3),
+					only_non_weekly=True, 
+					raise_exception=False
+				)
+		
+		send_holidays_reminder_in_advance(
+			self.test_employee.get('name'),
+			holidays
+		)
+
+		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+		self.assertEqual(len(email_queue), 1)
+
+	def test_advance_holiday_reminders_monthly(self):
+		from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
+		# Get HR settings and enable advance holiday reminders
+		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
+		hr_settings.send_holiday_reminders = 1
+		set_proceed_with_frequency_change()
+		hr_settings.frequency = 'Monthly'
+		hr_settings.save()
+
+		send_reminders_in_advance_monthly()
+
+		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+		self.assertTrue(len(email_queue) > 0)
+	
+	def test_advance_holiday_reminders_weekly(self):
+		from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
+		# Get HR settings and enable advance holiday reminders
+		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
+		hr_settings.send_holiday_reminders = 1
+		hr_settings.frequency = 'Weekly'
+		hr_settings.save()
+
+		send_reminders_in_advance_weekly()
+
+		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+		self.assertTrue(len(email_queue) > 0)
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 2396a8e..8aa3c0c 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -11,8 +11,14 @@
   "emp_created_by",
   "column_break_4",
   "standard_working_hours",
-  "stop_birthday_reminders",
   "expense_approver_mandatory_in_expense_claim",
+  "reminders_section",
+  "send_birthday_reminders",
+  "column_break_9",
+  "send_work_anniversary_reminders",
+  "column_break_11",
+  "send_holiday_reminders",
+  "frequency",
   "leave_settings",
   "send_leave_notification",
   "leave_approval_notification_template",
@@ -51,13 +57,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "default": "0",
-   "description": "Don't send employee birthday reminders",
-   "fieldname": "stop_birthday_reminders",
-   "fieldtype": "Check",
-   "label": "Stop Birthday Reminders"
-  },
-  {
    "default": "1",
    "fieldname": "expense_approver_mandatory_in_expense_claim",
    "fieldtype": "Check",
@@ -142,13 +141,53 @@
    "fieldname": "standard_working_hours",
    "fieldtype": "Int",
    "label": "Standard Working Hours"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "reminders_section",
+   "fieldtype": "Section Break",
+   "label": "Reminders"
+  },
+  {
+   "default": "1",
+   "fieldname": "send_holiday_reminders",
+   "fieldtype": "Check",
+   "label": "Holidays"
+  },
+  {
+   "default": "1",
+   "fieldname": "send_work_anniversary_reminders",
+   "fieldtype": "Check",
+   "label": "Work Anniversaries "
+  },
+  {
+   "default": "Weekly",
+   "depends_on": "eval:doc.send_holiday_reminders",
+   "fieldname": "frequency",
+   "fieldtype": "Select",
+   "label": "Set the frequency for holiday reminders",
+   "options": "Weekly\nMonthly"
+  },
+  {
+   "default": "1",
+   "fieldname": "send_birthday_reminders",
+   "fieldtype": "Check",
+   "label": "Birthdays"
+  },
+  {
+   "fieldname": "column_break_9",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "column_break_11",
+   "fieldtype": "Column Break"
   }
  ],
  "icon": "fa fa-cog",
  "idx": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-05-11 10:52:56.192773",
+ "modified": "2021-08-24 14:54:12.834162",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "HR Settings",
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py
index c99df26..a474093 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.py
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.py
@@ -1,17 +1,79 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
 # For license information, please see license.txt
 
-from __future__ import unicode_literals
 import frappe
+
 from frappe.model.document import Document
+from frappe.utils import format_date
+
+# Wether to proceed with frequency change
+PROCEED_WITH_FREQUENCY_CHANGE = False
 
 class HRSettings(Document):
 	def validate(self):
 		self.set_naming_series()
 
+		# Based on proceed flag
+		global PROCEED_WITH_FREQUENCY_CHANGE
+		if not PROCEED_WITH_FREQUENCY_CHANGE:
+			self.validate_frequency_change()
+		PROCEED_WITH_FREQUENCY_CHANGE = False
+
 	def set_naming_series(self):
 		from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
 		set_by_naming_series("Employee", "employee_number",
 			self.get("emp_created_by")=="Naming Series", hide_name_field=True)
+
+	def validate_frequency_change(self):
+		weekly_job, monthly_job = None, None
+
+		try:
+			weekly_job = frappe.get_doc(
+				'Scheduled Job Type',
+				'employee_reminders.send_reminders_in_advance_weekly'
+			)
+
+			monthly_job = frappe.get_doc(
+				'Scheduled Job Type',
+				'employee_reminders.send_reminders_in_advance_monthly'
+			)
+		except frappe.DoesNotExistError:
+			return
+
+		next_weekly_trigger = weekly_job.get_next_execution()
+		next_monthly_trigger = monthly_job.get_next_execution()
+
+		if self.freq_changed_from_monthly_to_weekly():
+			if next_monthly_trigger < next_weekly_trigger:
+				self.show_freq_change_warning(next_monthly_trigger, next_weekly_trigger)
+
+		elif self.freq_changed_from_weekly_to_monthly():
+			if next_monthly_trigger > next_weekly_trigger:
+				self.show_freq_change_warning(next_weekly_trigger, next_monthly_trigger)
+
+	def freq_changed_from_weekly_to_monthly(self):
+		return self.has_value_changed("frequency") and self.frequency == "Monthly"
+
+	def freq_changed_from_monthly_to_weekly(self):
+		return self.has_value_changed("frequency") and self.frequency == "Weekly"
+
+	def show_freq_change_warning(self, from_date, to_date):
+		from_date = frappe.bold(format_date(from_date))
+		to_date = frappe.bold(format_date(to_date))
+		frappe.msgprint(
+			msg=frappe._('Employees will miss holiday reminders from {} until {}. <br> Do you want to proceed with this change?').format(from_date, to_date),
+			title='Confirm change in Frequency',
+			primary_action={
+				'label': frappe._('Yes, Proceed'),
+				'client_action': 'erpnext.proceed_save_with_reminders_frequency_change'
+			},
+			raise_exception=frappe.ValidationError
+		)
+
+@frappe.whitelist()
+def set_proceed_with_frequency_change():
+	'''Enables proceed with frequency change'''
+	global PROCEED_WITH_FREQUENCY_CHANGE
+	PROCEED_WITH_FREQUENCY_CHANGE = True
diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
index 674c8e3..9c765d7 100644
--- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py
+++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
@@ -10,7 +10,7 @@
 from frappe.utils.csvutils import UnicodeWriter
 from frappe.model.document import Document
 from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
-from erpnext.hr.utils import get_holidays_for_employee
+from erpnext.hr.utils import get_holiday_dates_for_employee
 
 class UploadAttendance(Document):
 	pass
@@ -94,7 +94,7 @@
 	holidays = {}
 	for employee in employees:
 		holiday_list = get_holiday_list_for_employee(employee)
-		holiday = get_holidays_for_employee(employee, getdate(from_date), getdate(to_date))
+		holiday = get_holiday_dates_for_employee(employee, getdate(from_date), getdate(to_date))
 		if holiday_list not in holidays:
 			holidays[holiday_list] = holiday
 
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index a1026ce..15b237d 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -335,21 +335,44 @@
 		total_given_benefit_amount = sum_of_given_benefit[0].total_amount
 	return total_given_benefit_amount
 
-def get_holidays_for_employee(employee, start_date, end_date):
-	holiday_list = get_holiday_list_for_employee(employee)
+def get_holiday_dates_for_employee(employee, start_date, end_date):
+	"""return a list of holiday dates for the given employee between start_date and end_date"""
+	# return only date	
+	holidays = get_holidays_for_employee(employee, start_date, end_date) 
+	
+	return [cstr(h.holiday_date) for h in holidays]
 
-	holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
-		where
-			parent=%(holiday_list)s
-			and holiday_date >= %(start_date)s
-			and holiday_date <= %(end_date)s''', {
-				"holiday_list": holiday_list,
-				"start_date": start_date,
-				"end_date": end_date
-			})
 
-	holidays = [cstr(i) for i in holidays]
+def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False):
+	"""Get Holidays for a given employee
 
+		`employee` (str)
+		`start_date` (str or datetime)
+		`end_date` (str or datetime)
+		`raise_exception` (bool)
+		`only_non_weekly` (bool)
+
+		return: list of dicts with `holiday_date` and `description` 
+	"""
+	holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception)
+
+	if not holiday_list:
+		return []
+
+	filters = {
+		'parent': holiday_list,
+		'holiday_date': ('between', [start_date, end_date])
+	}
+
+	if only_non_weekly:
+		filters['weekly_off'] = False
+
+	holidays = frappe.get_all(
+		'Holiday', 
+		fields=['description', 'holiday_date'],
+		filters=filters
+	)
+	
 	return holidays
 
 @erpnext.allow_regional
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b86c236..bf0446b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -275,6 +275,7 @@
 erpnext.patches.v13_0.germany_make_custom_fields
 erpnext.patches.v13_0.germany_fill_debtor_creditor_number
 erpnext.patches.v13_0.set_pos_closing_as_failed
+erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders
 execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
 erpnext.patches.v13_0.update_timesheet_changes
 erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
diff --git a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py
new file mode 100644
index 0000000..1787a56
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py
@@ -0,0 +1,23 @@
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+	frappe.reload_doc('hr', 'doctype', 'hr_settings')
+
+	try:
+		# Rename the field
+		rename_field('HR Settings', 'stop_birthday_reminders', 'send_birthday_reminders')
+		
+		# Reverse the value
+		old_value = frappe.db.get_single_value('HR Settings', 'send_birthday_reminders')
+
+		frappe.db.set_value(
+			'HR Settings', 
+			'HR Settings', 
+			'send_birthday_reminders', 
+			1 if old_value == 0 else 0
+		)
+		
+	except Exception as e:
+		if e.args[0] != 1054:
+			raise
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
index c7fbb06..a1cde08 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
@@ -9,7 +9,7 @@
 from frappe.model.document import Document
 from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor
 from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
-from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee
+from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holiday_dates_for_employee, get_previous_claimed_amount, validate_active_employee
 
 class EmployeeBenefitApplication(Document):
 	def validate(self):
@@ -139,7 +139,7 @@
 			# Then the sum multiply with the no of lwp in that period
 			# Include that amount to the prev_sal_slip_flexi_total to get the actual
 			if have_depends_on_payment_days and per_day_amount_total > 0:
-				holidays = get_holidays_for_employee(employee, payroll_period_obj.start_date, on_date)
+				holidays = get_holiday_dates_for_employee(employee, payroll_period_obj.start_date, on_date)
 				working_days = date_diff(on_date, payroll_period_obj.start_date) + 1
 				leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days)
 				leave_days_amount = leave_days * per_day_amount_total
diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py
index ef3a6cc..66dec07 100644
--- a/erpnext/payroll/doctype/payroll_period/payroll_period.py
+++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py
@@ -7,7 +7,7 @@
 from frappe import _
 from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
 from frappe.model.document import Document
-from erpnext.hr.utils import get_holidays_for_employee
+from erpnext.hr.utils import get_holiday_dates_for_employee
 
 class PayrollPeriod(Document):
 	def validate(self):
@@ -65,7 +65,7 @@
 		actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1
 		working_days = actual_no_of_days
 		if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")):
-			holidays = get_holidays_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2]))
+			holidays = get_holiday_dates_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2]))
 			working_days -= len(holidays)
 		return payroll_period[0][0], working_days, actual_no_of_days
 	return False, False, False
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 5f5fdd5..6325351 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -11,6 +11,7 @@
 from frappe import msgprint, _
 from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
 from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from erpnext.hr.utils import get_holiday_dates_for_employee
 from erpnext.utilities.transaction_base import TransactionBase
 from frappe.utils.background_jobs import enqueue
 from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
@@ -337,20 +338,7 @@
 		return payment_days
 
 	def get_holidays_for_employee(self, start_date, end_date):
-		holiday_list = get_holiday_list_for_employee(self.employee)
-		holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
-			where
-				parent=%(holiday_list)s
-				and holiday_date >= %(start_date)s
-				and holiday_date <= %(end_date)s''', {
-					"holiday_list": holiday_list,
-					"start_date": start_date,
-					"end_date": end_date
-				})
-
-		holidays = [cstr(i) for i in holidays]
-
-		return holidays
+		return get_holiday_dates_for_employee(self.employee, start_date, end_date)
 
 	def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
 		lwp = 0
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f240b8c..9caf1de 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -82,6 +82,17 @@
 			});
 			frappe.set_route('Form','Journal Entry', journal_entry.name);
 		});
+	},
+
+	proceed_save_with_reminders_frequency_change: () => {
+		frappe.ui.hide_open_dialog();
+		
+		frappe.call({
+			method: 'erpnext.hr.doctype.hr_settings.hr_settings.set_proceed_with_frequency_change', 
+			callback: () => {
+				cur_frm.save();
+			}
+		});
 	}
 });
 
diff --git a/erpnext/templates/emails/anniversary_reminder.html b/erpnext/templates/emails/anniversary_reminder.html
new file mode 100644
index 0000000..ac9f7e4
--- /dev/null
+++ b/erpnext/templates/emails/anniversary_reminder.html
@@ -0,0 +1,25 @@
+<div class="gray-container text-center">
+    <div>
+		{% for person in anniversary_persons %}
+			{% if person.image  %}
+			<img
+				class="avatar-frame standard-image"
+				src="{{ person.image }}"
+				style="{{ css_style or '' }}"
+				title="{{ person.name }}">
+			</span>
+			{% else %}
+			<span
+				class="avatar-frame standard-image"
+				style="{{ css_style or '' }}"
+				title="{{ person.name }}">
+				{{ frappe.utils.get_abbr(person.name) }}
+			</span>
+			{% endif %}
+		{% endfor %}
+	</div>
+    <div style="margin-top: 15px">
+		<span>{{ reminder_text }}</span>
+		<p class="text-muted">{{ message }}</p>
+	</div>
+</div>
\ No newline at end of file
diff --git a/erpnext/templates/emails/holiday_reminder.html b/erpnext/templates/emails/holiday_reminder.html
new file mode 100644
index 0000000..e38d27b
--- /dev/null
+++ b/erpnext/templates/emails/holiday_reminder.html
@@ -0,0 +1,16 @@
+<div>
+    <span>{{ reminder_text }}</span>
+    <p class="text-muted">{{ message }}</p>
+</div>
+
+{% if advance_holiday_reminder %}
+    {% if holidays | len > 0 %}
+    <ol>
+        {% for holiday in holidays %}
+            <li>{{ frappe.format(holiday.holiday_date, 'Date') }} - {{ holiday.description }}</li>
+        {% endfor %}
+    </ol>
+    {% else %}
+    <p>You don't have no upcoming holidays this {{ frequency }}.</p>
+    {% endif %}
+{% endif %}