# 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()