From 6217df60b2135555b5f6f62da853c832e86368a4 Mon Sep 17 00:00:00 2001 From: vidhya Date: Thu, 24 Sep 2026 23:43:57 +0530 Subject: [PATCH] admin dashboard - changes --- .../data/education_base_menu.xml | 4 +- .../views/academic_year_views.xml | 10 +- .../__pycache__/main.cpython-312.pyc | Bin 5767 -> 8079 bytes .../education_dashboard/controllers/main.py | 183 +++-- .../static/src/js/education_dashboard.js | 305 ++++++--- .../src/xml/education_dashboard_templates.xml | 468 ++++++++----- .../views/dashboard_views.xml | 18 +- .../views/login_templates.xml | 2 +- addons/theme_student_erp_custom/__init__.py | 1 + .../theme_student_erp_custom/__manifest__.py | 30 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 151 bytes .../static/src/js/sidebar.js | 50 ++ .../static/src/scss/backend_forms.scss | 645 ++++++++++++++++++ .../static/src/scss/frontend_portal.scss | 56 ++ .../static/src/scss/primary_variables.scss | 18 + .../static/src/xml/sidebar.xml | 94 +++ 16 files changed, 1564 insertions(+), 320 deletions(-) create mode 100644 addons/theme_student_erp_custom/__init__.py create mode 100644 addons/theme_student_erp_custom/__manifest__.py create mode 100644 addons/theme_student_erp_custom/__pycache__/__init__.cpython-312.pyc create mode 100644 addons/theme_student_erp_custom/static/src/js/sidebar.js create mode 100644 addons/theme_student_erp_custom/static/src/scss/backend_forms.scss create mode 100644 addons/theme_student_erp_custom/static/src/scss/frontend_portal.scss create mode 100644 addons/theme_student_erp_custom/static/src/scss/primary_variables.scss create mode 100644 addons/theme_student_erp_custom/static/src/xml/sidebar.xml diff --git a/addons/education_base/data/education_base_menu.xml b/addons/education_base/data/education_base_menu.xml index 4009cd0..fc1a52b 100644 --- a/addons/education_base/data/education_base_menu.xml +++ b/addons/education_base/data/education_base_menu.xml @@ -1,6 +1,6 @@ - - + + diff --git a/addons/education_base/views/academic_year_views.xml b/addons/education_base/views/academic_year_views.xml index 4a7d3c7..2c8cc60 100644 --- a/addons/education_base/views/academic_year_views.xml +++ b/addons/education_base/views/academic_year_views.xml @@ -4,12 +4,18 @@ education.academic.year.tree education.academic.year - + - + diff --git a/addons/education_dashboard/controllers/__pycache__/main.cpython-312.pyc b/addons/education_dashboard/controllers/__pycache__/main.cpython-312.pyc index 0791b0f425d4da70e0267a1bc6837fa14b4f5847..808b76d019bf82f8eeffecff71fda23ab801e156 100644 GIT binary patch literal 8079 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 diff --git a/addons/education_dashboard/controllers/main.py b/addons/education_dashboard/controllers/main.py index 0337102..3177f77 100644 --- a/addons/education_dashboard/controllers/main.py +++ b/addons/education_dashboard/controllers/main.py @@ -1,7 +1,8 @@ # pyrefly: ignore [missing-import] -from odoo import http +from odoo import http, fields # pyrefly: ignore [missing-import] from odoo.http import request +import datetime class EducationDashboardController(http.Controller): @@ -9,96 +10,152 @@ 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 - # Base Student Domain + + # ── Filters ────────────────────────────────────────────────────────── student_domain = [] - payment_domain = [('status', '=', 'posted')] - - # Apply Filters if campus_id: student_domain.append(('campus_id', '=', int(campus_id))) - payment_domain.append(('student_id.campus_id', '=', int(campus_id))) if program_id: student_domain.append(('program_id', '=', int(program_id))) - payment_domain.append(('student_id.program_id', '=', int(program_id))) if academic_year_id: student_domain.append(('academic_year_id', '=', int(academic_year_id))) - payment_domain.append(('student_id.academic_year_id', '=', int(academic_year_id))) if semester_id: student_domain.append(('semester_id', '=', int(semester_id))) - payment_domain.append(('student_id.semester_id', '=', int(semester_id))) - # Fetch Student Records + # ── Students ───────────────────────────────────────────────────────── students = env['education.student'].search(student_domain) - - # Executive KPIs - total_students = len(students) + 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')) - withdrawn = len(students.filtered(lambda s: s.status == 'withdrawn')) + admissions = len(students.filtered(lambda s: s.status == 'admitted')) + graduates = len(students.filtered(lambda s: s.status == 'graduated')) - # Finance KPIs - total_fees = sum(students.mapped('total_fees')) - collected_fees = sum(students.mapped('total_paid')) + # ── 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 KPIs + # ── 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([]) + 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 (using read_group for speed) - program_data = env['education.student'].read_group( - student_domain, ['program_id'], ['program_id'] - ) + # ── Charts: Program distribution ────────────────────────────────────── program_chart = [] - for gd in program_data: + 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'], - }) + program_chart.append({'name': gd['program_id'][1], 'count': gd['program_id_count']}) - # Charts: Campus Distribution - campus_data = env['education.student'].read_group( - student_domain, ['campus_id'], ['campus_id'] - ) + # ── Charts: Campus distribution ─────────────────────────────────────── campus_chart = [] - for gd in campus_data: + 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'], - }) + campus_chart.append({'name': gd['campus_id'][1], 'count': gd['campus_id_count']}) - # Master Data lists for Filters - 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([])] + # ── 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 + + # ── 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 { - 'total_students': total_students, + # student KPIs + 'total_students': total_students, 'active_students': active_students, - 'admissions': admissions, - 'graduates': graduates, - 'withdrawn': withdrawn, - - 'total_fees': total_fees, - 'collected_fees': collected_fees, + '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, - - 'program_chart': program_chart, - 'campus_chart': campus_chart, - - 'campuses': campuses, - 'programs': programs, - 'years': years, + '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, + # context + 'user_name': user_name, + 'today': today_str, + 'current_year_name': current_year_name, + # filter options + 'campuses': campuses, + 'programs': programs, + 'years': years, 'semesters': semesters, } diff --git a/addons/education_dashboard/static/src/js/education_dashboard.js b/addons/education_dashboard/static/src/js/education_dashboard.js index 355a60b..3cce0d8 100644 --- a/addons/education_dashboard/static/src/js/education_dashboard.js +++ b/addons/education_dashboard/static/src/js/education_dashboard.js @@ -6,40 +6,43 @@ import { useService } from "@web/core/utils/hooks"; import { loadJS } from "@web/core/assets"; import { rpc } from "@web/core/network/rpc"; +const CHART_COLORS = ['#1f8f8f', '#0d9488', '#0891b2', '#7c3aed', '#ea580c', '#64748b']; + class EducationDashboard extends Component { setup() { this.actionService = useService("action"); this.state = useState({ kpis: {}, - filters: { - campus_id: "", - program_id: "", - academic_year_id: "", - semester_id: "" - } + filters: { campus_id: "", program_id: "", academic_year_id: "", semester_id: "" } }); - - this.programChartRef = useRef("programChart"); - this.campusChartRef = useRef("campusChart"); + + this.enrollmentChartRef = useRef("enrollmentChart"); + this.distributionChartRef = useRef("distributionChart"); + this.feeDonutChartRef = useRef("feeDonutChart"); onWillStart(async () => { - await loadJS("/web/static/lib/Chart/Chart.js"); + // Try loading Chart.js from multiple known Odoo paths + const paths = [ + "/web/static/lib/Chart/Chart.js", + "/web/static/lib/chart.js/chart.js", + "/web/static/lib/chartjs/chart.js", + ]; + for (const p of paths) { + try { await loadJS(p); break; } catch (_) { /* try next */ } + } await this.loadData(); }); - onMounted(() => { - this.renderCharts(); - }); + onMounted(() => this.renderCharts()); } + // ── Data ────────────────────────────────────────────────────────────────── async loadData() { - // Map empty string filters to null const params = {}; for (const [k, v] of Object.entries(this.state.filters)) { params[k] = v ? parseInt(v) : null; } - const data = await rpc("/web/education/dashboard/kpis", params); - this.state.kpis = data; + this.state.kpis = await rpc("/web/education/dashboard/kpis", params); } async onFilterChange(filterKey, ev) { @@ -48,88 +51,226 @@ class EducationDashboard extends Component { this.renderCharts(); } - renderCharts() { - const pCtx = this.programChartRef.el; - if (pCtx) { - if (this.pChart) this.pChart.destroy(); - const labels = this.state.kpis.program_chart.map(x => x.name); - const data = this.state.kpis.program_chart.map(x => x.count); - this.pChart = new Chart(pCtx, { - type: 'bar', - data: { - labels: labels, - datasets: [{ - label: 'Students Count', - data: data, - backgroundColor: '#4F81BD' - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - beginAtZero: true, - ticks: { stepSize: 1 } - } - } - } - }); - } - - const cCtx = this.campusChartRef.el; - if (cCtx) { - if (this.cChart) this.cChart.destroy(); - const labels = this.state.kpis.campus_chart.map(x => x.name); - const data = this.state.kpis.campus_chart.map(x => x.count); - this.cChart = new Chart(cCtx, { - type: 'doughnut', - data: { - labels: labels, - datasets: [{ - data: data, - backgroundColor: ['#4F81BD', '#C0504D', '#9BBB59', '#8064A2', '#4BACC6'] - }] - }, - options: { - responsive: true, - maintainAspectRatio: false - } - }); - } + // ── Formatting helper ───────────────────────────────────────────────────── + fmt(amount) { + if (!amount) return "₹ 0"; + if (amount >= 10000000) return "₹ " + (amount / 10000000).toFixed(2) + " Cr"; + if (amount >= 100000) return "₹ " + (amount / 100000).toFixed(2) + " L"; + if (amount >= 1000) return "₹ " + (amount / 1000).toFixed(1) + " K"; + return "₹ " + amount; } + // ── Charts ──────────────────────────────────────────────────────────────── + renderCharts() { + if (typeof Chart === "undefined") { + console.warn("[EduDashboard] Chart.js not loaded — charts skipped."); + return; + } + this._renderEnrollment(); + this._renderDistribution(); + this._renderFeeDonut(); + } + + _renderEnrollment() { + const ctx = this.enrollmentChartRef.el; + if (!ctx) return; + if (this._enrollChart) this._enrollChart.destroy(); + + const trend = this.state.kpis.enrollment_trend || []; + const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + const labels = trend.length ? trend.map(x => x.month) : months; + const newD = trend.length ? trend.map(x => x.new) : Array(12).fill(0); + const totD = trend.length ? trend.map(x => x.total) : Array(12).fill(0); + + this._enrollChart = new Chart(ctx, { + type: "line", + data: { + labels, + datasets: [ + { + label: "New Admissions", + data: newD, + borderColor: "#0d9488", + backgroundColor: "rgba(13,148,136,0.1)", + borderWidth: 2, + fill: true, + tension: 0.4, + pointBackgroundColor: "#0d9488", + pointRadius: 3, + pointHoverRadius: 5, + }, + { + label: "Total Students", + data: totD, + borderColor: "#1f8f8f", + backgroundColor: "rgba(31,143,143,0.06)", + borderWidth: 2, + fill: true, + tension: 0.4, + pointBackgroundColor: "#1f8f8f", + pointRadius: 3, + pointHoverRadius: 5, + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: "#0f172a", + titleColor: "#fff", + bodyColor: "#94a3b8", + padding: 10, + cornerRadius: 8, + } + }, + scales: { + x: { grid: { display: false }, ticks: { color: "#94a3b8", font: { size: 11 } } }, + y: { + beginAtZero: true, + grid: { color: "#f1f5f9" }, + ticks: { color: "#94a3b8", font: { size: 11 }, stepSize: 1 } + } + } + } + }); + } + + _renderDistribution() { + const ctx = this.distributionChartRef.el; + if (!ctx) return; + if (this._distChart) this._distChart.destroy(); + + const progs = this.state.kpis.program_chart || []; + const labels = progs.map(p => p.name); + const data = progs.map(p => p.count); + + this._distChart = new Chart(ctx, { + type: "doughnut", + data: { + labels, + datasets: [{ + data: data.length ? data : [1], + backgroundColor: data.length ? CHART_COLORS.slice(0, data.length) : ["#e2e8f0"], + borderWidth: 0, + hoverOffset: 4, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + cutout: "60%", + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: "#0f172a", + titleColor: "#fff", + bodyColor: "#94a3b8", + padding: 10, + cornerRadius: 8, + } + } + } + }); + } + + _renderFeeDonut() { + const ctx = this.feeDonutChartRef.el; + if (!ctx) return; + if (this._feeChart) this._feeChart.destroy(); + + const pct = this.state.kpis.fee_percent || 0; + const rest = Math.max(0, 100 - pct); + + this._feeChart = new Chart(ctx, { + type: "doughnut", + data: { + datasets: [{ + data: [pct, rest], + backgroundColor: ["#1f8f8f", "#f1f5f9"], + borderWidth: 0, + }] + }, + options: { + responsive: false, + maintainAspectRatio: false, + cutout: "72%", + plugins: { legend: { display: false }, tooltip: { enabled: false } } + } + }); + } + + // ── Navigation actions ──────────────────────────────────────────────────── openStudents(status) { - const domain = []; - if (status) { - domain.push(['status', '=', status]); - } - if (this.state.filters.campus_id) { - domain.push(['campus_id', '=', parseInt(this.state.filters.campus_id)]); - } - if (this.state.filters.program_id) { - domain.push(['program_id', '=', parseInt(this.state.filters.program_id)]); - } - + const domain = status ? [["status", "=", status]] : []; + if (this.state.filters.campus_id) domain.push(["campus_id", "=", parseInt(this.state.filters.campus_id)]); + if (this.state.filters.program_id) domain.push(["program_id", "=", parseInt(this.state.filters.program_id)]); this.actionService.doAction({ - name: "Students List", - type: "ir.actions.act_window", + name: "Students", type: "ir.actions.act_window", res_model: "education.student", views: [[false, "list"], [false, "form"]], - domain: domain, - target: "current" + domain, target: "current" }); } openPrograms() { this.actionService.doAction({ - name: "Active Programs", - type: "ir.actions.act_window", + name: "Programs", type: "ir.actions.act_window", res_model: "education.program", views: [[false, "list"], [false, "form"]], target: "current" }); } + + openAddStudent() { + this.actionService.doAction({ + name: "New Student", type: "ir.actions.act_window", + res_model: "education.student", + views: [[false, "form"]], target: "current" + }); + } + + openAddFaculty() { + this.actionService.doAction({ + name: "Faculty", type: "ir.actions.act_window", + res_model: "hr.employee", + views: [[false, "list"], [false, "form"]], target: "current" + }); + } + + openCreateCourse() { + this.actionService.doAction({ + name: "Courses", type: "ir.actions.act_window", + res_model: "education.course", + views: [[false, "list"], [false, "form"]], target: "current" + }); + } + + openScheduleExam() { + this.actionService.doAction({ + name: "Examinations", type: "ir.actions.act_window", + res_model: "education.exam", + views: [[false, "list"], [false, "form"]], target: "current" + }); + } + + openAttendance() { + this.actionService.doAction({ + name: "Attendance", type: "ir.actions.act_window", + res_model: "education.attendance", + views: [[false, "list"], [false, "form"]], target: "current" + }); + } + + openFees() { + this.actionService.doAction({ + name: "Fee Payments", type: "ir.actions.act_window", + res_model: "education.fee.payment", + views: [[false, "list"], [false, "form"]], target: "current" + }); + } } EducationDashboard.template = "education_dashboard.DashboardMain"; diff --git a/addons/education_dashboard/static/src/xml/education_dashboard_templates.xml b/addons/education_dashboard/static/src/xml/education_dashboard_templates.xml index ffc32d6..db07d3b 100644 --- a/addons/education_dashboard/static/src/xml/education_dashboard_templates.xml +++ b/addons/education_dashboard/static/src/xml/education_dashboard_templates.xml @@ -1,172 +1,314 @@ - -
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ +
- -
-
-
-
-
-
-
Total Students
-

