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
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index 1a19716..fe033d4 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -104,6 +104,7 @@
student_attendance.date = date
student_attendance.status = status
student_attendance.save()
+ student_attendance.submit()
@frappe.whitelist()
@@ -363,9 +364,9 @@
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 6b545d9..e0d7514 100644
--- a/erpnext/education/doctype/student/student.py
+++ b/erpnext/education/doctype/student/student.py
@@ -25,7 +25,7 @@
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 @@
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 23e10e6..55384b9 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 06ac4fb..c1b6850 100644
--- a/erpnext/education/doctype/student_attendance/student_attendance.py
+++ b/erpnext/education/doctype/student_attendance/student_attendance.py
@@ -6,52 +6,63 @@
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 fe38b87..ad53976 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 410f0cc..c8841c9 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 @@
}, 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 0000000..fdcc147
--- /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 ddb30ac..e9b568a 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 @@
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 c0a7359..17bc367 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 @@
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 8e6ce51..4e57cc6 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 @@
if not filters.get("date"):
msgprint(_("Please select date"), raise_exception=1)
-
+
columns = get_columns(filters)
date = filters.get("date")
@@ -26,27 +26,27 @@
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 @@
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 646e3f7..c65d233 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 @@
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 @@
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 3f1d5b3..d820bfb 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 @@
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_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