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.flush_all()