import datetime from odoo import http from odoo.http import request # Inherit existing StudentPortalController to reuse _get_student() from odoo.addons.education_portal.controllers.main import StudentPortalController class TimetablePortalController(StudentPortalController): def _format_float_time(self, float_time): """Convert float time (e.g. 9.5) to formatted string (09:30 AM)""" hours = int(float_time) minutes = int(round((float_time - hours) * 60)) if minutes == 60: minutes = 0 hours += 1 # Handle 24-hour wrap around hours = hours % 24 am_pm = 'AM' if hours < 12 else 'PM' display_hours = hours % 12 if display_hours == 0: display_hours = 12 return f"{display_hours:02d}:{minutes:02d} {am_pm}" @http.route(['/my/timetable'], type='http', auth="user", website=True) def student_timetable(self, **kw): student = self._get_student() if not student: return request.redirect('/my/education/dashboard') # Determine eligibility exactly mapping to student's current academic placement program_id = student.program_id.id if student.program_id else False year_id = student.academic_year_id.id if student.academic_year_id else False term_id = student.academic_term_id.id if student.academic_term_id else False batch_id = student.batch_id.id if student.batch_id else False if not program_id or not year_id: # If a student isn't actively placed in a program/year, they have no timetable. return request.render("education_timetable.portal_student_timetable", { 'student': student, 'grouped_slots': {}, 'page_name': 'timetable', 'error': 'You must be assigned to an Academic Year and Program to view your timetable.' }) # Build Domain: The portal query must match the student's actual academic placement. # Respecting optional-field logic (established in Phase C): # - term_id = False means it applies to all terms. # - batch_id = False means it applies to all batches. domain = [ ('program_id', '=', program_id), ('academic_year_id', '=', year_id), ('active', '=', True) ] # We must use sudo() since portal users don't have backend employee groups. # The domain rigorously isolates records to ONLY those belonging to the student's exact academic profile. slot_model = request.env['education.timetable.slot'].sudo() # Handle nullable term_id if term_id: domain += ['|', ('term_id', '=', False), ('term_id', '=', term_id)] else: domain += [('term_id', '=', False)] # Handle nullable batch_id if batch_id: domain += ['|', ('batch_id', '=', False), ('batch_id', '=', batch_id)] else: domain += [('batch_id', '=', False)] # Optional: Filter by date if provided in query params filter_date = kw.get('date') if filter_date: try: # Ensure it's a valid date string parsed_date = datetime.datetime.strptime(filter_date, '%Y-%m-%d').date() domain += [('date', '=', parsed_date)] except ValueError: pass slots = slot_model.search(domain, order="date asc, start_time asc") # Group records by Date for clean UI display grouped_slots = {} for slot in slots: date_obj = slot.date if date_obj not in grouped_slots: grouped_slots[date_obj] = [] # Format time natively in Python so templates stay clean formatted_start = self._format_float_time(slot.start_time) formatted_end = self._format_float_time(slot.end_time) grouped_slots[date_obj].append({ 'id': slot.id, 'start_time': formatted_start, 'end_time': formatted_end, 'subject': slot.subject_id.name, 'faculty': slot.faculty_id.name, 'room': slot.room_id.name, 'batch': slot.batch_id.name if slot.batch_id else 'All Batches', 'term': slot.term_id.name if slot.term_id else 'All Terms', }) values = { 'student': student, 'grouped_slots': grouped_slots, 'page_name': 'timetable', } return request.render("education_timetable.portal_student_timetable", values)