feat: Consider Holiday List in Student Leave Application and Attendance (#23388)
* feat: Consider holiday list in Student Attendance and Leave Application
* feat: Show Holidays as 'H' in Student Monthly Attendance Sheet
* fix: check if date is a holiday in attendance reports
* test: skip attendance record creation for holidays
* fix: holiday list validation
* fix: clean up after test
* fix: codacy
* fix: show date in user format
* fix: remove ununsed imports
* fix: sider
* fix: test
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
diff --git a/erpnext/education/doctype/student_attendance/student_attendance.py b/erpnext/education/doctype/student_attendance/student_attendance.py
index 72a8f55..2e9e6cf 100644
--- a/erpnext/education/doctype/student_attendance/student_attendance.py
+++ b/erpnext/education/doctype/student_attendance/student_attendance.py
@@ -6,8 +6,10 @@
import frappe
from frappe.model.document import Document
from frappe import _
-from frappe.utils import get_link_to_form, getdate
+from frappe.utils import get_link_to_form, getdate, formatdate
+from erpnext import get_default_company
from erpnext.education.api import get_student_group_students
+from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
class StudentAttendance(Document):
def validate(self):
@@ -17,6 +19,7 @@
self.set_student_group()
self.validate_student()
self.validate_duplication()
+ self.validate_is_holiday()
def set_date(self):
if self.course_schedule:
@@ -78,3 +81,18 @@
record = get_link_to_form('Student Attendance', attendance_record)
frappe.throw(_('Student Attendance record {0} already exists against the Student {1}')
.format(record, frappe.bold(self.student)), title=_('Duplicate Entry'))
+
+ def validate_is_holiday(self):
+ holiday_list = get_holiday_list()
+ if is_holiday(holiday_list, self.date):
+ frappe.throw(_('Attendance cannot be marked for {0} as it is a holiday.').format(
+ frappe.bold(formatdate(self.date))))
+
+def get_holiday_list(company=None):
+ if not company:
+ company = get_default_company() or frappe.get_all('Company')[0].name
+
+ holiday_list = frappe.get_cached_value('Company', company, 'default_holiday_list')
+ if not holiday_list:
+ frappe.throw(_('Please set a default Holiday List for Company {0}').format(frappe.bold(get_default_company())))
+ return holiday_list
diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
index be26440..028db91 100644
--- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
+++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.py
@@ -20,10 +20,10 @@
student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] , \
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
- if not student_list:
- student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
+ if not student_list:
+ student_list = frappe.get_list("Student Group Student", fields=["student", "student_name", "group_roll_number"] ,
filters={"parent": student_group, "active": 1}, order_by= "group_roll_number")
-
+
if course_schedule:
student_attendance_list= frappe.db.sql('''select student, status from `tabStudent Attendance` where \
course_schedule= %s''', (course_schedule), as_dict=1)
@@ -32,7 +32,7 @@
student_group= %s and date= %s and \
(course_schedule is Null or course_schedule='')''',
(student_group, date), as_dict=1)
-
+
for attendance in student_attendance_list:
for student in student_list:
if student.student == attendance.student:
diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application.json b/erpnext/education/doctype/student_leave_application/student_leave_application.json
index ad53976..31b3da2 100644
--- a/erpnext/education/doctype/student_leave_application/student_leave_application.json
+++ b/erpnext/education/doctype/student_leave_application/student_leave_application.json
@@ -11,6 +11,7 @@
"column_break_3",
"from_date",
"to_date",
+ "total_leave_days",
"section_break_5",
"attendance_based_on",
"student_group",
@@ -110,11 +111,17 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "total_leave_days",
+ "fieldtype": "Float",
+ "label": "Total Leave Days",
+ "read_only": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-07-08 13:22:38.329002",
+ "modified": "2020-09-21 18:10:24.440669",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Leave Application",
diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application.py b/erpnext/education/doctype/student_leave_application/student_leave_application.py
index c8841c9..ef67012 100644
--- a/erpnext/education/doctype/student_leave_application/student_leave_application.py
+++ b/erpnext/education/doctype/student_leave_application/student_leave_application.py
@@ -6,11 +6,14 @@
import frappe
from frappe import _
from datetime import timedelta
-from frappe.utils import get_link_to_form, getdate
+from frappe.utils import get_link_to_form, getdate, date_diff, flt
+from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from frappe.model.document import Document
class StudentLeaveApplication(Document):
def validate(self):
+ self.validate_holiday_list()
self.validate_duplicate()
self.validate_from_to_dates('from_date', 'to_date')
@@ -39,10 +42,19 @@
frappe.throw(_('Leave application {0} already exists against the student {1}')
.format(link, frappe.bold(self.student)), title=_('Duplicate Entry'))
+ def validate_holiday_list(self):
+ holiday_list = get_holiday_list()
+ self.total_leave_days = get_number_of_leave_days(self.from_date, self.to_date, holiday_list)
+
def update_attendance(self):
+ holiday_list = get_holiday_list()
+
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime('%Y-%m-%d')
+ if is_holiday(holiday_list, date):
+ continue
+
attendance = frappe.db.exists('Student Attendance', {
'student': self.student,
'date': date,
@@ -89,3 +101,19 @@
def daterange(start_date, end_date):
for n in range(int ((end_date - start_date).days)+1):
yield start_date + timedelta(n)
+
+def get_number_of_leave_days(from_date, to_date, holiday_list):
+ number_of_days = date_diff(to_date, from_date) + 1
+
+ holidays = frappe.db.sql("""
+ SELECT
+ COUNT(DISTINCT holiday_date)
+ FROM `tabHoliday` h1,`tabHoliday List` h2
+ WHERE
+ h1.parent = h2.name and
+ h1.holiday_date between %s and %s and
+ h2.name = %s""", (from_date, to_date, holiday_list))[0][0]
+
+ number_of_days = flt(number_of_days) - flt(holidays)
+
+ return number_of_days
diff --git a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py
index e9b568a..fcdd428 100644
--- a/erpnext/education/doctype/student_leave_application/test_student_leave_application.py
+++ b/erpnext/education/doctype/student_leave_application/test_student_leave_application.py
@@ -5,13 +5,15 @@
import frappe
import unittest
-from frappe.utils import getdate, add_days
+from frappe.utils import getdate, add_days, add_months
+from erpnext import get_default_company
from erpnext.education.doctype.student_group.test_student_group import get_random_group
from erpnext.education.doctype.student.test_student import create_student
class TestStudentLeaveApplication(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabStudent Leave Application`""")
+ create_holiday_list()
def test_attendance_record_creation(self):
leave_application = create_leave_application()
@@ -35,20 +37,45 @@
attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus')
self.assertTrue(attendance_status, 2)
+ def test_holiday(self):
+ today = getdate()
+ leave_application = create_leave_application(from_date=today, to_date= add_days(today, 1), submit=0)
-def create_leave_application(from_date=None, to_date=None, mark_as_present=0):
+ # holiday list validation
+ company = get_default_company() or frappe.get_all('Company')[0].name
+ frappe.db.set_value('Company', company, 'default_holiday_list', '')
+ self.assertRaises(frappe.ValidationError, leave_application.save)
+
+ frappe.db.set_value('Company', company, 'default_holiday_list', 'Test Holiday List for Student')
+ leave_application.save()
+
+ leave_application.reload()
+ self.assertEqual(leave_application.total_leave_days, 1)
+
+ # check no attendance record created for a holiday
+ leave_application.submit()
+ self.assertIsNone(frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'date': add_days(today, 1)}))
+
+ def tearDown(self):
+ company = get_default_company() or frappe.get_all('Company')[0].name
+ frappe.db.set_value('Company', company, 'default_holiday_list', '_Test Holiday List')
+
+
+def create_leave_application(from_date=None, to_date=None, mark_as_present=0, submit=1):
student = get_student()
- leave_application = frappe.get_doc({
- 'doctype': 'Student Leave Application',
- 'student': student.name,
- 'attendance_based_on': 'Student Group',
- 'student_group': get_random_group().name,
- 'from_date': from_date if from_date else getdate(),
- 'to_date': from_date if from_date else getdate(),
- 'mark_as_present': mark_as_present
- }).insert()
- leave_application.submit()
+ leave_application = frappe.new_doc('Student Leave Application')
+ leave_application.student = student.name
+ leave_application.attendance_based_on = 'Student Group'
+ leave_application.student_group = get_random_group().name
+ leave_application.from_date = from_date if from_date else getdate()
+ leave_application.to_date = from_date if from_date else getdate()
+ leave_application.mark_as_present = mark_as_present
+
+ if submit:
+ leave_application.insert()
+ leave_application.submit()
+
return leave_application
def create_student_attendance(date=None, status=None):
@@ -67,4 +94,22 @@
email='test_student@gmail.com',
first_name='Test',
last_name='Student'
- ))
\ No newline at end of file
+ ))
+
+def create_holiday_list():
+ holiday_list = 'Test Holiday List for Student'
+ today = getdate()
+ if not frappe.db.exists('Holiday List', holiday_list):
+ frappe.get_doc(dict(
+ doctype = 'Holiday List',
+ holiday_list_name = holiday_list,
+ from_date = add_months(today, -6),
+ to_date = add_months(today, 6),
+ holidays = [
+ dict(holiday_date=add_days(today, 1), description = 'Test')
+ ]
+ )).insert()
+
+ company = get_default_company() or frappe.get_all('Company')[0].name
+ frappe.db.set_value('Company', company, 'default_holiday_list', holiday_list)
+ return holiday_list
\ No newline at end of file
diff --git a/erpnext/education/report/absent_student_report/absent_student_report.py b/erpnext/education/report/absent_student_report/absent_student_report.py
index 4e57cc6..c3487cc 100644
--- a/erpnext/education/report/absent_student_report/absent_student_report.py
+++ b/erpnext/education/report/absent_student_report/absent_student_report.py
@@ -3,8 +3,10 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import cstr, cint, getdate
+from frappe.utils import formatdate
from frappe import msgprint, _
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
+from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
def execute(filters=None):
if not filters: filters = {}
@@ -15,6 +17,11 @@
columns = get_columns(filters)
date = filters.get("date")
+ holiday_list = get_holiday_list()
+ if is_holiday(holiday_list, filters.get("date")):
+ msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date")))))
+
+
absent_students = get_absent_students(date)
leave_applicants = get_leave_applications(date)
if absent_students:
diff --git a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py
index c65d233..7793dcf 100644
--- a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py
+++ b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.py
@@ -3,8 +3,10 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import cstr, cint, getdate
+from frappe.utils import formatdate
from frappe import msgprint, _
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
+from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
def execute(filters=None):
if not filters: filters = {}
@@ -12,6 +14,10 @@
if not filters.get("date"):
msgprint(_("Please select date"), raise_exception=1)
+ holiday_list = get_holiday_list()
+ if is_holiday(holiday_list, filters.get("date")):
+ msgprint(_("No attendance has been marked for {0} as it is a Holiday").format(frappe.bold(formatdate(filters.get("date")))))
+
columns = get_columns(filters)
active_student_group = get_active_student_group()
diff --git a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py
index d820bfb..04dc8c0 100644
--- a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py
+++ b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.py
@@ -7,6 +7,8 @@
from frappe import msgprint, _
from calendar import monthrange
from erpnext.education.api import get_student_group_students
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
+from erpnext.support.doctype.issue.issue import get_holidays
def execute(filters=None):
if not filters: filters = {}
@@ -19,26 +21,32 @@
students_list = get_students_list(students)
att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list)
data = []
+
for stud in students:
row = [stud.student, stud.student_name]
student_status = frappe.db.get_value("Student", stud.student, "enabled")
date = from_date
total_p = total_a = 0.0
+
for day in range(total_days_in_month):
status="None"
+
if att_map.get(stud.student):
status = att_map.get(stud.student).get(date, "None")
elif not student_status:
status = "Inactive"
else:
status = "None"
- status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-"}
+
+ status_map = {"Present": "P", "Absent": "A", "None": "", "Inactive":"-", "Holiday":"H"}
row.append(status_map[status])
+
if status == "Present":
total_p += 1
elif status == "Absent":
total_a += 1
date = add_days(date, 1)
+
row += [total_p, total_a]
data.append(row)
return columns, data
@@ -63,14 +71,19 @@
and date between %s and %s
order by student, date''',
(student_group, from_date, to_date), as_dict=1)
+
att_map = {}
students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list)
for d in attendance_list:
att_map.setdefault(d.student, frappe._dict()).setdefault(d.date, "")
+
if students_with_leave_application.get(d.date) and d.student in students_with_leave_application.get(d.date):
att_map[d.student][d.date] = "Present"
else:
att_map[d.student][d.date] = d.status
+
+ att_map = mark_holidays(att_map, from_date, to_date, students_list)
+
return att_map
def get_students_with_leave_application(from_date, to_date, students_list):
@@ -108,3 +121,14 @@
if not year_list:
year_list = [getdate().year]
return "\n".join(str(year) for year in year_list)
+
+def mark_holidays(att_map, from_date, to_date, students_list):
+ holiday_list = get_holiday_list()
+ holidays = get_holidays(holiday_list)
+
+ for dt in daterange(getdate(from_date), getdate(to_date)):
+ if dt in holidays:
+ for student in students_list:
+ att_map.setdefault(student, frappe._dict()).setdefault(dt, "Holiday")
+
+ return att_map