# pyrefly: ignore [missing-import] from odoo import http, fields # pyrefly: ignore [missing-import] from odoo.http import request import datetime class EducationDashboardController(http.Controller): @http.route(['/web/education/dashboard/kpis'], type='json', auth="user") def get_dashboard_kpis(self, campus_id=None, program_id=None, academic_year_id=None, semester_id=None, env=None, **kw): env = env or request.env # ── Filters ────────────────────────────────────────────────────────── student_domain = [] if campus_id: student_domain.append(('campus_id', '=', int(campus_id))) if program_id: student_domain.append(('program_id', '=', int(program_id))) if academic_year_id: student_domain.append(('academic_year_id', '=', int(academic_year_id))) if semester_id: student_domain.append(('semester_id', '=', int(semester_id))) # ── Students ───────────────────────────────────────────────────────── students = env['education.student'].search(student_domain) total_students = len(students) active_students = len(students.filtered(lambda s: s.status == 'active')) admissions = len(students.filtered(lambda s: s.status == 'admitted')) graduates = len(students.filtered(lambda s: s.status == 'graduated')) # ── Finance ─────────────────────────────────────────────────────────── total_fees = sum(students.mapped('total_fees')) collected_fees = sum(students.mapped('total_paid')) outstanding_fees = sum(students.mapped('outstanding_amount')) fee_percent = int(collected_fees / total_fees * 100) if total_fees > 0 else 0 # ── Academic counts ─────────────────────────────────────────────────── programs_count = env['education.program'].search_count([]) dept_count = env['education.department'].search_count([]) campus_count = env['education.campus'].search_count([]) years_count = env['education.academic.year'].search_count([]) # ── Charts: Program distribution ────────────────────────────────────── program_chart = [] for gd in env['education.student'].read_group(student_domain, ['program_id'], ['program_id']): if gd.get('program_id'): program_chart.append({'name': gd['program_id'][1], 'count': gd['program_id_count']}) # ── Charts: Campus distribution ─────────────────────────────────────── campus_chart = [] for gd in env['education.student'].read_group(student_domain, ['campus_id'], ['campus_id']): if gd.get('campus_id'): campus_chart.append({'name': gd['campus_id'][1], 'count': gd['campus_id_count']}) # ── Monthly enrollment trend (current calendar year) ───────────────── month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] cur_year = datetime.date.today().year enrollment_trend = [] cumulative = 0 for m in range(1, 13): ny = cur_year if m < 12 else cur_year + 1 nm = m + 1 if m < 12 else 1 count = env['education.student'].search_count([ ('create_date', '>=', f'{cur_year}-{m:02d}-01'), ('create_date', '<', f'{ny}-{nm:02d}-01'), ]) cumulative += count enrollment_trend.append({ 'month': month_names[m - 1], 'new': count, 'total': cumulative if cumulative else total_students, }) # ── Upcoming exams ──────────────────────────────────────────────────── upcoming_exams = [] try: exams = env['education.exam'].search( [('state', 'not in', ['cancelled', 'completed', 'done'])], order='date asc', limit=4 ) for e in exams: upcoming_exams.append({ 'name': e.name, 'date': e.date.strftime('%b %d, %Y') if e.date else '', 'program': e.program_id.name if getattr(e, 'program_id', False) and e.program_id else '', }) except Exception: pass # ── Recent activities ───────────────────────────────────────────────── recent_activities = [] try: msgs = env['mail.message'].search([ ('model', 'in', ['education.student', 'education.exam', 'education.fee.payment']), ('message_type', 'in', ['email', 'comment']), ], order='date desc', limit=5) for msg in msgs: body = msg.subject or '' if not body and msg.body: import re body = re.sub('<[^>]+>', '', msg.body)[:60] recent_activities.append({ 'text': body or 'Activity recorded', 'time': msg.date.strftime('%I:%M %p') if msg.date else '', 'model': msg.model or '', }) except Exception: pass # ── Announcements ───────────────────────────────────────────────────── announcements_data = [] try: announcements = env['education.announcement'].search( [('active', '=', True)], order='date_published desc', limit=4 ) for ann in announcements: import re subtitle = re.sub('<[^>]+>', '', ann.content or '')[:60] type_colors = { 'notice': ('#fee2e2', '#b91c1c'), 'circular': ('#e0f2fe', '#0369a1'), 'event': ('#fef9c3', '#a16207') } bg_color, text_color = type_colors.get(ann.announcement_type, ('#d1fae5', '#065f46')) ann_type = dict(ann._fields['announcement_type'].selection).get(ann.announcement_type) if ann.announcement_type else 'Notice' announcements_data.append({ 'title': ann.title, 'subtitle': subtitle + ('...' if len(subtitle) == 60 else ''), 'type': ann_type, 'bg_color': bg_color, 'text_color': text_color, }) except Exception: pass # ── User / date context ─────────────────────────────────────────────── user_name = env.user.name today_str = datetime.date.today().strftime('%A, %b %d, %Y') current_year_name = '' try: cy = env['education.academic.year'].search([('current_year', '=', True)], limit=1) if cy: current_year_name = cy.name except Exception: pass # ── Filter dropdown data ────────────────────────────────────────────── campuses = [{'id': c.id, 'name': c.name} for c in env['education.campus'].search([])] programs = [{'id': p.id, 'name': p.name} for p in env['education.program'].search([])] years = [{'id': y.id, 'name': y.name} for y in env['education.academic.year'].search([])] semesters = [{'id': s.id, 'name': s.name} for s in env['education.semester'].search([])] return { # student KPIs 'total_students': total_students, 'active_students': active_students, 'admissions': admissions, 'graduates': graduates, # finance 'total_fees': total_fees, 'collected_fees': collected_fees, 'outstanding_fees': outstanding_fees, 'fee_percent': fee_percent, # academic 'programs_count': programs_count, 'dept_count': dept_count, 'campus_count': campus_count, 'years_count': years_count, # charts 'program_chart': program_chart, 'campus_chart': campus_chart, 'enrollment_trend': enrollment_trend, # operational 'upcoming_exams': upcoming_exams, 'recent_activities': recent_activities, 'announcements': announcements_data, # context 'user_name': user_name, 'today': today_str, 'current_year_name': current_year_name, # filter options 'campuses': campuses, 'programs': programs, 'years': years, 'semesters': semesters, }