@@ -38,7 +39,7 @@ frappe.ui.form.on('Twitter Settings', {
);
}
},
- login: function(frm){
+ login: function(frm) {
if (frm.doc.consumer_key && frm.doc.consumer_secret){
frappe.dom.freeze();
frappe.call({
@@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', {
});
}
},
- after_save: function(frm){
+ after_save: function(frm) {
frm.trigger("login");
}
});
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
index 36776e5c20..8d05877f06 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
@@ -2,6 +2,7 @@
"actions": [],
"creation": "2020-01-30 10:29:08.562108",
"doctype": "DocType",
+ "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -77,7 +78,7 @@
"image_field": "profile_pic",
"issingle": 1,
"links": [],
- "modified": "2020-05-13 17:50:47.934776",
+ "modified": "2021-02-18 15:18:07.900031",
"modified_by": "Administrator",
"module": "CRM",
"name": "Twitter Settings",
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
index 1e1beab2d2..47756560ec 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -32,7 +32,9 @@ class TwitterSettings(Document):
try:
auth.get_access_token(oauth_verifier)
- api = self.get_api(auth.access_token, auth.access_token_secret)
+ self.access_token = auth.access_token
+ self.access_token_secret = auth.access_token_secret
+ api = self.get_api()
user = api.me()
profile_pic = (user._json["profile_image_url"]).replace("_normal","")
@@ -50,11 +52,11 @@ class TwitterSettings(Document):
frappe.msgprint(_("Error! Failed to get access token."))
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
- def get_api(self, access_token, access_token_secret):
- # authentication of consumer key and secret
- auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
- # authentication of access token and secret
- auth.set_access_token(access_token, access_token_secret)
+ def get_api(self):
+ # authentication of consumer key and secret
+ auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
+ # authentication of access token and secret
+ auth.set_access_token(self.access_token, self.access_token_secret)
return tweepy.API(auth)
@@ -68,13 +70,13 @@ class TwitterSettings(Document):
def upload_image(self, media):
media = get_file_path(media)
- api = self.get_api(self.access_token, self.access_token_secret)
+ api = self.get_api()
media = api.media_upload(media)
return media.media_id
def send_tweet(self, text, media_id=None):
- api = self.get_api(self.access_token, self.access_token_secret)
+ api = self.get_api()
try:
if media_id:
response = api.update_status(status = text, media_ids = [media_id])
@@ -84,12 +86,32 @@ class TwitterSettings(Document):
return response
except TweepError as e:
- content = json.loads(e.response.content)
- content = content["errors"][0]
- if e.response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason))
+ self.api_error(e)
+
+ def delete_tweet(self, tweet_id):
+ api = self.get_api()
+ try:
+ api.destroy_status(tweet_id)
+ except TweepError as e:
+ self.api_error(e)
+
+ def get_tweet(self, tweet_id):
+ api = self.get_api()
+ try:
+ response = api.get_status(tweet_id, trim_user=True, include_entities=True)
+ except TweepError as e:
+ self.api_error(e)
+
+ return response._json
+
+ def api_error(self, e):
+ content = json.loads(e.response.content)
+ content = content["errors"][0]
+ if e.response.status_code == 401:
+ self.db_set("session_status", "Expired")
+ frappe.db.commit()
+ frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason))
+
@frappe.whitelist(allow_guest=True)
def callback(oauth_token = None, oauth_verifier = None):
From 255b99ebdc1ce3be6de02d9641fc72685de1a6a6 Mon Sep 17 00:00:00 2001
From: Frappe PR Bot
Date: Tue, 24 Aug 2021 20:19:22 +0530
Subject: [PATCH 22/52] 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
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>
---
erpnext/hooks.py | 9 +-
.../compensatory_leave_request.py | 4 +-
erpnext/hr/doctype/employee/employee.py | 127 +++------
.../hr/doctype/employee/employee_reminders.py | 247 ++++++++++++++++++
erpnext/hr/doctype/employee/test_employee.py | 25 --
.../employee/test_employee_reminders.py | 173 ++++++++++++
.../hr/doctype/hr_settings/hr_settings.json | 57 +++-
erpnext/hr/doctype/hr_settings/hr_settings.py | 66 ++++-
.../upload_attendance/upload_attendance.py | 4 +-
erpnext/hr/utils.py | 47 +++-
erpnext/patches.txt | 1 +
.../rename_stop_to_send_birthday_reminders.py | 23 ++
.../employee_benefit_application.py | 4 +-
.../doctype/payroll_period/payroll_period.py | 4 +-
.../doctype/salary_slip/salary_slip.py | 16 +-
erpnext/public/js/utils.js | 11 +
.../emails/anniversary_reminder.html | 25 ++
.../templates/emails/holiday_reminder.html | 16 ++
18 files changed, 691 insertions(+), 168 deletions(-)
create mode 100644 erpnext/hr/doctype/employee/employee_reminders.py
create mode 100644 erpnext/hr/doctype/employee/test_employee_reminders.py
create mode 100644 erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py
create mode 100644 erpnext/templates/emails/anniversary_reminder.html
create mode 100644 erpnext/templates/emails/holiday_reminder.html
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 74977cd8bc..4854bfd1e1 100644
--- a/erpnext/hooks.py
+++ b/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"
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 0d7fded921..3db81654a6 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 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)))
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index f4280152c5..643f3da2ff 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.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 += "
"
- 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"])
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
new file mode 100644
index 0000000000..2155c027a9
--- /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 += "
"
+ 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 += "
"
+ 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 8fc7cf1934..5feb6de8f2 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 @@ 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")
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 0000000000..7e560f512d
--- /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 2396a8eee9..8aa3c0ca9f 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",
@@ -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",
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py
index c99df269cc..a47409363c 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 {}.
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 674c8e3eb4..9c765d7371 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 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
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index a1026ce055..15b237d93c 100644
--- a/erpnext/hr/utils.py
+++ b/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
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b86c236a7f..bf0446bb68 100644
--- a/erpnext/patches.txt
+++ b/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
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 0000000000..1787a56025
--- /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 c7fbb06b10..a1cde08a74 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.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
diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py
index ef3a6cc006..66dec075d8 100644
--- a/erpnext/payroll/doctype/payroll_period/payroll_period.py
+++ b/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
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 5f5fdd5c83..6325351deb 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.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
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f240b8ca91..9caf1defe9 100755
--- a/erpnext/public/js/utils.js
+++ b/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();
+ }
+ });
}
});
diff --git a/erpnext/templates/emails/anniversary_reminder.html b/erpnext/templates/emails/anniversary_reminder.html
new file mode 100644
index 0000000000..ac9f7e4993
--- /dev/null
+++ b/erpnext/templates/emails/anniversary_reminder.html
@@ -0,0 +1,25 @@
+
+
+ {% for person in anniversary_persons %}
+ {% if person.image %}
+
+
+ {% else %}
+
+ {{ frappe.utils.get_abbr(person.name) }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
{{ reminder_text }}
+
{{ message }}
+
+
\ 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 0000000000..e38d27bf8b
--- /dev/null
+++ b/erpnext/templates/emails/holiday_reminder.html
@@ -0,0 +1,16 @@
+
+
{{ reminder_text }}
+
{{ message }}
+
+
+{% if advance_holiday_reminder %}
+ {% if holidays | len > 0 %}
+
+ {% for holiday in holidays %}
+ - {{ frappe.format(holiday.holiday_date, 'Date') }} - {{ holiday.description }}
+ {% endfor %}
+
+ {% else %}
+ You don't have no upcoming holidays this {{ frequency }}.
+ {% endif %}
+{% endif %}
From 7c957d72b362fbe9f74b5bee7409c7fb0fa33773 Mon Sep 17 00:00:00 2001
From: Pruthvi Patel
Date: Tue, 24 Aug 2021 21:24:25 +0530
Subject: [PATCH 23/52] perf: reduce number of queries to validate selling
price (#26225)
* perf: reduce number of queries to validate selling price
* fix: improved flow and formatting
* fix: improve condition and use of `as_dict`
Co-authored-by: Sagar Vora
---
erpnext/controllers/selling_controller.py | 112 ++++++++++++++++------
1 file changed, 84 insertions(+), 28 deletions(-)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index da2765dede..fc2cc97e0a 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime
-from frappe import _, throw
+from frappe import _, bold, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor
@@ -16,7 +16,6 @@ from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
-
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency,
self.grand_total)
@@ -169,39 +168,96 @@ class SellingController(StockController):
def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field):
- bold_net_rate = frappe.bold("net rate")
- msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""")
- .format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate)))
- msg += "
"
- msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""")
- .format(get_link_to_form("Selling Settings", "Selling Settings")))
- frappe.throw(msg, title=_("Invalid Selling Price"))
+ throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}.
+ Selling {3} should be atleast {4}.
Alternatively,
+ you can disable selling price validation in {5} to bypass
+ this validation.""").format(
+ idx,
+ bold(item_name),
+ bold(ref_rate_field),
+ bold("net rate"),
+ bold(rate),
+ get_link_to_form("Selling Settings", "Selling Settings"),
+ ), title=_("Invalid Selling Price"))
- if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
- return
- if hasattr(self, "is_return") and self.is_return:
+ if (
+ self.get("is_return")
+ or not frappe.db.get_single_value("Selling Settings", "validate_selling_price")
+ ):
return
- for it in self.get("items"):
- if not it.item_code:
+ is_internal_customer = self.get('is_internal_customer')
+ valuation_rate_map = {}
+
+ for item in self.items:
+ if not item.item_code:
continue
- last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
- last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1)
- if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
- throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate")
+ last_purchase_rate, is_stock_item = frappe.get_cached_value(
+ "Item", item.item_code, ("last_purchase_rate", "is_stock_item")
+ )
- last_valuation_rate = frappe.db.sql("""
- SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s
- AND warehouse = %s AND valuation_rate > 0
- ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1
- """, (it.item_code, it.warehouse))
- if last_valuation_rate:
- last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1)
- if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \
- and not self.get('is_internal_customer'):
- throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")
+ last_purchase_rate_in_sales_uom = (
+ last_purchase_rate * (item.conversion_factor or 1)
+ )
+ if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
+ throw_message(
+ item.idx,
+ item.item_name,
+ last_purchase_rate_in_sales_uom,
+ "last purchase rate"
+ )
+
+ if is_internal_customer or not is_stock_item:
+ continue
+
+ valuation_rate_map[(item.item_code, item.warehouse)] = None
+
+ if not valuation_rate_map:
+ return
+
+ or_conditions = (
+ f"""(item_code = {frappe.db.escape(valuation_rate[0])}
+ and warehouse = {frappe.db.escape(valuation_rate[1])})"""
+ for valuation_rate in valuation_rate_map
+ )
+
+ valuation_rates = frappe.db.sql(f"""
+ select
+ item_code, warehouse, valuation_rate
+ from
+ `tabBin`
+ where
+ ({" or ".join(or_conditions)})
+ and valuation_rate > 0
+ """, as_dict=True)
+
+ for rate in valuation_rates:
+ valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
+
+ for item in self.items:
+ if not item.item_code:
+ continue
+
+ last_valuation_rate = valuation_rate_map.get(
+ (item.item_code, item.warehouse)
+ )
+
+ if not last_valuation_rate:
+ continue
+
+ last_valuation_rate_in_sales_uom = (
+ last_valuation_rate * (item.conversion_factor or 1)
+ )
+
+ if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
+ throw_message(
+ item.idx,
+ item.item_name,
+ last_valuation_rate_in_sales_uom,
+ "valuation rate"
+ )
def get_item_list(self):
il = []
From c30fb04e960c944cda3ea84865f2563bc50b29ed Mon Sep 17 00:00:00 2001
From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Date: Tue, 24 Aug 2021 21:57:25 +0530
Subject: [PATCH 24/52] fix(minor): Update GSTR-1 json version (#27074)
---
erpnext/regional/report/gstr_1/gstr_1.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 4b7309440c..9d4f9206f5 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -588,7 +588,7 @@ def get_json(filters, report_name, data):
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
- gst_json = {"version": "GST2.2.9",
+ gst_json = {"version": "GST3.0.4",
"hash": "hash", "gstin": gstin, "fp": fp}
res = {}
@@ -765,7 +765,7 @@ def get_cdnr_reg_json(res, gstin):
"ntty": invoice[0]["document_type"],
"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
"rchrg": invoice[0]["reverse_charge"],
- "inv_type": get_invoice_type_for_cdnr(invoice[0])
+ "inv_typ": get_invoice_type_for_cdnr(invoice[0])
}
inv_item["itms"] = []
From 512ddc589f07cb9a938f0ef742db5a2e075ce55d Mon Sep 17 00:00:00 2001
From: Frappe PR Bot
Date: Tue, 24 Aug 2021 22:00:32 +0530
Subject: [PATCH 25/52] fix: timesheet amount issue (#25993) (#26889)
* fix: timesheet amount issue
* fix: timesheet detail rate conversion
* fix: condition to check timesheet currency
* fix: removing console statement
(cherry picked from commit a6aa6cd7d63eb426b986d995985985c2aae4f553)
Co-authored-by: Anupam Kumar
Co-authored-by: Nabin Hait
---
erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 2071827d99..d8dee99b99 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -443,6 +443,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
this.frm.refresh_field("outstanding_amount");
this.frm.refresh_field("paid_amount");
this.frm.refresh_field("base_paid_amount");
+ },
+
+ currency() {
+ this._super();
+ $.each(cur_frm.doc.timesheets, function(i, d) {
+ let row = frappe.get_doc(d.doctype, d.name)
+ set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
+ });
+ calculate_total_billing_amount(cur_frm)
}
currency() {
From 1d6ef4b0d3dd84ad84c64c602a919f6776c62eb4 Mon Sep 17 00:00:00 2001
From: Anupam Kumar
Date: Tue, 24 Aug 2021 22:06:19 +0530
Subject: [PATCH 26/52] fix: replacing $.each -> forEach in SI (#26995)
* fix: replacing $.each -> forEach in SI
* fix: removed console log
Co-authored-by: Nabin Hait
---
.../doctype/sales_invoice/sales_invoice.js | 23 +++++++++++--------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index d8dee99b99..1e37e4b207 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -154,9 +154,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
return
}
- $.each(doc["items"], function(i, row) {
+ doc.items.forEach((row) => {
if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note)
- })
+ });
}
set_default_print_format() {
@@ -455,12 +455,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
currency() {
+ var me = this;
super.currency();
- $.each(cur_frm.doc.timesheets, function(i, d) {
- let row = frappe.get_doc(d.doctype, d.name)
- set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
- });
- calculate_total_billing_amount(cur_frm)
+ if (this.frm.doc.timesheets) {
+ this.frm.doc.timesheets.forEach((d) => {
+ let row = frappe.get_doc(d.doctype, d.name)
+ set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
+ });
+ calculate_total_billing_amount(this.frm);
+ }
}
};
@@ -983,9 +986,9 @@ var calculate_total_billing_amount = function(frm) {
doc.total_billing_amount = 0.0
if (doc.timesheets) {
- $.each(doc.timesheets, function(index, data){
- doc.total_billing_amount += flt(data.billing_amount)
- })
+ doc.timesheets.forEach((d) => {
+ doc.total_billing_amount += flt(d.billing_amount)
+ });
}
refresh_field('total_billing_amount')
From c7bad657b1f73aa2d1232b416752128afc5f77e8 Mon Sep 17 00:00:00 2001
From: Dany Robert
Date: Tue, 24 Aug 2021 22:10:14 +0530
Subject: [PATCH 27/52] fix: broken URL in supplier portal (#26823)
* fix: broken URL
The quotations are supplier quotations, not sales quotation.
* fix: remove erpnext from path
---
erpnext/templates/includes/rfq/rfq_items.html | 2 +-
erpnext/templates/pages/rfq.html | 4 +---
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html
index caa15f386b..04cf922664 100644
--- a/erpnext/templates/includes/rfq/rfq_items.html
+++ b/erpnext/templates/includes/rfq/rfq_items.html
@@ -1,4 +1,4 @@
-{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %}
+{% from "templates/includes/rfq/rfq_macros.html" import item_name_and_description %}
{% for d in doc.items %}
diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html
index 6e2edb6391..6516482c23 100644
--- a/erpnext/templates/pages/rfq.html
+++ b/erpnext/templates/pages/rfq.html
@@ -86,7 +86,7 @@
{{d.transaction_date}}