diff --git a/erpnext/education/doctype/student_attendance/student_attendance.py b/erpnext/education/doctype/student_attendance/student_attendance.py index 72a8f55c66..2e9e6cf8d6 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.py +++ b/erpnext/education/doctype/student_attendance/student_attendance.py @@ -6,8 +6,10 @@ from __future__ import unicode_literals 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 @@ class StudentAttendance(Document): 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 @@ class StudentAttendance(Document): 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 be2644077a..028db91881 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 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour 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 @@ def get_student_attendance_records(based_on, date=None, student_group=None, cour 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 ad5397629b..31b3da2fbd 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 c8841c999a..ef670124c3 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 @@ from __future__ import unicode_literals 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 @@ class StudentLeaveApplication(Document): 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 @@ class StudentLeaveApplication(Document): 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 e9b568ad70..fcdd42825f 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 @@ from __future__ import unicode_literals 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 @@ class TestStudentLeaveApplication(unittest.TestCase): 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 @@ def get_student(): 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 4e57cc6c22..c3487ccaff 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 @@ def execute(filters=None): 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 c65d233ccc..7793dcf395 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 @@ def execute(filters=None): 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 d820bfbb21..04dc8c0e56 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.utils import cstr, cint, getdate, get_first_day, get_last_day, date_ 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 @@ def execute(filters=None): 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 @@ def get_attendance_list(from_date, to_date, student_group, students_list): 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 @@ def get_attendance_years(): 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