Merge pull request #72 from expsa/odex_theme_branch

Odex theme branch
This commit is contained in:
Tahir Hassan 2026-01-05 16:02:55 +04:00 committed by GitHub
commit ba35f1899c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 1202 additions and 2579 deletions

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
{
'name': 'Expert Theme',
'version': '18.0.1.0.0',
'category': 'Theme/Backend',
'version': '1.0.0',
'category': 'Themes',
'summary': 'Custom backend theme with installed modules home page',
'description': """
Expert Theme
@ -14,8 +14,8 @@
- Easy navigation to installed applications
""",
'author': 'Expert',
'website': '',
'depends': ['base', 'web'],
'website': 'https://www.exp-sa.com',
'depends': ['base', 'web','website'],
'data': [
'security/ir.model.access.csv',
'data/expert_login_template_data.xml',
@ -25,23 +25,23 @@
'views/expert_home_views.xml',
'views/expert_menu_views.xml',
],
'assets': {
'web.assets_backend': [
'expert_theme/static/src/css/expert_theme_config.css',
'expert_theme/static/src/css/expert_theme.css',
'expert_theme/static/src/js/expert_theme_dynamic.js',
'expert_theme/static/src/js/expert_theme_config.js',
'expert_theme/static/src/js/expert_home.js',
'expert_theme/static/src/js/expert_login_template_list.js',
'expert_theme/static/src/xml/expert_home.xml',
],
'web.assets_frontend': [
'expert_theme/static/src/css/expert_login.css',
'expert_theme/static/src/css/login_modern.css',
'expert_theme/static/src/css/login_minimal.css',
'expert_theme/static/src/js/expert_login_template.js',
],
},
'assets': {
'web.assets_frontend': [
'expert_theme/static/src/scss/login_modern.scss',
'expert_theme/static/src/scss/expert_login.scss',
'expert_theme/static/src/scss/login_minimal.scss',
'expert_theme/static/src/js/expert_login_template.js',
],
'web.assets_backend': [
'expert_theme/static/src/scss/expert_theme_config.scss',
'expert_theme/static/src/scss/expert_theme.scss',
'expert_theme/static/src/js/expert_theme_dynamic.js',
'expert_theme/static/src/js/expert_theme_config.js',
'expert_theme/static/src/js/expert_home.js',
'expert_theme/static/src/js/expert_login_template_list.js',
'expert_theme/static/src/xml/expert_home.xml',
]
},
'installable': True,
'auto_install': False,
'application': False,

View File

@ -55,46 +55,6 @@ class ExpertController(http.Controller):
'error': str(e)
})
@http.route('/web/login', type='http', auth='none', methods=['GET', 'POST'], csrf=False)
def web_login(self, redirect=None, **kw):
"""Override Odoo login to use expert login templates when configured.
- POST: delegate to standard Odoo login logic (authentication, redirects, etc.).
- GET: render the active expert login template if not 'default', otherwise use standard login.
"""
# Import Home from its new location in Odoo 18
from odoo.addons.web.controllers.home import Home
# Handle login submission via standard controller
if request.httprequest.method == 'POST':
home = Home()
return home.web_login(redirect=redirect, **kw)
# GET: decide which template to render
try:
login_template = request.env['expert.login.template'].sudo().get_active_template()
template_name = login_template.get_template_name()
except Exception as e:
_logger.error("Error getting active login template: %s", e)
template_name = 'web.login'
# For default template, just call the standard login
if template_name == 'web.login':
home = Home()
return home.web_login(redirect=redirect, **kw)
# Try to render the selected expert template; fall back to default on error
try:
return request.render(template_name, {
'redirect': redirect or '',
'login': kw.get('login', ''),
'login_template': login_template,
})
except Exception as e:
_logger.error("Error rendering login template '%s': %s", template_name, e)
home = Home()
return home.web_login(redirect=redirect, **kw)
@http.route('/expert_theme/get_login_template_styles', type='http', auth='public', methods=['GET'])
def get_login_template_styles(self):
"""Get CSS styles for the active login template (public access for login page)"""
@ -181,106 +141,4 @@ class ExpertController(http.Controller):
'error': str(e)
})
@http.route('/web/signup', type='http', auth='public', methods=['GET', 'POST'], website=True, csrf=False)
def web_auth_signup(self, redirect=None, **kw):
"""Override Odoo signup to use expert signup templates when modern template is active.
- POST: delegate to standard Odoo signup logic (authentication, redirects, etc.).
- GET: render the active expert signup template if modern template is active, otherwise use standard signup.
"""
# Import AuthSignupHome from auth_signup module
try:
from odoo.addons.auth_signup.controllers.main import AuthSignupHome
except ImportError:
# If auth_signup is not installed, return 404
from werkzeug.exceptions import NotFound
raise NotFound()
# Handle signup submission via standard controller
if request.httprequest.method == 'POST':
auth_signup_home = AuthSignupHome()
return auth_signup_home.web_auth_signup(redirect=redirect, **kw)
# GET: decide which template to render
try:
login_template = request.env['expert.login.template'].sudo().get_active_template()
template_name = login_template.get_signup_template_name()
except Exception as e:
_logger.error("Error getting active signup template: %s", e)
template_name = 'auth_signup.signup'
# For default template, just call the standard signup
if template_name == 'auth_signup.signup':
auth_signup_home = AuthSignupHome()
return auth_signup_home.web_auth_signup(redirect=redirect, **kw)
# Try to render the selected expert template; fall back to default on error
try:
# Get signup context from auth_signup
auth_signup_home = AuthSignupHome()
qcontext = auth_signup_home.get_auth_signup_qcontext()
qcontext.update({
'redirect': redirect or '',
'login': kw.get('login', ''),
'login_template': login_template,
})
response = request.render(template_name, qcontext)
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
return response
except Exception as e:
_logger.error("Error rendering signup template '%s': %s", template_name, e)
auth_signup_home = AuthSignupHome()
return auth_signup_home.web_auth_signup(redirect=redirect, **kw)
@http.route('/web/reset_password', type='http', auth='public', methods=['GET', 'POST'], website=True, csrf=False)
def web_auth_reset_password(self, redirect=None, **kw):
"""Override Odoo reset password to use expert reset password templates when modern template is active.
- POST: delegate to standard Odoo reset password logic (authentication, redirects, etc.).
- GET: render the active expert reset password template if modern template is active, otherwise use standard reset password.
"""
# Import AuthSignupHome from auth_signup module
try:
from odoo.addons.auth_signup.controllers.main import AuthSignupHome
except ImportError:
# If auth_signup is not installed, return 404
from werkzeug.exceptions import NotFound
raise NotFound()
# Handle reset password submission via standard controller
if request.httprequest.method == 'POST':
auth_signup_home = AuthSignupHome()
return auth_signup_home.web_auth_reset_password(redirect=redirect, **kw)
# GET: decide which template to render
try:
login_template = request.env['expert.login.template'].sudo().get_active_template()
template_name = login_template.get_reset_password_template_name()
except Exception as e:
_logger.error("Error getting active reset password template: %s", e)
template_name = 'auth_signup.reset_password'
# For default template, just call the standard reset password
if template_name == 'auth_signup.reset_password':
auth_signup_home = AuthSignupHome()
return auth_signup_home.web_auth_reset_password(redirect=redirect, **kw)
# Try to render the selected expert template; fall back to default on error
try:
# Get reset password context from auth_signup
auth_signup_home = AuthSignupHome()
qcontext = auth_signup_home.get_auth_signup_qcontext()
qcontext.update({
'redirect': redirect or '',
'login': kw.get('login', ''),
'login_template': login_template,
})
response = request.render(template_name, qcontext)
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
return response
except Exception as e:
_logger.error("Error rendering reset password template '%s': %s", template_name, e)
auth_signup_home = AuthSignupHome()
return auth_signup_home.web_auth_reset_password(redirect=redirect, **kw)

View File

