diff --git a/odex30_base/expert_theme/README.md b/odex30_base/expert_theme/README.md
new file mode 100644
index 0000000..b50cee3
--- /dev/null
+++ b/odex30_base/expert_theme/README.md
@@ -0,0 +1,30 @@
+# Expert Theme
+
+A simple and clean theme for Odoo 18 backend that provides a custom home page displaying only installed modules.
+
+## Features
+
+- **Custom Home Page**: Clean interface showing only your installed modules
+- **Simple Navigation**: Easy access to all your applications
+- **Responsive Design**: Works on all devices
+- **Minimal Interface**: No clutter, just functionality
+
+## Installation
+
+1. Copy this module to your Odoo addons directory
+2. Update the apps list in Odoo
+3. Install the "Expert Theme" module
+4. The custom home page will be available after installation
+
+## Usage
+
+After installation, when you log into Odoo, you'll see the new home page with all your installed modules displayed in a clean grid layout. Simply click on any module to access it.
+
+## Requirements
+
+- Odoo 18.0
+- No additional dependencies
+
+## Support
+
+This is a simple theme module designed for basic customization of the Odoo backend home page.
diff --git a/odex30_base/expert_theme/__init__.py b/odex30_base/expert_theme/__init__.py
new file mode 100644
index 0000000..c3d410e
--- /dev/null
+++ b/odex30_base/expert_theme/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import models
+from . import controllers
diff --git a/odex30_base/expert_theme/__manifest__.py b/odex30_base/expert_theme/__manifest__.py
new file mode 100644
index 0000000..fd5b3b7
--- /dev/null
+++ b/odex30_base/expert_theme/__manifest__.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'Expert Theme',
+ 'version': '18.0.1.0.0',
+ 'category': 'Theme/Backend',
+ 'summary': 'Custom backend theme with installed modules home page',
+ 'description': """
+ Expert Theme
+ ============
+
+ A simple custom theme for Odoo 18 backend that provides:
+ - Custom home page displaying only installed modules
+ - Clean and minimal interface
+ - Easy navigation to installed applications
+ """,
+ 'author': 'Expert',
+ 'website': '',
+ 'depends': ['base', 'web'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'data/expert_login_template_data.xml',
+ 'views/login_templates.xml',
+ 'views/expert_login_template_views.xml',
+ 'views/expert_theme_config_views.xml',
+ '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',
+ ],
+ },
+ 'installable': True,
+ 'auto_install': False,
+ 'application': False,
+ 'license': 'LGPL-3',
+}
diff --git a/odex30_base/expert_theme/controllers/__init__.py b/odex30_base/expert_theme/controllers/__init__.py
new file mode 100644
index 0000000..2b4d723
--- /dev/null
+++ b/odex30_base/expert_theme/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import expert_controller
diff --git a/odex30_base/expert_theme/controllers/expert_controller.py b/odex30_base/expert_theme/controllers/expert_controller.py
new file mode 100644
index 0000000..4f5b646
--- /dev/null
+++ b/odex30_base/expert_theme/controllers/expert_controller.py
@@ -0,0 +1,286 @@
+# -*- coding: utf-8 -*-
+
+from odoo import http, fields
+from odoo.http import request
+import json
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class ExpertController(http.Controller):
+
+ @http.route('/expert_theme/test', type='http', auth='user')
+ def test_controller(self):
+ """Simple test endpoint to verify controller is working"""
+ return request.make_json_response({
+ 'success': True,
+ 'message': 'Controller is working!',
+ 'timestamp': fields.Datetime.now()
+ })
+
+ @http.route('/expert_theme/get_css_variables', type='http', auth='user')
+ def get_css_variables(self):
+ """Get CSS variables for the active theme configuration"""
+ try:
+ config = request.env['expert.theme.config'].get_active_config()
+ css_variables = config.get_css_variables()
+ return request.make_json_response({
+ 'success': True,
+ 'css_variables': css_variables
+ })
+ except Exception as e:
+ return request.make_json_response({
+ 'success': False,
+ 'error': str(e)
+ })
+
+ @http.route('/expert_theme/apply_theme', type='http', auth='user', methods=['POST'])
+ def apply_theme(self):
+ """Apply theme changes immediately"""
+ try:
+ # Get the active configuration
+ config = request.env['expert.theme.config'].get_active_config()
+
+ # Apply the theme (this could trigger cache invalidation, etc.)
+ config.apply_theme()
+
+ return request.make_json_response({
+ 'success': True,
+ 'message': 'Theme applied successfully!'
+ })
+ except Exception as e:
+ return request.make_json_response({
+ 'success': False,
+ '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)"""
+ try:
+ template = request.env['expert.login.template'].sudo().get_active_template()
+ styles = template.get_template_styles()
+ return request.make_json_response({
+ 'success': True,
+ 'styles': styles
+ })
+ except Exception as e:
+ # Return default styles if error
+ return request.make_json_response({
+ 'success': False,
+ 'error': str(e),
+ 'styles': {'background_color': '#FFFFFF'}
+ })
+
+ @http.route('/expert_theme/get_installed_modules_http', type='http', auth='user')
+ def get_installed_modules_http(self):
+ """Return list of installed modules for the home page via HTTP"""
+ try:
+ # Get all installed modules
+ installed_modules = request.env['ir.module.module'].search([
+ ('state', '=', 'installed'),
+ ('name', '!=', 'expert_theme') # Exclude our own module
+ ])
+
+ # Get all top-level menu items that belong to installed modules
+ menu_items = request.env['ir.ui.menu'].search([
+ ('parent_id', '=', False), # Top level menus only
+ ('active', '=', True),
+ ('name', 'not in', ['Expert Home', 'Dashboard']) # Exclude our own menus
+ ])
+
+ result = []
+ installed_module_names = installed_modules.mapped('name')
+
+ debug_info = {
+ 'total_installed_modules': len(installed_modules),
+ 'total_menu_items': len(menu_items)
+ }
+
+ for menu in menu_items:
+ # Check if this menu belongs to an installed module
+ module_name = None
+ if menu.web_icon and ',' in menu.web_icon:
+ module_name = menu.web_icon.split(',')[0]
+
+ # Include menu if it belongs to an installed module OR if it has no web_icon (base modules)
+ if (module_name and module_name in installed_module_names) or not menu.web_icon:
+
+ # Create URL based on action type - use proper Odoo navigation
+ if menu.action and menu.action.type == 'ir.actions.act_window':
+ # For act_window, use the action ID
+ url = f'/web#action={menu.action.id}'
+ elif menu.action and menu.action.type == 'ir.actions.client':
+ # For client actions, use the action ID
+ url = f'/web#action={menu.action.id}'
+ elif menu.action and menu.action.type == 'ir.actions.server':
+ # For server actions, use the action ID
+ url = f'/web#action={menu.action.id}'
+ else:
+ # For menus without actions, use menu_id
+ url = f'/web#menu_id={menu.id}'
+
+ result.append({
+ 'id': menu.id,
+ 'name': menu.name,
+ 'web_icon': menu.web_icon or 'base,static/description/icon.png',
+ 'action': menu.action.id if menu.action else False,
+ 'url': url,
+ 'module_name': module_name
+ })
+
+ return request.make_json_response({
+ 'success': True,
+ 'modules': result,
+ 'debug_info': debug_info
+ })
+ except Exception as e:
+ return request.make_json_response({
+ 'success': False,
+ '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)
diff --git a/odex30_base/expert_theme/data/expert_login_template_data.xml b/odex30_base/expert_theme/data/expert_login_template_data.xml
new file mode 100644
index 0000000..dfd3b38
--- /dev/null
+++ b/odex30_base/expert_theme/data/expert_login_template_data.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+ Default Template
+ True
+ 1
+ default
+ #FFFFFF
+ True
+ Standard Odoo login page template
+
+
+
+
+ Modern Template
+ False
+ 2
+ modern
+ #667eea
+ False
+ Modern card design with gradient background
+
+
+
+
+ Minimal Template
+ False
+ 3
+ minimal
+ #F8F9FA
+ False
+ Clean minimal design template
+
+
+
+
+ Corporate Template
+ False
+ 4
+ corporate
+ #1a1a1a
+ False
+ Professional corporate dark theme
+
+
+
+
diff --git a/odex30_base/expert_theme/models/__init__.py b/odex30_base/expert_theme/models/__init__.py
new file mode 100644
index 0000000..6878437
--- /dev/null
+++ b/odex30_base/expert_theme/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import expert_theme_config
+from . import expert_login_template
diff --git a/odex30_base/expert_theme/models/expert_login_template.py b/odex30_base/expert_theme/models/expert_login_template.py
new file mode 100644
index 0000000..a17dce2
--- /dev/null
+++ b/odex30_base/expert_theme/models/expert_login_template.py
@@ -0,0 +1,289 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, fields, api
+
+
+class ExpertLoginTemplate(models.Model):
+ _name = 'expert.login.template'
+ _description = 'Expert Login Page Template'
+ _rec_name = 'name'
+ _order = 'sequence, name'
+
+ name = fields.Char(string='Template Name', required=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')
+
+ # Template Design Selection
+ template_type = fields.Selection([
+ ('default', 'Default Odoo Login'),
+ ('modern', 'Modern Card Design'),
+ ('minimal', 'Minimal Design'),
+ ('corporate', 'Corporate Design'),
+ ], string='Template Design', default='default', required=True,
+ help='Select the login page template design. Each design has a completely different HTML structure and layout.')
+
+ # Template Settings (for backward compatibility and CSS customization)
+ background_color = fields.Char(string='Background Color', default='#FFFFFF', help='Background color for the login page')
+ is_default = fields.Boolean(string='Is Default Template', default=False, help='Default template to use')
+
+ # Template Images
+ modern_template_image = fields.Binary(string='Modern Template Image', help='Image to display on the right side of Modern login, signup, and reset password pages')
+ minimal_template_image = fields.Binary(string='Minimal Template Image', help='Image to display on the right side of Minimal login, signup, and reset password pages')
+ corporate_template_image = fields.Binary(string='Corporate Template Image', help='Image to display on the right side of Corporate login, signup, and reset password pages')
+
+ # Template Logos
+ modern_template_logo = fields.Binary(string='Modern Template Logo', help='Company logo to display on Modern login, signup, and reset password pages')
+ minimal_template_logo = fields.Binary(string='Minimal Template Logo', help='Company logo to display on Minimal login, signup, and reset password pages')
+ 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_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_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')
+
+ # 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_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_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')
+
+ # Description
+ description = fields.Text(string='Description', help='Description of this template')
+
+ @api.model
+ def get_active_template(self):
+ """Get the active login template or return default"""
+ try:
+ template = self.search([('active', '=', True)], limit=1, order='sequence')
+ if not template:
+ # Create a default template if none exists
+ template = self.create({
+ 'name': 'Default Template',
+ 'active': True,
+ 'template_type': 'default',
+ 'background_color': '#FFFFFF',
+ 'is_default': True,
+ 'sequence': 1
+ })
+ return template
+ except Exception as e:
+ # If there's an error, try to return any template or create a minimal one
+ import logging
+ _logger = logging.getLogger(__name__)
+ _logger.error(f"Error in get_active_template: {str(e)}")
+ # Try to get any template
+ template = self.search([], limit=1)
+ if template:
+ return template
+ # Last resort - return a recordset that won't cause errors
+ return self.browse([])
+
+ @api.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
+ # Only deactivate others if this one is being set to active
+ if vals.get('active'):
+ # Deactivate all other templates (excluding the one being created)
+ self.search([('active', '=', True)]).write({'active': False})
+ return super(ExpertLoginTemplate, self).create(vals)
+
+ def write(self, vals):
+ """Ensure only one template is active at a time"""
+ # Track if we need to reload
+ need_reload = False
+
+ # Check if we're activating a template
+ if 'active' in vals and vals.get('active'):
+ # Deactivate all other templates (excluding the current recordset)
+ other_templates = self.search([('active', '=', True), ('id', 'not in', self.ids)])
+ if other_templates:
+ # Use super().write to avoid recursion
+ super(ExpertLoginTemplate, other_templates).write({'active': False})
+ need_reload = True
+
+ result = super(ExpertLoginTemplate, self).write(vals)
+
+ # Update template views if active state changed
+ if 'active' in vals or 'template_type' in vals:
+ self._update_template_views()
+
+ # If we're in a list view context and need reload, return reload action
+ if need_reload and self.env.context.get('from_list_view'):
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
+
+ return result
+
+ @api.onchange('active')
+ def _onchange_active(self):
+ """When activating a template in the form view, show a warning about deactivating others"""
+ if self.active and not self._origin.active:
+ other_active = self.search([('active', '=', True), ('id', '!=', self.id)])
+ if other_active:
+ return {
+ 'warning': {
+ 'title': 'Other Templates Will Be Deactivated',
+ 'message': f'Activating this template will automatically deactivate: {", ".join(other_active.mapped("name"))}'
+ }
+ }
+
+ def get_template_styles(self):
+ """Get CSS styles for this template"""
+ return {
+ 'background_color': self.background_color or '#FFFFFF',
+ }
+
+ def get_template_name(self):
+ """Get the QWeb template name to render"""
+ template_map = {
+ 'default': 'web.login', # Use standard Odoo login
+ 'modern': 'expert_theme.login_template_modern_page',
+ 'minimal': 'expert_theme.login_template_minimal_page',
+ 'corporate': 'expert_theme.login_template_corporate_page',
+ }
+ return template_map.get(self.template_type, 'web.login')
+
+ def get_signup_template_name(self):
+ """Get the QWeb template name for signup page"""
+ template_map = {
+ 'default': 'auth_signup.signup', # Use standard Odoo signup
+ 'modern': 'expert_theme.signup_template_modern_page',
+ 'minimal': 'expert_theme.signup_template_minimal_page',
+ 'corporate': 'auth_signup.signup', # Use default for now
+ }
+ return template_map.get(self.template_type, 'auth_signup.signup')
+
+ def get_reset_password_template_name(self):
+ """Get the QWeb template name for reset password page"""
+ template_map = {
+ 'default': 'auth_signup.reset_password', # Use standard Odoo reset password
+ 'modern': 'expert_theme.reset_password_template_modern_page',
+ 'minimal': 'expert_theme.reset_password_template_minimal_page',
+ 'corporate': 'auth_signup.reset_password', # Use default for now
+ }
+ return template_map.get(self.template_type, 'auth_signup.reset_password')
+
+ def toggle_active(self):
+ """Toggle active state - ensures only one template is active"""
+ # Ensure we're working with exactly one record
+ self.ensure_one()
+
+ # Get the specific record ID to ensure we activate the correct one
+ template_id = self.id
+
+ # Deactivate all other templates first (including any that might be active)
+ self.env['expert.login.template'].search([
+ ('id', '!=', template_id),
+ ('active', '=', True)
+ ]).write({'active': False})
+
+ # Activate this specific template by ID using browse to ensure we're working with the correct record
+ self.browse(template_id).write({'active': True})
+
+ # Update template view active states
+ self._update_template_views()
+
+ # Return True instead of reload action to avoid potential issues
+ return True
+
+ def _update_template_views(self):
+ """Update the active state of template views based on active template"""
+ try:
+ # Map template types to their view XML IDs
+ template_view_map = {
+ 'modern': 'expert_theme.login_template_modern_page',
+ 'minimal': 'expert_theme.login_template_minimal_page',
+ 'corporate': 'expert_theme.login_template_corporate_page',
+ }
+
+ # Get the active template
+ active_template = self.search([('active', '=', True)], limit=1)
+ if not active_template:
+ # If no active template, deactivate all custom template views
+ for view_xmlid in template_view_map.values():
+ try:
+ view = self.env.ref(view_xmlid, raise_if_not_found=False)
+ if view:
+ view.write({'active': False})
+ except Exception:
+ pass # View doesn't exist, skip
+ return
+
+ # Deactivate all template views first
+ for view_xmlid in template_view_map.values():
+ try:
+ view = self.env.ref(view_xmlid, raise_if_not_found=False)
+ if view:
+ view.write({'active': False})
+ except Exception:
+ pass # View doesn't exist, skip
+
+ # Activate the view for the active template type (if not default)
+ if active_template.template_type != 'default':
+ view_xmlid = template_view_map.get(active_template.template_type)
+ if view_xmlid:
+ try:
+ view = self.env.ref(view_xmlid, raise_if_not_found=False)
+ if view:
+ view.write({'active': True})
+ except Exception:
+ pass # View doesn't exist, skip
+ except Exception:
+ pass # Silently fail if views don't exist
+
+ def action_duplicate(self):
+ """Duplicate the current template"""
+ self.ensure_one()
+ copy_vals = {
+ 'name': self.name + ' (Copy)',
+ 'active': False, # Don't activate the copy
+ 'sequence': self.sequence + 1,
+ 'template_type': self.template_type,
+ 'background_color': self.background_color,
+ 'description': self.description,
+ 'is_default': False,
+ }
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Login Page Template',
+ 'res_model': 'expert.login.template',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'res_id': self.create(copy_vals).id,
+ 'context': {'default_name': copy_vals['name']},
+ }
+
diff --git a/odex30_base/expert_theme/models/expert_theme_config.py b/odex30_base/expert_theme/models/expert_theme_config.py
new file mode 100644
index 0000000..6d0060d
--- /dev/null
+++ b/odex30_base/expert_theme/models/expert_theme_config.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, fields, api
+
+
+class ExpertThemeConfig(models.Model):
+ _name = 'expert.theme.config'
+ _description = 'Expert Theme Color Configuration'
+ _rec_name = 'name'
+
+ name = fields.Char(string='Configuration Name', required=True, default='Default Theme')
+ active = fields.Boolean(string='Active', default=True)
+
+ # Primary Colors
+ primary_color = fields.Char(string='Primary Color', default='#875A7B', help='Main brand color - affects navbar, primary buttons, and accents')
+ primary_hover = fields.Char(string='Primary Hover', default='#6B4C6B', help='Hover state for primary elements')
+ primary_light = fields.Char(string='Primary Light', default='#A67B9B', help='Light version of primary color')
+
+ # Secondary Colors
+ secondary_color = fields.Char(string='Secondary Color', default='#00A09D', help='Secondary brand color - affects upgrade buttons and secondary elements')
+ secondary_hover = fields.Char(string='Secondary Hover', default='#008B8A', help='Hover state for secondary elements')
+
+ # Button Colors
+ btn_primary = fields.Char(string='Primary Button', default='#875A7B', help='Primary buttons (like "Activate" buttons)')
+ btn_primary_hover = fields.Char(string='Primary Button Hover', default='#6B4C6B', help='Primary button hover state')
+ btn_secondary = fields.Char(string='Secondary Button', default='#00A09D', help='Secondary buttons (like "Upgrade" buttons)')
+ btn_secondary_hover = fields.Char(string='Secondary Button Hover', default='#008B8A', help='Secondary button hover state')
+ btn_light = fields.Char(string='Light Button', default='#F8F9FA', help='Light buttons (like "Learn More" buttons)')
+ btn_light_hover = fields.Char(string='Light Button Hover', default='#E9ECEF', help='Light button hover state')
+ btn_light_text = fields.Char(string='Light Button Text', default='#6C757D', help='Light button text color')
+
+ # Background Colors
+ bg_primary = fields.Char(string='Primary Background', default='#875A7B', help='Navbar background')
+ bg_secondary = fields.Char(string='Secondary Background', default='#F8F9FA', help='Sidebar background')
+ bg_content = fields.Char(string='Content Background', default='#FFFFFF', help='Main content background')
+ bg_gradient_start = fields.Char(string='Gradient Start', default='#f5f7fa', help='Page gradient start color')
+ bg_gradient_end = fields.Char(string='Gradient End', default='#c3cfe2', help='Page gradient end color')
+
+ # Text Colors
+ text_primary = fields.Char(string='Primary Text', default='#212529', help='Main text color (dark)')
+ text_secondary = fields.Char(string='Secondary Text', default='#6C757D', help='Secondary text color (gray)')
+ text_light = fields.Char(string='Light Text', default='#FFFFFF', help='Light text (for dark backgrounds)')
+
+ # Border Colors
+ border_color = fields.Char(string='Border Color', default='#DEE2E6', help='Default border color')
+ border_light = fields.Char(string='Light Border', default='#E9ECEF', help='Light border color')
+
+ # Status Colors
+ success_color = fields.Char(string='Success Color', default='#28A745', help='Success/Active state (green)')
+ warning_color = fields.Char(string='Warning Color', default='#FFC107', help='Warning state (yellow)')
+ danger_color = fields.Char(string='Danger Color', default='#DC3545', help='Danger/Error state (red)')
+ info_color = fields.Char(string='Info Color', default='#17A2B8', help='Info state (blue)')
+
+ @api.model
+ def get_active_config(self):
+ """Get the active configuration or create a default one"""
+ config = self.search([('active', '=', True)], limit=1)
+ if not config:
+ config = self.create({
+ 'name': 'Default Theme',
+ 'active': True
+ })
+ return config
+
+ @api.model
+ def get_css_variables(self):
+ """Get CSS variables for the active configuration"""
+ config = self.get_active_config()
+ return {
+ '--expert-primary-color': config.primary_color,
+ '--expert-primary-hover': config.primary_hover,
+ '--expert-primary-light': config.primary_light,
+ '--expert-secondary-color': config.secondary_color,
+ '--expert-secondary-hover': config.secondary_hover,
+ '--expert-btn-primary': config.btn_primary,
+ '--expert-btn-primary-hover': config.btn_primary_hover,
+ '--expert-btn-secondary': config.btn_secondary,
+ '--expert-btn-secondary-hover': config.btn_secondary_hover,
+ '--expert-btn-light': config.btn_light,
+ '--expert-btn-light-hover': config.btn_light_hover,
+ '--expert-btn-light-text': config.btn_light_text,
+ '--expert-bg-primary': config.bg_primary,
+ '--expert-bg-secondary': config.bg_secondary,
+ '--expert-bg-content': config.bg_content,
+ '--expert-bg-gradient-start': config.bg_gradient_start,
+ '--expert-bg-gradient-end': config.bg_gradient_end,
+ '--expert-text-primary': config.text_primary,
+ '--expert-text-secondary': config.text_secondary,
+ '--expert-text-light': config.text_light,
+ '--expert-border-color': config.border_color,
+ '--expert-border-light': config.border_light,
+ '--expert-success': config.success_color,
+ '--expert-warning': config.warning_color,
+ '--expert-danger': config.danger_color,
+ '--expert-info': config.info_color,
+ }
+
+ def apply_theme(self):
+ """Apply the current theme configuration"""
+ # This method can be called to trigger theme updates
+ return True
diff --git a/odex30_base/expert_theme/security/ir.model.access.csv b/odex30_base/expert_theme/security/ir.model.access.csv
new file mode 100644
index 0000000..f0e95b4
--- /dev/null
+++ b/odex30_base/expert_theme/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_expert_theme_config_user,expert.theme.config.user,model_expert_theme_config,base.group_user,1,1,1,1
+access_expert_theme_config_admin,expert.theme.config.admin,model_expert_theme_config,base.group_system,1,1,1,1
+access_expert_login_template_user,expert.login.template.user,model_expert_login_template,base.group_user,1,1,1,1
+access_expert_login_template_admin,expert.login.template.admin,model_expert_login_template,base.group_system,1,1,1,1
diff --git a/odex30_base/expert_theme/static/description/index.html b/odex30_base/expert_theme/static/description/index.html
new file mode 100644
index 0000000..51a1105
--- /dev/null
+++ b/odex30_base/expert_theme/static/description/index.html
@@ -0,0 +1,65 @@
+
+
+
+
+ Expert Theme
+
+
+
+
+
🎨
+
Expert Theme
+
+
+
Custom Home Page
+
A clean and modern home page that displays only your installed modules for easy navigation.
+
+
+
+
Simple Interface
+
Minimal design focused on functionality - no clutter, just your modules.
+
+
+
+
Responsive Design
+
Works perfectly on desktop, tablet, and mobile devices.
+
+
+
+
Easy Installation
+
Simply install the module and your new home page will be ready to use.
+
+
+
+
diff --git a/odex30_base/expert_theme/static/src/css/expert_login.css b/odex30_base/expert_theme/static/src/css/expert_login.css
new file mode 100644
index 0000000..0cc1dc6
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/css/expert_login.css
@@ -0,0 +1,27 @@
+/* Expert Theme - Login Page Styles */
+
+:root {
+ --expert-login-bg-color: #FFFFFF;
+}
+
+/* Apply background color to login page */
+.expert_login_page {
+ background-color: var(--expert-login-bg-color) !important;
+}
+
+/* Ensure the background applies to the entire login area */
+body.oe_login_layout,
+body {
+ background-color: var(--expert-login-bg-color) !important;
+}
+
+/* Apply to main login container */
+.oe_login_form {
+ background-color: var(--expert-login-bg-color) !important;
+}
+
+/* Apply to html element as well */
+html {
+ background-color: var(--expert-login-bg-color) !important;
+}
+
diff --git a/odex30_base/expert_theme/static/src/css/expert_theme.css b/odex30_base/expert_theme/static/src/css/expert_theme.css
new file mode 100644
index 0000000..6f4b521
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/css/expert_theme.css
@@ -0,0 +1,314 @@
+/* Expert Theme Styles */
+
+/* CSS Variables for Customizable Colors */
+:root {
+ /* Primary Colors */
+ --expert-primary-color: #875A7B; /* Main brand color (Odoo purple) */
+ --expert-primary-hover: #6B4C6B; /* Primary hover state */
+ --expert-primary-light: #A67B9B; /* Light primary */
+
+ /* Secondary Colors */
+ --expert-secondary-color: #00A09D; /* Teal for upgrades */
+ --expert-secondary-hover: #008B8A; /* Teal hover */
+
+ /* Button Colors */
+ --expert-btn-primary: #875A7B; /* Primary button (Activate) */
+ --expert-btn-primary-hover: #6B4C6B; /* Primary button hover */
+ --expert-btn-secondary: #00A09D; /* Secondary button (Upgrade) */
+ --expert-btn-secondary-hover: #008B8A; /* Secondary button hover */
+ --expert-btn-light: #F8F9FA; /* Light button (Learn More) */
+ --expert-btn-light-hover: #E9ECEF; /* Light button hover */
+ --expert-btn-light-text: #6C757D; /* Light button text */
+
+ /* Background Colors */
+ --expert-bg-primary: #875A7B; /* Navbar background */
+ --expert-bg-secondary: #F8F9FA; /* Sidebar background */
+ --expert-bg-content: #FFFFFF; /* Main content background */
+ --expert-bg-gradient-start: #f5f7fa; /* Gradient start */
+ --expert-bg-gradient-end: #c3cfe2; /* Gradient end */
+
+ /* Text Colors */
+ --expert-text-primary: #212529; /* Primary text */
+ --expert-text-secondary: #6C757D; /* Secondary text */
+ --expert-text-light: #FFFFFF; /* Light text (on dark backgrounds) */
+
+ /* Border Colors */
+ --expert-border-color: #DEE2E6; /* Default border */
+ --expert-border-light: #E9ECEF; /* Light border */
+
+ /* Status Colors */
+ --expert-success: #28A745; /* Success/Active state */
+ --expert-warning: #FFC107; /* Warning state */
+ --expert-danger: #DC3545; /* Danger/Error state */
+ --expert-info: #17A2B8; /* Info state */
+}
+
+/* Apply gradient to the entire page background */
+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;
+}
+
+/* Ensure the gradient shows on the main content area */
+.o_action_manager,
+.o_action_manager .o_view_controller,
+.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;
+}
+
+/* Override any Odoo background colors that might interfere */
+.o_web_client {
+ 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;
+}
+
+.expert-home-container {
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+ background: transparent;
+}
+
+.expert-theme-controls {
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.expert-theme-btn {
+ background: linear-gradient(135deg, var(--expert-primary-color) 0%, var(--expert-primary-hover) 100%);
+ border: none;
+ color: white;
+ padding: 12px 24px;
+ border-radius: 25px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
+}
+
+.expert-theme-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(0,0,0,0.15);
+ background: linear-gradient(135deg, var(--expert-primary-hover) 0%, var(--expert-primary-color) 100%);
+}
+
+.expert-home-header {
+ text-align: center;
+ margin-bottom: 40px;
+ padding: 20px;
+ background: linear-gradient(135deg, var(--expert-primary-color) 0%, var(--expert-primary-hover) 100%);
+ color: var(--expert-text-light);
+ border-radius: 10px;
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
+}
+
+.expert-home-header h1 {
+ margin: 0 0 10px 0;
+ font-size: 2.5rem;
+ font-weight: 300;
+}
+
+.expert-home-header p {
+ margin: 0;
+ font-size: 1.1rem;
+ opacity: 0.9;
+}
+
+.expert-modules-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.expert-module-card {
+ background: var(--expert-bg-content);
+ border-radius: 10px;
+ padding: 20px;
+ text-align: center;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ transition: all 0.3s ease;
+ cursor: pointer;
+ border: 1px solid var(--expert-border-light);
+}
+
+.expert-module-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
+ border-color: var(--expert-primary-color);
+}
+
+.expert-module-icon {
+ margin-bottom: 15px;
+}
+
+.module-icon {
+ width: 64px;
+ height: 64px;
+ object-fit: contain;
+ border-radius: 8px;
+}
+
+.expert-module-info h3 {
+ margin: 0 0 10px 0;
+ color: var(--expert-text-primary);
+ font-size: 1.2rem;
+ font-weight: 500;
+}
+
+.expert-module-info p {
+ margin: 0;
+ color: var(--expert-text-secondary);
+ font-size: 0.9rem;
+}
+
+.expert-loading {
+ text-align: center;
+ padding: 40px;
+}
+
+.spinner-border {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: text-bottom;
+ border: 0.25em solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: spinner-border 0.75s linear infinite;
+}
+
+@keyframes spinner-border {
+ to { transform: rotate(360deg); }
+}
+
+.expert-error,
+.expert-no-modules {
+ text-align: center;
+ padding: 20px;
+}
+
+.alert {
+ padding: 15px;
+ margin-bottom: 20px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+}
+
+.alert-danger {
+ color: #721c24;
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+}
+
+.alert-info {
+ color: #0c5460;
+ background-color: #d1ecf1;
+ border-color: #bee5eb;
+}
+
+/* Override Odoo's default colors */
+.o_main_navbar {
+ background-color: var(--expert-bg-primary) !important;
+ border-bottom: 1px solid var(--expert-border-color) !important;
+}
+
+.o_main_navbar .o_menu_item > a {
+ color: var(--expert-text-light) !important;
+}
+
+.o_main_navbar .o_menu_item > a:hover {
+ background-color: var(--expert-primary-hover) !important;
+ color: var(--expert-text-light) !important;
+}
+
+/* Button overrides */
+.btn-primary {
+ background-color: var(--expert-btn-primary) !important;
+ border-color: var(--expert-btn-primary) !important;
+}
+
+.btn-primary:hover {
+ background-color: var(--expert-btn-primary-hover) !important;
+ border-color: var(--expert-btn-primary-hover) !important;
+}
+
+.btn-secondary {
+ background-color: var(--expert-btn-secondary) !important;
+ border-color: var(--expert-btn-secondary) !important;
+}
+
+.btn-secondary:hover {
+ background-color: var(--expert-btn-secondary-hover) !important;
+ border-color: var(--expert-btn-secondary-hover) !important;
+}
+
+.btn-light {
+ background-color: var(--expert-btn-light) !important;
+ border-color: var(--expert-btn-light) !important;
+ color: var(--expert-btn-light-text) !important;
+}
+
+.btn-light:hover {
+ background-color: var(--expert-btn-light-hover) !important;
+ border-color: var(--expert-btn-light-hover) !important;
+ color: var(--expert-btn-light-text) !important;
+}
+
+/* Sidebar overrides */
+.o_main_navbar .o_menu_sections {
+ background-color: var(--expert-bg-secondary) !important;
+}
+
+.o_main_navbar .o_menu_sections .o_menu_item > a {
+ color: var(--expert-text-primary) !important;
+}
+
+.o_main_navbar .o_menu_sections .o_menu_item > a:hover {
+ background-color: var(--expert-primary-light) !important;
+ color: var(--expert-text-light) !important;
+}
+
+/* Content area overrides */
+.o_action_manager {
+ background-color: var(--expert-bg-content) !important;
+}
+
+/* Status colors */
+.text-success {
+ color: var(--expert-success) !important;
+}
+
+.text-warning {
+ color: var(--expert-warning) !important;
+}
+
+.text-danger {
+ color: var(--expert-danger) !important;
+}
+
+.text-info {
+ color: var(--expert-info) !important;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .expert-modules-grid {
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 15px;
+ }
+
+ .expert-home-header h1 {
+ font-size: 2rem;
+ }
+
+ .expert-home-container {
+ padding: 15px;
+ }
+}
diff --git a/odex30_base/expert_theme/static/src/css/expert_theme_config.css b/odex30_base/expert_theme/static/src/css/expert_theme_config.css
new file mode 100644
index 0000000..96c9168
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/css/expert_theme_config.css
@@ -0,0 +1,134 @@
+/* Expert Theme Color Configuration */
+/*
+ * This file contains the color variables for the Expert Theme.
+ * You can easily customize the colors by changing the values below.
+ *
+ * Color Format: Use hex codes (e.g., #FF0000) or CSS color names (e.g., red, blue)
+ *
+ * To apply changes:
+ * 1. Modify the colors below
+ * 2. Save the file
+ * 3. Refresh your Odoo page
+ */
+
+:root {
+ /* ===== PRIMARY COLORS ===== */
+ /* Main brand color - affects navbar, primary buttons, and accents */
+ --expert-primary-color: #875A7B; /* Default: Odoo Purple */
+ --expert-primary-hover: #6B4C6B; /* Hover state for primary elements */
+ --expert-primary-light: #A67B9B; /* Light version of primary */
+
+ /* ===== SECONDARY COLORS ===== */
+ /* Secondary brand color - affects upgrade buttons and secondary elements */
+ --expert-secondary-color: #00A09D; /* Default: Teal */
+ --expert-secondary-hover: #008B8A; /* Hover state for secondary elements */
+
+ /* ===== BUTTON COLORS ===== */
+ /* Primary buttons (like "Activate" buttons) */
+ --expert-btn-primary: #875A7B; /* Same as primary color */
+ --expert-btn-primary-hover: #6B4C6B; /* Hover state */
+
+ /* Secondary buttons (like "Upgrade" buttons) */
+ --expert-btn-secondary: #00A09D; /* Same as secondary color */
+ --expert-btn-secondary-hover: #008B8A; /* Hover state */
+
+ /* Light buttons (like "Learn More" buttons) */
+ --expert-btn-light: #F8F9FA; /* Light gray background */
+ --expert-btn-light-hover: #E9ECEF; /* Slightly darker on hover */
+ --expert-btn-light-text: #6C757D; /* Dark gray text */
+
+ /* ===== BACKGROUND COLORS ===== */
+ /* Main backgrounds */
+ --expert-bg-primary: #875A7B; /* Navbar background */
+ --expert-bg-secondary: #F8F9FA; /* Sidebar background */
+ --expert-bg-content: #FFFFFF; /* Main content background */
+
+ /* Gradient background for the page */
+ --expert-bg-gradient-start: #f5f7fa; /* Gradient start color */
+ --expert-bg-gradient-end: #c3cfe2; /* Gradient end color */
+
+ /* ===== TEXT COLORS ===== */
+ --expert-text-primary: #212529; /* Main text color (dark) */
+ --expert-text-secondary: #6C757D; /* Secondary text color (gray) */
+ --expert-text-light: #FFFFFF; /* Light text (for dark backgrounds) */
+
+ /* ===== BORDER COLORS ===== */
+ --expert-border-color: #DEE2E6; /* Default border color */
+ --expert-border-light: #E9ECEF; /* Light border color */
+
+ /* ===== STATUS COLORS ===== */
+ --expert-success: #28A745; /* Success/Active state (green) */
+ --expert-warning: #FFC107; /* Warning state (yellow) */
+ --expert-danger: #DC3545; /* Danger/Error state (red) */
+ --expert-info: #17A2B8; /* Info state (blue) */
+}
+
+/* ===== EXAMPLE COLOR SCHEMES ===== */
+
+/*
+ * BLUE THEME EXAMPLE:
+ * Uncomment the section below to use a blue color scheme
+ */
+/*
+:root {
+ --expert-primary-color: #007BFF;
+ --expert-primary-hover: #0056B3;
+ --expert-primary-light: #66B3FF;
+ --expert-secondary-color: #6F42C1;
+ --expert-secondary-hover: #5A32A3;
+ --expert-btn-primary: #007BFF;
+ --expert-btn-primary-hover: #0056B3;
+ --expert-btn-secondary: #6F42C1;
+ --expert-btn-secondary-hover: #5A32A3;
+ --expert-bg-primary: #007BFF;
+ --expert-bg-gradient-start: #E3F2FD;
+ --expert-bg-gradient-end: #BBDEFB;
+}
+*/
+
+/*
+ * GREEN THEME EXAMPLE:
+ * Uncomment the section below to use a green color scheme
+ */
+/*
+:root {
+ --expert-primary-color: #28A745;
+ --expert-primary-hover: #1E7E34;
+ --expert-primary-light: #5CB85C;
+ --expert-secondary-color: #20C997;
+ --expert-secondary-hover: #17A2B8;
+ --expert-btn-primary: #28A745;
+ --expert-btn-primary-hover: #1E7E34;
+ --expert-btn-secondary: #20C997;
+ --expert-btn-secondary-hover: #17A2B8;
+ --expert-bg-primary: #28A745;
+ --expert-bg-gradient-start: #E8F5E8;
+ --expert-bg-gradient-end: #C3E6C3;
+}
+*/
+
+/*
+ * DARK THEME EXAMPLE:
+ * Uncomment the section below to use a dark color scheme
+ */
+/*
+:root {
+ --expert-primary-color: #343A40;
+ --expert-primary-hover: #23272B;
+ --expert-primary-light: #495057;
+ --expert-secondary-color: #6C757D;
+ --expert-secondary-hover: #545B62;
+ --expert-btn-primary: #343A40;
+ --expert-btn-primary-hover: #23272B;
+ --expert-btn-secondary: #6C757D;
+ --expert-btn-secondary-hover: #545B62;
+ --expert-bg-primary: #343A40;
+ --expert-bg-secondary: #495057;
+ --expert-bg-content: #F8F9FA;
+ --expert-bg-gradient-start: #2C3E50;
+ --expert-bg-gradient-end: #34495E;
+ --expert-text-primary: #FFFFFF;
+ --expert-text-secondary: #E9ECEF;
+ --expert-text-light: #FFFFFF;
+}
+*/
diff --git a/odex30_base/expert_theme/static/src/css/login_minimal.css b/odex30_base/expert_theme/static/src/css/login_minimal.css
new file mode 100644
index 0000000..df4cbc8
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/css/login_minimal.css
@@ -0,0 +1,269 @@
+/* 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;
+ }
+}
+
diff --git a/odex30_base/expert_theme/static/src/css/login_modern.css b/odex30_base/expert_theme/static/src/css/login_modern.css
new file mode 100644
index 0000000..8bd771b
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/css/login_modern.css
@@ -0,0 +1,234 @@
+/* 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;
+ }
+}
+
diff --git a/odex30_base/expert_theme/static/src/img/minimal-login-img.png b/odex30_base/expert_theme/static/src/img/minimal-login-img.png
new file mode 100644
index 0000000..bca7cd3
Binary files /dev/null and b/odex30_base/expert_theme/static/src/img/minimal-login-img.png differ
diff --git a/odex30_base/expert_theme/static/src/img/modern-template-bg.png b/odex30_base/expert_theme/static/src/img/modern-template-bg.png
new file mode 100644
index 0000000..eb6c4f9
Binary files /dev/null and b/odex30_base/expert_theme/static/src/img/modern-template-bg.png differ
diff --git a/odex30_base/expert_theme/static/src/js/expert_home.js b/odex30_base/expert_theme/static/src/js/expert_home.js
new file mode 100644
index 0000000..844fa5f
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/js/expert_home.js
@@ -0,0 +1,134 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { Component, onMounted, useState } from "@odoo/owl";
+import { rpc } from "@web/core/network/rpc";
+
+export class ExpertHome extends Component {
+ setup() {
+ this.state = useState({
+ modules: [],
+ loading: true,
+ error: null
+ });
+
+ onMounted(() => {
+ this.loadModules();
+ });
+ }
+
+ async loadModules() {
+ try {
+ this.state.loading = true;
+
+ // Try to get modules dynamically from the controller
+ try {
+ const response = await fetch('/expert_theme/get_installed_modules_http');
+
+ // Check if response is HTML (error page) instead of JSON
+ const contentType = response.headers.get('content-type');
+ if (!contentType || !contentType.includes('application/json')) {
+ throw new Error(`Server returned HTML (status: ${response.status}). Check if controller is working.`);
+ }
+
+ const result = await response.json();
+
+ if (result.success && result.modules && result.modules.length > 0) {
+ this.state.modules = result.modules;
+ return;
+ }
+ } catch (httpError) {
+ // Don't set error state, try fallback instead
+ }
+
+ // Fallback: Try to get modules from DOM
+ try {
+ // Try multiple selectors to find the sidebar menus
+ let sidebarMenus = document.querySelectorAll('.o_main_navbar .o_menu_item a');
+ if (sidebarMenus.length === 0) {
+ sidebarMenus = document.querySelectorAll('.o_menu_item a');
+ }
+ if (sidebarMenus.length === 0) {
+ sidebarMenus = document.querySelectorAll('nav a');
+ }
+ if (sidebarMenus.length === 0) {
+ sidebarMenus = document.querySelectorAll('a[href*="/web#"]');
+ }
+
+ const modules = [];
+
+ sidebarMenus.forEach((link, index) => {
+ const name = link.textContent.trim();
+ const href = link.href;
+
+ if (name && href &&
+ !name.includes('Expert Home') &&
+ !name.includes('Dashboard') &&
+ name.length > 0 &&
+ href.includes('/web#')) {
+ modules.push({
+ id: index + 1,
+ name: name,
+ web_icon: 'base,static/description/icon.png',
+ action: true,
+ url: href
+ });
+ }
+ });
+
+ if (modules.length > 0) {
+ this.state.modules = modules;
+ return;
+ }
+ } catch (domError) {
+ // DOM fallback failed
+ }
+
+ // Fallback: Show empty state with message
+ this.state.modules = [];
+
+ } catch (error) {
+ this.state.error = error.message;
+ } finally {
+ this.state.loading = false;
+ }
+ }
+
+ getModuleIcon(webIcon) {
+ // Custom icon mapping for modules
+ const customIcons = {
+ 'Discuss': '/web/static/src/img/discuss-icon.png',
+ 'To-do': '/web/static/src/img/todo-icon.png',
+ 'Calendar': '/web/static/src/img/calendar-icon.png',
+ 'Contacts': '/web/static/src/img/contacts-icon.png',
+ 'CRM': '/web/static/src/img/crm-icon.png',
+ 'Project': '/web/static/src/img/project-icon.png',
+ 'Website': '/web/static/src/img/website-icon.png',
+ 'Email Marketing': '/web/static/src/img/email-icon.png',
+ 'Surveys': '/web/static/src/img/survey-icon.png',
+ 'Employees': '/web/static/src/img/employees-icon.png',
+ 'Link Tracker': '/web/static/src/img/link-icon.png',
+ 'Apps': '/web/static/src/img/apps-icon.png',
+ 'Settings': '/web/static/src/img/settings-icon.png'
+ };
+
+ // Try to get custom icon first, fallback to default
+ return customIcons[webIcon] || '/web/static/img/placeholder.png';
+ }
+
+ openModule(module) {
+ if (module.url) {
+ window.location.href = module.url;
+ }
+ }
+
+ openThemeColors() {
+ // Navigate to the theme colors configuration
+ window.location.href = '/web#action=expert_theme.action_expert_theme_config';
+ }
+}
+
+ExpertHome.template = "expert_theme.ExpertHomeTemplate";
+
+// Register the component as a client action
+registry.category("actions").add("expert_home", ExpertHome);
diff --git a/odex30_base/expert_theme/static/src/js/expert_login_template.js b/odex30_base/expert_theme/static/src/js/expert_login_template.js
new file mode 100644
index 0000000..f3b3a9a
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/js/expert_login_template.js
@@ -0,0 +1,54 @@
+/** @odoo-module **/
+
+// Load login template styles dynamically
+// Temporarily disabled to fix login page access
+/*
+(function() {
+ function applyLoginStyles() {
+ fetch('/expert_theme/get_login_template_styles')
+ .then(response => response.json())
+ .then(data => {
+ if (data.success && data.styles) {
+ const root = document.documentElement;
+ if (data.styles.background_color) {
+ // Set CSS variable
+ root.style.setProperty('--expert-login-bg-color', data.styles.background_color);
+
+ // Apply background color to body (main background)
+ if (document.body) {
+ document.body.style.backgroundColor = data.styles.background_color;
+ }
+
+ // Apply to login container if exists
+ const loginContainer = document.querySelector('.oe_login_form') ||
+ document.querySelector('.o_database_list') ||
+ document.querySelector('main') ||
+ document.body;
+ if (loginContainer) {
+ loginContainer.style.backgroundColor = data.styles.background_color;
+ }
+
+ // Also apply to html element
+ if (document.documentElement) {
+ document.documentElement.style.backgroundColor = data.styles.background_color;
+ }
+ }
+ }
+ })
+ .catch(error => {
+ console.warn('Could not load login template styles:', error);
+ });
+ }
+
+ // Apply styles when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', applyLoginStyles);
+ } else {
+ applyLoginStyles();
+ }
+
+ // Also apply after a short delay to ensure everything is loaded
+ setTimeout(applyLoginStyles, 100);
+})();
+*/
+
diff --git a/odex30_base/expert_theme/static/src/js/expert_login_template_list.js b/odex30_base/expert_theme/static/src/js/expert_login_template_list.js
new file mode 100644
index 0000000..527e976
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/js/expert_login_template_list.js
@@ -0,0 +1,26 @@
+/** @odoo-module **/
+
+console.log('expert_login_template_list.js loaded');
+
+import { patch } from "@web/core/utils/patch";
+import { ListBooleanToggleField } from "@web/views/fields/boolean_toggle/list_boolean_toggle_field";
+
+// Patch ListBooleanToggleField to reload after toggle
+patch(ListBooleanToggleField.prototype, {
+ async onClick() {
+ const isActiveField = this.props.name === 'active' && this.props.record.resModel === 'expert.login.template';
+ console.log('ListBooleanToggleField onClick - isActiveField:', isActiveField, 'name:', this.props.name, 'resModel:', this.props.record.resModel);
+
+ // Call parent method first
+ await super.onClick(...arguments);
+
+ // If this is the active field, reload the page
+ if (isActiveField) {
+ console.log('Active field toggled! Reloading page in 500ms...');
+ setTimeout(() => {
+ console.log('Reloading now!');
+ window.location.reload();
+ }, 500);
+ }
+ },
+});
diff --git a/odex30_base/expert_theme/static/src/js/expert_theme_config.js b/odex30_base/expert_theme/static/src/js/expert_theme_config.js
new file mode 100644
index 0000000..2508de0
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/js/expert_theme_config.js
@@ -0,0 +1,120 @@
+/** @odoo-module **/
+
+import { FormController } from "@web/views/form/form_controller";
+import { registry } from "@web/core/registry";
+
+export class ExpertThemeConfigController extends FormController {
+ setup() {
+ super.setup();
+
+ // Listen for field changes to apply colors in real-time
+ this.env.bus.addEventListener('FIELD_CHANGED', this.onFieldChanged.bind(this));
+ }
+
+ onFieldChanged(event) {
+ // Check if the changed field is a color field
+ if (event.detail.fieldName && event.detail.fieldName.includes('color')) {
+ this.applyThemePreview();
+ }
+ }
+
+ async applyThemePreview() {
+ try {
+ // Get current form values
+ const formData = this.model.root;
+ const record = formData.data;
+
+ // Create CSS variables object from form data
+ const cssVariables = {
+ '--expert-primary-color': record.primary_color || '#875A7B',
+ '--expert-primary-hover': record.primary_hover || '#6B4C6B',
+ '--expert-primary-light': record.primary_light || '#A67B9B',
+ '--expert-secondary-color': record.secondary_color || '#00A09D',
+ '--expert-secondary-hover': record.secondary_hover || '#008B8A',
+ '--expert-btn-primary': record.btn_primary || '#875A7B',
+ '--expert-btn-primary-hover': record.btn_primary_hover || '#6B4C6B',
+ '--expert-btn-secondary': record.btn_secondary || '#00A09D',
+ '--expert-btn-secondary-hover': record.btn_secondary_hover || '#008B8A',
+ '--expert-btn-light': record.btn_light || '#F8F9FA',
+ '--expert-btn-light-hover': record.btn_light_hover || '#E9ECEF',
+ '--expert-btn-light-text': record.btn_light_text || '#6C757D',
+ '--expert-bg-primary': record.bg_primary || '#875A7B',
+ '--expert-bg-secondary': record.bg_secondary || '#F8F9FA',
+ '--expert-bg-content': record.bg_content || '#FFFFFF',
+ '--expert-bg-gradient-start': record.bg_gradient_start || '#f5f7fa',
+ '--expert-bg-gradient-end': record.bg_gradient_end || '#c3cfe2',
+ '--expert-text-primary': record.text_primary || '#212529',
+ '--expert-text-secondary': record.text_secondary || '#6C757D',
+ '--expert-text-light': record.text_light || '#FFFFFF',
+ '--expert-border-color': record.border_color || '#DEE2E6',
+ '--expert-border-light': record.border_light || '#E9ECEF',
+ '--expert-success': record.success_color || '#28A745',
+ '--expert-warning': record.warning_color || '#FFC107',
+ '--expert-danger': record.danger_color || '#DC3545',
+ '--expert-info': record.info_color || '#17A2B8',
+ };
+
+ // Apply the CSS variables
+ this.applyCSSVariables(cssVariables);
+
+ } catch (error) {
+ console.warn('Error applying theme preview:', error);
+ }
+ }
+
+ applyCSSVariables(cssVariables) {
+ const root = document.documentElement;
+
+ // Apply each CSS variable to the root element
+ Object.entries(cssVariables).forEach(([property, value]) => {
+ if (value) {
+ root.style.setProperty(property, value);
+ }
+ });
+ }
+
+ async save() {
+ const result = await super.save();
+
+ // After saving, apply the theme
+ if (result) {
+ await this.applyTheme();
+ }
+
+ return result;
+ }
+
+ async applyTheme() {
+ try {
+ const response = await fetch('/expert_theme/apply_theme', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ // Show success message
+ this.env.services.notification.add('Theme applied successfully!', {
+ type: 'success',
+ });
+ } else {
+ this.env.services.notification.add('Error applying theme: ' + result.error, {
+ type: 'danger',
+ });
+ }
+ } catch (error) {
+ this.env.services.notification.add('Error applying theme: ' + error.message, {
+ type: 'danger',
+ });
+ }
+ }
+}
+
+// Register the controller for the expert.theme.config model
+registry.category("views").add("expert_theme_config_form", {
+ ...registry.category("views").get("form"),
+ Controller: ExpertThemeConfigController,
+});
diff --git a/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js b/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js
new file mode 100644
index 0000000..d3bffc1
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js
@@ -0,0 +1,80 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { Component, onMounted, onWillStart } from "@odoo/owl";
+
+export class ExpertThemeDynamic extends Component {
+ setup() {
+ onWillStart(() => {
+ this.loadThemeColors();
+ });
+
+ onMounted(() => {
+ this.loadThemeColors();
+ });
+ }
+
+ async loadThemeColors() {
+ try {
+ const response = await fetch('/expert_theme/get_css_variables');
+ const result = await response.json();
+
+ if (result.success && result.css_variables) {
+ this.applyCSSVariables(result.css_variables);
+ }
+ } catch (error) {
+ console.warn('Could not load dynamic theme colors:', error);
+ }
+ }
+
+ applyCSSVariables(cssVariables) {
+ const root = document.documentElement;
+
+ // Apply each CSS variable to the root element
+ Object.entries(cssVariables).forEach(([property, value]) => {
+ if (value) {
+ root.style.setProperty(property, value);
+ }
+ });
+ }
+}
+
+// Register the component to run on every page load
+registry.category("services").add("expert_theme_dynamic", {
+ start() {
+ const component = new ExpertThemeDynamic();
+ component.loadThemeColors();
+
+ // Also load colors when the page is refreshed or navigated
+ window.addEventListener('load', () => {
+ component.loadThemeColors();
+ });
+
+ return component;
+ }
+});
+
+// Also create a standalone function that can be called manually
+window.expertThemeApplyColors = async function() {
+ try {
+ const response = await fetch('/expert_theme/get_css_variables');
+ const result = await response.json();
+
+ if (result.success && result.css_variables) {
+ const root = document.documentElement;
+ Object.entries(result.css_variables).forEach(([property, value]) => {
+ if (value) {
+ root.style.setProperty(property, value);
+ }
+ });
+ console.log('Expert Theme colors applied successfully!');
+ }
+ } catch (error) {
+ console.error('Error applying Expert Theme colors:', error);
+ }
+};
+
+// Auto-apply colors when the script loads
+document.addEventListener('DOMContentLoaded', function() {
+ window.expertThemeApplyColors();
+});
diff --git a/odex30_base/expert_theme/static/src/scss/login_modern.scss b/odex30_base/expert_theme/static/src/scss/login_modern.scss
new file mode 100644
index 0000000..976f9fd
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/scss/login_modern.scss
@@ -0,0 +1,258 @@
+/* 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;
+
+ .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;
+ }
+ }
+
+ // 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);
+ 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;
+ }
+ }
+ }
+
+ 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;
+
+ &: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;
+ }
+ }
+ }
+ }
+
+ // Right Column: Image Section
+ .expert-login-right {
+ padding: 0;
+ height: 100%;
+ overflow: hidden;
+ border-radius: 20px 0 0 20px;
+
+ .expert-login-image {
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 25%, #667eea 75%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ .expert-spiral-decoration {
+ width: 400px;
+ height: 400px;
+ position: relative;
+ opacity: 0.9;
+
+ .spiral-circle {
+ position: absolute;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
+
+ &.circle-1 {
+ width: 100%;
+ height: 100%;
+ transform: rotate(45deg);
+ }
+
+ &.circle-2 {
+ width: 80%;
+ height: 80%;
+ top: 10%;
+ left: 10%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.08) 0%, transparent 70%);
+ transform: rotate(-45deg);
+ }
+
+ &.circle-3 {
+ width: 60%;
+ height: 60%;
+ top: 20%;
+ left: 20%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.06) 0%, transparent 70%);
+ transform: rotate(30deg);
+ }
+ }
+ }
+ }
+ }
+}
+
+// Responsive adjustments
+@media (max-width: 991.98px) {
+ .expert-login-modern {
+ .expert-login-left {
+ padding: 30px 20px;
+ }
+
+ .expert-login-welcome {
+ h1 {
+ font-size: 28px;
+
+ .welcome-emoji {
+ font-size: 28px;
+ }
+ }
+
+ p {
+ font-size: 14px;
+ }
+ }
+ }
+}
+
diff --git a/odex30_base/expert_theme/static/src/xml/expert_home.xml b/odex30_base/expert_theme/static/src/xml/expert_home.xml
new file mode 100644
index 0000000..42b98f8
--- /dev/null
+++ b/odex30_base/expert_theme/static/src/xml/expert_home.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
Loading your modules...
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
No installed modules found.
+
This could mean:
+
+ - No modules are currently installed
+ - The module detection is not working properly
+ - Check the browser console for debugging information
+
+
Try refreshing the page or check if modules are properly installed in Odoo.
+
+
+
+
+
diff --git a/odex30_base/expert_theme/views/expert_home_views.xml b/odex30_base/expert_theme/views/expert_home_views.xml
new file mode 100644
index 0000000..71944e0
--- /dev/null
+++ b/odex30_base/expert_theme/views/expert_home_views.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ expert.theme.home
+ ir.ui.menu
+ qweb
+
+
+
+
+
+
+
+
+
+ Expert Home
+ expert_home
+ current
+
+
+
+
diff --git a/odex30_base/expert_theme/views/expert_login_template_views.xml b/odex30_base/expert_theme/views/expert_login_template_views.xml
new file mode 100644
index 0000000..11c775e
--- /dev/null
+++ b/odex30_base/expert_theme/views/expert_login_template_views.xml
@@ -0,0 +1,233 @@
+
+
+
+
+
+ expert.login.template.form
+ expert.login.template
+
+
+
+
+
+
+
+ expert.login.template.tree
+ expert.login.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ expert.login.template.search
+ expert.login.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Login Page Templates
+ expert.login.template
+ list,form
+ []
+ {'active_test': False}
+
+
+
+ Create your first login page template!
+
+
+ Configure different login page templates and switch between them.
+ Each template can have different colors, backgrounds, and styles.
+
+
+
+
+
+
diff --git a/odex30_base/expert_theme/views/expert_menu_views.xml b/odex30_base/expert_theme/views/expert_menu_views.xml
new file mode 100644
index 0000000..1728e82
--- /dev/null
+++ b/odex30_base/expert_theme/views/expert_menu_views.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odex30_base/expert_theme/views/expert_theme_config_views.xml b/odex30_base/expert_theme/views/expert_theme_config_views.xml
new file mode 100644
index 0000000..b4719f3
--- /dev/null
+++ b/odex30_base/expert_theme/views/expert_theme_config_views.xml
@@ -0,0 +1,128 @@
+
+
+
+
+
+ expert.theme.config.form
+ expert.theme.config
+
+
+
+
+
+
+
+ expert.theme.config.list
+ expert.theme.config
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ expert.theme.config.search
+ expert.theme.config
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Theme Colors
+ expert.theme.config
+ list,form
+
+
+
+ Create your first theme configuration!
+
+
+ Customize the colors of your Expert Theme by creating a new configuration.
+ You can set primary colors, button colors, backgrounds, and more.
+
+
+
+
+
+
diff --git a/odex30_base/expert_theme/views/login_templates.xml b/odex30_base/expert_theme/views/login_templates.xml
new file mode 100644
index 0000000..2ce16fb
--- /dev/null
+++ b/odex30_base/expert_theme/views/login_templates.xml
@@ -0,0 +1,888 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odex30_base/expert_theme/views/login_templates_temp.xml b/odex30_base/expert_theme/views/login_templates_temp.xml
new file mode 100644
index 0000000..6b50974
--- /dev/null
+++ b/odex30_base/expert_theme/views/login_templates_temp.xml
@@ -0,0 +1,785 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+