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:
commit
5b5fc5c955
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
@ -20,9 +20,14 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Academic Level">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" string="Level Name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="e.g. Undergraduate"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'active'"
|
||||
decoration-danger="state == 'inactive'"
|
||||
decoration-secondary="state in ('closed', 'draft')"/>
|
||||
decoration-info="state in ('closed', 'draft')"/>
|
||||
<field name="current_year"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</list>
|
||||
@ -31,7 +31,7 @@
|
||||
<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_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>
|
||||
<sheet>
|
||||
<group>
|
||||
|
||||
@ -33,4 +33,4 @@
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -109,6 +109,33 @@ class EducationDashboardController(http.Controller):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Announcements ─────────────────────────────────────────────────────
|
||||
announcements_data = []
|
||||
try:
|
||||
announcements = env['education.announcement'].search(
|
||||
[('active', '=', True)],
|
||||
order='date_published desc', limit=4
|
||||
)
|
||||
for ann in announcements:
|
||||
import re
|
||||
subtitle = re.sub('<[^>]+>', '', ann.content or '')[:60]
|
||||
type_colors = {
|
||||
'notice': ('#fee2e2', '#b91c1c'),
|
||||
'circular': ('#e0f2fe', '#0369a1'),
|
||||
'event': ('#fef9c3', '#a16207')
|
||||
}
|
||||
bg_color, text_color = type_colors.get(ann.announcement_type, ('#d1fae5', '#065f46'))
|
||||
ann_type = dict(ann._fields['announcement_type'].selection).get(ann.announcement_type) if ann.announcement_type else 'Notice'
|
||||
announcements_data.append({
|
||||
'title': ann.title,
|
||||
'subtitle': subtitle + ('...' if len(subtitle) == 60 else ''),
|
||||
'type': ann_type,
|
||||
'bg_color': bg_color,
|
||||
'text_color': text_color,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── User / date context ───────────────────────────────────────────────
|
||||
user_name = env.user.name
|
||||
today_str = datetime.date.today().strftime('%A, %b %d, %Y')
|
||||
@ -149,6 +176,7 @@ class EducationDashboardController(http.Controller):
|
||||
# operational
|
||||
'upcoming_exams': upcoming_exams,
|
||||
'recent_activities': recent_activities,
|
||||
'announcements': announcements_data,
|
||||
# context
|
||||
'user_name': user_name,
|
||||
'today': today_str,
|
||||
|
||||
BIN
addons/education_dashboard/static/src/img/card-img.png
Normal file
BIN
addons/education_dashboard/static/src/img/card-img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
addons/education_dashboard/static/src/img/dashboard-bg.png
Normal file
BIN
addons/education_dashboard/static/src/img/dashboard-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 588 KiB |
BIN
addons/education_dashboard/static/src/img/favicon.png
Normal file
BIN
addons/education_dashboard/static/src/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
BIN
addons/education_dashboard/static/src/img/logo.png
Normal file
BIN
addons/education_dashboard/static/src/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@ -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() {
|
||||
this.actionService.doAction({
|
||||
name: "New Student", type: "ir.actions.act_window",
|
||||
@ -235,7 +244,7 @@ class EducationDashboard extends Component {
|
||||
openAddFaculty() {
|
||||
this.actionService.doAction({
|
||||
name: "Faculty", type: "ir.actions.act_window",
|
||||
res_model: "hr.employee",
|
||||
res_model: "res.users",
|
||||
views: [[false, "list"], [false, "form"]], target: "current"
|
||||
});
|
||||
}
|
||||
@ -243,7 +252,7 @@ class EducationDashboard extends Component {
|
||||
openCreateCourse() {
|
||||
this.actionService.doAction({
|
||||
name: "Courses", type: "ir.actions.act_window",
|
||||
res_model: "education.course",
|
||||
res_model: "education.lms.course",
|
||||
views: [[false, "list"], [false, "form"]], target: "current"
|
||||
});
|
||||
}
|
||||
@ -267,7 +276,7 @@ class EducationDashboard extends Component {
|
||||
openFees() {
|
||||
this.actionService.doAction({
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
</style>
|
||||
|
||||
<!-- ── 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;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;">
|
||||
@ -61,7 +61,7 @@
|
||||
<div style="font-size:0.72rem;color:#64748b;font-weight:600;margin-top:2px;">Programs</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>
|
||||
<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>
|
||||
<span style="font-size:0.72rem;color:#1f8f8f;font-weight:600;cursor:pointer;">View All</span>
|
||||
</div>
|
||||
<div class="edu-ann-row">
|
||||
<span class="edu-pill" style="background:#fee2e2;color:#b91c1c;margin-top:1px;">Important</span>
|
||||
<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.7rem;color:#94a3b8;">Mid-term schedule has been published.</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">
|
||||
<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="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;"><t t-esc="ann.subtitle"/></div>
|
||||
</div>
|
||||
<i class="fa fa-chevron-right" style="color:#cbd5e1;font-size:0.65rem;align-self:center;flex-shrink:0;"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div style="text-align:center;padding:28px 0;color:#94a3b8;">
|
||||
<i class="fa fa-bullhorn" style="font-size:2rem;display:block;margin-bottom:8px;"/>
|
||||
<div style="font-size:0.8rem;">No recent announcements</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:#e0f2fe;color:#0369a1;margin-top:1px;">General</span>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-size:0.8rem;font-weight:600;color:#0f172a;margin-bottom:1px;">Library Hours Extended</div>
|
||||
<div style="font-size:0.7rem;color:#94a3b8;">Open till 8 PM this week.</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:#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>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
14
addons/education_demo_data/__init__.py
Normal file
14
addons/education_demo_data/__init__.py
Normal 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}")
|
||||
24
addons/education_demo_data/__manifest__.py
Normal file
24
addons/education_demo_data/__manifest__.py
Normal 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',
|
||||
}
|
||||
2
addons/education_demo_data/controllers/__init__.py
Normal file
2
addons/education_demo_data/controllers/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import demo_login
|
||||
79
addons/education_demo_data/controllers/demo_login.py
Normal file
79
addons/education_demo_data/controllers/demo_login.py
Normal 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'])
|
||||
27
addons/education_demo_data/data/demo_users.xml
Normal file
27
addons/education_demo_data/data/demo_users.xml
Normal 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>
|
||||
225
addons/education_demo_data/demo_data_generator.py
Normal file
225
addons/education_demo_data/demo_data_generator.py
Normal 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()
|
||||
|
||||
1
addons/education_demo_data/tests/__init__.py
Normal file
1
addons/education_demo_data/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import test_demo_login
|
||||
32
addons/education_demo_data/tests/test_demo_login.py
Normal file
32
addons/education_demo_data/tests/test_demo_login.py
Normal 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")
|
||||
@ -552,6 +552,20 @@
|
||||
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 */
|
||||
@media (max-width: 991px) {
|
||||
.portal-layout-wrapper {
|
||||
@ -603,10 +617,18 @@
|
||||
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;
|
||||
padding: 0 12px !important;
|
||||
height: 60px;
|
||||
}
|
||||
.portal-logo-area span {
|
||||
@ -783,7 +805,7 @@
|
||||
|
||||
<div class="premium-card">
|
||||
<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>
|
||||
<t t-if="not exams">
|
||||
<div class="alert alert-info border-0 shadow-sm" style="background: #e6f7f7; color: #144d4d; border-radius: 12px;">
|
||||
@ -821,9 +843,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
@ -917,7 +939,7 @@
|
||||
|
||||
<div class="premium-card">
|
||||
<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>
|
||||
<t t-if="not results">
|
||||
<div class="alert alert-info border-0 shadow-sm" style="background: #e6f7f7; color: #144d4d; border-radius: 12px;">
|
||||
@ -961,9 +983,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
@ -1143,9 +1165,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
name="Finance"
|
||||
parent="education_base.menu_education_base_root"
|
||||
sequence="30"
|
||||
groups="group_finance_executive"/>
|
||||
groups="education_finance.group_finance_executive"/>
|
||||
|
||||
<!-- Finance Submenus -->
|
||||
<menuitem id="menu_education_fee_assignment"
|
||||
@ -31,28 +31,28 @@
|
||||
parent="menu_education_finance_root"
|
||||
action="action_education_student_discount"
|
||||
sequence="40"
|
||||
groups="group_finance_manager"/>
|
||||
groups="education_finance.group_finance_manager"/>
|
||||
|
||||
<menuitem id="menu_education_student_scholarship_menu"
|
||||
name="Scholarships"
|
||||
parent="menu_education_finance_root"
|
||||
action="action_education_student_scholarship"
|
||||
sequence="50"
|
||||
groups="group_finance_manager"/>
|
||||
groups="education_finance.group_finance_manager"/>
|
||||
|
||||
<menuitem id="menu_education_student_refund_menu"
|
||||
name="Refunds"
|
||||
parent="menu_education_finance_root"
|
||||
action="action_education_student_refund"
|
||||
sequence="60"
|
||||
groups="group_finance_manager"/>
|
||||
groups="education_finance.group_finance_manager"/>
|
||||
|
||||
<!-- Configuration Menu Items (Placed under Base Configuration) -->
|
||||
<menuitem id="menu_education_finance_config_root"
|
||||
name="Finance Configuration"
|
||||
parent="education_base.menu_education_base_configuration"
|
||||
sequence="20"
|
||||
groups="group_finance_admin"/>
|
||||
groups="education_finance.group_finance_admin"/>
|
||||
|
||||
<menuitem id="menu_education_fee_category"
|
||||
name="Fee Categories"
|
||||
|
||||
@ -243,6 +243,87 @@
|
||||
width: 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>
|
||||
|
||||
<!-- Header -->
|
||||
@ -300,6 +381,12 @@
|
||||
<t t-out="0"/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
3
addons/education_mobile_api/__init__.py
Normal file
3
addons/education_mobile_api/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import controllers
|
||||
18
addons/education_mobile_api/__manifest__.py
Normal file
18
addons/education_mobile_api/__manifest__.py
Normal 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,
|
||||
}
|
||||
3
addons/education_mobile_api/controllers/__init__.py
Normal file
3
addons/education_mobile_api/controllers/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import auth
|
||||
from . import student
|
||||
106
addons/education_mobile_api/controllers/auth.py
Normal file
106
addons/education_mobile_api/controllers/auth.py
Normal 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)
|
||||
314
addons/education_mobile_api/controllers/student.py
Normal file
314
addons/education_mobile_api/controllers/student.py
Normal 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)
|
||||
2
addons/education_mobile_api/models/__init__.py
Normal file
2
addons/education_mobile_api/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import mobile_device
|
||||
11
addons/education_mobile_api/models/mobile_device.py
Normal file
11
addons/education_mobile_api/models/mobile_device.py
Normal 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')
|
||||
2
addons/education_mobile_api/security/ir.model.access.csv
Normal file
2
addons/education_mobile_api/security/ir.model.access.csv
Normal 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
|
||||
|
@ -32,6 +32,7 @@
|
||||
'views/portal_payment_templates.xml',
|
||||
'views/student_views_inherit.xml',
|
||||
'views/login_templates.xml',
|
||||
'views/web_layout_override.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
|
||||
Binary file not shown.
@ -24,3 +24,5 @@ class EducationAnnouncement(models.Model):
|
||||
default=lambda self: self.env.company,
|
||||
index=True,
|
||||
)
|
||||
circular_file = fields.Binary(string='Circular File', attachment=True)
|
||||
circular_filename = fields.Char(string='Circular Filename')
|
||||
|
||||
@ -30,6 +30,8 @@
|
||||
<field name="announcement_type"/>
|
||||
<field name="date_published"/>
|
||||
<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>
|
||||
<field name="active"/>
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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 -->
|
||||
<template id="custom_login_layout" inherit_id="web.login_layout" name="Education Portal Custom Login Layout">
|
||||
<xpath expr="//div[hasclass('container')]" position="replace">
|
||||
@ -76,9 +83,10 @@
|
||||
z-index: 2;
|
||||
}
|
||||
.left-campus-logo {
|
||||
max-height: 55px;
|
||||
<!-- max-height: 55px; -->
|
||||
width: auto;
|
||||
margin-bottom: 24px;
|
||||
margin-left: -20px;
|
||||
<!-- margin-bottom: 24px; -->
|
||||
}
|
||||
.left-welcome-tag {
|
||||
color: #1f8f8f;
|
||||
@ -257,7 +265,7 @@
|
||||
|
||||
<div class="left-header-area">
|
||||
<!-- Campus Logo -->
|
||||
<img src="/web/image?model=res.company&id=1&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-main-title">Education Portal</div>
|
||||
@ -320,8 +328,8 @@
|
||||
|
||||
<!-- Bottom Page Footer -->
|
||||
<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>© 2026 My Company. All rights reserved.</div>
|
||||
<div>Powered by <a href="https://metatroncubesolutions.com/" style="color: #144d4d; font-weight: 700; text-decoration: none;">Metatroncube</a></div>
|
||||
<div>© 2026 Metatron EduPortal. All rights reserved.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -424,13 +432,43 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Social / SSO Divider & Buttons -->
|
||||
<!-- <div class="text-center my-4 position-relative">
|
||||
<!-- Quick Demo Access Section -->
|
||||
<div class="text-center my-4 position-relative">
|
||||
<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 class="text-center mb-3">
|
||||
<span class="small text-muted">Demo accounts contain fictional data.</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-2">
|
||||
<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 & 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 & Classes</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- <div class="row g-2 mb-2">
|
||||
<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;">
|
||||
<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>
|
||||
@ -443,7 +481,7 @@
|
||||
Microsoft
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
</div> -->
|
||||
|
||||
<input type="hidden" name="redirect" t-att-value="redirect"/>
|
||||
</form>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Payment Success Template -->
|
||||
<template id="portal_payment_success" name="Payment Success">
|
||||
@ -71,7 +71,7 @@
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<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;">
|
||||
<i class="fa fa-download mr-2"/> Download PDF Receipt
|
||||
<i class="fa fa-download mr-2 me-2"/> Download PDF Receipt
|
||||
</a>
|
||||
</t>
|
||||
<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>
|
||||
|
||||
<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 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;">
|
||||
<i class="fa fa-refresh mr-2"/> Retry Payment
|
||||
<i class="fa fa-refresh mr-2 me-2"/> Retry Payment
|
||||
</a>
|
||||
<a href="/my/education/dashboard" class="btn btn-outline-secondary font-weight-bold px-4 py-2" style="border-radius: 10px;">
|
||||
Dashboard
|
||||
@ -159,10 +159,10 @@
|
||||
<div class="history-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@ -439,6 +439,7 @@
|
||||
border-radius: 50%;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.welcome-banner-fallback {
|
||||
width: 90px;
|
||||
@ -451,6 +452,7 @@
|
||||
justify-content: center;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.welcome-text-title {
|
||||
font-size: 1.7rem;
|
||||
@ -666,6 +668,9 @@
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fin-card.blue .fin-label { color: #1e40af; }
|
||||
.fin-card.green .fin-label { color: #166534; }
|
||||
@ -674,6 +679,9 @@
|
||||
.fin-amount {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fin-card.blue .fin-amount { color: #1e3a8a; }
|
||||
.fin-card.green .fin-amount { color: #14532d; }
|
||||
@ -687,6 +695,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.95rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.fin-card.blue .fin-icon-box { background: #dbeafe; color: #1d4ed8; }
|
||||
.fin-card.green .fin-icon-box { background: #dcfce7; color: #15803d; }
|
||||
@ -759,6 +768,18 @@
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
.portal-layout-wrapper {
|
||||
flex-direction: column;
|
||||
@ -809,10 +830,18 @@
|
||||
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;
|
||||
padding: 0 12px !important;
|
||||
height: 60px;
|
||||
}
|
||||
.portal-logo-area span {
|
||||
@ -822,6 +851,22 @@
|
||||
padding: 20px !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>
|
||||
|
||||
@ -849,10 +894,10 @@
|
||||
<i class="fa fa-angle-down"/>
|
||||
</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;">
|
||||
<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/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/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 me-2"/> Dashboard</a>
|
||||
<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>
|
||||
@ -1035,7 +1080,7 @@
|
||||
<div class="premium-card">
|
||||
<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">
|
||||
<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>
|
||||
<a href="/my/education/profile" class="header-link-action">View Details ></a>
|
||||
</div>
|
||||
@ -1081,36 +1126,36 @@
|
||||
|
||||
<!-- Finance Overview -->
|
||||
<div class="premium-card">
|
||||
<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">
|
||||
<i class="fa fa-credit-card text-success mr-2"/>Finance Overview
|
||||
<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 text-truncate" style="min-width: 0; flex: 1;">
|
||||
<i class="fa fa-credit-card text-success mr-2 me-2"/> Finance Overview
|
||||
</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 class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="fin-card blue">
|
||||
<div>
|
||||
<div class="fin-label">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 style="min-width: 0; flex: 1; margin-right: 8px;">
|
||||
<div class="fin-label" title="TOTAL FEES">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"/>&nbsp;<t t-esc="'{0:,.2f}'.format(student.total_fees)"/></div>
|
||||
</div>
|
||||
<div class="fin-icon-box"><i class="fa fa-calculator"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="fin-card green">
|
||||
<div>
|
||||
<div class="fin-label">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 style="min-width: 0; flex: 1; margin-right: 8px;">
|
||||
<div class="fin-label" title="TOTAL PAID">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"/>&nbsp;<t t-esc="'{0:,.2f}'.format(student.total_paid)"/></div>
|
||||
</div>
|
||||
<div class="fin-icon-box"><i class="fa fa-check-circle"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="fin-card red">
|
||||
<div>
|
||||
<div class="fin-label">OUTSTANDING</div>
|
||||
<div class="fin-amount"><t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(student.outstanding_amount)"/></div>
|
||||
<div style="min-width: 0; flex: 1; margin-right: 8px;">
|
||||
<div class="fin-label" title="OUTSTANDING">OUTSTANDING</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"/>&nbsp;<t t-esc="'{0:,.2f}'.format(student.outstanding_amount)"/></div>
|
||||
</div>
|
||||
<div class="fin-icon-box"><i class="fa fa-exclamation-circle"/></div>
|
||||
</div>
|
||||
@ -1123,11 +1168,11 @@
|
||||
<div class="col-lg-4">
|
||||
<!-- Announcements/Upcoming Card -->
|
||||
<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">
|
||||
<h5 class="font-weight-bold text-dark mb-0">
|
||||
<i class="fa fa-calendar-check-o text-warning mr-2"/>Upcoming
|
||||
<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 text-truncate" style="min-width: 0; flex: 1;">
|
||||
<i class="fa fa-calendar-check-o text-warning me-2 mr-2"/> Upcoming
|
||||
</h5>
|
||||
<a href="#" class="header-link-action">View Calendar</a>
|
||||
<a href="#" class="header-link-action text-nowrap">View Calendar</a>
|
||||
</div>
|
||||
|
||||
<t t-if="exams">
|
||||
@ -1139,7 +1184,7 @@
|
||||
<div class="event-month"><t t-esc="exam.start_date.strftime('%b')"/></div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
@ -1172,9 +1217,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
@ -1412,7 +1457,7 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.card-deco-dots {
|
||||
position: absolute;
|
||||
display: none;position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
color: #d1f1f1;
|
||||
@ -1599,6 +1644,18 @@
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
.portal-layout-wrapper {
|
||||
flex-direction: column;
|
||||
@ -1649,10 +1706,18 @@
|
||||
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;
|
||||
padding: 0 12px !important;
|
||||
height: 60px;
|
||||
}
|
||||
.portal-logo-area span {
|
||||
@ -1697,10 +1762,10 @@
|
||||
<i class="fa fa-angle-down"/>
|
||||
</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;">
|
||||
<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/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/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 me-2"/> Dashboard</a>
|
||||
<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>
|
||||
@ -1970,9 +2035,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
@ -2324,6 +2389,18 @@
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
.portal-layout-wrapper {
|
||||
flex-direction: column;
|
||||
@ -2374,6 +2451,14 @@
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
||||
.portal-footer-area {
|
||||
height: auto;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 16px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2401,10 +2486,10 @@
|
||||
<i class="fa fa-angle-down"/>
|
||||
</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;">
|
||||
<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/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/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 me-2"/> Dashboard</a>
|
||||
<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>
|
||||
@ -2484,7 +2569,7 @@
|
||||
<!-- Overview Summary Section -->
|
||||
<div class="premium-card">
|
||||
<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>
|
||||
<div class="row g-3">
|
||||
<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>
|
||||
<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>
|
||||
<p class="text-muted small mb-0">Select individual fee items or pay all outstanding dues online securely via Razorpay / UPI.</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -2561,7 +2646,7 @@
|
||||
<td><span t-field="fee.academic_year_id.name"/></td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
<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>
|
||||
<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 id="selectedTotalAmount" class="font-weight-bold" style="color: #2b6cb0; font-size: 1.25rem;">₹0.00</span>
|
||||
</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);">
|
||||
<i class="fa fa-shield mr-2"/> Proceed to Payment
|
||||
<i class="fa fa-shield mr-2 me-2"/> Proceed to Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -2606,7 +2691,7 @@
|
||||
<div class="premium-card">
|
||||
<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">
|
||||
<i class="fa fa-exchange text-primary mr-2"/>Ledger & Transaction Details
|
||||
<i class="fa fa-exchange text-primary mr-2 me-2"/> Ledger & Transaction Details
|
||||
</h5>
|
||||
<button class="btn-filter">
|
||||
<i class="fa fa-filter"/> Filter
|
||||
@ -2628,7 +2713,7 @@
|
||||
<t t-if="ledger_lines">
|
||||
<t t-foreach="ledger_lines" t-as="line">
|
||||
<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="text-right text-danger font-weight-bold">
|
||||
<t t-if="line.debit > 0">+ <t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(line.debit)"/></t>
|
||||
@ -2663,7 +2748,7 @@
|
||||
<!-- Official Payment Receipts -->
|
||||
<div class="premium-card">
|
||||
<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>
|
||||
<div class="table-responsive">
|
||||
<table class="premium-table">
|
||||
@ -2681,7 +2766,7 @@
|
||||
<t t-foreach="payments" t-as="pay">
|
||||
<tr>
|
||||
<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 class="text-right font-weight-bold text-success">
|
||||
<t t-esc="student.currency_id.symbol"/> <t t-esc="'{0:,.2f}'.format(pay.amount)"/>
|
||||
@ -2745,7 +2830,7 @@
|
||||
function initiatePayment(selectedFeeIds) {
|
||||
if (!btnPayOnline) return;
|
||||
btnPayOnline.disabled = true;
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-spinner fa-spin mr-2"></i> Initializing...';
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-spinner fa-spin mr-2 me-2"></i> Initializing...';
|
||||
|
||||
fetch('/my/education/payment/create', {
|
||||
method: 'POST',
|
||||
@ -2765,7 +2850,7 @@
|
||||
if (data.error) {
|
||||
alert("Payment Error: " + data.error);
|
||||
btnPayOnline.disabled = false;
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2"></i> Proceed to Payment';
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2 me-2"></i> Proceed to Payment';
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2797,7 +2882,7 @@
|
||||
"modal": {
|
||||
"ondismiss": function() {
|
||||
btnPayOnline.disabled = false;
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2"></i> Proceed to Payment';
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2 me-2"></i> Proceed to Payment';
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -2806,13 +2891,13 @@
|
||||
} else {
|
||||
alert("Razorpay SDK could not be loaded. Please check your internet connection.");
|
||||
btnPayOnline.disabled = false;
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2"></i> Proceed to Payment';
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2 me-2"></i> Proceed to Payment';
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
alert("Server Connection Error: " + err);
|
||||
btnPayOnline.disabled = false;
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2"></i> Proceed to Payment';
|
||||
btnPayOnline.innerHTML = '<i class="fa fa-shield mr-2 me-2"></i> Proceed to Payment';
|
||||
});
|
||||
}
|
||||
|
||||
@ -2863,9 +2948,9 @@
|
||||
</script>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
@ -3118,10 +3203,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.document-icon-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
min-width: 42px;
|
||||
min-height: 42px;
|
||||
border-radius: 50%;
|
||||
background: #e6f7f7;
|
||||
color: #1f8f8f;
|
||||
@ -3129,6 +3218,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.15rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.document-meta-title {
|
||||
font-size: 0.95rem;
|
||||
@ -3256,6 +3346,18 @@
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
.portal-layout-wrapper {
|
||||
flex-direction: column;
|
||||
@ -3285,6 +3387,70 @@
|
||||
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>
|
||||
|
||||
<!-- Top Header -->
|
||||
@ -3311,10 +3477,10 @@
|
||||
<i class="fa fa-angle-down"/>
|
||||
</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;">
|
||||
<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/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/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 me-2"/> Dashboard</a>
|
||||
<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>
|
||||
@ -3590,9 +3756,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
|
||||
<!-- Frontend Logic to show selected file name -->
|
||||
@ -3632,10 +3798,10 @@
|
||||
<!-- Header -->
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -3727,7 +3893,7 @@
|
||||
</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()">
|
||||
<i class="fa fa-lock mr-2"/> Submit Secure Payment
|
||||
<i class="fa fa-lock mr-2 me-2"/> Submit Secure Payment
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -4019,6 +4185,78 @@
|
||||
.dropdown-toggle::after {
|
||||
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>
|
||||
|
||||
<!-- Top Header -->
|
||||
@ -4045,10 +4283,10 @@
|
||||
<i class="fa fa-angle-down"/>
|
||||
</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;">
|
||||
<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/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/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 me-2"/> Dashboard</a>
|
||||
<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>
|
||||
@ -4183,7 +4421,7 @@
|
||||
<div class="premium-card">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -4202,7 +4440,7 @@
|
||||
<t t-foreach="attendance_records" t-as="att">
|
||||
<tr>
|
||||
<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"/>
|
||||
</td>
|
||||
<td style="color: #475569;">
|
||||
@ -4241,9 +4479,9 @@
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
@ -4253,7 +4491,7 @@
|
||||
<div class="container py-4">
|
||||
<div class="card mb-4 border-0 shadow-sm">
|
||||
<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 class="card-body">
|
||||
<div class="row mb-4">
|
||||
@ -4305,7 +4543,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
@ -4319,11 +4557,11 @@
|
||||
<div class="container py-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<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 class="card-body">
|
||||
<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="badge badge-secondary px-2 py-1"><t t-esc="announcement.announcement_type.capitalize()"/></span>
|
||||
</div>
|
||||
@ -4331,7 +4569,7 @@
|
||||
<t t-out="announcement.content"/>
|
||||
</div>
|
||||
<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>
|
||||
@ -4541,7 +4779,7 @@
|
||||
box-shadow: 0 8px 24px 0 rgba(31, 143, 143, 0.06) !important;
|
||||
}
|
||||
.card-deco-dots {
|
||||
position: absolute;
|
||||
display: none;position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
color: #d1f1f1;
|
||||
@ -4894,6 +5132,18 @@
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
.portal-layout-wrapper {
|
||||
flex-direction: column;
|
||||
@ -4911,7 +5161,8 @@
|
||||
max-height: none !important;
|
||||
}
|
||||
.sidebar-link {
|
||||
padding: 10px 16px;
|
||||
padding: 10px 5px;
|
||||
gap: 3px !important;
|
||||
}
|
||||
.sidebar-promo-card {
|
||||
display: none !important;
|
||||
@ -4944,10 +5195,18 @@
|
||||
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;
|
||||
padding: 0 12px !important;
|
||||
height: 60px;
|
||||
}
|
||||
.portal-logo-area span {
|
||||
@ -4957,6 +5216,27 @@
|
||||
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>
|
||||
|
||||
@ -4984,10 +5264,10 @@
|
||||
<i class="fa fa-angle-down"/>
|
||||
</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;">
|
||||
<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/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/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 me-2"/> Dashboard</a>
|
||||
<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>
|
||||
@ -5060,9 +5340,9 @@
|
||||
</div> <!-- End portal-layout-wrapper -->
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
9
addons/education_portal/views/web_layout_override.xml
Normal file
9
addons/education_portal/views/web_layout_override.xml
Normal 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>
|
||||
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Add Timetable to Portal Home -->
|
||||
<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 span { color: #a0aec0; }
|
||||
.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 {
|
||||
border-radius: 12px;
|
||||
@ -205,6 +217,78 @@
|
||||
text-align: center;
|
||||
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>
|
||||
|
||||
<!-- Top Header -->
|
||||
@ -342,6 +426,12 @@
|
||||
</t>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@ -48,10 +48,10 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
|
||||
.o_main_navbar {
|
||||
--o-navbar-height: 70px;
|
||||
font-size: 0.975rem;
|
||||
background: linear-gradient(135deg, #e6f7f7 0%, #d1f1f1 100%) !important;
|
||||
border-bottom: 1px solid #e2e8f0 !important;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05) !important;
|
||||
color: #144d4d !important;
|
||||
background: linear-gradient(135deg, #1f8f8f 0%, #144d4d 55%, #0d3d3d 100%) !important;
|
||||
border-bottom: 1px solid #0d3535 !important;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2) !important;
|
||||
color: #ffffff !important;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
// Navbar Links
|
||||
.o_menu_item > a, .o_nav_entry, .dropdown-toggle {
|
||||
color: #144d4d !important;
|
||||
// Navbar Links and Icons
|
||||
.o_menu_item > a, .o_nav_entry, .dropdown-toggle, i, .fa {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: #144d4d !important;
|
||||
background-color: rgba(20, 77, 77, 0.05) !important; // Very light teal hover
|
||||
color: #ffffff !important;
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// App Switcher Icon (Grid)
|
||||
.o_menu_toggle {
|
||||
color: #144d4d !important;
|
||||
color: #ffffff !important;
|
||||
&:hover {
|
||||
background-color: rgba(20, 77, 77, 0.1) !important;
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Brand Name (e.g. Education)
|
||||
.o_menu_brand {
|
||||
color: #144d4d !important;
|
||||
color: #ffffff !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
|
||||
|
||||
&.btn-primary {
|
||||
&.btn-primary, &[aria-current="step"], &[aria-checked="true"], &.text-bg-primary, &.o_arrow_button_current {
|
||||
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;
|
||||
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)
|
||||
.o_field_widget {
|
||||
input:not([type="checkbox"]):not([type="radio"]), select, textarea {
|
||||
width: 100% !important;
|
||||
background-color: #f8fafc !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
border-radius: 6px !important;
|
||||
@ -237,6 +238,23 @@ body.o_web_client:has(.o_sidebar_custom) .o_action_manager {
|
||||
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
|
||||
@ -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;
|
||||
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 {
|
||||
border-radius: 8px !important;
|
||||
font-size: 0.875rem !important;
|
||||
|
||||
@ -48,17 +48,24 @@
|
||||
|
||||
/* 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;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
margin: auto 16px 20px 16px;
|
||||
border: 1px solid rgba(31, 143, 143, 0.15);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(31, 143, 143, 0.2);
|
||||
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 {
|
||||
font-size: 2.5rem;
|
||||
color: #1f8f8f;
|
||||
color: #0d3d3d;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -107,11 +114,11 @@
|
||||
<!-- Sidebar Promo Card -->
|
||||
<div class="sidebar-promo-card mt-auto">
|
||||
<div class="promo-icon-box">
|
||||
<i class="fa fa-rocket"/>
|
||||
<i class="fa fa-rocket" style="color: #0d3d3d !important;"></i>
|
||||
</div>
|
||||
<h6 class="fw-bold mb-1" style="color: #144d4d;">Admin Workspace</h6>
|
||||
<p class="small mb-0" style="color: #4a5568;">Manage your entire</p>
|
||||
<p class="small mb-0" style="color: #4a5568;">institution efficiently.</p>
|
||||
<h6 class="fw-bold mb-1" style="color: #0d3d3d;">Admin Workspace</h6>
|
||||
<p class="small mb-0" style="color: #0d3d3d;">Manage your entire</p>
|
||||
<p class="small mb-0" style="color: #0d3d3d;">institution efficiently.</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
1
check.py
Normal file
1
check.py
Normal 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
120
create_hierarchy.py
Normal 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
11
create_student.py
Normal 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
36
create_student_debug.py
Normal 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
39
create_student_file.py
Normal 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
23
create_student_full.py
Normal 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
30
create_student_full2.py
Normal 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
4
debug_student.py
Normal 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)
|
||||
@ -1,7 +1,7 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
container_name: odoo_student_metatron_db
|
||||
container_name: metatron_academic_db
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: odoo
|
||||
@ -12,7 +12,7 @@ services:
|
||||
|
||||
odoo:
|
||||
image: odoo:19.0
|
||||
container_name: odoo_student_metatron
|
||||
container_name: metatron_academic
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
@ -22,7 +22,7 @@ services:
|
||||
USER: odoo
|
||||
PASSWORD: odoo
|
||||
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:
|
||||
- client1_odoo_data:/var/lib/odoo
|
||||
- ./addons:/mnt/extra-addons
|
||||
|
||||
12
fix_demo_data.py
Normal file
12
fix_demo_data.py
Normal 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
29
fix_mr.py
Normal 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
43
force_create.py
Normal 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
1
link.py
Normal 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
7
run_demo.py
Normal 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
15
setup_demo_student.py
Normal 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
4
verify_student.py
Normal 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)
|
||||
@ -721,9 +721,8 @@
|
||||
|
||||
<!-- 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;">
|
||||
<span>© 2026 My Company. All rights reserved.</span>
|
||||
<span>Powered by <span style="color: #714B67; font-weight: 700;">odoo</span></span>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user