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>
This commit is contained in:
Rucha Mahabal 2020-11-09 18:41:03 +05:30 committed by GitHub
parent 7915a3acae
commit 30d58cc3d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 23 deletions

View File

@ -6,8 +6,10 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ 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.education.api import get_student_group_students
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
class StudentAttendance(Document): class StudentAttendance(Document):
def validate(self): def validate(self):
@ -17,6 +19,7 @@ class StudentAttendance(Document):
self.set_student_group() self.set_student_group()
self.validate_student() self.validate_student()
self.validate_duplication() self.validate_duplication()
self.validate_is_holiday()
def set_date(self): def set_date(self):
if self.course_schedule: if self.course_schedule:
@ -78,3 +81,18 @@ class StudentAttendance(Document):
record = get_link_to_form('Student Attendance', attendance_record) record = get_link_to_form('Student Attendance', attendance_record)
frappe.throw(_('Student Attendance record {0} already exists against the Student {1}') frappe.throw(_('Student Attendance record {0} already exists against the Student {1}')
.format(record, frappe.bold(self.student)), title=_('Duplicate Entry')) .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

View File

@ -11,6 +11,7 @@
"column_break_3", "column_break_3",
"from_date", "from_date",
"to_date", "to_date",
"total_leave_days",
"section_break_5", "section_break_5",
"attendance_based_on", "attendance_based_on",
"student_group", "student_group",
@ -110,11 +111,17 @@
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "total_leave_days",
"fieldtype": "Float",
"label": "Total Leave Days",
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-08 13:22:38.329002", "modified": "2020-09-21 18:10:24.440669",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Student Leave Application", "name": "Student Leave Application",

View File

@ -6,11 +6,14 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from datetime import timedelta 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 from frappe.model.document import Document
class StudentLeaveApplication(Document): class StudentLeaveApplication(Document):
def validate(self): def validate(self):
self.validate_holiday_list()
self.validate_duplicate() self.validate_duplicate()
self.validate_from_to_dates('from_date', 'to_date') self.validate_from_to_dates('from_date', 'to_date')
@ -39,10 +42,19 @@ class StudentLeaveApplication(Document):
frappe.throw(_('Leave application {0} already exists against the student {1}') frappe.throw(_('Leave application {0} already exists against the student {1}')
.format(link, frappe.bold(self.student)), title=_('Duplicate Entry')) .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): def update_attendance(self):
holiday_list = get_holiday_list()
for dt in daterange(getdate(self.from_date), getdate(self.to_date)): for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime('%Y-%m-%d') date = dt.strftime('%Y-%m-%d')
if is_holiday(holiday_list, date):
continue
attendance = frappe.db.exists('Student Attendance', { attendance = frappe.db.exists('Student Attendance', {
'student': self.student, 'student': self.student,
'date': date, 'date': date,
@ -89,3 +101,19 @@ class StudentLeaveApplication(Document):
def daterange(start_date, end_date): def daterange(start_date, end_date):
for n in range(int ((end_date - start_date).days)+1): for n in range(int ((end_date - start_date).days)+1):
yield start_date + timedelta(n) 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

View File

@ -5,13 +5,15 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest 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_group.test_student_group import get_random_group
from erpnext.education.doctype.student.test_student import create_student from erpnext.education.doctype.student.test_student import create_student
class TestStudentLeaveApplication(unittest.TestCase): class TestStudentLeaveApplication(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.db.sql("""delete from `tabStudent Leave Application`""") frappe.db.sql("""delete from `tabStudent Leave Application`""")
create_holiday_list()
def test_attendance_record_creation(self): def test_attendance_record_creation(self):
leave_application = create_leave_application() leave_application = create_leave_application()
@ -35,20 +37,45 @@ class TestStudentLeaveApplication(unittest.TestCase):
attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus') attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus')
self.assertTrue(attendance_status, 2) 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() student = get_student()
leave_application = frappe.get_doc({ leave_application = frappe.new_doc('Student Leave Application')
'doctype': 'Student Leave Application', leave_application.student = student.name
'student': student.name, leave_application.attendance_based_on = 'Student Group'
'attendance_based_on': 'Student Group', leave_application.student_group = get_random_group().name
'student_group': get_random_group().name, leave_application.from_date = from_date if from_date else getdate()
'from_date': from_date if from_date else getdate(), leave_application.to_date = from_date if from_date else getdate()
'to_date': from_date if from_date else getdate(), leave_application.mark_as_present = mark_as_present
'mark_as_present': mark_as_present
}).insert() if submit:
leave_application.submit() leave_application.insert()
leave_application.submit()
return leave_application return leave_application
def create_student_attendance(date=None, status=None): def create_student_attendance(date=None, status=None):
@ -68,3 +95,21 @@ def get_student():
first_name='Test', first_name='Test',
last_name='Student' last_name='Student'
)) ))
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

View File

@ -3,8 +3,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cstr, cint, getdate from frappe.utils import formatdate
from frappe import msgprint, _ 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): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
@ -15,6 +17,11 @@ def execute(filters=None):
columns = get_columns(filters) columns = get_columns(filters)
date = filters.get("date") 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) absent_students = get_absent_students(date)
leave_applicants = get_leave_applications(date) leave_applicants = get_leave_applications(date)
if absent_students: if absent_students:

