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:
parent
7915a3acae
commit
30d58cc3d7
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user