-
- -
-
-
-
-
-
-
-
-
-
Active Students
-

-
- -
-
-
-
-
-
-
-
-
-
Admissions
-

-
- -
-
-
-
-
-
-
-
-
-
Graduates
-

-
- -
-
-
-
-
+ - -
-
-
-
Total Fees Assigned
-

-
-
-
-
-
Collected Fees
-

-
-
-
-
-
Outstanding Fees
-

-
-
-
+ +
+
+
+
+
Admin Dashboard
+
Welcome back, !
+
Manage your institution efficiently with real-time insights.
+
+
+
+
+
+
+
+
+
+
- -
-
-
-
Program-wise Distribution
-
- -
-
-
-
-
-
Campus Distribution
-
- -
-
-
-
+ +
+
+
+
+
+
Total Students
+
+
+
+
+
+
+
Active Students
+
+
+
+
+
+
+
Programs
+
+
+
+
+
+
+
Departments
+
+
+
+
+
+
+
Fees Collected
+
+
+
- -
-
-
-
Programs
-

-
-
-
-
-
Departments
-

-
-
-
-
-
Campuses
-

-
-
-
-
-
Academic Years
-

-
-
-
+ +
+ + +
+
+
+ + Student Enrollment Trend +
+ This Academic Year +
+
+
+
+
New Admissions +
+
+
Total Students +
+
+
+ + +
+
+
+ + Student Distribution +
+ By Program +
+
+
+ +
+
+
+ +
+
+
+ +
No program data yet
+
+
+
+ + + +
+ + +
+ + +
+
+
+ + Upcoming Examinations +
+ View All +
+ + +
+
+
+
+
+
+
+
+ +
+
+
+ +
+ +
No upcoming exams scheduled
+
+
+
+ + +
+
+
+ + Fee Collection Overview +
+ This Academic Year +
+
+
+
+
Collected
+
+
+ +
+ +
+
+
+
+
+
+
+
+
Collected
+ +
+
+
Pending
+ +
+
+
Total
+ +
+
+
+ + +
+
+
+ + Announcements +
+ View All +
+
+ Important +
+
Exam Schedule Released
+
Mid-term schedule has been published.
+
+ +
+
+ General +
+
Library Hours Extended
+
Open till 8 PM this week.
+
+ +
+
+ Academic +
+
Course Registration Open
+
Elective course registration is now open.
+
+ +
+
+ Event +
+
Annual Tech Fest
+
Scheduled for next month.
+
+ +
+
+
+ + +
+
+
+ + Recent Activities +
+ View All +
+
+ + +
+
+
+ +
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+
+
+ +
+
diff --git a/addons/education_dashboard/views/dashboard_views.xml b/addons/education_dashboard/views/dashboard_views.xml index 8c8074a..4e69491 100644 --- a/addons/education_dashboard/views/dashboard_views.xml +++ b/addons/education_dashboard/views/dashboard_views.xml @@ -6,11 +6,15 @@ education_dashboard_main_tag - - + + + + + + + diff --git a/addons/education_portal/views/login_templates.xml b/addons/education_portal/views/login_templates.xml index 5469a00..cb0c898 100644 --- a/addons/education_portal/views/login_templates.xml +++ b/addons/education_portal/views/login_templates.xml @@ -306,7 +306,7 @@