206 lines
8.4 KiB
Python
206 lines
8.4 KiB
Python
# pyrefly: ignore [missing-import]
|
|
from odoo.tests.common import TransactionCase
|
|
# pyrefly: ignore [missing-import]
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class TestStudentAttendance(TransactionCase):
|
|
|
|
def setUp(self):
|
|
super(TestStudentAttendance, self).setUp()
|
|
|
|
self.company = self.env.ref('base.main_company')
|
|
|
|
# Setup master records
|
|
self.campus = self.env['education.campus'].create({
|
|
'name': 'Attendance Test Campus',
|
|
'code': 'ATC',
|
|
})
|
|
self.department = self.env['education.department'].create({
|
|
'name': 'Attendance Test Dept',
|
|
'code': 'ATD',
|
|
'campus_id': self.campus.id,
|
|
})
|
|
self.level = self.env['education.academic.level'].create({
|
|
'name': 'Attendance Test Level',
|
|
'code': 'ATL',
|
|
})
|
|
self.program = self.env['education.program'].create({
|
|
'name': 'Attendance Test Program',
|
|
'code': 'ATP',
|
|
'department_id': self.department.id,
|
|
'level_id': self.level.id,
|
|
})
|
|
self.year = self.env['education.academic.year'].create({
|
|
'name': 'Attendance Test Year 2026',
|
|
'code': 'ATY26',
|
|
'start_date': '2026-01-01',
|
|
'end_date': '2026-12-31',
|
|
})
|
|
self.term = self.env['education.academic.term'].create({
|
|
'name': 'Attendance Test Term 1',
|
|
'code': 'ATT1',
|
|
'academic_year_id': self.year.id,
|
|
'start_date': '2026-01-01',
|
|
'end_date': '2026-06-30',
|
|
})
|
|
self.batch = self.env['education.batch'].create({
|
|
'name': 'Attendance Test Batch 2026',
|
|
'code': 'ATB26',
|
|
})
|
|
self.semester = self.env['education.semester'].create({
|
|
'name': 'Attendance Test Semester 1',
|
|
'code': 'ATS1',
|
|
})
|
|
self.section = self.env['education.section'].create({
|
|
'name': 'Attendance Test Section A',
|
|
'code': 'ATSA',
|
|
})
|
|
|
|
# Create 3 active students
|
|
self.student_1 = self.env['education.student'].create({
|
|
'first_name': 'TestStudent',
|
|
'last_name': 'One',
|
|
'student_id': 'ATSTU01',
|
|
'admission_number': 'ATADM01',
|
|
'roll_number': 'ATRL01',
|
|
'campus_id': self.campus.id,
|
|
'department_id': self.department.id,
|
|
'program_id': self.program.id,
|
|
'level_id': self.level.id,
|
|
'academic_year_id': self.year.id,
|
|
'academic_term_id': self.term.id,
|
|
'batch_id': self.batch.id,
|
|
'semester_id': self.semester.id,
|
|
'section_id': self.section.id,
|
|
'status': 'active',
|
|
})
|
|
|
|
self.student_2 = self.env['education.student'].create({
|
|
'first_name': 'TestStudent',
|
|
'last_name': 'Two',
|
|
'student_id': 'ATSTU02',
|
|
'admission_number': 'ATADM02',
|
|
'roll_number': 'ATRL02',
|
|
'campus_id': self.campus.id,
|
|
'department_id': self.department.id,
|
|
'program_id': self.program.id,
|
|
'level_id': self.level.id,
|
|
'academic_year_id': self.year.id,
|
|
'academic_term_id': self.term.id,
|
|
'batch_id': self.batch.id,
|
|
'semester_id': self.semester.id,
|
|
'section_id': self.section.id,
|
|
'status': 'active',
|
|
})
|
|
|
|
self.student_3 = self.env['education.student'].create({
|
|
'first_name': 'TestStudent',
|
|
'last_name': 'Three',
|
|
'student_id': 'ATSTU03',
|
|
'admission_number': 'ATADM03',
|
|
'roll_number': 'ATRL03',
|
|
'campus_id': self.campus.id,
|
|
'department_id': self.department.id,
|
|
'program_id': self.program.id,
|
|
'level_id': self.level.id,
|
|
'academic_year_id': self.year.id,
|
|
'academic_term_id': self.term.id,
|
|
'batch_id': self.batch.id,
|
|
'semester_id': self.semester.id,
|
|
'section_id': self.section.id,
|
|
'status': 'active',
|
|
})
|
|
|
|
# Draft Session
|
|
self.session = self.env['education.attendance.session'].create({
|
|
'name': 'Test Lecture Period 1',
|
|
'campus_id': self.campus.id,
|
|
'department_id': self.department.id,
|
|
'program_id': self.program.id,
|
|
'academic_level_id': self.level.id,
|
|
'academic_year_id': self.year.id,
|
|
'academic_term_id': self.term.id,
|
|
'attendance_date': '2026-02-01',
|
|
'period': 'period_1',
|
|
})
|
|
|
|
def test_attendance_session_workflow(self):
|
|
"""Test workflow of opening session, marking attendance, completing it, and reverse logic"""
|
|
# Session should start in draft with no lines
|
|
self.assertEqual(self.session.state, 'draft')
|
|
self.assertEqual(len(self.session.line_ids), 0)
|
|
|
|
# Open session (should auto-generate lines for our 3 active students)
|
|
self.session.action_open()
|
|
self.assertEqual(self.session.state, 'open')
|
|
self.assertEqual(len(self.session.line_ids), 3)
|
|
|
|
# Confirm default state is 'present'
|
|
for line in self.session.line_ids:
|
|
self.assertEqual(line.status, 'present')
|
|
|
|
# Test bulk marking: set all to 'absent'
|
|
self.session.action_bulk_absent()
|
|
for line in self.session.line_ids:
|
|
self.assertEqual(line.status, 'absent')
|
|
|
|
# Let's set Student 1 to Present, Student 2 to Late, Student 3 to Medical
|
|
line_1 = self.session.line_ids.filtered(lambda l: l.student_id == self.student_1)
|
|
line_2 = self.session.line_ids.filtered(lambda l: l.student_id == self.student_2)
|
|
line_3 = self.session.line_ids.filtered(lambda l: l.student_id == self.student_3)
|
|
|
|
line_1.write({'status': 'present'})
|
|
line_2.write({'status': 'late'})
|
|
line_3.write({'status': 'medical_leave'})
|
|
|
|
# Complete Session (should write to permanent history education.attendance)
|
|
self.session.action_complete()
|
|
self.assertEqual(self.session.state, 'completed')
|
|
|
|
# Verify history matches
|
|
history_1 = self.env['education.attendance'].search([('student_id', '=', self.student_1.id), ('session_id', '=', self.session.id)])
|
|
history_2 = self.env['education.attendance'].search([('student_id', '=', self.student_2.id), ('session_id', '=', self.session.id)])
|
|
history_3 = self.env['education.attendance'].search([('student_id', '=', self.student_3.id), ('session_id', '=', self.session.id)])
|
|
|
|
self.assertEqual(len(history_1), 1)
|
|
self.assertEqual(history_1.status, 'present')
|
|
self.assertEqual(history_2.status, 'late')
|
|
self.assertEqual(history_3.status, 'medical_leave')
|
|
|
|
# Invalidate student record cache to read computed metrics
|
|
self.student_1.invalidate_recordset()
|
|
self.student_2.invalidate_recordset()
|
|
self.student_3.invalidate_recordset()
|
|
|
|
# Present, Late, Medical should all count as attended (100% attendance for this single session)
|
|
self.assertEqual(self.student_1.attendance_pct, 100.0)
|
|
self.assertEqual(self.student_2.attendance_pct, 100.0)
|
|
self.assertEqual(self.student_3.attendance_pct, 100.0)
|
|
|
|
self.assertEqual(self.student_1.present_count, 1)
|
|
self.assertEqual(self.student_2.late_count, 1)
|
|
self.assertEqual(self.student_3.medical_leave_count, 1)
|
|
|
|
# Reset to draft (should unlink lines and history)
|
|
self.session.action_draft()
|
|
self.assertEqual(self.session.state, 'draft')
|
|
self.assertEqual(len(self.session.line_ids), 0)
|
|
self.assertEqual(len(self.env['education.attendance'].search([('session_id', '=', self.session.id)])), 0)
|
|
|
|
def test_duplicate_session_constraint(self):
|
|
"""Test SQL constraints: cannot create duplicate session for same period and date"""
|
|
with self.assertRaises(Exception):
|
|
self.env['education.attendance.session'].create({
|
|
'name': 'Duplicate Session',
|
|
'campus_id': self.campus.id,
|
|
'department_id': self.department.id,
|
|
'program_id': self.program.id,
|
|
'academic_level_id': self.level.id,
|
|
'academic_year_id': self.year.id,
|
|
'academic_term_id': self.term.id,
|
|
'attendance_date': '2026-02-01',
|
|
'period': 'period_1',
|
|
})
|
|
self.env['education.attendance.session'].flush_model()
|