Browse Source

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>
develop
Frappe PR Bot 3 years ago
committed by GitHub
parent
commit
255b99ebdc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      erpnext/hooks.py
  2. 4
      erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
  3. 127
      erpnext/hr/doctype/employee/employee.py
  4. 247
      erpnext/hr/doctype/employee/employee_reminders.py
  5. 25
      erpnext/hr/doctype/employee/test_employee.py
  6. 173
      erpnext/hr/doctype/employee/test_employee_reminders.py
  7. 57
      erpnext/hr/doctype/hr_settings/hr_settings.json
  8. 66
      erpnext/hr/doctype/hr_settings/hr_settings.py
  9. 4
      erpnext/hr/doctype/upload_attendance/upload_attendance.py
  10. 47
      erpnext/hr/utils.py
  11. 1
      erpnext/patches.txt
  12. 23
      erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py
  13. 4
      erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
  14. 4
      erpnext/payroll/doctype/payroll_period/payroll_period.py
  15. 16
      erpnext/payroll/doctype/salary_slip/salary_slip.py
  16. 11
      erpnext/public/js/utils.js
  17. 25
      erpnext/templates/emails/anniversary_reminder.html
  18. 16
      erpnext/templates/emails/holiday_reminder.html

9
erpnext/hooks.py

@ -355,7 +355,8 @@ scheduler_events = {
"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 @@ scheduler_events = {
"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"

4
erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py

@ -8,7 +8,7 @@ from frappe import _
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 @@ class CompensatoryLeaveRequest(Document):
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)))

127
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.model.naming import set_name_by_naming_series
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 @@ def update_user_permissions(doc, method):
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 @@ def get_holiday_list_for_employee(employee, raise_exception=True):
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 @@ def get_children(doctype, parent=None, company=None, is_root=False, is_tree=Fals
return employees
def on_doctype_update():
frappe.db.add_index("Employee", ["lft", "rgt"])

247
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 🎊️🎊️")
)

25
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 @@ from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
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")

173
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)

57
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",
@ -50,13 +56,6 @@
"fieldname": "column_break_4",
"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",
@ -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",

66
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

4
erpnext/hr/doctype/upload_attendance/upload_attendance.py

@ -10,7 +10,7 @@ from frappe import _
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 @@ def get_holidays_for_employees(employees, from_date, to_date):
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

47
erpnext/hr/utils.py

@ -335,21 +335,44 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
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

1
erpnext/patches.txt

@ -275,6 +275,7 @@ erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
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

23
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

4
erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py

@ -9,7 +9,7 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt
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 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
# 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

4
erpnext/payroll/doctype/payroll_period/payroll_period.py

@ -7,7 +7,7 @@ import frappe
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 @@ def get_payroll_period_days(start_date, end_date, employee, company=None):
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

16
erpnext/payroll/doctype/salary_slip/salary_slip.py

@ -11,6 +11,7 @@ from frappe.model.naming import make_autoname
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 @@ class SalarySlip(TransactionBase):
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

11
erpnext/public/js/utils.js

@ -82,6 +82,17 @@ $.extend(erpnext, {
});
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();
}
});
}
});

25
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>

16
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 %}
Loading…
Cancel
Save