Merge unrelated histories from origin/main

Reconciles local main (diverged after origin remote was repointed to
a different repo URL) with origin/main's newer history. Content
conflicts resolved in favor of origin's version; dropped committed
.pyc bytecode and local session cookie files that origin's history
had tracked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-02 00:39:08 -04:00
commit 5b5fc5c955
60 changed files with 2023 additions and 188 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*.pyc
.DS_Store

View File

@ -20,9 +20,14 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Academic Level"> <form string="Academic Level">
<sheet> <sheet>
<div class="oe_title">
<label for="name" string="Level Name"/>
<h1>
<field name="name" placeholder="e.g. Undergraduate"/>
</h1>
</div>
<group> <group>
<group> <group>
<field name="name"/>
<field name="code"/> <field name="code"/>
<field name="sequence"/> <field name="sequence"/>
</group> </group>

View File

@ -15,7 +15,7 @@
<field name="state" widget="badge" <field name="state" widget="badge"
decoration-success="state == 'active'" decoration-success="state == 'active'"
decoration-danger="state == 'inactive'" decoration-danger="state == 'inactive'"
decoration-secondary="state in ('closed', 'draft')"/> decoration-info="state in ('closed', 'draft')"/>
<field name="current_year"/> <field name="current_year"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
</list> </list>
@ -31,7 +31,7 @@
<button name="action_activate" type="object" string="Activate" class="btn-primary" invisible="state != 'draft'"/> <button name="action_activate" type="object" string="Activate" class="btn-primary" invisible="state != 'draft'"/>
<button name="action_close" type="object" string="Close" invisible="state != 'active'"/> <button name="action_close" type="object" string="Close" invisible="state != 'active'"/>
<button name="action_set_draft" type="object" string="Set to Draft" invisible="state != 'closed'"/> <button name="action_set_draft" type="object" string="Set to Draft" invisible="state != 'closed'"/>
<field name="state" widget="statusbar"/> <field name="state" widget="statusbar" statusbar_visible="draft,active,closed"/>
</header> </header>
<sheet> <sheet>
<group> <group>

View File