View File

@ -3,8 +3,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cstr, cint, getdate from frappe.utils import formatdate
from frappe import msgprint, _ 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): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
@ -12,6 +14,10 @@ def execute(filters=None):
if not filters.get("date"): if not filters.get("date"):
msgprint(_("Please select date"), raise_exception=1) 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) columns = get_columns(filters)
active_student_group = get_active_student_group() active_student_group = get_active_student_group()

View File

@ -7,6 +7,8 @@ from frappe.utils import cstr, cint, getdate, get_first_day, get_last_day, date_
from frappe import msgprint, _ from frappe import msgprint, _
from calendar import monthrange from calendar import monthrange
from erpnext.education.api import get_student_group_students 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): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
@ -19,26 +21,32 @@ def execute(filters=None):
students_list = get_students_list(students) students_list = get_students_list(students)
att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list) att_map = get_attendance_list(from_date, to_date, filters.get("student_group"), students_list)
data = [] data = []
for stud in students: for stud in students:
row = [stud.student, stud.student_name] row = [stud.student, stud.student_name]
student_status = frappe.db.get_value("Student", stud.student, "enabled") student_status = frappe.db.get_value("Student", stud.student, "enabled")
date = from_date date = from_date
total_p = total_a = 0.0 total_p = total_a = 0.0
for day in range(total_days_in_month): for day in range(total_days_in_month):
status="None" status="None"
if att_map.get(stud.student): if att_map.get(stud.student):
status = att_map.get(stud.student).get(date, "None") status = att_map.get(stud.student).get(date, "None")
elif not student_status: elif not student_status:
status = "Inactive" status = "Inactive"
else: else:
status = "None" 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]) row.append(status_map[status])
if status == "Present": if status == "Present":
total_p += 1 total_p += 1
elif status == "Absent": elif status == "Absent":
total_a += 1 total_a += 1
date = add_days(date, 1) date = add_days(date, 1)
row += [total_p, total_a] row += [total_p, total_a]
data.append(row) data.append(row)
return columns, data return columns, data
@ -63,14 +71,19 @@ def get_attendance_list(from_date, to_date, student_group, students_list):
and date between %s and %s and date between %s and %s
order by student, date''', order by student, date''',
(student_group, from_date, to_date), as_dict=1) (student_group, from_date, to_date), as_dict=1)
att_map = {} att_map = {}
students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list) students_with_leave_application = get_students_with_leave_application(from_date, to_date, students_list)
for d in attendance_list: for d in attendance_list:
att_map.setdefault(d.student, frappe._dict()).setdefault(d.date, "") 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): 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" att_map[d.student][d.date] = "Present"
else: else:
att_map[d.student][d.date] = d.status att_map[d.student][d.date] = d.status
att_map = mark_holidays(att_map, from_date, to_date, students_list)
return att_map return att_map
def get_students_with_leave_application(from_date, to_date, students_list): def get_students_with_leave_application(from_date, to_date, students_list):
@ -108,3 +121,14 @@ def get_attendance_years():
if not year_list: if not year_list:
year_list = [getdate().year] year_list = [getdate().year]
return "\n".join(str(year) for year in year_list) 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