@ -9,7 +9,7 @@ class ExpertLoginTemplate(models.Model):
_rec_name = 'name'
_order = 'sequence, name'
name = fields.Char(string='Template Name', required=True, help='Name of the login page template')
name = fields.Char(string='Template Name', required=True, translate=True, help='Name of the login page template')
active = fields.Boolean(string='Active', default=False, help='Only one template can be active at a time. Activate this template to use it on the login page.')
sequence = fields.Integer(string='Sequence', default=10, help='Order of display')
@ -37,47 +37,47 @@ class ExpertLoginTemplate(models.Model):
corporate_template_logo = fields.Binary(string='Corporate Template Logo', help='Company logo to display on Corporate login, signup, and reset password pages')
# Modern Template Text Fields
modern_login_title = fields.Char(string='Login Title', default='Welcome to Expert 👋', help='Title text for Modern template login page')
modern_login_subtitle = fields.Char(string='Login Subtitle', default='Kindly fill in your details below to sign in to your account', help='Subtitle text for Modern template login page')
modern_login_button_text = fields.Char(string='Login Button Text', default='Sign In', help='Button text for Modern template login page')
modern_login_title = fields.Char(string='Login Title', default='Welcome to Expert 👋', translate=True, help='Title text for Modern template login page')
modern_login_subtitle = fields.Char(string='Login Subtitle', default='Kindly fill in your details below to sign in to your account', translate=True, help='Subtitle text for Modern template login page')
modern_login_button_text = fields.Char(string='Login Button Text', default='Sign In', translate=True, help='Button text for Modern template login page')
modern_login_button_bg_color = fields.Char(string='Login Button Background', default='#007bff', help='Background color for login button')
modern_login_button_text_color = fields.Char(string='Login Button Text Color', default='#FFFFFF', help='Text color for login button')
modern_login_button_bg_hover = fields.Char(string='Login Button BG Hover', default='#0056b3', help='Background color for login button on hover')
modern_login_button_text_hover = fields.Char(string='Login Button Text Hover', default='#FFFFFF', help='Text color for login button on hover')
modern_signup_title = fields.Char(string='Signup Title', default='Create an account', help='Title text for Modern template signup page')
modern_signup_subtitle = fields.Char(string='Signup Subtitle', default='Join us today and get started', help='Subtitle text for Modern template signup page')
modern_signup_button_text = fields.Char(string='Signup Button Text', default='Create an account', help='Button text for Modern template signup page')
modern_signup_title = fields.Char(string='Signup Title', default='Create an account', translate=True, help='Title text for Modern template signup page')
modern_signup_subtitle = fields.Char(string='Signup Subtitle', default='Join us today and get started', translate=True, help='Subtitle text for Modern template signup page')
modern_signup_button_text = fields.Char(string='Signup Button Text', default='Create an account', translate=True, help='Button text for Modern template signup page')
modern_signup_button_bg_color = fields.Char(string='Signup Button Background', default='#28a745', help='Background color for signup button')
modern_signup_button_text_color = fields.Char(string='Signup Button Text Color', default='#FFFFFF', help='Text color for signup button')
modern_signup_button_bg_hover = fields.Char(string='Signup Button BG Hover', default='#218838', help='Background color for signup button on hover')
modern_signup_button_text_hover = fields.Char(string='Signup Button Text Hover', default='#FFFFFF', help='Text color for signup button on hover')
modern_reset_title = fields.Char(string='Reset Password Title', default='Reset your password', help='Title text for Modern template reset password page')
modern_reset_subtitle = fields.Char(string='Reset Password Subtitle', default='Enter your email to receive reset instructions', help='Subtitle text for Modern template reset password page')
modern_reset_title = fields.Char(string='Reset Password Title', default='Reset your password', translate=True, help='Title text for Modern template reset password page')
modern_reset_subtitle = fields.Char(string='Reset Password Subtitle', default='Enter your email to receive reset instructions', translate=True, help='Subtitle text for Modern template reset password page')
# Minimal Template Text Fields
minimal_login_title = fields.Char(string='Login Title', default='Welcome to Expert 👋', help='Title text for Minimal template login page')
minimal_login_subtitle = fields.Char(string='Login Subtitle', default='Kindly fill in your details below to sign in to your account', help='Subtitle text for Minimal template login page')
minimal_login_button_text = fields.Char(string='Login Button Text', default='Sign In', help='Button text for Minimal template login page')
minimal_login_title = fields.Char(string='Login Title', default='Welcome to Expert 👋', translate=True, help='Title text for Minimal template login page')
minimal_login_subtitle = fields.Char(string='Login Subtitle', default='Kindly fill in your details below to sign in to your account', translate=True, help='Subtitle text for Minimal template login page')
minimal_login_button_text = fields.Char(string='Login Button Text', default='Sign In', translate=True, help='Button text for Minimal template login page')
minimal_login_button_bg_color = fields.Char(string='Login Button Background', default='#E5E5E5', help='Background color for login button')
minimal_login_button_text_color = fields.Char(string='Login Button Text Color', default='#000000', help='Text color for login button')
minimal_login_button_bg_hover = fields.Char(string='Login Button BG Hover', default='#D0D0D0', help='Background color for login button on hover')
minimal_login_button_text_hover = fields.Char(string='Login Button Text Hover', default='#000000', help='Text color for login button on hover')
minimal_signup_title = fields.Char(string='Signup Title', default='Create an account', help='Title text for Minimal template signup page')
minimal_signup_subtitle = fields.Char(string='Signup Subtitle', default='Join us today and get started', help='Subtitle text for Minimal template signup page')
minimal_signup_button_text = fields.Char(string='Signup Button Text', default='Create an account', help='Button text for Minimal template signup page')
minimal_signup_title = fields.Char(string='Signup Title', default='Create an account', translate=True, help='Title text for Minimal template signup page')
minimal_signup_subtitle = fields.Char(string='Signup Subtitle', default='Join us today and get started', translate=True, help='Subtitle text for Minimal template signup page')
minimal_signup_button_text = fields.Char(string='Signup Button Text', default='Create an account', translate=True, help='Button text for Minimal template signup page')
minimal_signup_button_bg_color = fields.Char(string='Signup Button Background', default='#000000', help='Background color for signup button')
minimal_signup_button_text_color = fields.Char(string='Signup Button Text Color', default='#FFFFFF', help='Text color for signup button')
minimal_signup_button_bg_hover = fields.Char(string='Signup Button BG Hover', default='#333333', help='Background color for signup button on hover')
minimal_signup_button_text_hover = fields.Char(string='Signup Button Text Hover', default='#FFFFFF', help='Text color for signup button on hover')
minimal_reset_title = fields.Char(string='Reset Password Title', default='Reset your password', help='Title text for Minimal template reset password page')
minimal_reset_subtitle = fields.Char(string='Reset Password Subtitle', default='Enter your email to receive reset instructions', help='Subtitle text for Minimal template reset password page')
minimal_reset_title = fields.Char(string='Reset Password Title', default='Reset your password', translate=True, help='Title text for Minimal template reset password page')
minimal_reset_subtitle = fields.Char(string='Reset Password Subtitle', default='Enter your email to receive reset instructions', translate=True, help='Subtitle text for Minimal template reset password page')
# Description
description = fields.Text(string='Description', help='Description of this template')
description = fields.Text(string='Description', translate=True, help='Description of this template')
@api.model
def get_active_template(self):
@ -111,13 +111,14 @@ class ExpertLoginTemplate(models.Model):
def create(self, vals):
"""Ensure only one template is active at a time"""
# If no active value is set, default to False (don't auto-activate new templates)
if 'active' not in vals:
vals['active'] = False
if 'active' not in vals[0]:
vals[0]['active'] = False
# Only deactivate others if this one is being set to active
if vals.get('active'):
print(vals[0])
if vals[0].get('active'):
# Deactivate all other templates (excluding the one being created)
self.search([('active', '=', True)]).write({'active': False})
return super(ExpertLoginTemplate, self).create(vals)
return super(ExpertLoginTemplate, self).create(vals[0])
def write(self, vals):
"""Ensure only one template is active at a time"""
@ -133,6 +134,7 @@ class ExpertLoginTemplate(models.Model):
super(ExpertLoginTemplate, other_templates).write({'active': False})
need_reload = True
view = self.env.ref('custom_auth_theme.view_custom_login_inherit', raise_if_not_found=False)
result = super(ExpertLoginTemplate, self).write(vals)
# Update template views if active state changed

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,269 +0,0 @@
/* Expert Theme - Minimal Login Template Styles */
.expert-login-minimal {
background: #FFFFFF;
min-height: 100vh;
padding: 20px;
}
.expert-login-minimal .container-fluid {
height: calc(100vh - 40px);
}
.expert-login-minimal .row {
height: 100%;
margin: 0;
}
/* Left Column: Login Form */
.expert-login-minimal .expert-login-left {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
text-align: center;
}
/* Logo Section */
.expert-login-minimal .expert-login-logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40px;
width: 100%;
}
.expert-login-minimal .expert-login-logo .expert-logo-icon {
width: 40px;
height: 40px;
background: #000000;
border-radius: 8px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.expert-login-minimal .expert-login-logo .expert-logo-icon span {
color: white;
font-weight: bold;
font-size: 20px;
}
.expert-login-minimal .expert-login-logo .expert-logo-text {
color: #000000;
font-size: 20px;
font-weight: 600;
}
/* Welcome Section */
.expert-login-minimal .expert-login-welcome {
width: 100%;
text-align: center;
margin-bottom: 40px;
}
.expert-login-minimal .expert-login-welcome h1 {
color: #000000;
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.2;
text-align: center;
}
.expert-login-minimal .expert-login-welcome h1 .welcome-emoji {
font-size: 32px;
}
.expert-login-minimal .expert-login-welcome p {
color: rgba(0, 0, 0, 0.7);
font-size: 16px;
margin-bottom: 0;
text-align: center;
}
/* Form Section */
.expert-login-minimal .expert-login-form {
max-width: 450px;
width: 100%;
margin: 0 auto;
background-color: transparent !important;
}
.expert-login-minimal .expert-login-form .form-group {
margin-bottom: 24px;
}
.expert-login-minimal .expert-login-form .form-group label {
font-weight: 500;
color: #000000;
display: block;
margin-bottom: 8px;
font-size: 14px;
text-align: start;
}
.expert-login-minimal .expert-login-form .form-group input[type="text"],
.expert-login-minimal .expert-login-form .form-group input[type="password"] {
background: #FFFFFF !important;
border: 1px solid #000000 !important;
border-radius: 8px;
padding: 12px 16px;
width: 100%;
box-sizing: border-box;
color: #000000 !important;
font-size: 14px;
}
.expert-login-minimal .expert-login-form .form-group input[type="text"]::placeholder,
.expert-login-minimal .expert-login-form .form-group input[type="password"]::placeholder {
color: rgba(0, 0, 0, 0.5) !important;
}
.expert-login-minimal .expert-login-form .form-group input[type="text"]:focus,
.expert-login-minimal .expert-login-form .form-group input[type="password"]:focus {
background: #FFFFFF !important;
border-color: #000000 !important;
outline: none;
color: #000000 !important;
}
.expert-login-minimal .expert-login-form .form-check {
margin-bottom: 32px;
}
.expert-login-minimal .expert-login-form .form-check label {
color: #000000;
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 5px;
font-size: 14px;
cursor: pointer;
}
.expert-login-minimal .expert-login-form .form-check label input[type="checkbox"] {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #000000;
}
.expert-login-minimal .expert-login-form button[type="submit"] {
width: 100%;
background: #E5E5E5;
border: none;
border-radius: 8px;
padding: 14px;
font-weight: 600;
color: #000000;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.expert-login-minimal .expert-login-form button[type="submit"]:hover {
background: #D5D5D5;
}
.expert-login-minimal .expert-login-form button[type="submit"]:active {
background: #CCCCCC;
}
/* Login Link Section */
.expert-login-minimal .expert-login-link {
margin-top: 24px;
max-width: 450px;
width: 100%;
text-align: center;
}
.expert-login-minimal .expert-login-link p {
color: rgba(0, 0, 0, 0.7);
font-size: 14px;
margin: 0;
}
.expert-login-minimal .expert-login-link p a {
color: #000000;
text-decoration: underline;
font-weight: 500;
}
.expert-login-minimal .expert-login-link p a:hover {
text-decoration: none;
}
/* Right Column: Image Section */
.expert-login-minimal .expert-login-right {
padding: 0;
height: 100%;
overflow: hidden;
border-radius: 0;
}
.expert-login-minimal .expert-login-right .expert-login-image {
width: 100%;
height: 100%;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.expert-login-minimal .expert-login-right .expert-login-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.expert-login-minimal .expert-minimal-illustration {
width: 600px !important;
height: 600px !important;
object-fit: contain;
}
/* Alert Messages */
.expert-login-minimal .alert {
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
text-align: start;
}
.expert-login-minimal .alert-danger {
background-color: #F8D7DA;
border: 1px solid #F5C6CB;
color: #721C24;
}
.expert-login-minimal .alert-success {
background-color: #D4EDDA;
border: 1px solid #C3E6CB;
color: #155724;
}
/* Responsive adjustments */
@media (max-width: 991.98px) {
.expert-login-minimal .expert-login-left {
padding: 30px 20px;
}
.expert-login-minimal .expert-login-welcome h1 {
font-size: 28px;
}
.expert-login-minimal .expert-login-welcome h1 .welcome-emoji {
font-size: 28px;
}
.expert-login-minimal .expert-login-welcome p {
font-size: 14px;
}
}

View File

@ -1,234 +0,0 @@
/* Expert Theme - Modern Login Template Styles */
.expert-login-modern {
background: #19181F !important;
min-height: 100vh;
padding: 20px;
}
.expert-login-modern .container-fluid {
height: calc(100vh - 40px);
}
.expert-login-modern .row {
height: 100%;
margin: 0;
}
/* Left Column: Login Form */
.expert-login-modern .expert-login-left {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
text-align: center;
}
/* Logo Section */
.expert-login-modern .expert-login-logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40px;
width: 100%;
}
.expert-login-modern .expert-login-logo .expert-logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.expert-login-modern .expert-login-logo .expert-logo-icon span {
color: white;
font-weight: bold;
font-size: 20px;
}
.expert-login-modern .expert-login-logo .expert-logo-text {
color: white;
font-size: 20px;
font-weight: 600;
}
/* Welcome Section */
.expert-login-modern .expert-login-welcome {
width: 100%;
text-align: center;
}
.expert-login-modern .expert-login-welcome h1 {
color: white;
font-size: 36px;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.2;
text-align: center;
}
.expert-login-modern .expert-login-welcome h1 .welcome-emoji {
font-size: 36px;
}
.expert-login-modern .expert-login-welcome p {
color: rgba(255, 255, 255, 0.7);
font-size: 16px;
margin-bottom: 40px;
text-align: center;
}
/* Form Section */
.expert-login-modern .expert-login-form {
max-width: 450px;
width: 100%;
margin: 0 auto;
background-color: transparent !important;
}
.expert-login-modern .expert-login-form .form-group {
margin-bottom: 24px;
}
.expert-login-modern .expert-login-form .form-group label {
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
display: block;
margin-bottom: 8px;
font-size: 14px;
text-align: start;
}
.expert-login-modern .expert-login-form .form-group input[type="text"],
.expert-login-modern .expert-login-form .form-group input[type="password"] {
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
border-radius: 8px;
padding: 12px 16px;
width: 100%;
box-sizing: border-box;
color: white !important;
font-size: 14px;
}
.expert-login-modern .expert-login-form .form-group input[type="text"]::placeholder,
.expert-login-modern .expert-login-form .form-group input[type="password"]::placeholder {
color: rgba(255, 255, 255, 0.5) !important;
}
.expert-login-modern .expert-login-form .form-group input[type="text"]:focus,
.expert-login-modern .expert-login-form .form-group input[type="password"]:focus {
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(255, 255, 255, 0.4) !important;
outline: none;
color: white !important;
}
.expert-login-modern .expert-login-form .form-check {
margin-bottom: 32px;
}
.expert-login-modern .expert-login-form .form-check label {
color: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 14px;
cursor: pointer;
}
.expert-login-modern .expert-login-form .form-check label input[type="checkbox"] {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.expert-login-modern .expert-login-form button[type="submit"] {
width: 100%;
background: #764ba2;
border: none !important;
border-radius: 8px;
padding: 14px;
font-weight: 600;
color: white;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.expert-login-modern .expert-login-form button[type="submit"]:hover {
transform: translateY(-2px);
}
/* Login Link Section */
.expert-login-modern .expert-login-link {
margin-top: 24px;
max-width: 450px;
width: 100%;
text-align: center;
}
.expert-login-modern .expert-login-link p {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
margin: 0;
}
.expert-login-modern .expert-login-link p a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.expert-login-modern .expert-login-link p a:hover {
text-decoration: underline;
}
/* Right Column: Image Section */
.expert-login-modern .expert-login-right {
padding: 0;
height: 100%;
overflow: hidden;
border-radius: 20px;
}
.expert-login-modern .expert-login-right .expert-login-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.expert-login-modern .expert-login-right .expert-login-image img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
}
/* Responsive adjustments */
@media (max-width: 991.98px) {
.expert-login-modern .expert-login-left {
padding: 30px 20px;
}
.expert-login-modern .expert-login-welcome h1 {
font-size: 28px;
}
.expert-login-modern .expert-login-welcome h1 .welcome-emoji {
font-size: 28px;
}
.expert-login-modern .expert-login-welcome p {
font-size: 14px;
}
}

View File

@ -47,9 +47,9 @@
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
background: linear-gradient(135deg, var(--expert-bg-gradient-start, #f5f7fa) 0%, var(--expert-bg-gradient-end, #c3cfe2) 100%) !important;
min-height: 100vh !important;
margin: 0 !important;
padding: 0 !important;
// min-height: 100vh !important;
// margin: 0 !important;
// padding: 0 !important;
}
/* Ensure the gradient shows on the main content area */
@ -58,7 +58,7 @@ body {
.o_action_manager .o_view_controller .o_content {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
background: linear-gradient(135deg, var(--expert-bg-gradient-start, #f5f7fa) 0%, var(--expert-bg-gradient-end, #c3cfe2) 100%) !important;
min-height: 100vh !important;
// min-height: 100vh !important;
}
/* Override any Odoo background colors that might interfere */

View File

@ -0,0 +1,280 @@
/* Expert Theme - Minimal Login Template Styles */
.expert-login-minimal {
background: #FFFFFF;
min-height: 100vh;
.expert-login-left {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
min-height: 100vh;
.expert-login-form-container {
max-width: 450px;
.expert-login-logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40px;
width: 100%;
.expert-logo-icon {
width: 40px;
height: 40px;
background: #000000;
border-radius: 8px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
span {
color: white;
font-weight: bold;
font-size: 20px;
}
}
.expert-logo-text {
color: #000000;
font-size: 20px;
font-weight: 600;
}
}
.expert-login-welcome {
width: 100%;
text-align: center;
margin-bottom: 40px;
h1 {
color: #000000;
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.2;
text-align: center;
.welcome-emoji {
font-size: 32px;
}
}
p {
color: rgba(0, 0, 0, 0.7);
font-size: 16px;
margin-bottom: 0;
text-align: center;
}
}
.expert-login-form {
max-width: 450px;
width: 100%;
margin: 0 auto;
background-color: transparent !important;
.oe_login_form,
.oe_signup_form,
.oe_reset_password_form {
max-width: 450px;
background-color: transparent !important;
label {
font-weight: 500;
color: #000000;
display: block;
margin-bottom: 8px;
font-size: 14px;
text-align: start;
}
input {
&[type="text"],
&[type="password"] {
background: #FFFFFF !important;
border: 1px solid #000000 !important;
border-radius: 8px;
padding: 12px 16px;
width: 100%;
box-sizing: border-box;
color: #000000 !important;
font-size: 14px;
}
&[type="text"]::placeholder,
&[type="password"]::placeholder {
color: rgba(0, 0, 0, 0.5) !important;
}
&[type="text"]:focus,
&[type="password"]:focus {
background: #FFFFFF !important;
border-color: #000000 !important;
outline: none;
color: #000000 !important;
}
}
}
.form-check {
margin-bottom: 32px;
label {
color: #000000;
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 5px;
font-size: 14px;
cursor: pointer;
input[type="checkbox"] {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #000000;
}
}
}
.btn-link {
color: #5a5a5a;
}
.btn-primary {
width: 100%;
background: #E5E5E5;
border: none;
border-radius: 8px;
padding: 14px;
font-weight: 600;
color: #000000;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 10px;
&:hover {
background: #D5D5D5;
}
&:active {
background: #CCCCCC;
}
}
}
.expert-login-link {
margin-top: 24px;
max-width: 450px;
width: 100%;
text-align: center;
p {
color: rgba(0, 0, 0, 0.7);
font-size: 14px;
margin: 0;
a {
color: #000000;
text-decoration: underline;
font-weight: 500;
&:hover {
text-decoration: none;
}
}
}
}
}
}
.expert-login-right {
padding: 0;
height: 100%;
overflow: hidden;
min-height: 100vh;
.expert-login-image {
width: 100%;
min-height: 100vh;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
}
.expert-minimal-illustration {
width: 600px !important;
height: 600px !important;
object-fit: contain;
}
.alert {
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 20px;
text-align: start;
}
.alert-danger {
background-color: #F8D7DA;
border: 1px solid #F5C6CB;
color: #721C24;
}
.alert-success {
background-color: #D4EDDA;
border: 1px solid #C3E6CB;
color: #155724;
}
}
/* Left Column: Login Form */
/* Logo Section */
/* Welcome Section */
/* Form Section */
/* Login Link Section */
/* Right Column: Image Section */
/* Alert Messages */
/* Responsive adjustments */
@media (max-width: 991.98px) {
.expert-login-minimal {
.expert-login-left {
padding: 30px 20px;
}
.expert-login-welcome {
h1 {
font-size: 28px;
.welcome-emoji {
font-size: 28px;
}
}
p {
font-size: 14px;
}
}
}
}

View File

@ -1,177 +1,180 @@
/* Expert Theme - Modern Login Template Styles */
.expert-login-modern {
background: #19181F;
min-height: 100vh;
padding: 20px;
.container-fluid {
height: calc(100vh - 40px);
}
.row {
height: 100%;
margin: 0;
}
// Left Column: Login Form
.expert-login-left {
display: flex;
flex-direction: column;
justify-content: center;
padding: 40px;
}
// Logo Section
.expert-login-logo {
display: flex;
align-items: center;
margin-bottom: 40px;
min-height: 100vh;
.expert-logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
.expert-login-form-container {
max-width: 450px;
span {
color: white;
font-weight: bold;
font-size: 20px;
}
}
.expert-logo-text {
color: white;
font-size: 20px;
font-weight: 600;
}
}
// Welcome Section
.expert-login-welcome {
h1 {
color: white;
font-size: 36px;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.2;
.welcome-emoji {
font-size: 36px;
}
}
p {
color: rgba(255, 255, 255, 0.7);
font-size: 16px;
margin-bottom: 40px;
}
}
// Form Section
.expert-login-form {
max-width: 450px;
.form-group {
margin-bottom: 24px;
label {
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
display: block;
margin-bottom: 8px;
font-size: 14px;
}
input[type="text"],
input[type="password"] {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px 16px;
width: 100%;
box-sizing: border-box;
color: white;
font-size: 14px;
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
&:focus {
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(255, 255, 255, 0.4) !important;
outline: none;
color: white;
}
}
}
.form-check {
margin-bottom: 32px;
label {
color: rgba(255, 255, 255, 0.8);
// Logo Section
.expert-login-logo {
display: flex;
align-items: center;
font-size: 14px;
cursor: pointer;
margin-bottom: 40px;
input[type="checkbox"] {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
.expert-logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
span {
color: white;
font-weight: bold;
font-size: 20px;
}
}
.expert-logo-text {
color: white;
font-size: 20px;
font-weight: 600;
}
}
}
button[type="submit"] {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
padding: 14px;
font-weight: 600;
color: white;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
// Welcome Section
.expert-login-welcome {
h1 {
color: white;
font-size: 36px;
font-weight: 700;
margin-bottom: 12px;
line-height: 1.2;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
.welcome-emoji {
font-size: 36px;
}
}
&:active {
transform: translateY(0);
p {
color: rgba(255, 255, 255, 0.7);
font-size: 16px;
margin-bottom: 40px;
}
}
}
// Login Link Section
.expert-login-link {
margin-top: 24px;
max-width: 450px;
// Form Section
.expert-login-form {
p {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
margin: 0;
.oe_login_form,
.oe_signup_form,
.oe_reset_password_form {
max-width: 450px;
background-color: transparent !important;
a {
color: #667eea;
text-decoration: none;
font-weight: 500;
.form-label,
label {
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
display: block;
margin-bottom: 8px;
font-size: 14px;
}
&:hover {
text-decoration: underline;
.form-control {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px 16px;
width: 100%;
box-sizing: border-box;
color: white;
font-size: 14px;
&::placeholder {
color: rgba(255, 255, 255, 0.5);
}
&:focus {
background: rgba(255, 255, 255, 0.15) !important;
border-color: rgba(255, 255, 255, 0.4) !important;
outline: none;
color: white;
}
}
.form-check {
margin-bottom: 32px;
label {
color: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
font-size: 14px;
cursor: pointer;
input[type="checkbox"] {
margin-right: 8px;
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
}
}
.btn-link {
color: #939393;
}
.btn-primary {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
padding: 14px;
font-weight: 600;
color: white;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin-bottom: 10px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
&:active {
transform: translateY(0);
}
}
}
}
// Login Link Section
.expert-login-link {
margin-top: 24px;
max-width: 450px;
p {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
margin: 0;
a {
color: #667eea;
text-decoration: none;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
}
@ -183,15 +186,27 @@
height: 100%;
overflow: hidden;
border-radius: 20px 0 0 20px;
min-height: 100vh;
.expert-login-image {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 25%, #667eea 75%, #764ba2 100%);
// background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 25%, #667eea 75%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 20px;
.expert-login-image-cover {
height: 100%;
width: 100%;
background-color: #667eea;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
border-radius: 16px;
}
.expert-spiral-decoration {
width: 400px;
@ -254,5 +269,4 @@
}
}
}
}
}

View File

@ -14,7 +14,6 @@
<p>Your installed modules are listed below</p>
</div>
<div class="expert-modules-grid" id="expert-modules-container">
<!-- Modules will be loaded here via JavaScript -->
</div>
</div>
</t>

View File

@ -3,31 +3,33 @@
<data>
<!-- Create a new top-level menu for Expert Home -->
<menuitem id="menu_expert_root"
name="Expert Home"
sequence="1"/>
<menuitem id="menu_expert_home"
name="Dashboard"
parent="menu_expert_root"
action="action_expert_home"
sequence="1"/>
<menuitem id="menu_expert_login_templates"
name="Login Page Templates"
parent="menu_expert_root"
action="action_expert_login_template"
sequence="2"/>
<menuitem id="menu_expert_theme_colors"
name="Theme Colors"
parent="menu_expert_root"
action="action_expert_theme_config"
sequence="3"/>
<!-- Also add Theme Colors to the main menu for easier access -->
<menuitem id="menu_theme_colors_main"
name="Theme Colors"
action="action_expert_theme_config"
sequence="100"/>
name="Odex Theme"
web_icon="expert_theme,static/description/icon.png"
sequence="1000" />
<!-- <menuitem id="menu_expert_home"
name="Dashboard"
parent="menu_expert_root"
action="action_expert_home"
sequence="1" /> -->
<menuitem id="menu_expert_theme_colors"
name="Theme Colors"
parent="menu_expert_root"
action="action_expert_theme_config"
sequence="1" />
<menuitem id="menu_expert_login_templates"
name="Login Page Templates"
parent="menu_expert_root"
action="action_expert_login_template"
sequence="2" />
<!-- Also add Theme Colors to the main menu for easier access -->
<!-- <menuitem id="menu_theme_colors_main"
name="Theme Colors"
action="action_expert_theme_config"
sequence="100" /> -->
</data>
</odoo>
</odoo>

File diff suppressed because it is too large Load Diff

View File

@ -1,785 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Multiple Login Page Templates with Different HTML Structures
Standalone templates rendered by the /web/login override.
Each template calls web.layout and defines its own login form.
-->
<!-- Template 1: Modern Card Design -->
<template id="login_template_modern_page" name="Modern Login Template">
<t t-call="web.layout">
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
</t>
<t t-set="body_classname" t-value="'bg-100'"/>
<div class="oe_login_form expert-login-modern">
<div class="container-fluid p-0">
<div class="row">
<!-- Left Column: Login Form (6 columns) -->
<div class="col-lg-6 col-md-12 expert-login-left">
<!-- Logo and Text -->
<div class="expert-login-logo">
<t t-if="login_template and login_template.modern_template_logo">
<img t-att-src="'data:image/png;base64,%s' % login_template.modern_template_logo.decode('utf-8')"
alt="Company Logo"
style="width: 40px; height: 40px; object-fit: contain; margin-right: 12px;"/>
<span class="expert-logo-text">Expert</span>
</t>
<t t-else="">
<div class="expert-logo-icon">
<span>E</span>
</div>
<span class="expert-logo-text">Expert</span>
</t>
</div>
<!-- Welcome Text -->
<div class="expert-login-welcome">
<h1>
<t t-if="login_template and login_template.modern_login_title">
<t t-esc="login_template.modern_login_title"/>
</t>
<t t-else="">
Welcome to Expert <span class="welcome-emoji">👋</span>
</t>
</h1>
<p>
<t t-if="login_template and login_template.modern_login_subtitle">
<t t-esc="login_template.modern_login_subtitle"/>
</t>
<t t-else="">
Kindly fill in your details below to sign in to your account
</t>
</p>
</div>
<!-- Login Form -->
<form class="oe_login_form expert-login-form" role="form" method="post" t-att-action="request.httprequest.path">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="redirect" t-att-value="redirect or ''"/>
<div class="form-group">
<label>Email or Username</label>
<input type="text" name="login" class="form-control"
placeholder="Enter your email or username"
t-att-value="login"/>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" class="form-control"
placeholder="Enter your password"/>
</div>
<div class="form-check">
<label>
<input type="checkbox" name="remember"/>
Remember me
</label>
</div>
<button type="submit" class="btn btn-primary">
<t t-if="login_template and login_template.modern_login_button_text"><t t-esc="login_template.modern_login_button_text"/></t><t t-else="">Sign In</t>
</button>
</form>
<!-- Signup Link -->
<div class="expert-login-link">
<p>
Don't have an account?
<a t-attf-href="/web/signup?{{ keep_query() }}">Sign Up</a>
</p>
</div>
</div>
<!-- Right Column: Image (6 columns) -->
<div class="col-lg-6 col-md-12 d-none d-lg-block expert-login-right">
<div class="expert-login-image">
<img t-if="login_template and login_template.modern_template_image"
t-att-src="'data:image/png;base64,%s' % login_template.modern_template_image.decode('utf-8')"
alt="Login Image" class="img-fluid"/>
<img t-else=""
src="/expert_theme/static/src/img/modern-template-bg.png"
alt="Login Image" class="img-fluid"/>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Modern Signup Template -->
<template id="signup_template_modern_page" name="Modern Signup Template">
<t t-call="web.layout">
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
</t>
<t t-set="body_classname" t-value="'bg-100'"/>
<div class="oe_login_form expert-login-modern">
<div class="container-fluid p-0">
<div class="row">
<!-- Left Column: Signup Form (6 columns) -->
<div class="col-lg-6 col-md-12 expert-login-left">
<!-- Logo and Text -->
<div class="expert-login-logo">
<t t-if="login_template and login_template.modern_template_logo">
<img t-att-src="'data:image/png;base64,%s' % login_template.modern_template_logo.decode('utf-8')"
alt="Company Logo"
style="width: 40px; height: 40px; object-fit: contain; margin-right: 12px;"/>
<span class="expert-logo-text">Expert</span>
</t>
<t t-else="">
<div class="expert-logo-icon">
<span>E</span>
</div>
<span class="expert-logo-text">Expert</span>
</t>
</div>
<!-- Welcome Text -->
<div class="expert-login-welcome">
<h1>
<t t-if="login_template and login_template.modern_signup_title">
<t t-esc="login_template.modern_signup_title"/>
</t>
<t t-else="">
Create an account
</t>
</h1>
<p>
<t t-if="login_template and login_template.modern_signup_subtitle">
<t t-esc="login_template.modern_signup_subtitle"/>
</t>
<t t-else="">
Join us today and get started
</t>
</p>
</div>
<!-- Signup Form -->
<form class="oe_signup_form expert-login-form" role="form" method="post" t-if="not message">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="redirect" t-att-value="redirect or ''"/>
<input type="hidden" name="token" t-att-value="token or ''"/>
<div class="form-group">
<label>Your Email</label>
<input type="text" name="login" class="form-control"
placeholder="Enter your email"
t-att-value="login"
t-att-readonly="'readonly' if (token and not invalid_token) else None"
autofocus="autofocus"
autocapitalize="off"
required="required"/>
</div>
<div class="form-group" t-if="not (token and not invalid_token)">
<label>Your Name</label>
<input type="text" name="name" class="form-control"
placeholder="e.g. John Doe"
t-att-value="name"
required="required"/>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" id="password" class="form-control"
placeholder="Enter your password"
required="required"
t-att-autofocus="'autofocus' if (token and not invalid_token) else None"/>
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" name="confirm_password" id="confirm_password" class="form-control"
placeholder="Confirm your password"
required="required"/>
</div>
<p class="alert alert-danger" t-if="error" role="alert">
<t t-esc="error"/>
</p>
<button type="submit" class="btn btn-primary">
<t t-if="login_template and login_template.modern_signup_button_text">
<t t-esc="login_template.modern_signup_button_text"/>
</t>
<t t-else="">
Sign Up
</t>
</button>
</form>
<!-- Signup Link -->
<div class="expert-login-link">
<p>
Already have an account?
<a t-attf-href="/web/login?{{ keep_query() }}">Log In</a>
</p>
</div>
</div>
<!-- Right Column: Image (6 columns) -->
<div class="col-lg-6 col-md-12 d-none d-lg-block expert-login-right">
<div class="expert-login-image">
<img t-if="login_template and login_template.modern_template_image"
t-att-src="'data:image/png;base64,%s' % login_template.modern_template_image.decode('utf-8')"
alt="Signup Image" class="img-fluid"/>
<img t-else=""
src="/expert_theme/static/src/img/modern-template-bg.png"
alt="Signup Image" class="img-fluid"/>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Modern Reset Password Template -->
<template id="reset_password_template_modern_page" name="Modern Reset Password Template">
<t t-call="web.layout">
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
</t>
<t t-set="body_classname" t-value="'bg-100'"/>
<div class="oe_login_form expert-login-modern">
<div class="container-fluid p-0">
<div class="row">
<!-- Left Column: Reset Password Form (6 columns) -->
<div class="col-lg-6 col-md-12 expert-login-left">
<!-- Logo and Text -->
<div class="expert-login-logo">
<t t-if="login_template and login_template.modern_template_logo">
<img t-att-src="'data:image/png;base64,%s' % login_template.modern_template_logo.decode('utf-8')"
alt="Company Logo"
style="width: 40px; height: 40px; object-fit: contain; margin-right: 12px;"/>
<span class="expert-logo-text">Expert</span>
</t>
<t t-else="">
<div class="expert-logo-icon">
<span>E</span>
</div>
<span class="expert-logo-text">Expert</span>
</t>
</div>
<!-- Welcome Text -->
<div class="expert-login-welcome">
<h1>
<t t-if="login_template and login_template.modern_reset_title">
<t t-esc="login_template.modern_reset_title"/>
</t>
<t t-else="">
Reset Password <span class="welcome-emoji">🔐</span>
</t>
</h1>
<p t-if="not token">
<t t-if="login_template and login_template.modern_reset_subtitle">
<t t-esc="login_template.modern_reset_subtitle"/>
</t>
<t t-else="">
Enter your email address and we'll send you instructions to reset your password
</t>
</p>
<p t-if="token and not invalid_token">
Enter your new password below
</p>
</div>
<!-- Success Message -->
<div t-if="message" class="expert-login-form">
<p class="alert alert-success" role="status">
<t t-esc="message"/>
</p>
<a href="/web/login" class="btn btn-link" style="color: #667eea;">Back to Login</a>
</div>
<!-- Reset Password Form -->
<form class="oe_reset_password_form expert-login-form" role="form" method="post" t-if="not message">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="redirect" t-att-value="redirect or ''"/>
<input type="hidden" name="token" t-att-value="token or ''"/>
<!-- Token-based reset (set new password) -->
<t t-if="token and not invalid_token">
<div class="form-group">
<label>New Password</label>
<input type="password" name="password" id="password" class="form-control"
placeholder="Enter your new password"
required="required"
autofocus="autofocus"/>
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" name="confirm_password" id="confirm_password" class="form-control"
placeholder="Confirm your new password"
required="required"/>
</div>
</t>
<!-- Email-based reset (request reset) -->
<t t-if="not token">
<div class="form-group">
<label>Your Email</label>
<input type="text" name="login" class="form-control"
placeholder="Enter your email"
t-att-value="login"
autofocus="autofocus"
required="required"
autocapitalize="off"/>
</div>
</t>
<p class="alert alert-danger" t-if="error" role="alert">
<t t-esc="error"/>
</p>
<button type="submit" class="btn btn-primary">
<t t-if="token and not invalid_token">Reset Password</t>
<t t-if="not token">Send Reset Instructions</t>
</button>
</form>
<!-- Reset Password Link -->
<div class="expert-login-link" t-if="not message">
<p>
<a t-if="not token" t-attf-href="/web/login?{{ keep_query() }}" style="color: #667eea;">Back to Login</a>
<a t-if="invalid_token" href="/web/login" style="color: #667eea;">Back to Login</a>
</p>
</div>
</div>
<!-- Right Column: Image (6 columns) -->
<div class="col-lg-6 col-md-12 d-none d-lg-block expert-login-right">
<div class="expert-login-image">
<img t-if="login_template and login_template.modern_template_image"
t-att-src="'data:image/png;base64,%s' % login_template.modern_template_image.decode('utf-8')"
alt="Reset Password Image" class="img-fluid"/>
<img t-else=""
src="/expert_theme/static/src/img/modern-template-bg.png"
alt="Reset Password Image" class="img-fluid"/>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Template 2: Minimal Design -->
<template id="login_template_minimal_page" name="Minimal Login Template">
<t t-call="web.layout">
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
</t>
<t t-set="body_classname" t-value="'bg-100'"/>
<div class="oe_login_form expert-login-minimal">
<div class="container-fluid p-0">
<div class="row">
<!-- Left Column: Login Form (6 columns) -->
<div class="col-lg-6 col-md-12 expert-login-left">
<!-- Logo and Text -->
<div class="expert-login-logo">
<t t-if="login_template and login_template.minimal_template_logo">
<img t-att-src="'data:image/png;base64,%s' % login_template.minimal_template_logo.decode('utf-8')"
alt="Company Logo"
style="width: 40px; height: 40px; object-fit: contain; margin-right: 12px;"/>
<span class="expert-logo-text">Expert</span>
</t>
<t t-else="">
<div class="expert-logo-icon">
<span>E</span>
</div>
<span class="expert-logo-text">Expert</span>
</t>
</div>
<!-- Welcome Text -->
<div class="expert-login-welcome">
<h1>
<t t-if="login_template and login_template.minimal_login_title">
<t t-esc="login_template.minimal_login_title"/>
</t>
<t t-else="">
Welcome to Expert <span class="welcome-emoji">👋</span>
</t>
</h1>
<p>
<t t-if="login_template and login_template.minimal_login_subtitle">
<t t-esc="login_template.minimal_login_subtitle"/>
</t>
<t t-else="">
Kindly fill in your details below to sign in to your account
</t>
</p>
</div>
<!-- Login Form -->
<form class="oe_login_form expert-login-form" role="form" method="post" t-att-action="request.httprequest.path">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="redirect" t-att-value="redirect or ''"/>
<div class="form-group">
<label>Email or Username</label>
<input type="text" name="login" class="form-control"
placeholder="Enter your email or username"
t-att-value="login"/>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" class="form-control"
placeholder="Enter your password"/>
</div>
<div class="form-check">
<label>
<input type="checkbox" name="remember"/>
Remember me
</label>
</div>
<button type="submit" class="btn btn-primary">
<t t-if="login_template and login_template.modern_login_button_text"><t t-esc="login_template.modern_login_button_text"/></t><t t-else="">Sign In</t>
</button>
</form>
<!-- Signup Link -->
<div class="expert-login-link">
<p>
Don't have an account?
<a t-attf-href="/web/signup?{{ keep_query() }}">Sign Up</a>
</p>
</div>
</div>
<!-- Right Column: Image (6 columns) -->
<div class="col-lg-6 col-md-12 d-none d-lg-block expert-login-right">
<div class="expert-login-image">
<img t-if="login_template and login_template.minimal_template_image"
t-att-src="'data:image/png;base64,%s' % login_template.minimal_template_image.decode('utf-8')"
alt="Login Illustration" class="img-fluid expert-minimal-illustration"/>
<img t-else=""
src="/expert_theme/static/src/img/minimal-login-img.png"
alt="Login Illustration" class="img-fluid expert-minimal-illustration"/>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Minimal Signup Template -->
<template id="signup_template_minimal_page" name="Minimal Signup Template">
<t t-call="web.layout">
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
</t>
<t t-set="body_classname" t-value="'bg-100'"/>
<div class="oe_login_form expert-login-minimal">
<div class="container-fluid p-0">
<div class="row">
<!-- Left Column: Signup Form (6 columns) -->
<div class="col-lg-6 col-md-12 expert-login-left">
<!-- Logo and Text -->
<div class="expert-login-logo">
<t t-if="login_template and login_template.minimal_template_logo">
<img t-att-src="'data:image/png;base64,%s' % login_template.minimal_template_logo.decode('utf-8')"
alt="Company Logo"
style="width: 40px; height: 40px; object-fit: contain; margin-right: 12px;"/>
<span class="expert-logo-text">Expert</span>
</t>
<t t-else="">
<div class="expert-logo-icon">
<span>E</span>
</div>
<span class="expert-logo-text">Expert</span>
</t>
</div>
<!-- Welcome Text -->
<div class="expert-login-welcome">
<h1>
<t t-if="login_template and login_template.minimal_signup_title">
<t t-esc="login_template.minimal_signup_title"/>
</t>
<t t-else="">
Create an account
</t>
</h1>
<p>
<t t-if="login_template and login_template.minimal_signup_subtitle">
<t t-esc="login_template.minimal_signup_subtitle"/>
</t>
<t t-else="">
<a t-attf-href="/web/login?{{ keep_query() }}" style="color: #000; text-decoration: underline;">log in instead</a>
</t>
</p>
</div>
<!-- Signup Form -->
<form class="oe_signup_form expert-login-form" role="form" method="post" t-if="not message">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="redirect" t-att-value="redirect or ''"/>
<input type="hidden" name="token" t-att-value="token or ''"/>
<div class="form-group" t-if="not (token and not invalid_token)">
<label>Your Name</label>
<input type="text" name="name" class="form-control"
placeholder="e.g. John Doe"
t-att-value="name"
required="required"
t-att-autofocus="'autofocus' if login and not (token and not invalid_token) else None"/>
</div>
<div class="form-group">
<label>Your Email</label>
<input type="text" name="login" class="form-control"
placeholder="Enter your email"
t-att-value="login"
t-att-readonly="'readonly' if (token and not invalid_token) else None"
autofocus="autofocus"
autocapitalize="off"
required="required"/>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" id="password" class="form-control"
placeholder="Enter your password"
required="required"
t-att-autofocus="'autofocus' if (token and not invalid_token) else None"/>
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" name="confirm_password" id="confirm_password" class="form-control"
placeholder="Confirm your password"
required="required"/>
</div>
<div class="form-check">
<label>
<input type="checkbox" name="terms" required="required"/>
By creating an account, I agree to our <a href="#" style="color: #000; text-decoration: underline;">Terms of use</a> and <a href="#" style="color: #000; text-decoration: underline;">Privacy Policy</a>
</label>
</div>
<p class="alert alert-danger" t-if="error" role="alert">
<t t-esc="error"/>
</p>
<button type="submit" class="btn btn-primary">
Create an account
</button>
</form>
<!-- Login Link -->
<div class="expert-login-link">
<p>
Already have an account?
<a t-attf-href="/web/login?{{ keep_query() }}" style="color: #000; text-decoration: underline;">Log In</a>
</p>
</div>
</div>
<!-- Right Column: Image (6 columns) -->
<div class="col-lg-6 col-md-12 d-none d-lg-block expert-login-right">
<div class="expert-login-image">
<img t-if="login_template and login_template.minimal_template_image"
t-att-src="'data:image/png;base64,%s' % login_template.minimal_template_image.decode('utf-8')"
alt="Signup Illustration" class="img-fluid expert-minimal-illustration"/>
<img t-else=""
src="/expert_theme/static/src/img/minimal-login-img.png"
alt="Signup Illustration" class="img-fluid expert-minimal-illustration"/>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Minimal Reset Password Template -->
<template id="reset_password_template_minimal_page" name="Minimal Reset Password Template">
<t t-call="web.layout">
<t t-set="head">
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
</t>
<t t-set="body_classname" t-value="'bg-100'"/>
<div class="oe_login_form expert-login-minimal">
<div class="container-fluid p-0">
<div class="row">
<!-- Left Column: Reset Password Form (6 columns) -->
<div class="col-lg-6 col-md-12 expert-login-left">
<!-- Logo and Text -->
<div class="expert-login-logo">
<t t-if="login_template and login_template.minimal_template_logo">
<img t-att-src="'data:image/png;base64,%s' % login_template.minimal_template_logo.decode('utf-8')"
alt="Company Logo"
style="width: 40px; height: 40px; object-fit: contain; margin-right: 12px;"/>
<span class="expert-logo-text">Expert</span>
</t>
<t t-else="">
<div class="expert-logo-icon">
<span>E</span>
</div>
<span class="expert-logo-text">Expert</span>
</t>
</div>
<!-- Welcome Text -->
<div class="expert-login-welcome">
<h1>
<t t-if="login_template and login_template.minimal_reset_title">
<t t-esc="login_template.minimal_reset_title"/>
</t>
<t t-else="">
Reset Password <span class="welcome-emoji">🔐</span>
</t>
</h1>
<p t-if="not token">
<t t-if="login_template and login_template.minimal_reset_subtitle">
<t t-esc="login_template.minimal_reset_subtitle"/>
</t>
<t t-else="">
Enter your email address and we'll send you instructions to reset your password
</t>
</p>
<p t-if="token and not invalid_token">
Enter your new password below
</p>
</div>
<!-- Success Message -->
<div t-if="message" class="expert-login-form">
<p class="alert alert-success" role="status">
<t t-esc="message"/>
</p>
<a href="/web/login" class="btn btn-link" style="color: #000; text-decoration: underline;">Back to Login</a>
</div>
<!-- Reset Password Form -->
<form class="oe_reset_password_form expert-login-form" role="form" method="post" t-if="not message">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="redirect" t-att-value="redirect or ''"/>
<input type="hidden" name="token" t-att-value="token or ''"/>
<!-- Token-based reset (set new password) -->
<t t-if="token and not invalid_token">
<div class="form-group">
<label>New Password</label>
<input type="password" name="password" id="password" class="form-control"
placeholder="Enter your new password"
required="required"
autofocus="autofocus"/>
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" name="confirm_password" id="confirm_password" class="form-control"
placeholder="Confirm your new password"
required="required"/>
</div>
</t>
<!-- Email-based reset (request reset) -->
<t t-if="not token">
<div class="form-group">
<label>Email</label>
<input type="text" name="login" class="form-control"
placeholder="Enter your email"
t-att-value="login"
autofocus="autofocus"
required="required"
autocapitalize="off"/>
</div>
</t>
<p class="alert alert-danger" t-if="error" role="alert">
<t t-esc="error"/>
</p>
<button type="submit" class="btn btn-primary">
<t t-if="token and not invalid_token">Reset Password</t>
<t t-if="not token">Send Reset Instructions</t>
</button>
</form>
<!-- Reset Password Link -->
<div class="expert-login-link" t-if="not message">
<p>
<a t-if="not token" t-attf-href="/web/login?{{ keep_query() }}" style="color: #000; text-decoration: underline;">Back to Login</a>
<a t-if="invalid_token" href="/web/login" style="color: #000; text-decoration: underline;">Back to Login</a>
</p>
</div>
</div>
<!-- Right Column: Image (6 columns) -->
<div class="col-lg-6 col-md-12 d-none d-lg-block expert-login-right">
<div class="expert-login-image">
<img t-if="login_template and login_template.minimal_template_image"
t-att-src="'data:image/png;base64,%s' % login_template.minimal_template_image.decode('utf-8')"
alt="Reset Password Illustration" class="img-fluid expert-minimal-illustration"/>
<img t-else=""
src="/expert_theme/static/src/img/minimal-login-img.png"
alt="Reset Password Illustration" class="img-fluid expert-minimal-illustration"/>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Template 3: Corporate Design -->
<template id="login_template_corporate_page" name="Corporate Login Template">
<t t-call="web.layout">
<div class="oe_login_form" style="background: #1a1a1a; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px;">
<div style="width: 100%; max-width: 500px; background: #2d2d2d; padding: 60px 50px; border-radius: 8px; border: 1px solid #404040;">
<div class="text-center mb-5">
<h2 style="color: #fff; font-size: 32px; font-weight: 600; margin-bottom: 10px;">Company Portal</h2>
<p style="color: #999; font-size: 14px; margin: 0;">Secure Login Access</p>
</div>
<form class="oe_login_form" role="form" method="post" t-att-action="request.httprequest.path">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="redirect" t-att-value="redirect or ''"/>
<div style="margin-bottom: 25px;">
<label style="color: #ccc; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; display: block;">Username</label>
<input type="text" name="login" class="form-control"
placeholder="Enter your username"
t-att-value="login"
style="background: #1a1a1a; border: 1px solid #404040; color: #fff; padding: 12px 15px; border-radius: 4px; width: 100%; box-sizing: border-box;"/>
</div>
<div style="margin-bottom: 35px;">
<label style="color: #ccc; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; display: block;">Password</label>
<input type="password" name="password" class="form-control"
placeholder="Enter your password"
style="background: #1a1a1a; border: 1px solid #404040; color: #fff; padding: 12px 15px; border-radius: 4px; width: 100%; box-sizing: border-box;"/>
</div>
<button type="submit" class="btn btn-block"
style="width: 100%; background: #007bff; color: white; border: none; padding: 14px; font-size: 15px; font-weight: 600; border-radius: 4px; text-transform: uppercase; letter-spacing: 0.5px;">
Access Portal
</button>
</form>
</div>
</div>
</t>
</template>
</odoo>

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import version

View File

@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
{
'name': 'Odex30 Web',
'category': 'Odex30-base',
'author': 'Expert Co. Ltd.',
'name': 'Odex Web Client',
'category': 'Hidden',
'version': '1.0',
'description': """
Odex Web Client.
===========================
This module modifies the web addon to provide Odex design and responsiveness.
Web Client customizations for Odex
""",
'summary': 'Web Client customizations for Odex',
'author': 'Exp SA',
'website': 'https://exp-sa.com',
'company': 'Expert Ltd.',
'depends': ['web', 'base_setup'],
'auto_install': ['web'],
'data': [
@ -27,7 +28,7 @@ This module modifies the web addon to provide Odex design and responsiveness.
('before', 'web/static/src/scss/bootstrap_overridden.scss', 'odex30_web/static/src/scss/bootstrap_overridden.scss'),
],
'web.assets_frontend': [
'odex30_web/static/src/webclient/home_menu/home_menu_background.scss',
'odex30_web/static/src/webclient/home_menu/home_menu_background.scss', # used by login page
'odex30_web/static/src/webclient/navbar/navbar.scss',
],
'web.assets_backend': [
@ -42,6 +43,7 @@ This module modifies the web addon to provide Odex design and responsiveness.
'odex30_web/static/src/views/**/*.xml',
('remove', 'odex30_web/static/src/views/pivot/**'),
# Don't include dark mode files in light mode
('remove', 'odex30_web/static/src/**/*.dark.scss'),
],
'web.assets_backend_lazy': [
@ -49,37 +51,29 @@ This module modifies the web addon to provide Odex design and responsiveness.
],
'web.assets_backend_lazy_dark': [
('include', 'web.dark_mode_variables'),
# web._assets_backend_helpers
('before', 'odex30_web/static/src/scss/bootstrap_overridden.scss', 'odex30_web/static/src/scss/bootstrap_overridden.dark.scss'),
('after', 'web/static/lib/bootstrap/scss/_functions.scss', 'odex30_web/static/src/scss/bs_functions_overridden.dark.scss'),
],
'web.assets_web': [
('replace', 'web/static/src/main.js', 'odex30_web/static/src/main.js'),
],
# ========= Dark Mode =========
"web.dark_mode_variables": [
# web._assets_primary_variables
('before', 'odex30_web/static/src/scss/primary_variables.scss', 'odex30_web/static/src/scss/primary_variables.dark.scss'),
('before', 'odex30_web/static/src/**/*.variables.scss', 'odex30_web/static/src/**/*.variables.dark.scss'),
# web._assets_secondary_variables
('before', 'odex30_web/static/src/scss/secondary_variables.scss', 'odex30_web/static/src/scss/secondary_variables.dark.scss'),
],
"web.assets_web_dark": [
('include', 'web.dark_mode_variables'),
# web._assets_backend_helpers
('before', 'odex30_web/static/src/scss/bootstrap_overridden.scss', 'odex30_web/static/src/scss/bootstrap_overridden.dark.scss'),
('after', 'web/static/lib/bootstrap/scss/_functions.scss', 'odex30_web/static/src/scss/bs_functions_overridden.dark.scss'),
# assets_backend
'odex30_web/static/src/**/*.dark.scss',
],
'web.tests_assets': [
'odex30_web/static/tests/*.js',
],
"web.assets_tests": [
"odex30_web/static/tests/tours/**/*.js",
],
'web.assets_unit_tests': [
'odex30_web/static/tests/**/*.test.js',
],
'web.qunit_suite_tests': [
'odex30_web/static/tests/views/**/*.js',
'odex30_web/static/tests/webclient/**/*.js',
('remove', 'odex30_web/static/tests/**/*.test.js'),
],
},
'license': 'OEEL-1',
'license': 'LGPL-3',
}

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_http
from . import res_users_settings

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import json
@ -10,10 +11,10 @@ class Http(models.AbstractModel):
@classmethod
def _post_logout(cls):
super()._post_logout()
request.future_response.set_cookie('color_scheme', max_age=0)
def webclient_rendering_context(self):
""" Extend the rendering context with session info."""
return {
'session_info': self.session_info(),
}
@ -21,17 +22,6 @@ class Http(models.AbstractModel):
def session_info(self):
ICP = self.env['ir.config_parameter'].sudo()
if self.env.user.has_group('base.group_system'):
warn_enterprise = 'admin'
elif self.env.user._is_internal():
warn_enterprise = 'user'
else:
warn_enterprise = False
result = super(Http, self).session_info()
result['support_url'] = "https://www.odoo.com/help"
if warn_enterprise:
result['warning'] = warn_enterprise
result['expiration_date'] = ICP.get_param('database.expiration_date')
result['expiration_reason'] = ICP.get_param('database.expiration_reason')
result['support_url'] = "https://exp-sa.com/support_center"
return result

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from odoo import fields, models

View File

@ -1,3 +1,4 @@
// Custom SCSS for enterprise version of notebook tabs
.o_notebook {
--notebook-link-border-color: #{$border-color};

View File

@ -1,11 +1,11 @@
/** @odoo-module **/
import { startWebClient } from "@web/start";
import { WebClientOdex } from "./webclient/webclient";
import { WebClientEnterprise } from "./webclient/webclient";
/**
* This file starts the webclient. In the manifest, it replaces
* This file starts the enterprise webclient. In the manifest, it replaces
* the community main.js to load a different webclient class
* (WebClientOdex instead of WebClient)
* (WebClientEnterprise instead of WebClient)
*/
startWebClient(WebClientOdex);
startWebClient(WebClientEnterprise);

View File

@ -61,7 +61,6 @@ $component-active-bg: $o-gray-300 !default;
// == Typography
$mark-bg: #ffdebc !default;
$mark-color: shift-color($mark-bg, -75%) !default;
// == Tables
$table-bg: $o-view-background-color !default;

View File

@ -1,5 +1,5 @@
$o-colors-original: lighten(#000, 46.7%), #e74e4e, #f4b660, #F7CD1F, #6cedeb, #8d5482,
#f07b50, #2C8397, #475577, #dc0457, #30C381, #9365B8 !default;
$o-colors-original: lighten(#000, 46.7%), #f07b50, #f4b660, #F7CD1F, #6cedeb, #8d5482,
#e74e4e, #2C8397, #475577, #dc0457, #30C381, #9365B8 !default;
$o-colors-secondary-original: #aa4b6b, #30C381, #97743a, #F7CD1F, #4285F4, #8E24AA,
#D6145F, #173e43, #348F50, #AA3A38, #795548, #5e0231,

View File

@ -1,5 +1,5 @@
.o_field_property_definition_type, .o_field_property_definition_type_menu {
img {
.o_field_property_dropdown > img {
-webkit-filter: invert(100%);
filter: invert(100%);
}
@ -11,3 +11,9 @@
}
}
}
.o_field_property_definition_type_popover.popover {
.dropdown-item img {
-webkit-filter: invert(100%);
filter: invert(100%);
}
}

View File

@ -60,9 +60,6 @@ export const patchListRendererDesktop = () => ({
// we set them to not editable too.
return false;
}
if (action.res_model === "account.bank.statement.line") {
return false; // bank reconciliation isn't editable
}
return Boolean(action.res_model);
};
const onUiUpdated = () => {

View File

@ -3,7 +3,7 @@ import { BurgerMenu } from "@web/webclient/burger_menu/burger_menu";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
export class EnterpriseBurgerMenu extends BurgerMenu {
export class OdexBurgerMenu extends BurgerMenu {
setup() {
super.setup();
this.hm = useService("home_menu");
@ -15,7 +15,7 @@ export class EnterpriseBurgerMenu extends BurgerMenu {
}
const systrayItem = {
Component: EnterpriseBurgerMenu,
Component: OdexBurgerMenu,
};
registry.category("systray").add("burger_menu", systrayItem, { sequence: 0, force: true });

View File

@ -4,7 +4,6 @@ import { hasTouch, isIosApp, isMacOS } from "@web/core/browser/feature_detection
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { user } from "@web/core/user";
import { useService } from "@web/core/utils/hooks";
import { ExpirationPanel } from "./expiration_panel";
import { useSortable } from "@web/core/utils/sortable_owl";
import {
@ -37,7 +36,7 @@ class FooterComponent extends Component {
*/
export class HomeMenu extends Component {
static template = "odex30_web.HomeMenu";
static components = { ExpirationPanel };
static components = { };
static props = {
apps: {
type: Array,
@ -93,7 +92,6 @@ export class HomeMenu extends Component {
this.command = useService("command");
this.menus = useService("menu");
this.homeMenuService = useService("home_menu");
this.subscription = useState(useService("enterprise_subscription"));
this.ui = useService("ui");
this.state = useState({
focusedIndex: null,
@ -351,9 +349,7 @@ export class HomeMenu extends Component {
_onInputSearch() {
const onClose = () => {
this._focusInput();
if (this.inputRef.el) {
this.inputRef.el.value = "";
}
this.inputRef.el.value = "";
};
const searchValue = this.compositionStart ? "/" : `/${this.inputRef.el.value.trim()}`;
this.compositionStart = false;

View File

@ -1,5 +1,9 @@
.o_home_menu_background {
// 'Home menu background' design is shared with enterprise login
// screens and it's located in './home_menu_background.scss'
// When applied on webclient (note: we do not specify the webclient class
// here to avoid breaking studio custom style)
&:not(.o_home_menu_background_custom):not(.o_in_studio) .o_main_navbar {
background: transparent;
border-bottom-color: transparent;
@ -36,8 +40,8 @@
.o_app_icon {
width: $o-home-menu-app-icon-max-width;
aspect-ratio: 1;
padding: $o-home-menu-app-icon-padding;
background-color: var(--AppSwitcherIcon-background, #{$o-home-menu-app-icon-background-color});
padding: 10px;
background-color: var(--AppSwitcherIcon-background, rgba(#fff, 1));
object-fit: cover;
transform-origin: center bottom;
transition: box-shadow ease-in 0.1s, transform ease-in 0.1s;

View File

@ -1,8 +1,6 @@
$o-home-menu-font-size-base: 1rem;
$o-home-menu-container-size: 850px;
$o-home-menu-app-icon-max-width: 70px;
$o-home-menu-app-icon-padding: 10px;
$o-home-menu-app-icon-background-color: rgba(#fff, 1);
$o-home-menu-caption-color: $o-gray-700 !default;
$o-home-menu-caption-shadow: none !default;

View File

@ -10,10 +10,7 @@
t-att-aria-expanded="displayedApps.length ? 'true' : 'false'"
aria-autocomplete="list"
aria-haspopup="listbox"
autocomplete="off"
/>
<!-- When the subscription has expired, the expiration panel is show over the whole UI instead of here -->
<ExpirationPanel t-if="subscription.warningType and !subscription.isWarningHidden and subscription.daysLeft &lt;= 30 and subscription.daysLeft > 0"/>
<div t-if="displayedApps.length" role="listbox" class="o_apps row user-select-none mt-5 mx-0">
<div t-foreach="displayedApps" t-as="app" t-key="app.id" class="col-3 col-md-2 o_draggable mb-3 px-0">
<a t-att-id="'result_app_' + app_index"

View File

@ -4,6 +4,6 @@
size: cover;
attachment: fixed;
color: var(--homeMenu-bg-color, #{$o-gray-200});
image: var(--homeMenu-bg-image, url("/odex30_web/static/img/background-light.svg"));
image: var(--homeMenu-bg-image, url("/odex30_web/static/img/background-light.svg"));
}
}

View File

@ -5,8 +5,8 @@ import { useService, useBus } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { useState, useEffect, useRef } from "@odoo/owl";
export class OdexNavBar extends NavBar {
static template = "odex30_web.OdexNavBar";
export class EnterpriseNavBar extends NavBar {
static template = "odex30_web.EnterpriseNavBar";
setup() {
super.setup();
this.hm = useState(useService("home_menu"));

View File

@ -14,7 +14,8 @@
}
}
// Ensuring SuperUser Design menu is not compressed in Enterprise
// ============================================================================
body.o_is_superuser .o_menu_systray {
border-image-outset: map-get($border-widths, 5);
}

View File

@ -1,4 +1,5 @@
// = Enterprise Main Navbar Variables
// ============================================================================
$o-navbar-background: $o-white !default;
$o-navbar-padding-v: 10px !default;
$o-navbar-border-bottom: 0 !default;

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="odex30_web.OdexNavBar" t-inherit="web.NavBar" t-inherit-mode="primary">
<t t-name="odex30_web.EnterpriseNavBar" t-inherit="web.NavBar" t-inherit-mode="primary">
<xpath expr="//nav" position="attributes">
<attribute name="t-ref">nav</attribute>
</xpath>
@ -45,7 +45,7 @@
<xpath expr="//DropdownItem[@t-esc='currentApp.name']" position="replace"/>
</t>
<t t-name="odex30_web.OdexNavBar.SectionsMenu" t-inherit="web.NavBar.SectionsMenu" t-inherit-mode="extension">
<t t-name="odex30_web.EnterpriseNavBar.SectionsMenu" t-inherit="web.NavBar.SectionsMenu" t-inherit-mode="extension">
<xpath expr="//Dropdown/button" position="attributes">
<attribute name="class" add="fw-normal" separator=" "/>
</xpath>

View File

@ -5,20 +5,10 @@
<xpath expr="//h3" position="replace">
<h3 class="px-0">
Odoo <t t-esc="serverVersion"/> (Enterprise Edition)
Odex 30
</h3>
</xpath>
<xpath expr="//*[@id='license']" position="replace">
<a id="license" target="_blank" href="https://www.odoo.com/documentation/master/legal/licenses.html" style="text-decoration: underline;">Odoo Enterprise Edition License V1.0</a>
</xpath>
<xpath expr="//h3" position="after">
<t t-if="expirationDate">
<h5>Database expiration: <t t-esc="expirationDate"/></h5>
</t>
</xpath>
</t>
</templates>

View File

@ -2,12 +2,12 @@
import { WebClient } from "@web/webclient/webclient";
import { useService } from "@web/core/utils/hooks";
import { OdexNavBar } from "./navbar/navbar";
import { EnterpriseNavBar } from "./navbar/navbar";
export class WebClientOdex extends WebClient {
export class WebClientEnterprise extends WebClient {
static components = {
...WebClient.components,
NavBar: OdexNavBar,
NavBar: EnterpriseNavBar,
};
setup() {
super.setup();

View File

@ -1 +0,0 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,142 @@
========================
Odex Sidebar Backend Theme v2
========================
Overview
========
This module provides a modern, collapsible sidebar menu system that replaces the default Odex app menu bar with an enhanced navigation experience. It delivers an improved user interface with better space management and responsive design.
Features
========
* **Collapsible/Expandable Sidebar Menu**: Toggle the sidebar to maximize screen space for your work
* **Smooth Navigation**: Seamlessly navigate across all Odex applications and menus
* **Responsive Design**: Adapts beautifully to different screen sizes (desktop, tablet, mobile)
* **Persistent State**: Sidebar collapse/expand state is saved and persists across user sessions
* **Enable/Disable Toggle**: Control the sidebar feature through the Settings panel
* **Optimized Performance**: Minimal performance impact with efficient menu rendering
Installation
============
1. Download or clone this module into your Odex addons directory
2. Restart the Odex server
3. Navigate to **Apps** and search for "Odex Sidebar Backend Theme 2"
4. Click **Install**
Dependencies
============
This module requires:
* Odex 18.0 (or compatible version)
* web module
* base module
Configuration
=============
After installation, you can configure the sidebar menu feature:
1. Navigate to **Settings > General Settings** (or **Settings > Sidebar Menu** if available)
2. Find the **Sidebar Menu** section
3. Enable or disable the sidebar menu feature as needed
4. Click **Save**
The setting is automatically saved and persists across all user sessions.
Usage
=====
Once enabled:
* Click the **menu icon** (≡) at the top of the sidebar to collapse/expand it
* The sidebar state is automatically saved for your next session
* All applications and menu items are accessible through the sidebar
* The responsive design automatically adjusts on smaller screens
Module Structure
================
::
odex_sidebar_backend_theme2/
├── __init__.py # Python initialization
├── __manifest__.py # Module metadata
├── views/
│ └── res_config_settings.xml # Settings configuration
├── static/
│ └── src/
│ ├── scss/
│ │ └── sidebar_menu.scss # Sidebar styling
│ ├── js/
│ │ ├── sidebar_menu.js # Main sidebar logic
│ │ ├── sidebar_css_loader.js # CSS loading functionality
│ │ ├── menu_item.js # Menu item handling
│ │ └── navbar_patch.js # Navigation bar customizations
│ └── xml/
│ ├── sidebar_menu_template.xml # Sidebar template
│ ├── menu_item_template.xml # Menu item template
│ └── navbar_patch.xml # Navbar patches
└── README.rst # This file
Technologies
=============
* JavaScript (ES6+)
* SCSS/CSS
* XML (Odex QWeb templates)
* Python
Browser Support
===============
* Chrome/Edge 90+
* Firefox 88+
* Safari 14+
* Mobile browsers (iOS Safari, Chrome Mobile)
Troubleshooting
===============
**Sidebar not appearing:**
- Clear your browser cache
- Log out and log back in
- Check that the module is enabled in Settings
**Sidebar not saving state:**
- Ensure cookies are enabled in your browser
- Check browser console for JavaScript errors (F12)
**Performance issues:**
- Clear browser cache and local storage
- Try disabling and re-enabling the module
Support
=======
For issues, questions, or feature requests, please contact Epxert Ltd.
* Website: https://www.exp-sa.com
* Email: support@exp-sa.com
License
=======
This module is licensed under the LGPL-3 License. See the LICENSE file for details.
Authors
=======
* Epxert Ltd. (https://www.exp-sa.com)
Changelog
=========
**Version 1.0.0** (Initial Release)
- Initial release of Odex Sidebar Backend Theme 2
- Collapsible sidebar menu functionality
- Persistent state management
- Responsive design support
- Enable/disable toggle in settings

View File

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

View File

@ -1,26 +1,46 @@
# -*- coding: utf-8 -*-
{
'name': 'Odex Sidebar Backend Theme2',
'version': '18.0.1.0.0',
'category': 'Web',
'summary': 'Custom menu for navigating all Odoo apps and menus smoothly',
'name': 'Odex Sidebar Backend Theme 2',
'version': '1.0.0',
'category': 'Web/Themes',
'summary': 'Enhanced collapsible sidebar menu for Odex backend navigation',
'description': """
Replace the default Odoo app menu bar with a collapsible sidebar menu.
Odex Sidebar Backend Theme 2
=============================
This module provides a modern, collapsible sidebar menu system that replaces
the default Odex app menu bar with an enhanced navigation experience.
Key Features:
- Collapsible/expandable sidebar menu for better space management
- Smooth navigation across all Odex applications and menus
- Responsive design that adapts to different screen sizes
- Persistent sidebar state (collapse/expand) across sessions
- Enable/disable sidebar via Settings panel
- Minimal performance impact with optimized menu rendering
Configuration:
Navigate to Settings > Sidebar Menu to enable or disable the sidebar menu feature.
The setting is automatically saved and persists across all user sessions.
""",
'author': 'Your Company',
'website': 'https://www.yourcompany.com',
'depends': ['web'],
'data': [],
'author': 'Epxert Ltd.',
'website': 'https://www.exp-sa.com',
'license': 'LGPL-3',
'depends': [
'web',
'base',
],
'data': [
'views/res_config_settings.xml',
],
'assets': {
'web.assets_backend': [
'odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss',
'odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml',
'odex_sidebar_backend_theme2/static/src/xml/menu_item_template.xml',
'odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js',
'odex_sidebar_backend_theme2/static/src/js/sidebar_css_loader.js',
'odex_sidebar_backend_theme2/static/src/js/menu_item.js',
'odex_sidebar_backend_theme2/static/src/xml/navbar_patch.xml',
'odex_sidebar_backend_theme2/static/src/js/navbar_patch.js',
],
@ -28,5 +48,4 @@
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
}

View File

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

View File

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from odoo import fields,api, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
sidebar_menu_enable = fields.Boolean(
config_parameter='odex_sidebar_backend_theme2.sidebar_menu_enable',
string='Enable Sidebar Menu',
help='Enable or disable the sidebar menu in the backend'
)
disable_nav_menu_section = fields.Boolean(
config_parameter='odex_sidebar_backend_theme2.disable_nav_menu_section',
string='Disable Navigation Menu Section',
help='Enable or disable the top navigation bar menu section in the backend interface'
)
sidebar_menu_icon = fields.Binary(
string="Sidebar Icon",
help="Upload an icon for the sidebar menu.",
)
# set default value for the setting
def get_values(self):
res = super(ResConfigSettings, self).get_values()
IrConfigParam = self.env['ir.config_parameter'].sudo()
sidebar_menu_enable = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable')
disable_nav_menu_section = IrConfigParam.get_param('odex_sidebar_backend_theme2.disable_nav_menu_section')
res.update(
sidebar_menu_enable=sidebar_menu_enable == 'True',
disable_nav_menu_section=disable_nav_menu_section == 'True',
sidebar_menu_icon=IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon')
)
return res
def _generate_sidebar_css(self):
"""Generate CSS rules for sidebar menu state"""
if self.disable_nav_menu_section:
return """
/* Sidebar Menu Disabled - Hide Top Menu Sections */
.o_main_navbar .o_menu_sections {
{
display: none !important;
visibility: hidden !important;
}
"""
return ""
# save the setting value
def set_values(self):
super(ResConfigSettings, self).set_values()
IrConfigParam = self.env['ir.config_parameter'].sudo()
IrConfigParam.set_param(
'odex_sidebar_backend_theme2.sidebar_menu_enable',
str(self.sidebar_menu_enable)
)
IrConfigParam.set_param(
'odex_sidebar_backend_theme2.disable_nav_menu_section',
str(self.disable_nav_menu_section)
)
IrConfigParam.set_param(
'odex_sidebar_backend_theme2.sidebar_menu_icon',
self.sidebar_menu_icon or ''
)
if self.sidebar_menu_icon:
# Store the image URL in config parameter
image_url = f"/web/image/res.config.settings/{self.id}/sidebar_menu_icon"
self.env['ir.config_parameter'].sudo().set_param('odex_sidebar_backend_theme2.sidebar_menu_icon_url', image_url)
else:
self.env['ir.config_parameter'].sudo().set_param('odex_sidebar_backend_theme2.sidebar_menu_icon_url', '')
# Generate and store CSS for sidebar state
css_code = self._generate_sidebar_css()
IrConfigParam.set_param(
'odex_sidebar_backend_theme2.sidebar_css',
css_code
)
return True
@api.model
def get_sidebar_setting(self):
IrConfigParam = self.env['ir.config_parameter'].sudo()
sidebar_enabled = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable') == 'True'
sidebar_icon_url = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon_url')
return {'sidebar_enabled': sidebar_enabled, 'sidebar_icon_url': sidebar_icon_url}

View File

@ -0,0 +1,54 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
/**
* Load and inject sidebar CSS rules based on configuration
*/
export function loadSidebarCSS() {
// Get the RPC service
const rpc = useService("rpc");
const loadCSS = async () => {
try {
// Fetch the stored CSS from config parameter
const css = await rpc(
'/web/dataset/call_kw/ir.config_parameter/get_param',
{
model: 'ir.config_parameter',
method: 'get_param',
args: ['odex_sidebar_backend_theme2.sidebar_css'],
kwargs: {},
}
);
if (css && css.trim()) {
// Create a style element and inject the CSS
const style = document.createElement('style');
style.type = 'text/css';
style.id = 'sidebar-dynamic-css';
style.innerHTML = css;
document.head.appendChild(style);
console.log('Sidebar CSS injected successfully');
}
} catch (error) {
console.error('Error loading sidebar CSS:', error);
}
};
// Load CSS when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadCSS);
} else {
loadCSS();
}
}
// Initialize on module load
registry.category("web_tour.tours").add("sidebar_css_loader", {
steps: [],
});
// Auto-load CSS on page load
// loadSidebarCSS();

View File

@ -4,6 +4,7 @@ import { Component, onMounted, useEffect, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { MenuItem } from "./menu_item";
import { rpc } from "@web/core/network/rpc"
export class SidebarMenu extends Component {
static template = "odex_sidebar_backend_theme2.SidebarMenu"
@ -14,13 +15,18 @@ export class SidebarMenu extends Component {
this.menuService = useService("menu");
this.busService = useService("bus_service");
this.actionService = useService("action");
this.rpc = rpc;
this.state = useState({
menus: [],
isOpen: true,
isCollapsed: false
isCollapsed: false,
sidebarEnabled: false,
sidebarMenuIconUrl: null,
});
this.loadSidebarSetting()
// =================== JavaScript Control Starts Here ===================
const applyLayoutChanges = (isOpen) => {
@ -92,6 +98,33 @@ export class SidebarMenu extends Component {
});
}
async loadSidebarSetting() {
try {
const result = await this.rpc('/web/dataset/call_kw', {
model: 'res.config.settings',
method: 'get_sidebar_setting',
args: [],
kwargs: {},
});
// Convert string result to boolean
this.state.sidebarEnabled = result.sidebar_enabled !== 'False' && result.sidebar_enabled !== false && result.sidebar_enabled !== '';
// If sidebar is disabled, close it
if (!this.state.sidebarEnabled) {
this.state.isOpen = false;
}
// Load sidebar menu icon URL
this.state.sidebarMenuIconUrl = result.sidebar_icon_url || '/odex_sidebar_backend_theme2/static/src/img/logo1.png';
} catch (error) {
console.error('Error loading sidebar setting:', error);
// Default to enabled if setting cannot be loaded
this.state.sidebarEnabled = true;
}
}
loadMenus() {
const allMenus = this.menuService.getAll();
const clonedMenus = structuredClone(allMenus);

View File

@ -461,11 +461,6 @@
display: none !important;
}
/* Hide submenus inside the top bar */
.o_main_navbar .o_menu_sections {
display: none !important;
}
.o_web_client {
margin-left: 0 !important;
transition: all 0.3s ease-in-out;

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="odex_sidebar_backend_theme2.SidebarMenu" owl="1">
<div class="custom_sidebar" t-att-class="{ 'is-open': state.isOpen, 'is-collapsed': state.isCollapsed }">
<div class="custom_sidebar" t-att-class="{ 'is-open': state.isOpen, 'is-collapsed': state.isCollapsed, 'is-disabled': !state.sidebarEnabled }" t-if="state.sidebarEnabled">
<!-- Sidebar Header -->
<header class="sidebar-header">
<a href="/" class="header-logo" t-att-title="state.isCollapsed ? 'Home' : ''">
<img src="/odex_sidebar_backend_theme2/static/src/img/logo1.png" alt="CodingNepal" />
<img t-att-src="state.sidebarMenuIconUrl" alt="CodingNepal" />
</a>
<button class="sidebar-toggler" t-on-click="() => this.toggleCollapse()">
<span class="material-symbols-rounded">

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_inherit_sidebar" model="ir.ui.view">
<field name="name">res.config.settings.form.inherit.sidebar</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app data-string="Odex Sidebar Menu" string="Odex Sidebar Menu"
name="odex_sidebar_backend_theme2">
<block title="Odex Sidebar Menu" name="odex_sidebar_settings_block">
<setting>
<div class="content-group">
<div class="row mt16">
<label for="sidebar_menu_enable" />
<div class="text-muted">
Enable or disable the sidebar menu in the backend
interface
</div>
<field name="sidebar_menu_enable" />
</div>
<div class="row mt16">
<label for="disable_nav_menu_section" />
<div class="text-muted">
Enable or disable the top navigation bar menu section
in the backend interface
</div>
<field name="disable_nav_menu_section" />
</div>
<div class="row mt16">
<label for="sidebar_menu_icon" />
<field name="sidebar_menu_icon" widget="image"
options="{'size': [128, 128]}"/>
</div>
</div>
</setting>
</block>
</app>
</xpath>
</field>
</record>
</odoo>