@ -109,6 +109,33 @@ class EducationDashboardController(http.Controller):
except Exception: except Exception:
pass 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 / date context ───────────────────────────────────────────────
user_name = env.user.name user_name = env.user.name
today_str = datetime.date.today().strftime('%A, %b %d, %Y') today_str = datetime.date.today().strftime('%A, %b %d, %Y')
@ -149,6 +176,7 @@ class EducationDashboardController(http.Controller):
# operational # operational
'upcoming_exams': upcoming_exams, 'upcoming_exams': upcoming_exams,
'recent_activities': recent_activities, 'recent_activities': recent_activities,
'announcements': announcements_data,
# context # context
'user_name': user_name, 'user_name': user_name,
'today': today_str, 'today': today_str,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -224,6 +224,15 @@ class EducationDashboard extends Component {
}); });
} }
openDepartments() {
this.actionService.doAction({
name: "Departments", type: "ir.actions.act_window",
res_model: "education.department",
views: [[false, "list"], [false, "form"]],
target: "current"
});
}
openAddStudent() { openAddStudent() {
this.actionService.doAction({ this.actionService.doAction({
name: "New Student", type: "ir.actions.act_window", name: "New Student", type: "ir.actions.act_window",
@ -235,7 +244,7 @@ class EducationDashboard extends Component {
openAddFaculty() { openAddFaculty() {
this.actionService.doAction({ this.actionService.doAction({
name: "Faculty", type: "ir.actions.act_window", name: "Faculty", type: "ir.actions.act_window",
res_model: "hr.employee", res_model: "res.users",
views: [[false, "list"], [false, "form"]], target: "current" views: [[false, "list"], [false, "form"]], target: "current"
}); });
} }
@ -243,7 +252,7 @@ class EducationDashboard extends Component {
openCreateCourse() { openCreateCourse() {
this.actionService.doAction({ this.actionService.doAction({
name: "Courses", type: "ir.actions.act_window", name: "Courses", type: "ir.actions.act_window",
res_model: "education.course", res_model: "education.lms.course",
views: [[false, "list"], [false, "form"]], target: "current" views: [[false, "list"], [false, "form"]], target: "current"
}); });
} }
@ -267,7 +276,7 @@ class EducationDashboard extends Component {
openFees() { openFees() {
this.actionService.doAction({ this.actionService.doAction({
name: "Fee Payments", type: "ir.actions.act_window", name: "Fee Payments", type: "ir.actions.act_window",
res_model: "education.fee.payment", res_model: "education.student.payment",
views: [[false, "list"], [false, "form"]], target: "current" views: [[false, "list"], [false, "form"]], target: "current"
}); });
} }

View File

@ -20,7 +20,7 @@
</style> </style>
<!-- ── WELCOME BANNER ─────────────────────────────────────── --> <!-- ── WELCOME BANNER ─────────────────────────────────────── -->
<div style="background:linear-gradient(135deg,#1f8f8f 0%,#144d4d 55%,#0d3535 100%); border-radius:16px; padding:26px 32px; margin-bottom:20px; display:flex; align-items:center; justify-content:space-between; position:relative; overflow:hidden;"> <div style="background:linear-gradient(135deg,rgba(31,143,143,0.65) 0%,rgba(20,77,77,0.70) 55%,rgba(13,53,53,0.75) 100%), url('/education_dashboard/static/src/img/dashboard-bg.png'); background-size:cover; background-position:center 60%; border-radius:16px; padding:26px 32px; margin-bottom:20px; display:flex; align-items:center; justify-content:space-between; position:relative; overflow:hidden;">
<div style="position:absolute;top:-30px;right:200px;width:180px;height:180px;border-radius:50%;background:rgba(255,255,255,0.05);pointer-events:none;"></div> <div style="position:absolute;top:-30px;right:200px;width:180px;height:180px;border-radius:50%;background:rgba(255,255,255,0.05);pointer-events:none;"></div>
<div style="position:absolute;bottom:-40px;right:60px;width:240px;height:240px;border-radius:50%;background:rgba(255,255,255,0.04);pointer-events:none;"></div> <div style="position:absolute;bottom:-40px;right:60px;width:240px;height:240px;border-radius:50%;background:rgba(255,255,255,0.04);pointer-events:none;"></div>
<div style="position:relative;z-index:1;"> <div style="position:relative;z-index:1;">
@ -61,7 +61,7 @@
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Programs</div> <div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Programs</div>
</div> </div>
</div> </div>
<div class="edu-kpi"> <div class="edu-kpi" t-on-click="openDepartments">
<div class="edu-icon" style="background:#fff7ed;color:#ea580c;"><i class="fa fa-sitemap"/></div> <div class="edu-icon" style="background:#fff7ed;color:#ea580c;"><i class="fa fa-sitemap"/></div>
<div> <div>
<div style="font-size:1.55rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="state.kpis.dept_count || 0"/></div> <div style="font-size:1.55rem;font-weight:800;color:#0f172a;line-height:1.1;"><t t-esc="state.kpis.dept_count || 0"/></div>
@ -239,38 +239,24 @@
</div> </div>
<span style="font-size:0.72rem;color:#1f8f8f;font-weight:600;cursor:pointer;">View All</span> <span style="font-size:0.72rem;color:#1f8f8f;font-weight:600;cursor:pointer;">View All</span>
</div> </div>
<t t-if="(state.kpis.announcements || []).length">
<t t-foreach="state.kpis.announcements" t-as="ann" t-key="ann_index">
<div class="edu-ann-row"> <div class="edu-ann-row">
<span class="edu-pill" style="background:#fee2e2;color:#b91c1c;margin-top:1px;">Important</span> <span class="edu-pill" t-attf-style="background:{{ann.bg_color}};color:{{ann.text_color}};margin-top:1px;"><t t-esc="ann.type"/></span>
<div style="flex:1;min-width:0;"> <div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Exam Schedule Released</div> <div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;"><t t-esc="ann.title"/></div>
<div style="font-size:0.7rem;color:#94a3b8;">Mid-term schedule has been published.</div> <div style="font-size:0.7rem;color:#94a3b8;"><t t-esc="ann.subtitle"/></div>
</div> </div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/> <i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div> </div>
<div class="edu-ann-row"> </t>
<span class="edu-pill" style="background:#e0f2fe;color:#0369a1;margin-top:1px;">General</span> </t>
<div style="flex:1;min-width:0;"> <t t-else="">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Library Hours Extended</div> <div style="text-align:center;padding:28px 0;color:#94a3b8;">
<div style="font-size:0.7rem;color:#94a3b8;">Open till 8 PM this week.</div> <i class="fa fa-bullhorn" style="font-size:2rem;display:block;margin-bottom:8px;"/>
</div> <div style="font-size:0.8rem;">No recent announcements</div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div>
<div class="edu-ann-row">
<span class="edu-pill" style="background:#d1fae5;color:#065f46;margin-top:1px;">Academic</span>
<div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Course Registration Open</div>
<div style="font-size:0.7rem;color:#94a3b8;">Elective course registration is now open.</div>
</div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div>
<div class="edu-ann-row">
<span class="edu-pill" style="background:#fef9c3;color:#a16207;margin-top:1px;">Event</span>
<div style="flex:1;min-width:0;">
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Annual Tech Fest</div>
<div style="font-size:0.7rem;color:#94a3b8;">Scheduled for next month.</div>
</div>
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
</div> </div>
</t>
</div> </div>
</div> </div>

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from . import controllers
def post_init_hook(env):
"""
Hook to run after module installation.
Creates basic demo data if it doesn't exist.
"""
from .demo_data_generator import generate_demo_data
try:
generate_demo_data(env)
except Exception as e:
import logging
logging.getLogger(__name__).error(f"Failed to generate demo data: {e}")

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
{
'name': 'Education Demo Data',
'version': '19.0.1.0.0',
'summary': 'Demo Data & Secure Demo Login for Education ERP',
'author': 'Metatroncube',
'category': 'Education',
'depends': [
'education_base',
'education_student',
'education_lms',
'education_portal',
'education_exam',
'education_timetable'
],
'data': [
'data/demo_users.xml',
],
'demo': [],
'installable': True,
'application': False,
'auto_install': False,
'post_init_hook': 'post_init_hook',
}

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import demo_login

View File

@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# pyrefly: ignore [missing-import]
from odoo import http, exceptions
# pyrefly: ignore [missing-import]
from odoo.http import request
class EducationDemoController(http.Controller):
@http.route('/demo/login', type='http', auth='public', methods=['POST'], website=True, csrf=False)
def demo_login(self, role=None, **kw):
"""
Secure Demo Login Endpoint.
Accepts only 'role' (admin, faculty, student).
Redirects to safe destinations.
"""
if not role:
raise exceptions.AccessDenied("Missing role parameter.")
# Map allowed roles to specific, pre-configured demo user logins
# Do not allow arbitrary user login
demo_accounts = {
'admin': {
'login': 'demo.admin',
'password': 'Demo@Admin2026',
'redirect': '/web'
},
'faculty': {
'login': 'demo.faculty',
'password': 'Demo@Faculty2026',
'redirect': '/web'
},
'student': {
'login': 'demo.student',
'password': 'Demo@Student2026',
'redirect': '/my/education/dashboard'
}
}
if role not in demo_accounts:
raise exceptions.AccessDenied("Invalid role.")
account = demo_accounts[role]
db = request.db
if not db:
# If no DB is set, force it to the single DB (we can get it if there's only one)
dbs = http.db_list()
if len(dbs) == 1:
db = dbs[0]
else:
return request.redirect('/web/login')
try:
credential = {
'login': account['login'],
'password': account['password'],
'type': 'password'
}
if request.env.cr:
auth_info = request.session.authenticate(request.env, credential)
uid = auth_info.get('uid') if auth_info else request.session.uid
else:
import odoo
with odoo.modules.registry.Registry(db).cursor() as cr:
env = odoo.api.Environment(cr, None, {})
auth_info = request.session.authenticate(env, credential)
request._save_session(env)
request.session.db = db
uid = auth_info.get('uid') if auth_info else request.session.uid
except Exception:
uid = False
if not uid:
# Maybe the demo user doesn't exist yet, or password was changed.
# Redirect to login with error.
return request.redirect('/web/login?error=Demo account not properly configured.')
# Protect against open redirects: Only use our pre-defined safe redirects
return request.redirect(account['redirect'])

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Demo Admin User -->
<record id="demo_user_admin" model="res.users">
<field name="name">Demo Admin</field>
<field name="login">demo.admin</field>
<field name="password">Demo@Admin2026</field>
<field name="email">demo.admin@example.com</field>
</record>
<!-- Demo Faculty User -->
<record id="demo_user_faculty" model="res.users">
<field name="name">Demo Faculty</field>
<field name="login">demo.faculty</field>
<field name="password">Demo@Faculty2026</field>
<field name="email">demo.faculty@example.com</field>
</record>
<!-- Demo Student Portal User -->
<!-- Usually students are portal users -->
<record id="demo_user_student" model="res.users">
<field name="name">Demo Student</field>
<field name="login">demo.student</field>
<field name="password">Demo@Student2026</field>
<field name="email">demo.student@example.com</field>
</record>
</odoo>

View File

@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
def generate_demo_data(env):
"""
Generate realistic demo data for the Education ERP.
We do this via ORM to avoid complex XML constraint failures.
"""
# 1. Company / Campus / Department
demo_company = env['res.company'].search([('name', '=', 'Metatron Demo Company')], limit=1)
if not demo_company:
demo_company = env['res.company'].create({
'name': 'Metatron Demo Company',
'email': 'demo@metatroncubesolutions.com'
})
# Explicitly ensure the main admin (ID 2) does not have access to this company
main_admin = env['res.users'].browse(2)
if main_admin.exists():
main_admin.sudo().write({'company_ids': [(3, demo_company.id)]})
company = demo_company
campus = env['education.campus'].search([('name', '=', 'Demo Campus')], limit=1)
if not campus:
campus = env['education.campus'].create({
'name': 'Demo Campus',
'code': 'DEMO-CAMPUS',
'company_id': company.id
})
dept = env['education.department'].search([('name', '=', 'Computer Science Demo')], limit=1)
if not dept:
dept = env['education.department'].create({
'name': 'Computer Science Demo',
'code': 'CS-DEMO',
'company_id': company.id,
'campus_id': campus.id
})
# 2. Academic Level & Program
level = env['education.academic.level'].search([('name', '=', 'Undergraduate Demo')], limit=1)
if not level:
level = env['education.academic.level'].create({
'name': 'Undergraduate Demo',
'code': 'UG-DEMO',
'company_id': company.id
})
program = env['education.program'].search([('code', '=', 'BTECH-CS-DEMO')], limit=1)
if not program:
program = env['education.program'].create({
'name': 'B.Tech Computer Science (Demo)',
'code': 'BTECH-CS-DEMO',
'level_id': level.id,
'department_id': dept.id,
'duration': 4,
'duration_unit': 'years'
})
# 3. Academic Year & Term
import datetime
today = datetime.date.today()
year = env['education.academic.year'].search([('name', '=', 'Demo Year 2026-2027')], limit=1)
if not year:
year = env['education.academic.year'].create({
'name': 'Demo Year 2026-2027',
'code': 'DY2627',
'start_date': today,
'end_date': today + datetime.timedelta(days=365),
'company_id': company.id
})
term = env['education.academic.term'].search([('code', '=', 'DT-FALL-26')], limit=1)
if not term:
term = env['education.academic.term'].create({
'name': 'Demo Fall 2026',
'code': 'DT-FALL-26',
'academic_year_id': year.id,
'start_date': today,
'end_date': today + datetime.timedelta(days=120)
})
# 4. Users & Roles
admin_user = env['res.users'].search([('login', '=', 'demo.admin')], limit=1)
faculty_user = env['res.users'].search([('login', '=', 'demo.faculty')], limit=1)
student_user = env['res.users'].search([('login', '=', 'demo.student')], limit=1)
# Assign groups dynamically
erp_manager_group = env.ref('base.group_erp_manager', raise_if_not_found=False)
system_group = env.ref('base.group_system', raise_if_not_found=False)
user_group = env.ref('base.group_user', raise_if_not_found=False)
portal_group = env.ref('base.group_portal', raise_if_not_found=False)
if admin_user:
admin_groups = []
if erp_manager_group: admin_groups.append(erp_manager_group.id)
if system_group: admin_groups.append(system_group.id)
if admin_groups:
admin_user.sudo().write({
'groups_id': [(4, g) for g in admin_groups],
'company_id': company.id,
'company_ids': [(6, 0, [company.id])]
})
if faculty_user and user_group:
faculty_groups = [user_group.id]
# Give basic student read access
student_user_grp = env.ref('education_student.group_education_student_user', raise_if_not_found=False)
if student_user_grp: faculty_groups.append(student_user_grp.id)
# Give LMS Faculty access
lms_faculty_grp = env.ref('education_lms.group_education_lms_faculty', raise_if_not_found=False)
if lms_faculty_grp: faculty_groups.append(lms_faculty_grp.id)
faculty_user.sudo().write({
'groups_id': [(4, g) for g in faculty_groups],
'company_id': company.id,
'company_ids': [(6, 0, [company.id])]
})
if student_user and portal_group:
student_user.sudo().write({
'groups_id': [(4, portal_group.id)],
'company_id': company.id,
'company_ids': [(6, 0, [company.id])]
})
# 5. Create Student Record
if student_user:
student = env['education.student'].search([('partner_id', '=', student_user.partner_id.id)], limit=1)
if not student:
student = env['education.student'].create({
'first_name': 'Demo',
'last_name': 'Student',
'partner_id': student_user.partner_id.id,
'program_id': program.id,
'campus_id': campus.id,
'academic_year_id': year.id,
'status': 'active',
'portal_user_id': student_user.id
})
# Map student ID to portal user if required by ERP architecture
student_user.partner_id.write({'is_student': True})
# 6. Create LMS Course with Faculty
if faculty_user:
course = env['education.lms.course'].search([('code', '=', 'CS101-DEMO')], limit=1)
if not course:
course = env['education.lms.course'].create({
'name': 'Introduction to Computer Science (Demo)',
'code': 'CS101-DEMO',
'faculty_id': faculty_user.id,
'program_id': program.id,
'state': 'published',
'description': 'A comprehensive demo course.'
})
# Create a section and material
section = env['education.lms.section'].create({
'name': 'Week 1: Basics',
'course_id': course.id,
'sequence': 1
})
env['education.lms.material'].create({
'name': 'Syllabus & Introduction',
'section_id': section.id,
'material_type': 'document',
'content': 'Welcome to the course!'
})
# Create an assignment
env['education.lms.assignment'].create({
'name': 'First Demo Assignment',
'course_id': course.id,
'due_date': today + datetime.timedelta(days=7),
'max_marks': 100,
'state': 'published'
})
# Enroll the demo student
if student_user:
student_record = env['education.student'].search([('partner_id', '=', student_user.partner_id.id)], limit=1)
if student_record:
env['education.lms.enrollment'].create({
'course_id': course.id,
'student_id': student_record.id,
'state': 'enrolled'
})
# 7. Library Demo Data (Optional, if library module exists)
if 'education.library.book' in env:
book = env['education.library.book'].search([('isbn', '=', 'DEMO-ISBN-123')], limit=1)
if not book:
book = env['education.library.book'].create({
'name': 'The Art of Computer Programming (Demo Edition)',
'author': 'Donald Knuth',
'isbn': 'DEMO-ISBN-123',
'total_copies': 5
})
# If student is a library member, create a checkout
if student_user:
student_record = env['education.student'].search([('partner_id', '=', student_user.partner_id.id)], limit=1)
if student_record:
member = env['education.library.member'].search([('student_id', '=', student_record.id)], limit=1)
if not member:
member = env['education.library.member'].create({
'student_id': student_record.id,
'member_type': 'student',
'state': 'active'
})
env['education.library.checkout'].create({
'member_id': member.id,
'book_id': book.id,
'checkout_date': today,
'due_date': today + datetime.timedelta(days=14),
'state': 'issued'
})
env.cr.commit()

View File

@ -0,0 +1 @@
from . import test_demo_login

View File

@ -0,0 +1,32 @@
import odoo.tests
from odoo.tests.common import HttpCase
from unittest.mock import patch
@odoo.tests.tagged('-at_install', 'post_install')
class TestDemoLogin(HttpCase):
def test_01_demo_login_admin(self):
""" Test admin demo login via endpoint """
response = self.url_open('/demo/login', data={'role': 'admin'})
self.assertEqual(response.status_code, 200, "Should redirect or load a page")
def test_02_demo_login_faculty(self):
""" Test faculty demo login """
response = self.url_open('/demo/login', data={'role': 'faculty'})
self.assertEqual(response.status_code, 200, "Should redirect or load a page")
def test_03_demo_login_student(self):
""" Test student demo login """
response = self.url_open('/demo/login', data={'role': 'student'})
self.assertEqual(response.status_code, 200, "Should redirect or load a page")
def test_04_demo_login_invalid_role(self):
""" Test invalid roles get rejected """
response = self.url_open('/demo/login', data={'role': 'superadmin'})
self.assertEqual(response.status_code, 403, "Should be Access Denied")
def test_05_demo_login_missing_role(self):
""" Test missing role gets rejected """
# using GET instead of POST causes a 405 Method Not Allowed, or missing role in POST causes 403
response = self.url_open('/demo/login', data={})
self.assertEqual(response.status_code, 403, "Should be Access Denied")

View File

@ -552,6 +552,20 @@
color: #144d4d; color: #144d4d;
} }
/* Footer */
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
/* Responsive Design overrides */ /* Responsive Design overrides */
@media (max-width: 991px) { @media (max-width: 991px) {
.portal-layout-wrapper { .portal-layout-wrapper {
@ -603,10 +617,18 @@
margin: 0; margin: 0;
width: auto; width: auto;
} }
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
} }
@media (max-width: 575px) { @media (max-width: 575px) {
.portal-header { .portal-header {
padding: 0 12px; padding: 0 12px !important;
height: 60px; height: 60px;
} }
.portal-logo-area span { .portal-logo-area span {
@ -783,7 +805,7 @@
<div class="premium-card"> <div class="premium-card">
<h5 class="font-weight-bold mb-3 text-dark"> <h5 class="font-weight-bold mb-3 text-dark">
<i class="fa fa-calendar text-primary mr-2"/>Scheduled Exams <i class="fa fa-calendar text-primary mr-2 me-2"/>Scheduled Exams
</h5> </h5>
<t t-if="not exams"> <t t-if="not exams">
<div class="alert alert-info border-0 shadow-sm" style="background: #e6f7f7; color: #144d4d; border-radius: 12px;"> <div class="alert alert-info border-0 shadow-sm" style="background: #e6f7f7; color: #144d4d; border-radius: 12px;">
@ -821,9 +843,9 @@
</div> </div>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>
@ -917,7 +939,7 @@
<div class="premium-card"> <div class="premium-card">
<h5 class="font-weight-bold mb-3 text-dark"> <h5 class="font-weight-bold mb-3 text-dark">
<i class="fa fa-trophy text-primary mr-2"/>Published Results <i class="fa fa-trophy text-primary mr-2 me-2"/>Published Results
</h5> </h5>
<t t-if="not results"> <t t-if="not results">
<div class="alert alert-info border-0 shadow-sm" style="background: #e6f7f7; color: #144d4d; border-radius: 12px;"> <div class="alert alert-info border-0 shadow-sm" style="background: #e6f7f7; color: #144d4d; border-radius: 12px;">
@ -961,9 +983,9 @@
</div> </div>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>
@ -1143,9 +1165,9 @@
</div> </div>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>

View File

@ -5,7 +5,7 @@
name="Finance" name="Finance"
parent="education_base.menu_education_base_root" parent="education_base.menu_education_base_root"
sequence="30" sequence="30"
groups="group_finance_executive"/> groups="education_finance.group_finance_executive"/>
<!-- Finance Submenus --> <!-- Finance Submenus -->
<menuitem id="menu_education_fee_assignment" <menuitem id="menu_education_fee_assignment"
@ -31,28 +31,28 @@
parent="menu_education_finance_root" parent="menu_education_finance_root"
action="action_education_student_discount" action="action_education_student_discount"
sequence="40" sequence="40"
groups="group_finance_manager"/> groups="education_finance.group_finance_manager"/>
<menuitem id="menu_education_student_scholarship_menu" <menuitem id="menu_education_student_scholarship_menu"
name="Scholarships" name="Scholarships"
parent="menu_education_finance_root" parent="menu_education_finance_root"
action="action_education_student_scholarship" action="action_education_student_scholarship"
sequence="50" sequence="50"
groups="group_finance_manager"/> groups="education_finance.group_finance_manager"/>
<menuitem id="menu_education_student_refund_menu" <menuitem id="menu_education_student_refund_menu"
name="Refunds" name="Refunds"
parent="menu_education_finance_root" parent="menu_education_finance_root"
action="action_education_student_refund" action="action_education_student_refund"
sequence="60" sequence="60"
groups="group_finance_manager"/> groups="education_finance.group_finance_manager"/>
<!-- Configuration Menu Items (Placed under Base Configuration) --> <!-- Configuration Menu Items (Placed under Base Configuration) -->
<menuitem id="menu_education_finance_config_root" <menuitem id="menu_education_finance_config_root"
name="Finance Configuration" name="Finance Configuration"
parent="education_base.menu_education_base_configuration" parent="education_base.menu_education_base_configuration"
sequence="20" sequence="20"
groups="group_finance_admin"/> groups="education_finance.group_finance_admin"/>
<menuitem id="menu_education_fee_category" <menuitem id="menu_education_fee_category"
name="Fee Categories" name="Fee Categories"

View File

@ -243,6 +243,87 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
/* Footer */
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
/* Responsive */
@media (max-width: 991px) {
.portal-layout-wrapper {
flex-direction: column;
}
.portal-sidebar {
width: 100% !important;
border-right: none !important;
border-bottom: 1px solid rgba(31, 143, 143, 0.1);
padding: 16px 20px;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
position: static !important;
max-height: none !important;
}
.sidebar-link {
padding: 10px 16px;
}
.portal-content-area {
padding: 24px !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
}
}
@media (max-width: 767px) {
.portal-header {
padding: 0 20px;
}
.portal-sidebar {
padding: 12px 8px;
gap: 4px;
}
.sidebar-link {
padding: 8px 12px;
font-size: 0.85rem;
gap: 8px;
}
.sidebar-link span {
display: none;
}
.sidebar-link i {
font-size: 1.3rem;
margin: 0;
width: auto;
}
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
}
@media (max-width: 575px) {
.portal-header {
padding: 0 12px !important;
height: 60px;
}
.portal-logo-area span {
display: none;
}
}
</style> </style>
<!-- Header --> <!-- Header -->
@ -300,6 +381,12 @@
<t t-out="0"/> <t t-out="0"/>
</div> </div>
</div> </div>
<!-- Bottom Footer Area -->
<div class="portal-footer-area">
<span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div>
</t> </t>
</template> </template>

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
{
'name': 'Education Mobile API',
'version': '1.0',
'summary': 'REST API for Mobile Application',
'description': """
Dedicated REST API layer in Odoo specifically for the mobile application.
""",
'category': 'Education',
'author': 'Antigravity',
'depends': ['base', 'web', 'education_portal'],
'data': [
'security/ir.model.access.csv',
],
'installable': True,
'application': False,
'auto_install': False,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import auth
from . import student

View File

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
import json
import logging
from odoo import http
from odoo.http import request
from odoo.exceptions import AccessError, AccessDenied
_logger = logging.getLogger(__name__)
def json_response(success, data=None, error_code=None, error_message=None, status=200):
if success:
body = {
"success": True,
"data": data or {}
}
else:
body = {
"success": False,
"error": {
"code": error_code or "ERROR",
"message": error_message or "An error occurred"
}
}
return request.make_response(
json.dumps(body),
headers=[('Content-Type', 'application/json')],
status=status
)
class MobileAuth(http.Controller):
@http.route('/api/mobile/v1/auth/login', type='http', auth='none', methods=['POST'], csrf=False, cors='*')
def login(self, **kwargs):
try:
data = json.loads(request.httprequest.data)
email = data.get('email')
password = data.get('password')
if not email or not password:
return json_response(False, error_code="INVALID_REQUEST", error_message="Email and password are required", status=400)
db = request.db or request.env.registry.db_name
# Authenticate creates a session securely
credential = {'login': email, 'password': password, 'type': 'password'}
auth_info = request.session.authenticate(request.env, credential)
uid = auth_info.get('uid')
if not uid:
return json_response(False, error_code="AUTH_FAILED", error_message="Invalid email or password", status=401)
request.update_env(user=uid)
user = request.env['res.users'].sudo().browse(uid)
student = user.student_id
return json_response(True, data={
"user": {
"id": user.id,
"name": user.name,
"email": user.login,
"role": "student" if student else "user"
},
"student": {
"id": student.id,
"name": student.full_name
} if student else None
})
except (AccessError, AccessDenied):
return json_response(False, error_code="AUTH_FAILED", error_message="Invalid email or password", status=401)
except Exception as e:
_logger.exception("Login error")
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
@http.route('/api/mobile/v1/auth/logout', type='http', auth='public', methods=['POST'], csrf=False, cors='*')
def logout(self, **kwargs):
if request.env.user._is_public():
return json_response(False, error_code="AUTH_REQUIRED", error_message="Authentication required", status=401)
try:
request.session.logout(keep_db=True)
return json_response(True, data={"message": "Logged out successfully"})
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
@http.route('/api/mobile/v1/auth/me', type='http', auth='public', methods=['GET'], cors='*')
def get_me(self, **kwargs):
if request.env.user._is_public():
return json_response(False, error_code="AUTH_REQUIRED", error_message="Authentication required", status=401)
try:
user = request.env.user
student = user.student_id
return json_response(True, data={
"user": {
"id": user.id,
"name": user.name,
"email": user.login,
"role": "student" if student else "user"
},
"student": {
"id": student.id,
"name": student.full_name
} if student else None
})
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)

View File

@ -0,0 +1,314 @@
# -*- coding: utf-8 -*-
import json
import logging
from datetime import datetime, date
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
def json_response(success, data=None, error_code=None, error_message=None, status=200):
if success:
body = {
"success": True,
"data": data or {}
}
else:
body = {
"success": False,
"error": {
"code": error_code or "ERROR",
"message": error_message or "An error occurred"
}
}
return request.make_response(
json.dumps(body, default=str),
headers=[('Content-Type', 'application/json')],
status=status
)
def require_student(func):
def wrapper(self, *args, **kwargs):
if request.env.user._is_public():
return json_response(False, error_code="AUTH_REQUIRED", error_message="Authentication required", status=401)
student = request.env.user.sudo().student_id
if not student:
return json_response(False, error_code="STUDENT_NOT_LINKED", error_message="No student profile is linked to this account", status=403)
return func(self, student, *args, **kwargs)
return wrapper
def format_float_time(float_time):
hours = int(float_time)
minutes = int(round((float_time - hours) * 60))
if minutes == 60:
minutes = 0
hours += 1
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}"
class MobileStudent(http.Controller):
def _get_rel(self, record):
if record:
return {"id": record.id, "name": record.name}
return None
@http.route('/api/mobile/v1/student/profile', type='http', auth='public', methods=['GET'], cors='*')
@require_student
def get_profile(self, student, **kwargs):
try:
data = {
"id": student.id,
"student_id": student.student_id,
"admission_number": student.admission_number,
"roll_number": student.roll_number,
"full_name": student.full_name,
"email": student.email,
"mobile": student.mobile,
"gender": student.gender,
"date_of_birth": student.date_of_birth,
"status": student.status,
"campus": self._get_rel(student.campus_id),
"department": self._get_rel(student.department_id),
"program": self._get_rel(student.program_id),
"level": self._get_rel(student.level_id),
"academic_year": self._get_rel(student.academic_year_id),
"academic_term": self._get_rel(student.academic_term_id),
"batch": self._get_rel(student.batch_id),
"semester": self._get_rel(student.semester_id),
"section": self._get_rel(student.section_id)
}
return json_response(True, data=data)
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
def _get_attendance_summary(self, student, from_date=None, to_date=None):
domain = [('student_id', '=', student.id)]
if from_date:
domain.append(('date', '>=', from_date))
if to_date:
domain.append(('date', '<=', to_date))
records = request.env['education.attendance'].search(domain)
total_sessions = len(records)
present = len([a for a in records if a.status == 'present'])
absent = len([a for a in records if a.status == 'absent'])
late = len([a for a in records if a.status == 'late'])
medical_leave = len([a for a in records if a.status == 'medical_leave'])
on_duty = len([a for a in records if a.status == 'on_duty'])
effective_present = present + on_duty + (late * 0.5)
percentage = round((effective_present / total_sessions * 100), 1) if total_sessions > 0 else 100.0
return {
"percentage": percentage,
"present": present,
"absent": absent,
"late": late,
"medical_leave": medical_leave,
"on_duty": on_duty
}
def _get_timetable_slots(self, student, date_filter=None):
if not student.program_id or not student.academic_year_id:
return []
domain = [
('program_id', '=', student.program_id.id),
('academic_year_id', '=', student.academic_year_id.id),
('active', '=', True)
]
if student.academic_term_id:
domain += ['|', ('term_id', '=', False), ('term_id', '=', student.academic_term_id.id)]
else:
domain += [('term_id', '=', False)]
if student.batch_id:
domain += ['|', ('batch_id', '=', False), ('batch_id', '=', student.batch_id.id)]
else:
domain += [('batch_id', '=', False)]
if date_filter:
domain += [('date', '=', date_filter)]
slots = request.env['education.timetable.slot'].sudo().search(domain, order="date asc, start_time asc")
return [{
"id": s.id,
"date": s.date,
"day": s.date.strftime('%A') if s.date else '',
"start_time": format_float_time(s.start_time),
"end_time": format_float_time(s.end_time),
"subject": self._get_rel(s.subject_id),
"faculty": self._get_rel(s.faculty_id),
"room": self._get_rel(s.room_id)
} for s in slots]
@http.route('/api/mobile/v1/student/dashboard', type='http', auth='public', methods=['GET'], cors='*')
@require_student
def get_dashboard(self, student, **kwargs):
try:
today = date.today()
# Upcoming Exams
exams = request.env['education.exam'].sudo().search_read([
('state', 'in', ['scheduled', 'ongoing']),
('program_id', '=', student.program_id.id),
('academic_year_id', '=', student.academic_year_id.id),
], fields=['id', 'name', 'state', 'start_date'], order='start_date asc', limit=5)
# Announcements
announcements = request.env['education.announcement'].search_read(
[('active', '=', True)],
fields=['id', 'name', 'date', 'summary'],
order='date desc, id desc',
limit=5
)
# Recent Results
results = request.env['education.exam.result'].search([
('student_id', '=', student.id),
('published', '=', True)
], order='published_date desc, id desc', limit=5)
results_data = []
for r in results:
results_data.append({
"id": r.id,
"exam": self._get_rel(r.exam_id),
"percentage": r.percentage,
"grade": r.grade_id.name if r.grade_id else None,
"status": r.result_status
})
data = {
"student": {
"id": student.id,
"full_name": student.full_name,
"program": self._get_rel(student.program_id)
},
"attendance": self._get_attendance_summary(student),
"today_timetable": self._get_timetable_slots(student, date_filter=today),
"upcoming_exams": exams,
"recent_results": results_data,
"announcements": announcements
}
return json_response(True, data=data)
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
@http.route('/api/mobile/v1/student/attendance', type='http', auth='public', methods=['GET'], cors='*')
@require_student
def get_attendance(self, student, **kwargs):
try:
from_date = kwargs.get('from_date')
to_date = kwargs.get('to_date')
if from_date:
from_date = datetime.strptime(from_date, '%Y-%m-%d').date()
if to_date:
to_date = datetime.strptime(to_date, '%Y-%m-%d').date()
summary = self._get_attendance_summary(student, from_date, to_date)
domain = [('student_id', '=', student.id)]
if from_date:
domain.append(('date', '>=', from_date))
if to_date:
domain.append(('date', '<=', to_date))
records = request.env['education.attendance'].search_read(
domain,
fields=['id', 'date', 'status'],
order='date desc, id desc',
limit=100
)
return json_response(True, data={
"summary": summary,
"records": records
})
except ValueError:
return json_response(False, error_code="INVALID_DATE", error_message="Dates must be in YYYY-MM-DD format", status=400)
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
@http.route('/api/mobile/v1/student/timetable', type='http', auth='public', methods=['GET'], cors='*')
@require_student
def get_timetable(self, student, **kwargs):
try:
slots = self._get_timetable_slots(student)
return json_response(True, data={"slots": slots})
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
@http.route('/api/mobile/v1/student/exams', type='http', auth='public', methods=['GET'], cors='*')
@require_student
def get_exams(self, student, **kwargs):
try:
exams = request.env['education.exam'].sudo().search([
('program_id', '=', student.program_id.id),
('academic_year_id', '=', student.academic_year_id.id),
], order='start_date desc')
exams_data = [{
"id": exam.id,
"name": exam.name,
"state": exam.state,
"start_date": exam.start_date,
"end_date": exam.end_date,
"program": self._get_rel(exam.program_id),
"academic_year": self._get_rel(exam.academic_year_id)
} for exam in exams]
return json_response(True, data={"exams": exams_data})
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)
@http.route('/api/mobile/v1/student/results', type='http', auth='public', methods=['GET'], cors='*')
@require_student
def get_results(self, student, **kwargs):
try:
results = request.env['education.exam.result'].search([
('student_id', '=', student.id),
('published', '=', True)
], order='published_date desc, id desc')
results_data = []
for r in results:
lines = []
for line in r.line_ids:
lines.append({
"subject": self._get_rel(line.subject_id),
"max_marks": line.max_marks,
"pass_marks": line.pass_marks,
"obtained_marks": line.obtained_marks,
"percentage": line.percentage,
"grade": line.grade_id.name if line.grade_id else None,
"status": line.status
})
results_data.append({
"id": r.id,
"exam": self._get_rel(r.exam_id),
"total_max_marks": r.total_max_marks,
"total_obtained_marks": r.total_obtained_marks,
"percentage": r.percentage,
"grade": r.grade_id.name if r.grade_id else None,
"status": r.result_status,
"published_date": r.published_date,
"lines": lines
})
return json_response(True, data={"results": results_data})
except Exception as e:
return json_response(False, error_code="SERVER_ERROR", error_message=str(e), status=500)

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import mobile_device

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class MobileDevice(models.Model):
_name = 'education.mobile.device'
_description = 'Mobile Device Information'
user_id = fields.Many2one('res.users', string='User', required=True)
device_token = fields.Char(string='Device Token')
platform = fields.Selection([('ios', 'iOS'), ('android', 'Android')], string='Platform')
last_login = fields.Datetime(string='Last Login')

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_education_mobile_device,education.mobile.device,model_education_mobile_device,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_education_mobile_device education.mobile.device model_education_mobile_device base.group_user 1 1 1 1

View File

@ -32,6 +32,7 @@
'views/portal_payment_templates.xml', 'views/portal_payment_templates.xml',
'views/student_views_inherit.xml', 'views/student_views_inherit.xml',
'views/login_templates.xml', 'views/login_templates.xml',
'views/web_layout_override.xml',
], ],
'installable': True, 'installable': True,
'application': True, 'application': True,

View File

@ -24,3 +24,5 @@ class EducationAnnouncement(models.Model):
default=lambda self: self.env.company, default=lambda self: self.env.company,
index=True, index=True,
) )
circular_file = fields.Binary(string='Circular File', attachment=True)
circular_filename = fields.Char(string='Circular Filename')

View File

@ -30,6 +30,8 @@
<field name="announcement_type"/> <field name="announcement_type"/>
<field name="date_published"/> <field name="date_published"/>
<field name="expiry_date"/> <field name="expiry_date"/>
<field name="circular_filename" invisible="1"/>
<field name="circular_file" filename="circular_filename" widget="binary" invisible="announcement_type != 'circular'"/>
</group> </group>
<group> <group>
<field name="active"/> <field name="active"/>

View File

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Override favicon globally -->
<template id="custom_favicon" inherit_id="web.layout">
<xpath expr="//link[@rel='shortcut icon']" position="replace">
<link type="image/png" rel="shortcut icon" href="/education_dashboard/static/src/img/favicon.png"/>
</xpath>
</template>
<!-- Custom Login Layout for Odoo Education Portal --> <!-- Custom Login Layout for Odoo Education Portal -->
<template id="custom_login_layout" inherit_id="web.login_layout" name="Education Portal Custom Login Layout"> <template id="custom_login_layout" inherit_id="web.login_layout" name="Education Portal Custom Login Layout">
<xpath expr="//div[hasclass('container')]" position="replace"> <xpath expr="//div[hasclass('container')]" position="replace">
@ -76,9 +83,10 @@
z-index: 2; z-index: 2;
} }
.left-campus-logo { .left-campus-logo {
max-height: 55px; <!-- max-height: 55px; -->
width: auto; width: auto;
margin-bottom: 24px; margin-left: -20px;
<!-- margin-bottom: 24px; -->
} }
.left-welcome-tag { .left-welcome-tag {
color: #1f8f8f; color: #1f8f8f;
@ -257,7 +265,7 @@
<div class="left-header-area"> <div class="left-header-area">
<!-- Campus Logo --> <!-- Campus Logo -->
<img src="/web/image?model=res.company&amp;id=1&amp;field=logo" alt="Logo" class="left-campus-logo"/> <img src="/education_dashboard/static/src/img/logo.png" alt="Logo" class="left-campus-logo"/>
<div class="left-welcome-tag">Welcome to</div> <div class="left-welcome-tag">Welcome to</div>
<div class="left-main-title">Education Portal</div> <div class="left-main-title">Education Portal</div>
@ -320,8 +328,8 @@
<!-- Bottom Page Footer --> <!-- Bottom Page Footer -->
<div class="text-center mt-4" style="font-size: 0.85rem; color: #718096; font-weight: 600;"> <div class="text-center mt-4" style="font-size: 0.85rem; color: #718096; font-weight: 600;">
<div>Powered by <span style="color: #1f8f8f; font-weight: 800;">Odoo</span> Education Portal</div> <div>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></div>
<div>© 2026 My Company. All rights reserved.</div> <div>© 2026 Metatron EduPortal. All rights reserved.</div>
</div> </div>
</div> </div>
</div> </div>
@ -424,13 +432,43 @@
</button> </button>
</div> </div>
<!-- Social / SSO Divider & Buttons --> <!-- Quick Demo Access Section -->
<!-- <div class="text-center my-4 position-relative"> <div class="text-center my-4 position-relative">
<hr style="border-color: #e2e8f0; margin: 0;"/> <hr style="border-color: #e2e8f0; margin: 0;"/>
<span class="position-absolute bg-white px-3 text-muted small font-weight-bold" style="top: 50%; left: 50%; transform: translate(-50%, -50%);">or continue with</span> <span class="position-absolute bg-white px-3 text-muted font-weight-bold" style="top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 0.95rem; color: #144d4d !important;">Quick Demo Access</span>
</div> </div>
<div class="row g-2 mb-2"> <div class="text-center mb-3">
<span class="small text-muted">Demo accounts contain fictional data.</span>
</div>
<div class="d-flex flex-row justify-content-between gap-2 mb-2">
<form action="/demo/login" method="post" class="flex-fill m-0" style="width: 32%;">
<input type="hidden" name="role" value="admin"/>
<button type="submit" class="btn d-flex flex-column align-items-center justify-content-center p-2" style="border-radius: 12px; border: 1.5px solid #1f8f8f; color: #144d4d; background: #eef9f9; transition: all 0.2s; height: 100%; width: 32%;">
<span class="font-weight-bold mb-1" style="font-size: 0.9rem;"><i class="fa fa-cogs d-block mb-1 text-center" style="font-size: 1.1rem;"/>Admin</span>
<span class="text-muted text-center" style="font-size: 0.7rem; line-height: 1.1;">Full Access</span>
</button>
</form>
<form action="/demo/login" method="post" class="flex-fill m-0" style="width: 32%;">
<input type="hidden" name="role" value="faculty"/>
<button type="submit" class="btn w-100 d-flex flex-column align-items-center justify-content-center p-2" style="border-radius: 12px; border: 1.5px solid #1f8f8f; color: #144d4d; background: #eef9f9; transition: all 0.2s; height: 100%;">
<span class="font-weight-bold mb-1" style="font-size: 0.9rem;"><i class="fa fa-graduation-cap d-block mb-1 text-center" style="font-size: 1.1rem;"/>Faculty</span>
<span class="text-muted text-center" style="font-size: 0.7rem; line-height: 1.1;">Courses &amp; LMS</span>
</button>
</form>
<form action="/demo/login" method="post" class="flex-fill m-0" style="width: 32%;">
<input type="hidden" name="role" value="student"/>
<button type="submit" class="btn w-100 d-flex flex-column align-items-center justify-content-center p-2" style="border-radius: 12px; border: 1.5px solid #1f8f8f; color: #144d4d; background: #eef9f9; transition: all 0.2s; height: 100%;">
<span class="font-weight-bold mb-1" style="font-size: 0.9rem;"><i class="fa fa-user d-block mb-1 text-center" style="font-size: 1.1rem;"/>Student</span>
<span class="text-muted text-center" style="font-size: 0.7rem; line-height: 1.1;">Portal &amp; Classes</span>
</button>
</form>
</div>
<!-- <div class="row g-2 mb-2">
<div class="col-6"> <div class="col-6">
<button type="button" class="btn btn-outline-secondary w-100 font-weight-bold d-flex align-items-center justify-content-center gap-2" style="border-radius: 12px; height: 44px; font-size: 0.85rem; border-color: #cbd5e1; color: #4a5568;"> <button type="button" class="btn btn-outline-secondary w-100 font-weight-bold d-flex align-items-center justify-content-center gap-2" style="border-radius: 12px; height: 44px; font-size: 0.85rem; border-color: #cbd5e1; color: #4a5568;">
<svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z"/></svg> <svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z"/></svg>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Payment Success Template --> <!-- Payment Success Template -->
<template id="portal_payment_success" name="Payment Success"> <template id="portal_payment_success" name="Payment Success">
@ -71,7 +71,7 @@
<div class="d-flex justify-content-center gap-3"> <div class="d-flex justify-content-center gap-3">
<t t-if="transaction.payment_id"> <t t-if="transaction.payment_id">
<a t-attf-href="/my/education/finance/receipt/{{ transaction.payment_id.id }}" class="btn text-white font-weight-bold px-4 py-2" style="background: #1f8f8f; border-radius: 10px;"> <a t-attf-href="/my/education/finance/receipt/{{ transaction.payment_id.id }}" class="btn text-white font-weight-bold px-4 py-2" style="background: #1f8f8f; border-radius: 10px;">
<i class="fa fa-download mr-2"/> Download PDF Receipt <i class="fa fa-download mr-2 me-2"/> Download PDF Receipt
</a> </a>
</t> </t>
<a href="/my/education/finance" class="btn btn-outline-secondary font-weight-bold px-4 py-2" style="border-radius: 10px;"> <a href="/my/education/finance" class="btn btn-outline-secondary font-weight-bold px-4 py-2" style="border-radius: 10px;">
@ -122,12 +122,12 @@
<p class="text-muted mb-3"><t t-esc="transaction.error_message or 'The payment process was cancelled or verification failed.'" me=""/></p> <p class="text-muted mb-3"><t t-esc="transaction.error_message or 'The payment process was cancelled or verification failed.'" me=""/></p>
<div class="alert alert-warning text-left mb-4" style="border-radius: 10px; font-size: 0.9rem;"> <div class="alert alert-warning text-left mb-4" style="border-radius: 10px; font-size: 0.9rem;">
<i class="fa fa-info-circle mr-1"/> Don't worry! No funds were deducted for incomplete ledger entries. You can safely retry. <i class="fa fa-info-circle mr-1 me-1"/> Don't worry! No funds were deducted for incomplete ledger entries. You can safely retry.
</div> </div>
<div class="d-flex justify-content-center gap-3"> <div class="d-flex justify-content-center gap-3">
<a href="/my/education/finance" class="btn btn-danger font-weight-bold px-4 py-2" style="border-radius: 10px;"> <a href="/my/education/finance" class="btn btn-danger font-weight-bold px-4 py-2" style="border-radius: 10px;">
<i class="fa fa-refresh mr-2"/> Retry Payment <i class="fa fa-refresh mr-2 me-2"/> Retry Payment
</a> </a>
<a href="/my/education/dashboard" class="btn btn-outline-secondary font-weight-bold px-4 py-2" style="border-radius: 10px;"> <a href="/my/education/dashboard" class="btn btn-outline-secondary font-weight-bold px-4 py-2" style="border-radius: 10px;">
Dashboard Dashboard
@ -159,10 +159,10 @@
<div class="history-card"> <div class="history-card">
<div class="d-flex align-items-center justify-content-between mb-4"> <div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="font-weight-bold mb-0" style="color: #144d4d;"> <h4 class="font-weight-bold mb-0" style="color: #144d4d;">
<i class="fa fa-history text-primary mr-2"/> Online Payment History <i class="fa fa-history text-primary mr-2 me-2"/> Online Payment History
</h4> </h4>
<a href="/my/education/finance" class="btn btn-sm btn-outline-primary font-weight-bold" style="border-radius: 8px;"> <a href="/my/education/finance" class="btn btn-sm btn-outline-primary font-weight-bold" style="border-radius: 8px;">
<i class="fa fa-arrow-left mr-1"/> Back to Dues <i class="fa fa-arrow-left mr-1 me-1"/> Back to Dues
</a> </a>
</div> </div>

View File

@ -439,6 +439,7 @@
border-radius: 50%; border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.25); border: 4px solid rgba(255, 255, 255, 0.25);
object-fit: cover; object-fit: cover;
flex-shrink: 0;
} }
.welcome-banner-fallback { .welcome-banner-fallback {
width: 90px; width: 90px;
@ -451,6 +452,7 @@
justify-content: center; justify-content: center;
font-size: 2.2rem; font-size: 2.2rem;
font-weight: 800; font-weight: 800;
flex-shrink: 0;
} }
.welcome-text-title { .welcome-text-title {
font-size: 1.7rem; font-size: 1.7rem;
@ -666,6 +668,9 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
margin-bottom: 4px; margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.fin-card.blue .fin-label { color: #1e40af; } .fin-card.blue .fin-label { color: #1e40af; }
.fin-card.green .fin-label { color: #166534; } .fin-card.green .fin-label { color: #166534; }
@ -674,6 +679,9 @@
.fin-amount { .fin-amount {
font-size: 1.15rem; font-size: 1.15rem;
font-weight: 800; font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.fin-card.blue .fin-amount { color: #1e3a8a; } .fin-card.blue .fin-amount { color: #1e3a8a; }
.fin-card.green .fin-amount { color: #14532d; } .fin-card.green .fin-amount { color: #14532d; }
@ -687,6 +695,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 0.95rem; font-size: 0.95rem;
flex-shrink: 0;
} }
.fin-card.blue .fin-icon-box { background: #dbeafe; color: #1d4ed8; } .fin-card.blue .fin-icon-box { background: #dbeafe; color: #1d4ed8; }
.fin-card.green .fin-icon-box { background: #dcfce7; color: #15803d; } .fin-card.green .fin-icon-box { background: #dcfce7; color: #15803d; }
@ -759,6 +768,18 @@
} }
/* Responsive Design overrides */ /* Responsive Design overrides */
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
@media (max-width: 991px) { @media (max-width: 991px) {
.portal-layout-wrapper { .portal-layout-wrapper {
flex-direction: column; flex-direction: column;
@ -809,10 +830,18 @@
margin: 0; margin: 0;
width: auto; width: auto;
} }
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
} }
@media (max-width: 575px) { @media (max-width: 575px) {
.portal-header { .portal-header {
padding: 0 12px; padding: 0 12px !important;
height: 60px; height: 60px;
} }
.portal-logo-area span { .portal-logo-area span {
@ -822,6 +851,22 @@
padding: 20px !important; padding: 20px !important;
border-radius: 18px !important; border-radius: 18px !important;
} }
.welcome-banner-left {
flex-direction: column;
text-align: center;
gap: 16px;
}
.welcome-banner-card {
padding: 24px 20px;
}
.welcome-banner-right-illustration {
display: none;
}
}
@media (max-width: 424px) {
.card-deco-dots {
display: none !important;
}
} }
</style> </style>
@ -849,10 +894,10 @@
<i class="fa fa-angle-down"/> <i class="fa fa-angle-down"/>
</div> </div>
<div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;"> <div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;">
<a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted"/> My Profile</a> <a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted me-2"/> My Profile</a>
<a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted"/> Dashboard</a> <a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted me-2"/> Dashboard</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2"/> Logout</a> <a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2 me-2"/> Logout</a>
</div> </div>
</div> </div>
</div> </div>
@ -1035,7 +1080,7 @@
<div class="premium-card"> <div class="premium-card">
<div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3"> <div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3">
<h5 class="font-weight-bold text-dark mb-0"> <h5 class="font-weight-bold text-dark mb-0">
<i class="fa fa-mortar-board text-primary mr-2"/>Academic Summary <i class="fa fa-mortar-board text-primary mr-2 me-2"/> Academic Summary
</h5> </h5>
<a href="/my/education/profile" class="header-link-action">View Details &gt;</a> <a href="/my/education/profile" class="header-link-action">View Details &gt;</a>
</div> </div>
@ -1081,36 +1126,36 @@
<!-- Finance Overview --> <!-- Finance Overview -->
<div class="premium-card"> <div class="premium-card">
<div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3"> <div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3 flex-wrap gap-2">
<h5 class="font-weight-bold text-dark mb-0"> <h5 class="font-weight-bold text-dark mb-0 text-truncate" style="min-width: 0; flex: 1;">
<i class="fa fa-credit-card text-success mr-2"/>Finance Overview <i class="fa fa-credit-card text-success mr-2 me-2"/> Finance Overview
</h5> </h5>
<a href="/my/education/finance" class="header-link-action">View All Transactions</a> <a href="/my/education/finance" class="header-link-action text-nowrap">View All Transactions</a>
</div> </div>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
<div class="fin-card blue"> <div class="fin-card blue">
<div> <div style="min-width: 0; flex: 1; margin-right: 8px;">
<div class="fin-label">TOTAL FEES</div> <div class="fin-label" title="TOTAL FEES">TOTAL FEES</div>
<div class="fin-amount"><t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(student.total_fees)"/></div> <div class="fin-amount" t-attf-title="#{student.currency_id.symbol} {'{0:,.2f}'.format(student.total_fees)}"><t t-esc="student.currency_id.symbol"/>&amp;nbsp;<t t-esc="'{0:,.2f}'.format(student.total_fees)"/></div>
</div> </div>
<div class="fin-icon-box"><i class="fa fa-calculator"/></div> <div class="fin-icon-box"><i class="fa fa-calculator"/></div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="fin-card green"> <div class="fin-card green">
<div> <div style="min-width: 0; flex: 1; margin-right: 8px;">
<div class="fin-label">TOTAL PAID</div> <div class="fin-label" title="TOTAL PAID">TOTAL PAID</div>
<div class="fin-amount"><t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(student.total_paid)"/></div> <div class="fin-amount" t-attf-title="#{student.currency_id.symbol} {'{0:,.2f}'.format(student.total_paid)}"><t t-esc="student.currency_id.symbol"/>&amp;nbsp;<t t-esc="'{0:,.2f}'.format(student.total_paid)"/></div>
</div> </div>
<div class="fin-icon-box"><i class="fa fa-check-circle"/></div> <div class="fin-icon-box"><i class="fa fa-check-circle"/></div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="fin-card red"> <div class="fin-card red">
<div> <div style="min-width: 0; flex: 1; margin-right: 8px;">
<div class="fin-label">OUTSTANDING</div> <div class="fin-label" title="OUTSTANDING">OUTSTANDING</div>
<div class="fin-amount"><t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(student.outstanding_amount)"/></div> <div class="fin-amount" t-attf-title="#{student.currency_id.symbol} {'{0:,.2f}'.format(student.outstanding_amount)}"><t t-esc="student.currency_id.symbol"/>&amp;nbsp;<t t-esc="'{0:,.2f}'.format(student.outstanding_amount)"/></div>
</div> </div>
<div class="fin-icon-box"><i class="fa fa-exclamation-circle"/></div> <div class="fin-icon-box"><i class="fa fa-exclamation-circle"/></div>
</div> </div>
@ -1123,11 +1168,11 @@
<div class="col-lg-4"> <div class="col-lg-4">
<!-- Announcements/Upcoming Card --> <!-- Announcements/Upcoming Card -->
<div class="premium-card" style="height: calc(100% - 16px);"> <div class="premium-card" style="height: calc(100% - 16px);">
<div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3"> <div class="d-flex align-items-center justify-content-between border-bottom pb-2 mb-3 flex-wrap gap-2">
<h5 class="font-weight-bold text-dark mb-0"> <h5 class="font-weight-bold text-dark mb-0 text-truncate" style="min-width: 0; flex: 1;">
<i class="fa fa-calendar-check-o text-warning mr-2"/>Upcoming <i class="fa fa-calendar-check-o text-warning me-2 mr-2"/> Upcoming
</h5> </h5>
<a href="#" class="header-link-action">View Calendar</a> <a href="#" class="header-link-action text-nowrap">View Calendar</a>
</div> </div>
<t t-if="exams"> <t t-if="exams">
@ -1139,7 +1184,7 @@
<div class="event-month"><t t-esc="exam.start_date.strftime('%b')"/></div> <div class="event-month"><t t-esc="exam.start_date.strftime('%b')"/></div>
</div> </div>
<div class="event-info"> <div class="event-info">
<div class="event-title"><i class="fa fa-graduation-cap mr-1 text-primary"/> <t t-esc="exam.name"/></div> <div class="event-title"><i class="fa fa-graduation-cap mr-1 text-primary me-1"/> <t t-esc="exam.name"/></div>
<div class="event-desc"><t t-esc="exam.code"/> - <t t-esc="exam.exam_type_id.name"/></div> <div class="event-desc"><t t-esc="exam.code"/> - <t t-esc="exam.exam_type_id.name"/></div>
</div> </div>
</div> </div>
@ -1172,9 +1217,9 @@
</div> </div>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>
@ -1412,7 +1457,7 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
.card-deco-dots { .card-deco-dots {
position: absolute; display: none;position: absolute;
top: 20px; top: 20px;
right: 24px; right: 24px;
color: #d1f1f1; color: #d1f1f1;
@ -1599,6 +1644,18 @@
} }
/* Responsive Design overrides */ /* Responsive Design overrides */
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
@media (max-width: 991px) { @media (max-width: 991px) {
.portal-layout-wrapper { .portal-layout-wrapper {
flex-direction: column; flex-direction: column;
@ -1649,10 +1706,18 @@
margin: 0; margin: 0;
width: auto; width: auto;
} }
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
} }
@media (max-width: 575px) { @media (max-width: 575px) {
.portal-header { .portal-header {
padding: 0 12px; padding: 0 12px !important;
height: 60px; height: 60px;
} }
.portal-logo-area span { .portal-logo-area span {
@ -1697,10 +1762,10 @@
<i class="fa fa-angle-down"/> <i class="fa fa-angle-down"/>
</div> </div>
<div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;"> <div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;">
<a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted"/> My Profile</a> <a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted me-2"/> My Profile</a>
<a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted"/> Dashboard</a> <a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted me-2"/> Dashboard</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2"/> Logout</a> <a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2 me-2"/> Logout</a>
</div> </div>
</div> </div>
</div> </div>
@ -1970,9 +2035,9 @@
</div> </div>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>
@ -2324,6 +2389,18 @@
} }
/* Responsive Design overrides */ /* Responsive Design overrides */
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
@media (max-width: 991px) { @media (max-width: 991px) {
.portal-layout-wrapper { .portal-layout-wrapper {
flex-direction: column; flex-direction: column;
@ -2374,6 +2451,14 @@
margin: 0; margin: 0;
width: auto; width: auto;
} }
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
} }
</style> </style>
@ -2401,10 +2486,10 @@
<i class="fa fa-angle-down"/> <i class="fa fa-angle-down"/>
</div> </div>
<div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;"> <div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;">
<a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted"/> My Profile</a> <a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted me-2"/> My Profile</a>
<a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted"/> Dashboard</a> <a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted me-2"/> Dashboard</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2"/> Logout</a> <a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2 me-2"/> Logout</a>
</div> </div>
</div> </div>
</div> </div>
@ -2484,7 +2569,7 @@
<!-- Overview Summary Section --> <!-- Overview Summary Section -->
<div class="premium-card"> <div class="premium-card">
<h5 class="font-weight-bold mb-3 text-dark"> <h5 class="font-weight-bold mb-3 text-dark">
<i class="fa fa-credit-card text-primary mr-2"/>Financial Statement Summary <i class="fa fa-credit-card text-primary mr-2 me-2"/> Financial Statement Summary
</h5> </h5>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
@ -2525,12 +2610,12 @@
<div class="d-flex align-items-center justify-content-between border-bottom pb-3 mb-3"> <div class="d-flex align-items-center justify-content-between border-bottom pb-3 mb-3">
<div> <div>
<h5 class="font-weight-bold text-dark mb-1"> <h5 class="font-weight-bold text-dark mb-1">
<i class="fa fa-shopping-cart text-primary mr-2"/>Itemized Pending Dues <i class="fa fa-shopping-cart text-primary mr-2 me-2"/> Itemized Pending Dues
</h5> </h5>
<p class="text-muted small mb-0">Select individual fee items or pay all outstanding dues online securely via Razorpay / UPI.</p> <p class="text-muted small mb-0">Select individual fee items or pay all outstanding dues online securely via Razorpay / UPI.</p>
</div> </div>
<a href="/my/education/payment/history" class="btn btn-sm btn-outline-info font-weight-bold" style="border-radius: 8px;"> <a href="/my/education/payment/history" class="btn btn-sm btn-outline-info font-weight-bold" style="border-radius: 8px;">
<i class="fa fa-history mr-1"/> Payment History <i class="fa fa-history mr-1 me-1"/> Payment History
</a> </a>
</div> </div>
@ -2561,7 +2646,7 @@
<td><span t-field="fee.academic_year_id.name"/></td> <td><span t-field="fee.academic_year_id.name"/></td>
<td> <td>
<span class="badge bg-light text-dark p-2"> <span class="badge bg-light text-dark p-2">
<i class="fa fa-calendar-o mr-1"/> <span t-field="fee.assignment_date"/> <i class="fa fa-calendar-o mr-1 me-1"/> <span t-field="fee.assignment_date"/>
</span> </span>
</td> </td>
<td class="text-right font-weight-bold text-dark" style="font-size: 1.05rem;"> <td class="text-right font-weight-bold text-dark" style="font-size: 1.05rem;">
@ -2582,13 +2667,13 @@
<div class="d-flex align-items-center justify-content-between p-3 rounded-lg" style="background: #e6fffa; border: 1px solid #b2f5ea; border-radius: 12px;"> <div class="d-flex align-items-center justify-content-between p-3 rounded-lg" style="background: #e6fffa; border: 1px solid #b2f5ea; border-radius: 12px;">
<div> <div>
<span class="text-muted font-weight-bold">Selected Items: </span> <span class="text-muted font-weight-bold">Selected Items: </span>
<span id="selectedCount" class="font-weight-bold text-dark mr-3">0</span> <span id="selectedCount" class="font-weight-bold text-dark mr-3 me-3">0</span>
<span class="text-muted font-weight-bold">Total Dues to Pay: </span> <span class="text-muted font-weight-bold">Total Dues to Pay: </span>
<span id="selectedTotalAmount" class="font-weight-bold" style="color: #2b6cb0; font-size: 1.25rem;">₹0.00</span> <span id="selectedTotalAmount" class="font-weight-bold" style="color: #2b6cb0; font-size: 1.25rem;">₹0.00</span>
</div> </div>
<div> <div>
<button type="button" id="btnPayOnline" class="btn text-white font-weight-bold px-4 py-2" style="background: linear-gradient(135deg, #1f8f8f 0%, #144d4d 100%); border-radius: 10px; box-shadow: 0 4px 12px rgba(31, 143, 143, 0.25);"> <button type="button" id="btnPayOnline" class="btn text-white font-weight-bold px-4 py-2" style="background: linear-gradient(135deg, #1f8f8f 0%, #144d4d 100%); border-radius: 10px; box-shadow: 0 4px 12px rgba(31, 143, 143, 0.25);">
<i class="fa fa-shield mr-2"/> Proceed to Payment <i class="fa fa-shield mr-2 me-2"/> Proceed to Payment
</button> </button>
</div> </div>
</div> </div>
@ -2606,7 +2691,7 @@
<div class="premium-card"> <div class="premium-card">
<div class="d-flex align-items-center justify-content-between border-bottom pb-3 mb-3"> <div class="d-flex align-items-center justify-content-between border-bottom pb-3 mb-3">
<h5 class="font-weight-bold text-dark mb-0"> <h5 class="font-weight-bold text-dark mb-0">
<i class="fa fa-exchange text-primary mr-2"/>Ledger &amp; Transaction Details <i class="fa fa-exchange text-primary mr-2 me-2"/> Ledger &amp; Transaction Details
</h5> </h5>
<button class="btn-filter"> <button class="btn-filter">
<i class="fa fa-filter"/> Filter <i class="fa fa-filter"/> Filter
@ -2628,7 +2713,7 @@
<t t-if="ledger_lines"> <t t-if="ledger_lines">
<t t-foreach="ledger_lines" t-as="line"> <t t-foreach="ledger_lines" t-as="line">
<tr> <tr>
<td><i class="fa fa-calendar-o mr-2 text-muted"/> <span t-field="line.date"/></td> <td><i class="fa fa-calendar-o mr-2 text-muted me-2"/> <span t-field="line.date"/></td>
<td class="font-weight-bold text-dark"><span t-field="line.description"/></td> <td class="font-weight-bold text-dark"><span t-field="line.description"/></td>
<td class="text-right text-danger font-weight-bold"> <td class="text-right text-danger font-weight-bold">
<t t-if="line.debit &gt; 0">+ <t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(line.debit)"/></t> <t t-if="line.debit &gt; 0">+ <t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(line.debit)"/></t>
@ -2663,7 +2748,7 @@
<!-- Official Payment Receipts --> <!-- Official Payment Receipts -->
<div class="premium-card"> <div class="premium-card">
<h5 class="font-weight-bold mb-3 text-dark"> <h5 class="font-weight-bold mb-3 text-dark">
<i class="fa fa-file-text-o text-info mr-2"/>Official Payment Receipts <i class="fa fa-file-text-o text-info mr-2 me-2"/> Official Payment Receipts
</h5> </h5>
<div class="table-responsive"> <div class="table-responsive">
<table class="premium-table"> <table class="premium-table">
@ -2681,7 +2766,7 @@
<t t-foreach="payments" t-as="pay"> <t t-foreach="payments" t-as="pay">
<tr> <tr>
<td class="font-weight-bold text-dark"><span t-field="pay.receipt_number"/></td> <td class="font-weight-bold text-dark"><span t-field="pay.receipt_number"/></td>
<td><i class="fa fa-calendar-o mr-2 text-muted"/> <span t-field="pay.payment_date"/></td> <td><i class="fa fa-calendar-o mr-2 text-muted me-2"/> <span t-field="pay.payment_date"/></td>
<td><span t-field="pay.payment_method"/></td> <td><span t-field="pay.payment_method"/></td>
<td class="text-right font-weight-bold text-success"> <td class="text-right font-weight-bold text-success">
<t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(pay.amount)"/> <t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(pay.amount)"/>
@ -2745,7 +2830,7 @@
function initiatePayment(selectedFeeIds) { function initiatePayment(selectedFeeIds) {
if (!btnPayOnline) return; if (!btnPayOnline) return;
btnPayOnline.disabled = true; btnPayOnline.disabled = true;
btnPayOnline.innerHTML = '&lt;i class="fa fa-spinner fa-spin mr-2"&gt;&lt;/i&gt; Initializing...'; btnPayOnline.innerHTML = '&lt;i class="fa fa-spinner fa-spin mr-2 me-2"&gt;&lt;/i&gt; Initializing...';
fetch('/my/education/payment/create', { fetch('/my/education/payment/create', {
method: 'POST', method: 'POST',
@ -2765,7 +2850,7 @@
if (data.error) { if (data.error) {
alert("Payment Error: " + data.error); alert("Payment Error: " + data.error);
btnPayOnline.disabled = false; btnPayOnline.disabled = false;
btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2"&gt;&lt;/i&gt; Proceed to Payment'; btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2 me-2"&gt;&lt;/i&gt; Proceed to Payment';
return; return;
} }
@ -2797,7 +2882,7 @@
"modal": { "modal": {
"ondismiss": function() { "ondismiss": function() {
btnPayOnline.disabled = false; btnPayOnline.disabled = false;
btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2"&gt;&lt;/i&gt; Proceed to Payment'; btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2 me-2"&gt;&lt;/i&gt; Proceed to Payment';
} }
} }
}; };
@ -2806,13 +2891,13 @@
} else { } else {
alert("Razorpay SDK could not be loaded. Please check your internet connection."); alert("Razorpay SDK could not be loaded. Please check your internet connection.");
btnPayOnline.disabled = false; btnPayOnline.disabled = false;
btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2"&gt;&lt;/i&gt; Proceed to Payment'; btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2 me-2"&gt;&lt;/i&gt; Proceed to Payment';
} }
}) })
.catch(function(err) { .catch(function(err) {
alert("Server Connection Error: " + err); alert("Server Connection Error: " + err);
btnPayOnline.disabled = false; btnPayOnline.disabled = false;
btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2"&gt;&lt;/i&gt; Proceed to Payment'; btnPayOnline.innerHTML = '&lt;i class="fa fa-shield mr-2 me-2"&gt;&lt;/i&gt; Proceed to Payment';
}); });
} }
@ -2863,9 +2948,9 @@
</script> </script>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>
@ -3118,10 +3203,14 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
flex: 1;
min-width: 0;
} }
.document-icon-avatar { .document-icon-avatar {
width: 42px; width: 42px;
height: 42px; height: 42px;
min-width: 42px;
min-height: 42px;
border-radius: 50%; border-radius: 50%;
background: #e6f7f7; background: #e6f7f7;
color: #1f8f8f; color: #1f8f8f;
@ -3129,6 +3218,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.15rem; font-size: 1.15rem;
flex-shrink: 0;
} }
.document-meta-title { .document-meta-title {
font-size: 0.95rem; font-size: 0.95rem;
@ -3256,6 +3346,18 @@
} }
/* Responsive Design overrides */ /* Responsive Design overrides */
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
@media (max-width: 991px) { @media (max-width: 991px) {
.portal-layout-wrapper { .portal-layout-wrapper {
flex-direction: column; flex-direction: column;
@ -3285,6 +3387,70 @@
overflow: visible !important; overflow: visible !important;
} }
} }
@media (max-width: 767px) {
.portal-header {
padding: 0 20px;
}
.portal-sidebar {
padding: 12px 8px;
gap: 4px;
}
.sidebar-link {
padding: 8px 12px;
font-size: 0.85rem;
gap: 8px;
}
.sidebar-link span {
display: none;
}
.sidebar-link i {
font-size: 1.3rem;
margin: 0;
width: auto;
}
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
}
@media (max-width: 575px) {
.portal-header {
padding: 0 12px !important;
height: 60px;
}
.portal-logo-area span {
display: none;
}
.premium-card {
padding: 20px !important;
border-radius: 18px !important;
}
.document-upload-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.btn-choose-file {
width: 100%;
justify-content: center;
}
.upload-footer-actions {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.btn-upload-submit {
width: 100%;
justify-content: center;
}
.encrypted-label {
justify-content: center;
}
}
</style> </style>
<!-- Top Header --> <!-- Top Header -->
@ -3311,10 +3477,10 @@
<i class="fa fa-angle-down"/> <i class="fa fa-angle-down"/>
</div> </div>
<div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;"> <div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;">
<a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted"/> My Profile</a> <a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted me-2"/> My Profile</a>
<a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted"/> Dashboard</a> <a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted me-2"/> Dashboard</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2"/> Logout</a> <a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2 me-2"/> Logout</a>
</div> </div>
</div> </div>
</div> </div>
@ -3590,9 +3756,9 @@
</div> </div>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
<!-- Frontend Logic to show selected file name --> <!-- Frontend Logic to show selected file name -->
@ -3632,10 +3798,10 @@
<!-- Header --> <!-- Header -->
<div class="d-flex align-items-center justify-content-between border-bottom pb-3 mb-4"> <div class="d-flex align-items-center justify-content-between border-bottom pb-3 mb-4">
<h5 class="font-weight-bold text-dark mb-0"> <h5 class="font-weight-bold text-dark mb-0">
<i class="fa fa-shield text-success mr-2"/>Secure Online Checkout <i class="fa fa-shield text-success mr-2 me-2"/> Secure Online Checkout
</h5> </h5>
<span class="badge badge-pill badge-light text-muted p-2" style="border: 1px solid #e2e8f0;"> <span class="badge badge-pill badge-light text-muted p-2" style="border: 1px solid #e2e8f0;">
<i class="fa fa-lock text-success mr-1"/> PCI-DSS Compliant <i class="fa fa-lock text-success mr-1 me-1"/> PCI-DSS Compliant
</span> </span>
</div> </div>
@ -3727,7 +3893,7 @@
</div> </div>
<button type="button" class="btn btn-primary btn-block shadow-sm py-2 font-weight-bold mt-4" style="border-radius: 8px;" onclick="startRealtimePayment()"> <button type="button" class="btn btn-primary btn-block shadow-sm py-2 font-weight-bold mt-4" style="border-radius: 8px;" onclick="startRealtimePayment()">
<i class="fa fa-lock mr-2"/> Submit Secure Payment <i class="fa fa-lock mr-2 me-2"/> Submit Secure Payment
</button> </button>
</form> </form>
</div> </div>
@ -4019,6 +4185,78 @@
.dropdown-toggle::after { .dropdown-toggle::after {
display: none !important; display: none !important;
} }
@media (max-width: 991px) {
.portal-layout-wrapper {
flex-direction: column;
}
.portal-sidebar {
width: 100% !important;
border-right: none !important;
border-bottom: 1px solid rgba(31, 143, 143, 0.1);
padding: 16px 20px;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
position: static !important;
max-height: none !important;
}
.sidebar-link {
padding: 10px 16px;
}
.sidebar-promo-card {
display: none !important;
}
.portal-content-area {
padding: 24px !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
}
}
@media (max-width: 767px) {
.portal-header {
padding: 0 20px;
}
.portal-sidebar {
padding: 12px 8px;
gap: 4px;
}
.sidebar-link {
padding: 8px 12px;
font-size: 0.85rem;
gap: 8px;
}
.sidebar-link span {
display: none;
}
.sidebar-link i {
font-size: 1.3rem;
margin: 0;
width: auto;
}
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
}
@media (max-width: 575px) {
.portal-header {
padding: 0 12px !important;
height: 60px;
}
.portal-logo-area span {
display: none;
}
.premium-card {
padding: 20px !important;
border-radius: 18px !important;
}
}
</style> </style>
<!-- Top Header --> <!-- Top Header -->
@ -4045,10 +4283,10 @@
<i class="fa fa-angle-down"/> <i class="fa fa-angle-down"/>
</div> </div>
<div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;"> <div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;">
<a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted"/> My Profile</a> <a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted me-2"/> My Profile</a>
<a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted"/> Dashboard</a> <a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted me-2"/> Dashboard</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2"/> Logout</a> <a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2 me-2"/> Logout</a>
</div> </div>
</div> </div>
</div> </div>
@ -4183,7 +4421,7 @@
<div class="premium-card"> <div class="premium-card">
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2"> <div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2">
<h5 class="font-weight-bold text-dark mb-0"> <h5 class="font-weight-bold text-dark mb-0">
<i class="fa fa-list text-primary mr-2"/> Attendance Session Logs (<t t-esc="total_sessions"/> Records) <i class="fa fa-list text-primary mr-2 me-2"/> Attendance Session Logs (<t t-esc="total_sessions"/> Records)
</h5> </h5>
</div> </div>
@ -4202,7 +4440,7 @@
<t t-foreach="attendance_records" t-as="att"> <t t-foreach="attendance_records" t-as="att">
<tr> <tr>
<td style="font-weight: 700; color: #1e293b;"> <td style="font-weight: 700; color: #1e293b;">
<i class="fa fa-calendar text-muted mr-1"/> <i class="fa fa-calendar text-muted mr-1 me-1"/>
<t t-esc="att.date"/> <t t-esc="att.date"/>
</td> </td>
<td style="color: #475569;"> <td style="color: #475569;">
@ -4241,9 +4479,9 @@
</div> </div>
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>
@ -4253,7 +4491,7 @@
<div class="container py-4"> <div class="container py-4">
<div class="card mb-4 border-0 shadow-sm"> <div class="card mb-4 border-0 shadow-sm">
<div class="card-header text-white border-0 py-3" style="background-color: #1f8f8f !important;"> <div class="card-header text-white border-0 py-3" style="background-color: #1f8f8f !important;">
<h4 class="mb-0 font-weight-bold"><i class="fa fa-graduation-cap mr-2"/> <t t-esc="exam.name"/></h4> <h4 class="mb-0 font-weight-bold"><i class="fa fa-graduation-cap mr-2 me-2"/> <t t-esc="exam.name"/></h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row mb-4"> <div class="row mb-4">
@ -4305,7 +4543,7 @@
</table> </table>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<a href="/my/education/dashboard" class="btn text-white px-4 py-2" style="background-color: #1f8f8f; border-radius: 8px;"><i class="fa fa-arrow-left mr-1"/> Back to Dashboard</a> <a href="/my/education/dashboard" class="btn text-white px-4 py-2" style="background-color: #1f8f8f; border-radius: 8px;"><i class="fa fa-arrow-left mr-1 me-1"/> Back to Dashboard</a>
</div> </div>
</div> </div>
</div> </div>
@ -4319,11 +4557,11 @@
<div class="container py-4"> <div class="container py-4">
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header text-white border-0 py-3" style="background-color: #1f8f8f !important;"> <div class="card-header text-white border-0 py-3" style="background-color: #1f8f8f !important;">
<h4 class="mb-0 font-weight-bold"><i class="fa fa-bullhorn mr-2"/> <t t-esc="announcement.title"/></h4> <h4 class="mb-0 font-weight-bold"><i class="fa fa-bullhorn mr-2 me-2"/> <t t-esc="announcement.title"/></h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="mb-4 text-muted border-bottom pb-2"> <div class="mb-4 text-muted border-bottom pb-2">
<i class="fa fa-calendar mr-1"/> Published on: <strong><t t-esc="announcement.date_published"/></strong> <i class="fa fa-calendar mr-1 me-1"/> Published on: <strong><t t-esc="announcement.date_published"/></strong>
<span class="mx-2">|</span> <span class="mx-2">|</span>
<span class="badge badge-secondary px-2 py-1"><t t-esc="announcement.announcement_type.capitalize()"/></span> <span class="badge badge-secondary px-2 py-1"><t t-esc="announcement.announcement_type.capitalize()"/></span>
</div> </div>
@ -4331,7 +4569,7 @@
<t t-out="announcement.content"/> <t t-out="announcement.content"/>
</div> </div>
<div class="mt-5"> <div class="mt-5">
<a href="/my/education/dashboard" class="btn text-white px-4 py-2" style="background-color: #1f8f8f; border-radius: 8px;"><i class="fa fa-arrow-left mr-1"/> Back to Dashboard</a> <a href="/my/education/dashboard" class="btn text-white px-4 py-2" style="background-color: #1f8f8f; border-radius: 8px;"><i class="fa fa-arrow-left mr-1 me-1"/> Back to Dashboard</a>
</div> </div>
</div> </div>
</div> </div>
@ -4541,7 +4779,7 @@
box-shadow: 0 8px 24px 0 rgba(31, 143, 143, 0.06) !important; box-shadow: 0 8px 24px 0 rgba(31, 143, 143, 0.06) !important;
} }
.card-deco-dots { .card-deco-dots {
position: absolute; display: none;position: absolute;
top: 20px; top: 20px;
right: 24px; right: 24px;
color: #d1f1f1; color: #d1f1f1;
@ -4894,6 +5132,18 @@
} }
/* Responsive Design overrides */ /* Responsive Design overrides */
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
@media (max-width: 991px) { @media (max-width: 991px) {
.portal-layout-wrapper { .portal-layout-wrapper {
flex-direction: column; flex-direction: column;
@ -4911,7 +5161,8 @@
max-height: none !important; max-height: none !important;
} }
.sidebar-link { .sidebar-link {
padding: 10px 16px; padding: 10px 5px;
gap: 3px !important;
} }
.sidebar-promo-card { .sidebar-promo-card {
display: none !important; display: none !important;
@ -4944,10 +5195,18 @@
margin: 0; margin: 0;
width: auto; width: auto;
} }
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
} }
@media (max-width: 575px) { @media (max-width: 575px) {
.portal-header { .portal-header {
padding: 0 12px; padding: 0 12px !important;
height: 60px; height: 60px;
} }
.portal-logo-area span { .portal-logo-area span {
@ -4957,6 +5216,27 @@
padding: 20px !important; padding: 20px !important;
border-radius: 18px !important; border-radius: 18px !important;
} }
.document-upload-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.btn-choose-file {
width: 100%;
justify-content: center;
}
.upload-footer-actions {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.btn-upload-submit {
width: 100%;
justify-content: center;
}
.encrypted-label {
justify-content: center;
}
} }
</style> </style>
@ -4984,10 +5264,10 @@
<i class="fa fa-angle-down"/> <i class="fa fa-angle-down"/>
</div> </div>
<div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;"> <div class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userMenuDropdown" style="border-radius: 12px; border: 1px solid rgba(31, 143, 143, 0.1); margin-top: 10px;">
<a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted"/> My Profile</a> <a class="dropdown-item py-2" href="/my/education/profile" style="font-weight: 600; color: #4a5568;"><i class="fa fa-user-circle mr-2 text-muted me-2"/> My Profile</a>
<a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted"/> Dashboard</a> <a class="dropdown-item py-2" href="/my/education/dashboard" style="font-weight: 600; color: #4a5568;"><i class="fa fa-home mr-2 text-muted me-2"/> Dashboard</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2"/> Logout</a> <a class="dropdown-item py-2 text-danger" href="/web/session/logout?redirect=/" style="font-weight: 600;"><i class="fa fa-sign-out mr-2 me-2"/> Logout</a>
</div> </div>
</div> </div>
</div> </div>
@ -5060,9 +5340,9 @@
</div> <!-- End portal-layout-wrapper --> </div> <!-- End portal-layout-wrapper -->
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div class="portal-footer-area">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Override web.layout to replace the default "Odoo" browser tab title -->
<template id="web_layout_title_override" inherit_id="web.layout" name="Metatroncube EduERP - Page Title">
<xpath expr="//title" position="replace">
<title t-esc="title or 'Metatroncube EduERP'"/>
</xpath>
</template>
</odoo>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Add Timetable to Portal Home --> <!-- Add Timetable to Portal Home -->
<template id="portal_my_home_timetable" name="Portal My Home: Timetable" inherit_id="portal.portal_my_home" priority="40"> <template id="portal_my_home_timetable" name="Portal My Home: Timetable" inherit_id="portal.portal_my_home" priority="40">
@ -158,6 +158,18 @@
.breadcrumb-premium a { color: #1f8f8f; text-decoration: none; } .breadcrumb-premium a { color: #1f8f8f; text-decoration: none; }
.breadcrumb-premium span { color: #a0aec0; } .breadcrumb-premium span { color: #a0aec0; }
.breadcrumb-premium .active { color: #1f8f8f; } .breadcrumb-premium .active { color: #1f8f8f; }
.portal-footer-area {
height: 60px;
background: #ffffff;
border-top: 1px solid rgba(31, 143, 143, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
font-size: 0.9rem;
color: #718096;
font-weight: 600;
}
.timetable-card { .timetable-card {
border-radius: 12px; border-radius: 12px;
@ -205,6 +217,78 @@
text-align: center; text-align: center;
margin-top: 2rem; margin-top: 2rem;
} }
@media (max-width: 991px) {
.portal-layout-wrapper {
flex-direction: column;
}
.portal-sidebar {
width: 100% !important;
border-right: none !important;
border-bottom: 1px solid rgba(31, 143, 143, 0.1);
padding: 16px 20px;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
position: static !important;
max-height: none !important;
}
.sidebar-link {
padding: 10px 16px;
}
.sidebar-promo-card {
display: none !important;
}
.portal-content-area {
padding: 24px !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
}
}
@media (max-width: 767px) {
.portal-header {
padding: 0 20px;
}
.portal-sidebar {
padding: 12px 8px;
gap: 4px;
}
.sidebar-link {
padding: 8px 12px;
font-size: 0.85rem;
gap: 8px;
}
.sidebar-link span {
display: none;
}
.sidebar-link i {
font-size: 1.3rem;
margin: 0;
width: auto;
}
.portal-footer-area {
height: auto;
flex-direction: column;
justify-content: center;
gap: 8px;
padding: 16px 20px;
text-align: center;
}
}
@media (max-width: 575px) {
.portal-header {
padding: 0 12px !important;
height: 60px;
}
.portal-logo-area span {
display: none;
}
.timetable-card {
padding: 20px !important;
border-radius: 18px !important;
}
}
</style> </style>
<!-- Top Header --> <!-- Top Header -->
@ -342,6 +426,12 @@
</t> </t>
</div> </div>
</div> </div>
<!-- Bottom Footer Area -->
<div class="portal-footer-area">
<span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div>
</t> </t>
</template> </template>

View File

@ -48,10 +48,10 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
.o_main_navbar { .o_main_navbar {
--o-navbar-height: 70px; --o-navbar-height: 70px;
font-size: 0.975rem; font-size: 0.975rem;
background: linear-gradient(135deg, #e6f7f7 0%, #d1f1f1 100%) !important; background: linear-gradient(135deg, #1f8f8f 0%, #144d4d 55%, #0d3d3d 100%) !important;
border-bottom: 1px solid #e2e8f0 !important; border-bottom: 1px solid #0d3535 !important;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05) !important; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2) !important;
color: #144d4d !important; color: #ffffff !important;
width: 100%; width: 100%;
z-index: 100; // Keep above sidebar z-index: 100; // Keep above sidebar
@ -74,28 +74,28 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
background-color: transparent !important; background-color: transparent !important;
} }
// Navbar Links // Navbar Links and Icons
.o_menu_item > a, .o_nav_entry, .dropdown-toggle { .o_menu_item > a, .o_nav_entry, .dropdown-toggle, i, .fa {
color: #144d4d !important; color: #ffffff !important;
font-weight: 600 !important; font-weight: 600 !important;
&:hover, &:focus { &:hover, &:focus {
color: #144d4d !important; color: #ffffff !important;
background-color: rgba(20, 77, 77, 0.05) !important; // Very light teal hover background-color: rgba(255, 255, 255, 0.1) !important;
} }
} }
// App Switcher Icon (Grid) // App Switcher Icon (Grid)
.o_menu_toggle { .o_menu_toggle {
color: #144d4d !important; color: #ffffff !important;
&:hover { &:hover {
background-color: rgba(20, 77, 77, 0.1) !important; background-color: rgba(255, 255, 255, 0.1) !important;
} }
} }
// Brand Name (e.g. Education) // Brand Name (e.g. Education)
.o_menu_brand { .o_menu_brand {
color: #144d4d !important; color: #ffffff !important;
font-weight: 800 !important; font-weight: 800 !important;
} }
} }
@ -179,9 +179,9 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
&::before, &::after { display: none !important; } // Remove standard chevron shapes &::before, &::after { display: none !important; } // Remove standard chevron shapes
&.btn-primary { &.btn-primary, &[aria-current="step"], &[aria-checked="true"], &.text-bg-primary, &.o_arrow_button_current {
color: var(--o-brand-primary) !important; color: var(--o-brand-primary) !important;
background-color: rgba(37, 99, 235, 0.1) !important; background-color: rgba(31, 143, 143, 0.15) !important;
border-radius: 20px !important; border-radius: 20px !important;
padding: 4px 16px !important; padding: 4px 16px !important;
} }
@ -222,6 +222,7 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
// Input fields (Modern SaaS style - clean gray boxes that highlight) // Input fields (Modern SaaS style - clean gray boxes that highlight)
.o_field_widget { .o_field_widget {
input:not([type="checkbox"]):not([type="radio"]), select, textarea { input:not([type="checkbox"]):not([type="radio"]), select, textarea {
width: 100% !important;
background-color: #f8fafc !important; background-color: #f8fafc !important;
border: 1px solid #e2e8f0 !important; border: 1px solid #e2e8f0 !important;
border-radius: 6px !important; border-radius: 6px !important;
@ -237,6 +238,23 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
outline: none !important; outline: none !important;
} }
} }
// File upload button
.o_select_file_button {
background-color: var(--o-brand-primary) !important;
color: #ffffff !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
padding: 8px 16px !important;
transition: all 0.2s ease;
&:hover {
background-color: #177a7a !important;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(31, 143, 143, 0.3) !important;
}
}
} }
// Group titles / Separators // Group titles / Separators
@ -609,6 +627,17 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
box-shadow: 0 10px 30px rgba(15,23,42,0.12) !important; box-shadow: 0 10px 30px rgba(15,23,42,0.12) !important;
padding: 6px !important; padding: 6px !important;
.dropdown-header:not(:first-child) {
margin-top: .3em;
font-size: 14px !important;
font-weight: bold;
padding-left: 15px !important;
}
.o-dropdown-item.dropdown-item.o-navigable.o_dropdown_menu_group_entry {
padding-left: 25px !important;
}
.dropdown-item { .dropdown-item {
border-radius: 8px !important; border-radius: 8px !important;
font-size: 0.875rem !important; font-size: 0.875rem !important;

View File

@ -48,17 +48,24 @@
/* Promo Card */ /* Promo Card */
.o_sidebar_custom .sidebar-promo-card { .o_sidebar_custom .sidebar-promo-card {
background: linear-gradient(135deg, #e6f7f7 0%, #d1f1f1 100%); background: url('/education_dashboard/static/src/img/card-img.png') center center / cover no-repeat;
border-radius: 16px; border-radius: 16px;
padding: 20px 16px; padding: 20px 16px;
text-align: center; text-align: center;
margin: auto 16px 20px 16px; margin: auto 16px 20px 16px;
border: 1px solid rgba(31, 143, 143, 0.15); border: 1px solid rgba(31, 143, 143, 0.2);
box-shadow: 0 4px 15px rgba(0,0,0,0.1); box-shadow: 0 4px 20px rgba(0,0,0,0.18);
overflow: hidden;
position: relative;
}
.o_sidebar_custom .sidebar-promo-card-inner {
padding: 20px 16px;
position: relative;
z-index: 1;
} }
.o_sidebar_custom .promo-icon-box { .o_sidebar_custom .promo-icon-box {
font-size: 2.5rem; font-size: 2.5rem;
color: #1f8f8f; color: #0d3d3d;
margin-bottom: 12px; margin-bottom: 12px;
} }
</style> </style>
@ -107,11 +114,11 @@
<!-- Sidebar Promo Card --> <!-- Sidebar Promo Card -->
<div class="sidebar-promo-card mt-auto"> <div class="sidebar-promo-card mt-auto">
<div class="promo-icon-box"> <div class="promo-icon-box">
<i class="fa fa-rocket"/> <i class="fa fa-rocket" style="color: #0d3d3d !important;"></i>
</div> </div>
<h6 class="fw-bold mb-1" style="color: #144d4d;">Admin Workspace</h6> <h6 class="fw-bold mb-1" style="color: #0d3d3d;">Admin Workspace</h6>
<p class="small mb-0" style="color: #4a5568;">Manage your entire</p> <p class="small mb-0" style="color: #0d3d3d;">Manage your entire</p>
<p class="small mb-0" style="color: #4a5568;">institution efficiently.</p> <p class="small mb-0" style="color: #0d3d3d;">institution efficiently.</p>
</div> </div>
</div> </div>
</t> </t>

1
check.py Normal file
View File

@ -0,0 +1 @@
user = env["res.users"].search([("login", "=", "demo.student")]); student = env["education.student"].search([("id", "=", 10)]); print("USER:", user.id); print("STUDENT PORTAL USER:", student.portal_user_id.id); print("USER STUDENT_ID:", user.student_id.id)

120
create_hierarchy.py Normal file
View File

@ -0,0 +1,120 @@
import traceback
def get_or_create(model, domain, vals):
rec = env[model].search(domain, limit=1)
if not rec:
rec = env[model].create(vals)
print(f"Created {model}: {rec.id}")
else:
print(f"Found {model}: {rec.id}")
return rec
try:
# 1. Company
company = env["res.company"].search([], limit=1)
# 2. Campus
campus = get_or_create("education.campus", [("name", "=", "Test Campus")], {
"name": "Test Campus",
"company_id": company.id,
"code": "TC"
})
# 3. Department
dept = get_or_create("education.department", [("name", "=", "Test Dept")], {
"name": "Test Dept",
"company_id": company.id,
"campus_id": campus.id,
"code": "TD"
})
# 4. Academic Level
level = get_or_create("education.academic.level", [("name", "=", "Test Level")], {
"name": "Test Level",
"company_id": company.id,
"code": "TL"
})
# 5. Program
program = get_or_create("education.program", [("name", "=", "Test Program")], {
"name": "Test Program",
"department_id": dept.id,
"level_id": level.id,
"code": "TP"
})
# 6. Academic Year
year = get_or_create("education.academic.year", [("name", "=", "Test Year")], {
"name": "Test Year",
"company_id": company.id,
"start_date": "2026-01-01",
"end_date": "2026-12-31"
})
# 7. Academic Term
term = get_or_create("education.academic.term", [("name", "=", "Test Term")], {
"name": "Test Term",
"academic_year_id": year.id,
"start_date": "2026-01-01",
"end_date": "2026-06-30"
})
# 8. Batch
batch = get_or_create("education.batch", [("name", "=", "Test Batch")], {
"name": "Test Batch",
"program_id": program.id,
"academic_year_id": year.id,
"start_date": "2026-01-01",
"end_date": "2026-12-31"
})
# 9. Semester
semester = get_or_create("education.semester", [("name", "=", "Test Semester")], {
"name": "Test Semester",
"program_id": program.id,
"academic_year_id": year.id,
"sequence": 1
})
# res.users (demo.student)
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
# 10. Student
student = env["education.student"].search([("portal_user_id", "=", user.id)], limit=1)
if not student:
student = env["education.student"].create({
"full_name": "Demo Student",
"first_name": "Demo",
"last_name": "Student",
"email": "demo.student@example.com",
"portal_user_id": user.id,
"company_id": company.id,
"campus_id": campus.id,
"department_id": dept.id,
"level_id": level.id,
"program_id": program.id,
"academic_year_id": year.id,
"academic_term_id": term.id,
"batch_id": batch.id,
"semester_id": semester.id,
"current_semester_id": semester.id,
"date_of_birth": "2000-01-01",
"gender": "other",
"admission_number": "ADM-9999",
"student_id": "STU-9999",
"status": "active"
})
print(f"Created Student: {student.id}")
else:
print(f"Found Student: {student.id}")
env.cr.commit()
with open('/mnt/extra-addons/success.log', 'w') as f:
f.write(f"ALL DONE. Student ID: {student.id}\n")
print("SUCCESS")
except Exception as e:
print("FAILED")
traceback.print_exc()
with open('/mnt/extra-addons/error.log', 'w') as f:
f.write(traceback.format_exc())

11
create_student.py Normal file
View File

@ -0,0 +1,11 @@
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
if user:
student = env["education.student"].create({
"full_name": "Demo Student",
"first_name": "Demo",
"last_name": "Student",
"email": "demo.student@example.com",
"portal_user_id": user.id,
})
env.cr.commit()
print("CREATED STUDENT ID:", student.id, "LINKED TO USER ID:", user.id)

36
create_student_debug.py Normal file
View File

@ -0,0 +1,36 @@
import traceback
try:
campus = env["education.campus"].search([], limit=1)
if not campus:
campus = env["education.campus"].create({"name": "Test Campus"})
dept = env["education.department"].search([], limit=1)
if not dept:
dept = env["education.department"].create({"name": "Test Dept"})
prog = env["education.program"].search([], limit=1)
if not prog:
prog = env["education.program"].create({"name": "Test Program"})
year = env["education.academic.year"].search([], limit=1)
if not year:
year = env["education.academic.year"].create({"name": "2026"})
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
student = env["education.student"].create({
"full_name": "Demo Student",
"first_name": "Demo",
"last_name": "Student",
"email": "demo.student@example.com",
"portal_user_id": user.id if user else False,
"campus_id": campus.id,
"department_id": dept.id,
"program_id": prog.id,
"academic_year_id": year.id,
"date_of_birth": "2000-01-01",
"gender": "other",
"admission_number": "ADM-9999",
"student_id": "STU-9999"
})
env.cr.commit()
print("SUCCESS, STUDENT ID:", student.id)
except Exception as e:
print("ERROR DURING CREATE:")
traceback.print_exc()

39
create_student_file.py Normal file
View File

@ -0,0 +1,39 @@
import traceback
with open('/tmp/student_create.log', 'w') as f:
try:
campus = env["education.campus"].search([], limit=1)
if not campus:
campus = env["education.campus"].create({"name": "Test Campus"})
dept = env["education.department"].search([], limit=1)
if not dept:
dept = env["education.department"].create({"name": "Test Dept"})
prog = env["education.program"].search([], limit=1)
if not prog:
prog = env["education.program"].create({"name": "Test Program"})
year = env["education.academic.year"].search([], limit=1)
if not year:
year = env["education.academic.year"].create({"name": "2026"})
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
f.write(f"Campus: {campus.id}, Dept: {dept.id}, Prog: {prog.id}, Year: {year.id}, User: {user.id}\n")
student = env["education.student"].create({
"full_name": "Demo Student",
"first_name": "Demo",
"last_name": "Student",
"email": "demo.student@example.com",
"portal_user_id": user.id if user else False,
"campus_id": campus.id,
"department_id": dept.id,
"program_id": prog.id,
"academic_year_id": year.id,
"date_of_birth": "2000-01-01",
"gender": "other",
"admission_number": "ADM-9999",
"student_id": "STU-9999"
})
env.cr.commit()
f.write(f"SUCCESS, STUDENT ID: {student.id}\n")
except Exception as e:
f.write("ERROR DURING CREATE:\n")
f.write(traceback.format_exc())

23
create_student_full.py Normal file
View File

@ -0,0 +1,23 @@
campus = env["education.campus"].search([], limit=1)
dept = env["education.department"].search([], limit=1)
prog = env["education.program"].search([], limit=1)
year = env["education.academic.year"].search([], limit=1)
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
if user and campus and dept and prog and year:
student = env["education.student"].create({
"full_name": "Demo Student",
"first_name": "Demo",
"last_name": "Student",
"email": "demo.student@example.com",
"portal_user_id": user.id,
"campus_id": campus.id,
"department_id": dept.id,
"program_id": prog.id,
"academic_year_id": year.id,
"date_of_birth": "2000-01-01",
"gender": "other",
"admission_number": "ADM-9999",
"student_id": "STU-9999"
})
env.cr.commit()
print("CREATED FULL STUDENT:", student.id)

30
create_student_full2.py Normal file
View File

@ -0,0 +1,30 @@
campus = env["education.campus"].search([], limit=1)
if not campus:
campus = env["education.campus"].create({"name": "Test Campus"})
dept = env["education.department"].search([], limit=1)
if not dept:
dept = env["education.department"].create({"name": "Test Dept"})
prog = env["education.program"].search([], limit=1)
if not prog:
prog = env["education.program"].create({"name": "Test Program"})
year = env["education.academic.year"].search([], limit=1)
if not year:
year = env["education.academic.year"].create({"name": "2026"})
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
if user:
student = env["education.student"].create({
"full_name": "Demo Student",
"first_name": "Demo",
"last_name": "Student",
"email": "demo.student@example.com",
"portal_user_id": user.id,
"campus_id": campus.id,
"department_id": dept.id,
"program_id": prog.id,
"academic_year_id": year.id,
"date_of_birth": "2000-01-01",
"gender": "other",
"admission_number": "ADM-9999",
"student_id": "STU-9999"
})
env.cr.commit()

4
debug_student.py Normal file
View File

@ -0,0 +1,4 @@
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
print("USER:", user.id)
print("STUDENTS:", user.student_ids.ids)
print("PROPERTY:", user.student_id)

View File

@ -1,7 +1,7 @@
services: services:
db: db:
image: postgres:15 image: postgres:15
container_name: odoo_student_metatron_db container_name: metatron_academic_db
environment: environment:
POSTGRES_DB: postgres POSTGRES_DB: postgres
POSTGRES_USER: odoo POSTGRES_USER: odoo
@ -12,7 +12,7 @@ services:
odoo: odoo:
image: odoo:19.0 image: odoo:19.0
container_name: odoo_student_metatron container_name: metatron_academic
depends_on: depends_on:
- db - db
ports: ports:
@ -22,7 +22,7 @@ services:
USER: odoo USER: odoo
PASSWORD: odoo PASSWORD: odoo
LIST_DB: "True" LIST_DB: "True"
command: odoo --proxy-mode -i education_base,education_student,education_attendance,education_finance,education_dashboard,education_exam,education_lms,education_portal,education_reports,education_timetable,education_hostel,education_transportation command: odoo --proxy-mode -i education_base,education_student,education_attendance,education_finance,education_dashboard,education_exam,education_lms,education_portal,education_reports,education_timetable,education_hostel,education_transportation,education_demo_data,education_mobile_api
volumes: volumes:
- client1_odoo_data:/var/lib/odoo - client1_odoo_data:/var/lib/odoo
- ./addons:/mnt/extra-addons - ./addons:/mnt/extra-addons

12
fix_demo_data.py Normal file
View File

@ -0,0 +1,12 @@
import sys
sys.path.append("/mnt/extra-addons")
sys.path.append("/usr/lib/python3/dist-packages/odoo/addons")
from education_demo_data.demo_data_generator import generate_demo_data
try:
generate_demo_data(env)
print("DEMO DATA GENERATED SUCCESSFULLY")
except Exception as e:
import traceback
traceback.print_exc()

29
fix_mr.py Normal file
View File

@ -0,0 +1,29 @@
import re
path = r'c:\Users\Admin\Metatron_Student_ERP\addons\education_portal\views\portal_templates.xml'
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
# Replace mr-X with mr-X me-X
def replace_class(m):
cls = m.group(1)
num = m.group(2)
if f'me-{num}' not in cls:
cls = cls + f' me-{num}'
return f'class="{cls}"'
new_content = re.sub(r'class="([^"]*?\bmr-(\d)\b[^"]*?)"', replace_class, content)
# Add space after /> if there is text directly
new_content = re.sub(r'(<i[^>]*?/>)([A-Za-z0-9])', r'\1 \2', new_content)
# Add space after </i> if there is text directly
new_content = re.sub(r'(</i>)([A-Za-z0-9])', r'\1 \2', new_content)
if new_content != content:
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
print('Updated portal_templates.xml')
else:
print('No changes needed in portal_templates.xml')

43
force_create.py Normal file
View File

@ -0,0 +1,43 @@
import sys
import traceback
try:
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
c = env["res.company"].search([], limit=1)
campus = env["education.campus"].search([], limit=1) or env["education.campus"].create({"name": "Test", "company_id": c.id})
dept = env["education.department"].search([], limit=1) or env["education.department"].create({"name": "Test", "company_id": c.id, "campus_id": campus.id})
level = env["education.academic.level"].search([], limit=1) or env["education.academic.level"].create({"name": "Test", "company_id": c.id})
prog = env["education.program"].search([], limit=1) or env["education.program"].create({"name": "Test", "code": "T", "department_id": dept.id, "level_id": level.id})
year = env["education.academic.year"].search([], limit=1) or env["education.academic.year"].create({"name": "Test", "company_id": c.id, "start_date": "2026-01-01", "end_date": "2026-12-31"})
term = env["education.academic.term"].search([], limit=1) or env["education.academic.term"].create({"name": "Test", "academic_year_id": year.id, "start_date": "2026-01-01", "end_date": "2026-12-31"})
batch = env["education.batch"].search([], limit=1) or env["education.batch"].create({"name": "Test", "program_id": prog.id, "academic_year_id": year.id})
sem = env["education.semester"].search([], limit=1) or env["education.semester"].create({"name": "Test", "program_id": prog.id, "academic_year_id": year.id, "sequence": 1})
student = env["education.student"].create({
"full_name": "Demo Student",
"first_name": "Demo",
"last_name": "Student",
"email": "demo.student@example.com",
"portal_user_id": user.id,
"company_id": c.id,
"campus_id": campus.id,
"department_id": dept.id,
"level_id": level.id,
"program_id": prog.id,
"academic_year_id": year.id,
"academic_term_id": term.id,
"batch_id": batch.id,
"semester_id": sem.id,
"current_semester_id": sem.id,
"date_of_birth": "2000-01-01",
"gender": "other",
"admission_number": "ADM-9999",
"student_id": "STU-9999",
"status": "active"
})
env.cr.commit()
with open('/mnt/extra-addons/error.log', 'w') as f:
f.write("SUCCESS: " + str(student.id))
except Exception as e:
with open('/mnt/extra-addons/error.log', 'w') as f:
f.write(traceback.format_exc())

1
link.py Normal file
View File

@ -0,0 +1 @@
user = env["res.users"].search([("login", "=", "demo.student")]); env["education.student"].search([], limit=1).write({"portal_user_id": user.id}); env.cr.commit()

7
run_demo.py Normal file
View File

@ -0,0 +1,7 @@
import traceback
try:
from odoo.addons.education_demo_data.demo_data_generator import generate_demo_data
generate_demo_data(env)
print("DEMO_GEN_SUCCESS")
except Exception:
traceback.print_exc()

15
setup_demo_student.py Normal file
View File

@ -0,0 +1,15 @@
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
if user:
student = env["education.student"].search([], limit=1)
if student:
student.write({"portal_user_id": user.id})
env.cr.commit()
print("LINKED STUDENT ID:", student.id, "TO USER ID:", user.id)
else:
print("NO STUDENT FOUND")
else:
print("NO USER FOUND")
print("USER:", user.id)
print("USER STUDENT:", user.student_id.id)
print("STUDENT NAME:", user.student_id.full_name)

4
verify_student.py Normal file
View File

@ -0,0 +1,4 @@
user = env["res.users"].search([("login", "=", "demo.student")], limit=1)
print("USER:", user.id)
print("USER STUDENT:", user.student_id.id)
print("STUDENT NAME:", user.student_id.full_name)

View File

@ -721,9 +721,8 @@
<!-- Bottom Footer Area --> <!-- Bottom Footer Area -->
<div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;"> <div style="height: 60px; background: #ffffff; border-top: 1px solid rgba(31, 143, 143, 0.1); display: flex; align-items: center; justify-content: space-between; padding: 0 40px; font-size: 0.9rem; color: #718096; font-weight: 600;">
<span>© 2026 My Company. All rights reserved.</span> <span>© 2026 Metatron EduPortal. All rights reserved.</span>
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span> <span>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></span>
</div> </div>
</t> </t>
</template> </template>