From 833682b03d02aef82f45825a3a0696c0c1681c75 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 23 Jul 2020 16:40:07 +0530 Subject: [PATCH] feat(Education): Student Attendance and Leave Enhancements (#22623) * feat: make Student Attendance doctype submittable * feat: add attendance related fields in Student Leave Application * feat: update Attendance records on Leave Application submission * refactor: better error messages and ORM queries * fix: show present only for leave applications with mark_as_present enabled in attendance reports * test: Student Leave Application * fix: filter for attendance records * fix: codacy issues --- erpnext/education/api.py | 5 +- erpnext/education/doctype/student/student.py | 4 +- .../student_attendance.json | 380 ++++---------- .../student_attendance/student_attendance.py | 63 ++- .../student_leave_application.json | 495 +++++------------- .../student_leave_application.py | 73 ++- .../student_leave_application_dashboard.py | 11 + .../test_student_leave_application.py | 64 ++- .../student_report_generation_tool.py | 2 +- .../absent_student_report.py | 38 +- .../student_batch_wise_attendance.py | 18 +- .../student_monthly_attendance_sheet.py | 13 +- 12 files changed, 468 insertions(+), 698 deletions(-) create mode 100644 erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py diff --git a/erpnext/education/api.py b/erpnext/education/api.py index 1a19716b50..fe033d4fc5 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -104,6 +104,7 @@ def make_attendance_records(student, student_name, status, course_schedule=None, student_attendance.date = date student_attendance.status = status student_attendance.save() + student_attendance.submit() @frappe.whitelist() @@ -363,9 +364,9 @@ def get_current_enrollment(student, academic_year=None): select name as program_enrollment, student_name, program, student_batch_name as student_batch, student_category, academic_term, academic_year - from + from `tabProgram Enrollment` - where + where student = %s and academic_year = %s order by creation''', (student, current_academic_year), as_dict=1) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 6b545d99be..e0d7514177 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -25,7 +25,7 @@ class Student(Document): for sibling in self.siblings: if sibling.date_of_birth and getdate(sibling.date_of_birth) > getdate(): frappe.throw(_("Row {0}:Sibling Date of Birth cannot be greater than today.").format(sibling.idx)) - + if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): frappe.throw(_("Date of Birth cannot be greater than today.")) @@ -157,5 +157,5 @@ def get_timeline_data(doctype, name): from `tabStudent Attendance` where student=%s and `date` > date_sub(curdate(), interval 1 year) - and status = 'Present' + and docstatus = 1 and status = 'Present' group by date''', name)) diff --git a/erpnext/education/doctype/student_attendance/student_attendance.json b/erpnext/education/doctype/student_attendance/student_attendance.json index 23e10e68c5..55384b9e53 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.json +++ b/erpnext/education/doctype/student_attendance/student_attendance.json @@ -1,287 +1,125 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2015-11-05 15:20:23.045996", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2015-11-05 15:20:23.045996", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "naming_series", + "student", + "student_name", + "course_schedule", + "student_group", + "column_break_3", + "date", + "status", + "leave_application", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Student", - "length": 0, - "no_copy": 0, - "options": "Student", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student", + "fieldtype": "Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Student", + "options": "Student", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "course_schedule", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Course Schedule", - "length": 0, - "no_copy": 0, - "options": "Course Schedule", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "course_schedule", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Course Schedule", + "options": "Course Schedule" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student.title", - "fieldname": "student_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_name", + "fieldtype": "Read Only", + "in_global_search": 1, + "label": "Student Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Student Group", - "length": 0, - "no_copy": 0, - "options": "Student Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_group", + "fieldtype": "Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Student Group", + "options": "Student Group" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Present", - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Present\nAbsent", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "Present", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Present\nAbsent", + "reqd": 1 + }, + { + "fieldname": "leave_application", + "fieldtype": "Link", + "label": "Leave Application", + "options": "Student Leave Application", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "EDU-ATT-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Student Attendance", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-07-27 10:48:22.301531", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Attendance", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-07-08 13:55:42.580181", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Attendance", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "student_name", - "track_changes": 0, - "track_seen": 0 + ], + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "student_name" } \ No newline at end of file diff --git a/erpnext/education/doctype/student_attendance/student_attendance.py b/erpnext/education/doctype/student_attendance/student_attendance.py index 06ac4fbc20..c1b6850c56 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.py +++ b/erpnext/education/doctype/student_attendance/student_attendance.py @@ -6,52 +6,63 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import cstr +from frappe.utils import get_link_to_form from erpnext.education.api import get_student_group_students class StudentAttendance(Document): def validate(self): - self.validate_date() self.validate_mandatory() - self.validate_course_schedule() + self.set_date() + self.set_student_group() self.validate_student() self.validate_duplication() - - def validate_date(self): + + def set_date(self): if self.course_schedule: - self.date = frappe.db.get_value("Course Schedule", self.course_schedule, "schedule_date") - + self.date = frappe.db.get_value('Course Schedule', self.course_schedule, 'schedule_date') + def validate_mandatory(self): if not (self.student_group or self.course_schedule): - frappe.throw(_("""Student Group or Course Schedule is mandatory""")) - - def validate_course_schedule(self): + frappe.throw(_('{0} or {1} is mandatory').format(frappe.bold('Student Group'), + frappe.bold('Course Schedule')), title=_('Mandatory Fields')) + + def set_student_group(self): if self.course_schedule: - self.student_group = frappe.db.get_value("Course Schedule", self.course_schedule, "student_group") - + self.student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group') + def validate_student(self): if self.course_schedule: - student_group = frappe.db.get_value("Course Schedule", self.course_schedule, "student_group") + student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group') else: student_group = self.student_group student_group_students = [d.student for d in get_student_group_students(student_group)] if student_group and self.student not in student_group_students: - frappe.throw(_('''Student {0}: {1} does not belong to Student Group {2}'''.format(self.student, self.student_name, student_group))) + student_group_doc = get_link_to_form('Student Group', student_group) + frappe.throw(_('Student {0}: {1} does not belong to Student Group {2}').format( + frappe.bold(self.student), self.student_name, frappe.bold(student_group_doc))) def validate_duplication(self): """Check if the Attendance Record is Unique""" - attendance_records=None + attendance_record = None if self.course_schedule: - attendance_records= frappe.db.sql("""select name from `tabStudent Attendance` where \ - student= %s and ifnull(course_schedule, '')= %s and name != %s""", - (self.student, cstr(self.course_schedule), self.name)) + attendance_record = frappe.db.exists('Student Attendance', { + 'student': self.student, + 'course_schedule': self.course_schedule, + 'docstatus': ('!=', 2), + 'name': ('!=', self.name) + }) else: - attendance_records= frappe.db.sql("""select name from `tabStudent Attendance` where \ - student= %s and student_group= %s and date= %s and name != %s and \ - (course_schedule is Null or course_schedule='')""", - (self.student, self.student_group, self.date, self.name)) - - if attendance_records: - frappe.throw(_("Attendance Record {0} exists against Student {1}") - .format(attendance_records[0][0], self.student)) + attendance_record = frappe.db.exists('Student Attendance', { + 'student': self.student, + 'student_group': self.student_group, + 'date': self.date, + 'docstatus': ('!=', 2), + 'name': ('!=', self.name), + 'course_schedule': '' + }) + + if attendance_record: + record = get_link_to_form('Attendance Record', attendance_record) + frappe.throw(_('Student Attendance record {0} already exists against the Student {1}') + .format(record, frappe.bold(self.student)), title=_('Duplicate Entry')) 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 fe38b87af3..ad5397629b 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application.json +++ b/erpnext/education/doctype/student_leave_application/student_leave_application.json @@ -1,375 +1,158 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "EDU-SLA-.YYYY.-.#####", - "beta": 0, - "creation": "2016-11-28 15:38:54.793854", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "EDU-SLA-.YYYY.-.#####", + "creation": "2016-11-28 15:38:54.793854", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "student", + "student_name", + "column_break_3", + "from_date", + "to_date", + "section_break_5", + "attendance_based_on", + "student_group", + "course_schedule", + "mark_as_present", + "column_break_11", + "reason", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student", - "length": 0, - "no_copy": 0, - "options": "Student", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student", + "fieldtype": "Link", + "in_global_search": 1, + "label": "Student", + "options": "Student", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "student.title", - "fieldname": "student_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Student Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "student.title", + "fieldname": "student_name", + "fieldtype": "Read Only", + "in_global_search": 1, + "label": "Student Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "From Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "To Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Will show the student as Present in Student Monthly Attendance Report", - "fieldname": "mark_as_present", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mark as Present", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "description": "Check this to mark the student as present in case the student is not attending the institute to participate or represent the institute in any event.\n\n", + "fieldname": "mark_as_present", + "fieldtype": "Check", + "label": "Mark as Present" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reason", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reason", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "reason", + "fieldtype": "Text", + "label": "Reason" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Student Leave Application", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Student Leave Application", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "default": "Student Group", + "fieldname": "attendance_based_on", + "fieldtype": "Select", + "label": "Attendance Based On", + "options": "Student Group\nCourse Schedule" + }, + { + "allow_in_quick_entry": 1, + "depends_on": "eval:doc.attendance_based_on === \"Student Group\";", + "fieldname": "student_group", + "fieldtype": "Link", + "label": "Student Group", + "mandatory_depends_on": "eval:doc.attendance_based_on === \"Student Group\";", + "options": "Student Group" + }, + { + "allow_in_quick_entry": 1, + "depends_on": "eval:doc.attendance_based_on === \"Course Schedule\";", + "fieldname": "course_schedule", + "fieldtype": "Link", + "label": "Course Schedule", + "mandatory_depends_on": "eval:doc.attendance_based_on === \"Course Schedule\";", + "options": "Course Schedule" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:50.807352", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Leave Application", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-07-08 13:22:38.329002", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Leave Application", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Instructor", - "set_user_permissions": 0, - "share": 0, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Instructor", + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "student_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "restrict_to_domain": "Education", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "student_name" } \ No newline at end of file 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 410f0cca3f..c8841c999a 100644 --- a/erpnext/education/doctype/student_leave_application/student_leave_application.py +++ b/erpnext/education/doctype/student_leave_application/student_leave_application.py @@ -5,17 +5,23 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import get_link_to_form +from datetime import timedelta +from frappe.utils import get_link_to_form, getdate from frappe.model.document import Document -from frappe import throw, _ class StudentLeaveApplication(Document): def validate(self): - self.validate_dates() self.validate_duplicate() + self.validate_from_to_dates('from_date', 'to_date') + + def on_submit(self): + self.update_attendance() + + def on_cancel(self): + self.cancel_attendance() def validate_duplicate(self): - data = frappe.db.sql(""" select name from `tabStudent Leave Application` + data = frappe.db.sql("""select name from `tabStudent Leave Application` where ((%(from_date)s > from_date and %(from_date)s < to_date) or (%(to_date)s > from_date and %(to_date)s < to_date) or @@ -29,10 +35,57 @@ class StudentLeaveApplication(Document): }, as_dict=1) if data: - link = get_link_to_form("Student Leave Application", data[0].name) - frappe.throw(_("Leave application {0} already exists against the student {1}") - .format(link, self.student)) + link = get_link_to_form('Student Leave Application', data[0].name) + frappe.throw(_('Leave application {0} already exists against the student {1}') + .format(link, frappe.bold(self.student)), title=_('Duplicate Entry')) - def validate_dates(self): - if self.to_date < self.from_date : - throw(_("To Date cannot be less than From Date")) \ No newline at end of file + def update_attendance(self): + for dt in daterange(getdate(self.from_date), getdate(self.to_date)): + date = dt.strftime('%Y-%m-%d') + + attendance = frappe.db.exists('Student Attendance', { + 'student': self.student, + 'date': date, + 'docstatus': ('!=', 2) + }) + + status = 'Present' if self.mark_as_present else 'Absent' + if attendance: + # update existing attendance record + values = dict() + values['status'] = status + values['leave_application'] = self.name + frappe.db.set_value('Student Attendance', attendance, values) + else: + # make a new attendance record + doc = frappe.new_doc('Student Attendance') + doc.student = self.student + doc.student_name = self.student_name + doc.date = date + doc.leave_application = self.name + doc.status = status + if self.attendance_based_on == 'Student Group': + doc.student_group = self.student_group + else: + doc.course_schedule = self.course_schedule + doc.insert(ignore_permissions=True, ignore_mandatory=True) + doc.submit() + + def cancel_attendance(self): + if self.docstatus == 2: + attendance = frappe.db.sql(""" + SELECT name + FROM `tabStudent Attendance` + WHERE + student = %s and + (date between %s and %s) and + docstatus < 2 + """, (self.student, self.from_date, self.to_date), as_dict=1) + + for name in attendance: + frappe.db.set_value('Student Attendance', name, 'docstatus', 2) + + +def daterange(start_date, end_date): + for n in range(int ((end_date - start_date).days)+1): + yield start_date + timedelta(n) diff --git a/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py b/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py new file mode 100644 index 0000000000..fdcc147479 --- /dev/null +++ b/erpnext/education/doctype/student_leave_application/student_leave_application_dashboard.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +def get_data(): + return { + 'fieldname': 'leave_application', + 'transactions': [ + { + 'items': ['Student Attendance'] + } + ] + } \ No newline at end of file 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 ddb30acb20..e9b568ad70 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,8 +5,66 @@ from __future__ import unicode_literals import frappe import unittest - -# test_records = frappe.get_test_records('Student Leave Application') +from frappe.utils import getdate, add_days +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): - pass + def setUp(self): + frappe.db.sql("""delete from `tabStudent Leave Application`""") + + def test_attendance_record_creation(self): + leave_application = create_leave_application() + attendance_record = frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'status': 'Absent'}) + self.assertTrue(attendance_record) + + # mark as present + date = add_days(getdate(), -1) + leave_application = create_leave_application(date, date, 1) + attendance_record = frappe.db.exists('Student Attendance', {'leave_application': leave_application.name, 'status': 'Present'}) + self.assertTrue(attendance_record) + + def test_attendance_record_updated(self): + attendance = create_student_attendance() + create_leave_application() + self.assertEqual(frappe.db.get_value('Student Attendance', attendance.name, 'status'), 'Absent') + + def test_attendance_record_cancellation(self): + leave_application = create_leave_application() + leave_application.cancel() + attendance_status = frappe.db.get_value('Student Attendance', {'leave_application': leave_application.name}, 'docstatus') + self.assertTrue(attendance_status, 2) + + +def create_leave_application(from_date=None, to_date=None, mark_as_present=0): + 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() + return leave_application + +def create_student_attendance(date=None, status=None): + student = get_student() + attendance = frappe.get_doc({ + 'doctype': 'Student Attendance', + 'student': student.name, + 'status': status if status else 'Present', + 'date': date if date else getdate(), + 'student_group': get_random_group().name + }).insert() + return attendance + +def get_student(): + return create_student(dict( + email='test_student@gmail.com', + first_name='Test', + last_name='Student' + )) \ No newline at end of file diff --git a/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py b/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py index c0a73596ac..17bc367826 100644 --- a/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py +++ b/erpnext/education/doctype/student_report_generation_tool/student_report_generation_tool.py @@ -80,7 +80,7 @@ def get_attendance_count(student, academic_year, academic_term=None): from_date, to_date = frappe.db.get_value("Academic Term", academic_term, ["term_start_date", "term_end_date"]) if from_date and to_date: attendance = dict(frappe.db.sql('''select status, count(student) as no_of_days - from `tabStudent Attendance` where student = %s + from `tabStudent Attendance` where student = %s and docstatus = 1 and date between %s and %s group by status''', (student, from_date, to_date))) if "Absent" not in attendance.keys(): 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 8e6ce5123f..4e57cc6c22 100644 --- a/erpnext/education/report/absent_student_report/absent_student_report.py +++ b/erpnext/education/report/absent_student_report/absent_student_report.py @@ -11,7 +11,7 @@ def execute(filters=None): if not filters.get("date"): msgprint(_("Please select date"), raise_exception=1) - + columns = get_columns(filters) date = filters.get("date") @@ -26,27 +26,27 @@ def execute(filters=None): if not student.student in leave_applicants: row = [student.student, student.student_name, student.student_group] stud_details = frappe.db.get_value("Student", student.student, ['student_email_id', 'student_mobile_number'], as_dict=True) - + if stud_details.student_email_id: row+=[stud_details.student_email_id] else: row+= [""] - + if stud_details.student_mobile_number: row+=[stud_details.student_mobile_number] else: row+= [""] if transportation_details.get(student.student): row += transportation_details.get(student.student) - + data.append(row) - + return columns, data def get_columns(filters): - columns = [ - _("Student") + ":Link/Student:90", - _("Student Name") + "::150", + columns = [ + _("Student") + ":Link/Student:90", + _("Student Name") + "::150", _("Student Group") + "::180", _("Student Email Address") + "::180", _("Student Mobile No.") + "::150", @@ -56,15 +56,29 @@ def get_columns(filters): return columns def get_absent_students(date): - absent_students = frappe.db.sql("""select student, student_name, student_group from `tabStudent Attendance` - where status="Absent" and date = %s order by student_group, student_name""", date, as_dict=1) + absent_students = frappe.db.sql(""" + SELECT student, student_name, student_group + FROM `tabStudent Attendance` + WHERE + status='Absent' and docstatus=1 and date = %s + ORDER BY + student_group, student_name""", + date, as_dict=1) return absent_students def get_leave_applications(date): leave_applicants = [] - for student in frappe.db.sql("""select student from `tabStudent Leave Application` - where docstatus = 1 and from_date <= %s and to_date >= %s""", (date, date)): + leave_applications = frappe.db.sql(""" + SELECT student + FROM + `tabStudent Leave Application` + WHERE + docstatus = 1 and mark_as_present = 1 and + from_date <= %s and to_date >= %s + """, (date, date)) + for student in leave_applications: leave_applicants.append(student[0]) + return leave_applicants def get_transportation_details(date, student_list): 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 646e3f7987..c65d233ccc 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 @@ -11,7 +11,7 @@ def execute(filters=None): if not filters.get("date"): msgprint(_("Please select date"), raise_exception=1) - + columns = get_columns(filters) active_student_group = get_active_student_group() @@ -37,28 +37,28 @@ def execute(filters=None): return columns, data def get_columns(filters): - columns = [ - _("Student Group") + ":Link/Student Group:250", - _("Student Group Strength") + "::170", - _("Present") + "::90", + columns = [ + _("Student Group") + ":Link/Student Group:250", + _("Student Group Strength") + "::170", + _("Present") + "::90", _("Absent") + "::90", _("Not Marked") + "::90" ] return columns def get_active_student_group(): - active_student_groups = frappe.db.sql("""select name from `tabStudent Group` where group_based_on = "Batch" + active_student_groups = frappe.db.sql("""select name from `tabStudent Group` where group_based_on = "Batch" and academic_year=%s order by name""", (frappe.defaults.get_defaults().academic_year), as_dict=1) return active_student_groups def get_student_group_strength(student_group): - student_group_strength = frappe.db.sql("""select count(*) from `tabStudent Group Student` + student_group_strength = frappe.db.sql("""select count(*) from `tabStudent Group Student` where parent = %s and active=1""", student_group)[0][0] return student_group_strength def get_student_attendance(student_group, date): - student_attendance = frappe.db.sql("""select count(*) as count, status from `tabStudent Attendance` where \ - student_group= %s and date= %s and\ + student_attendance = frappe.db.sql("""select count(*) as count, status from `tabStudent Attendance` where + student_group= %s and date= %s and docstatus = 1 and (course_schedule is Null or course_schedule='') group by status""", (student_group, date), as_dict=1) return student_attendance \ No newline at end of file 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 3f1d5b371b..d820bfbb21 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 @@ -57,8 +57,9 @@ def get_students_list(students): return student_list def get_attendance_list(from_date, to_date, student_group, students_list): - attendance_list = frappe.db.sql('''select student, date, status - from `tabStudent Attendance` where student_group = %s + attendance_list = frappe.db.sql('''select student, date, status + from `tabStudent Attendance` where student_group = %s + and docstatus = 1 and date between %s and %s order by student, date''', (student_group, from_date, to_date), as_dict=1) @@ -75,10 +76,10 @@ def get_attendance_list(from_date, to_date, student_group, students_list): def get_students_with_leave_application(from_date, to_date, students_list): if not students_list: return leave_applications = frappe.db.sql(""" - select student, from_date, to_date - from `tabStudent Leave Application` - where - mark_as_present and docstatus = 1 + select student, from_date, to_date + from `tabStudent Leave Application` + where + mark_as_present = 1 and docstatus = 1 and student in %(students)s and ( from_date between %(from_date)s and %(to_date)s