expert odoo 18 theme module

This commit is contained in:
MohamedGad100 2025-12-14 15:39:43 +02:00
parent 0d6eb2d291
commit d8169d509b
33 changed files with 5182 additions and 0 deletions

View File

@ -0,0 +1,176 @@
# Expert Theme - Color Customization Guide
## 🎨 Overview
The Expert Theme includes a comprehensive color customization system that allows you to easily change the appearance of your Odoo backend. You can customize colors for:
- **Navbar** (top navigation bar)
- **Buttons** (Activate, Upgrade, Learn More, etc.)
- **Sidebar** (left navigation menu)
- **Backgrounds** (page background, content areas)
- **Text** (headings, body text, links)
- **Status colors** (success, warning, error, info)
## 🚀 Quick Start
### Method 1: Edit Configuration File (Recommended)
1. Open the file: `static/src/css/expert_theme_config.css`
2. Find the color you want to change in the `:root` section
3. Replace the hex color code with your desired color
4. Save the file and refresh your Odoo page
### Method 2: Use Pre-made Themes
The configuration file includes example color schemes. To use them:
1. Open `static/src/css/expert_theme_config.css`
2. Find the theme you want (Blue, Green, or Dark)
3. Uncomment the theme section (remove `/*` and `*/`)
4. Comment out the default theme section
5. Save and refresh
## 🎯 Color Variables Reference
### Primary Colors
```css
--expert-primary-color: #875A7B; /* Main brand color */
--expert-primary-hover: #6B4C6B; /* Hover state */
--expert-primary-light: #A67B9B; /* Light version */
```
### Secondary Colors
```css
--expert-secondary-color: #00A09D; /* Secondary brand color */
--expert-secondary-hover: #008B8A; /* Secondary hover */
```
### Button Colors
```css
--expert-btn-primary: #875A7B; /* Primary buttons (Activate) */
--expert-btn-secondary: #00A09D; /* Secondary buttons (Upgrade) */
--expert-btn-light: #F8F9FA; /* Light buttons (Learn More) */
```
### Background Colors
```css
--expert-bg-primary: #875A7B; /* Navbar background */
--expert-bg-secondary: #F8F9FA; /* Sidebar background */
--expert-bg-content: #FFFFFF; /* Content background */
--expert-bg-gradient-start: #f5f7fa; /* Page gradient start */
--expert-bg-gradient-end: #c3cfe2; /* Page gradient end */
```
### Text Colors
```css
--expert-text-primary: #212529; /* Main text */
--expert-text-secondary: #6C757D; /* Secondary text */
--expert-text-light: #FFFFFF; /* Light text */
```
### Status Colors
```css
--expert-success: #28A745; /* Success/Active */
--expert-warning: #FFC107; /* Warning */
--expert-danger: #DC3545; /* Error/Danger */
--expert-info: #17A2B8; /* Info */
```
## 🎨 Example Color Schemes
### Blue Theme
```css
:root {
--expert-primary-color: #007BFF;
--expert-primary-hover: #0056B3;
--expert-secondary-color: #6F42C1;
--expert-bg-primary: #007BFF;
--expert-bg-gradient-start: #E3F2FD;
--expert-bg-gradient-end: #BBDEFB;
}
```
### Green Theme
```css
:root {
--expert-primary-color: #28A745;
--expert-primary-hover: #1E7E34;
--expert-secondary-color: #20C997;
--expert-bg-primary: #28A745;
--expert-bg-gradient-start: #E8F5E8;
--expert-bg-gradient-end: #C3E6C3;
}
```
### Dark Theme
```css
:root {
--expert-primary-color: #343A40;
--expert-primary-hover: #23272B;
--expert-secondary-color: #6C757D;
--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;
}
```
## 🔧 Advanced Customization
### Custom Color Combinations
You can create your own color schemes by:
1. **Choosing a primary color** - This will be your main brand color
2. **Creating a hover color** - Usually a darker shade of your primary
3. **Selecting a secondary color** - A complementary color for variety
4. **Setting background colors** - Light colors for readability
5. **Choosing text colors** - High contrast for accessibility
### Color Tools
Use these tools to help choose colors:
- **Color Picker**: Use your browser's developer tools
- **Color Palettes**: [Coolors.co](https://coolors.co), [Adobe Color](https://color.adobe.com)
- **Accessibility**: [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
## 📝 Tips
1. **Test your colors** - Always preview changes in your browser
2. **Consider accessibility** - Ensure good contrast between text and backgrounds
3. **Keep it consistent** - Use the same color family throughout
4. **Start simple** - Begin with just changing the primary color
5. **Backup your changes** - Save a copy of your working configuration
## 🐛 Troubleshooting
### Colors not changing?
- Make sure you saved the file
- Clear your browser cache
- Check for syntax errors in the CSS
- Verify the file is being loaded (check browser developer tools)
### Colors look wrong?
- Check for typos in color codes
- Ensure you're using valid hex codes (e.g., #FF0000)
- Make sure you're editing the right file
### Need to reset?
- Restore the original `expert_theme_config.css` file
- Or copy the default values from the documentation above
## 📞 Support
If you need help with color customization, check:
1. This documentation
2. The example themes in the config file
3. Browser developer tools for debugging
4. Odoo community forums for additional help
---
**Happy Theming! 🎨**

View File

@ -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.

View File

@ -0,0 +1,312 @@
# Testing Guide - Login Page Templates Feature
## Prerequisites
- Odoo 18 installed and running
- Expert Theme module installed
- Admin access to Odoo backend
---
## Step 1: Install/Upgrade the Module
1. **Go to Apps Menu**
- Navigate to: `Apps` → Search for "Expert Theme"
- Click on the module
2. **Upgrade the Module**
- Click the "Upgrade" button (if already installed)
- OR click "Install" if not installed yet
- Wait for the upgrade/installation to complete
3. **Verify Installation**
- Check that no errors appear in the logs
- The module should show as "Installed"
---
## Step 2: Access Login Template Configuration
1. **Navigate to Configuration**
- Go to: `Expert Home``Login Page Templates`
- OR use the menu: `Expert Home``Login Page Templates` (should be above "Theme Colors")
2. **Verify Default Templates**
- You should see **2 templates** created by default:
- **Default Template** (White background #FFFFFF) - Should be **Active**
- **Light Gray Template** (Gray background #F5F5F5) - Should be **Inactive**
---
## Step 3: Test Template Switching
### Test 3.1: Switch to Gray Template
1. **Open the Gray Template**
- Click on "Light Gray Template" from the list
2. **Activate the Template**
- Click the "Activate" button in the form header
- OR toggle the "Active" field to `True`
- Click "Save"
3. **Verify Switch**
- The "Default Template" should automatically become **Inactive**
- Only "Light Gray Template" should be **Active**
- Check the list view - only one template should have the Active toggle ON
### Test 3.2: Switch Back to Default Template
1. **Open Default Template**
- Click on "Default Template" from the list
2. **Activate it**
- Click "Activate" button or toggle "Active" field
- Click "Save"
3. **Verify**
- "Light Gray Template" should become inactive
- "Default Template" should be active
---
## Step 4: Test Login Page Appearance
### Test 4.1: Test with Default Template (White)
1. **Logout from Odoo**
- Click on your user menu (top right)
- Click "Log out"
2. **Check Login Page**
- The login page should have a **white background** (#FFFFFF)
- All login form elements should be visible and functional
3. **Verify JavaScript Loading**
- Open browser Developer Tools (F12)
- Go to Console tab
- Check for any errors
- You should see the CSS variable being set: `--expert-login-bg-color`
### Test 4.2: Test with Gray Template
1. **Switch to Gray Template** (from Step 3.1)
2. **Logout and Check Login Page**
- Logout from Odoo
- The login page should now have a **light gray background** (#F5F5F5)
- Login form should still be visible and functional
3. **Verify Background Applied**
- Check browser DevTools → Elements tab
- Inspect `<body>` or `<html>` element
- Should have `background-color: #F5F5F5` applied
---
## Step 5: Test Color Customization
### Test 5.1: Edit Template Background Color
1. **Edit a Template**
- Go to `Login Page Templates`
- Open any template (e.g., "Default Template")
2. **Change Background Color**
- Click on the "Background Color" field
- Choose a different color (e.g., `#E3F2FD` - light blue)
- Click "Save"
3. **Test on Login Page**
- Logout and check login page
- Background should be the new color you selected
### Test 5.2: Create New Template
1. **Create New Template**
- Go to `Login Page Templates`
- Click "Create" button
2. **Fill Template Details**
- Name: "Blue Template"
- Background Color: `#2196F3` (or any blue color)
- Description: "Blue themed login page"
- Sequence: 3
- **DO NOT** activate it yet
3. **Save and Activate**
- Click "Save"
- Then click "Activate" button
- Verify other templates become inactive
4. **Test on Login Page**
- Logout and verify blue background appears
---
## Step 6: Test Edge Cases
### Test 6.1: Multiple Templates (Only One Active)
1. **Create Multiple Templates**
- Create 2-3 templates
- Try to activate multiple templates at once
2. **Expected Behavior**
- Only the last activated template should be active
- All others should automatically become inactive
- This ensures only one template is applied at a time
### Test 6.2: No Active Template
1. **Deactivate All Templates**
- Go to all templates and set `Active = False`
- Save all
2. **Check Login Page**
- Logout and check login page
- Should still work (fallback to default white or system default)
- OR a default template should be auto-created
### Test 6.3: Invalid Color Code
1. **Test Invalid Color**
- Edit a template
- Enter invalid color code (e.g., "invalid" or "12345")
- Save
2. **Expected Behavior**
- Should either:
- Show validation error
- OR fallback to default color (#FFFFFF)
---
## Step 7: Test API Endpoint
### Test 7.1: Check API Response
1. **Open Browser DevTools**
- Press F12
- Go to Network tab
2. **Access Login Page**
- Go to login page (while logged out)
3. **Check API Call**
- Look for request: `/expert_theme/get_login_template_styles`
- Click on it to see response
- Should return JSON:
```json
{
"success": true,
"styles": {
"background_color": "#F5F5F5"
}
}
```
4. **Verify Public Access**
- The API should work without authentication (public access)
- Try accessing directly: `http://your-odoo-url/expert_theme/get_login_template_styles`
---
## Step 8: Test Browser Compatibility
Test the login page in different browsers:
- ✅ Chrome/Edge (Chromium)
- ✅ Firefox
- ✅ Safari (if available)
Verify that:
- Background colors apply correctly
- Login form remains functional
- No JavaScript errors in console
---
## Step 9: Performance Testing
1. **Check Page Load Time**
- Login page should load quickly
- API call should be fast (< 100ms ideally)
2. **Check for Console Errors**
- Open DevTools → Console
- Should see no errors
- Only warnings are acceptable (e.g., if API fails, it should gracefully handle)
---
## Step 10: Final Verification Checklist
- [ ] Module installs/upgrades without errors
- [ ] Menu "Login Page Templates" appears in Expert Home
- [ ] Default templates are created (2 templates)
- [ ] Can switch between templates
- [ ] Only one template can be active at a time
- [ ] Login page background changes when template is switched
- [ ] Can edit template colors
- [ ] Can create new templates
- [ ] Login form remains functional with all templates
- [ ] API endpoint works (public access)
- [ ] No JavaScript errors in console
- [ ] Works in multiple browsers
---
## Troubleshooting
### Issue: Login page background doesn't change
**Solutions:**
1. Clear browser cache (Ctrl+Shift+Delete)
2. Hard refresh login page (Ctrl+F5)
3. Check browser console for JavaScript errors
4. Verify API endpoint is accessible: `/expert_theme/get_login_template_styles`
5. Check that a template is actually active in the backend
### Issue: Menu "Login Page Templates" not visible
**Solutions:**
1. Upgrade the module again
2. Check user permissions (should be available to all users)
3. Clear browser cache
4. Restart Odoo server
### Issue: Multiple templates active at once
**Solutions:**
1. This shouldn't happen - check the model's `write()` method
2. Manually deactivate all except one
3. Report as a bug if it persists
### Issue: API returns error
**Solutions:**
1. Check Odoo logs for Python errors
2. Verify the model `expert.login.template` exists
3. Check database for template records
4. Verify controller route is registered
---
## Expected Results Summary
✅ **Success Criteria:**
- Admin can configure login page templates from backend
- Can switch between templates easily
- Login page background changes immediately after switching
- Only one template active at a time
- Login functionality remains intact
- No errors in console or logs
---
## Notes
- The feature is designed to be simple initially (only background color)
- Future enhancements can add more customization options
- All changes take effect immediately (no server restart needed)
- Templates are stored in database, so they persist after module upgrades

View File

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

View File

@ -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',
}

View File

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

View File

@ -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)

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Default Login Templates -->
<!-- Template 1: Default Odoo Login -->
<record id="expert_login_template_default" model="expert.login.template">
<field name="name">Default Template</field>
<field name="active">True</field>
<field name="sequence">1</field>
<field name="template_type">default</field>
<field name="background_color">#FFFFFF</field>
<field name="is_default">True</field>
<field name="description">Standard Odoo login page template</field>
</record>
<!-- Template 2: Modern Card Design -->
<record id="expert_login_template_modern" model="expert.login.template">
<field name="name">Modern Template</field>
<field name="active">False</field>
<field name="sequence">2</field>
<field name="template_type">modern</field>
<field name="background_color">#667eea</field>
<field name="is_default">False</field>
<field name="description">Modern card design with gradient background</field>
</record>
<!-- Template 3: Minimal Design -->
<record id="expert_login_template_minimal" model="expert.login.template">
<field name="name">Minimal Template</field>
<field name="active">False</field>
<field name="sequence">3</field>
<field name="template_type">minimal</field>
<field name="background_color">#F8F9FA</field>
<field name="is_default">False</field>
<field name="description">Clean minimal design template</field>
</record>
<!-- Template 4: Corporate Design -->
<record id="expert_login_template_corporate" model="expert.login.template">
<field name="name">Corporate Template</field>
<field name="active">False</field>
<field name="sequence">4</field>
<field name="template_type">corporate</field>
<field name="background_color">#1a1a1a</field>
<field name="is_default">False</field>
<field name="description">Professional corporate dark theme</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import expert_theme_config
from . import expert_login_template

View File

@ -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']},
}

View File

@ -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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_expert_theme_config_user expert.theme.config.user model_expert_theme_config base.group_user 1 1 1 1
3 access_expert_theme_config_admin expert.theme.config.admin model_expert_theme_config base.group_system 1 1 1 1
4 access_expert_login_template_user expert.login.template.user model_expert_login_template base.group_user 1 1 1 1
5 access_expert_login_template_admin expert.login.template.admin model_expert_login_template base.group_system 1 1 1 1

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Expert Theme</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f8f9fa;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #667eea;
text-align: center;
margin-bottom: 30px;
}
.feature {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.icon {
text-align: center;
font-size: 48px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">🎨</div>
<h1>Expert Theme</h1>
<div class="feature">
<h3>Custom Home Page</h3>
<p>A clean and modern home page that displays only your installed modules for easy navigation.</p>
</div>
<div class="feature">
<h3>Simple Interface</h3>
<p>Minimal design focused on functionality - no clutter, just your modules.</p>
</div>
<div class="feature">
<h3>Responsive Design</h3>
<p>Works perfectly on desktop, tablet, and mobile devices.</p>
</div>
<div class="feature">
<h3>Easy Installation</h3>
<p>Simply install the module and your new home page will be ready to use.</p>
</div>
</div>
</body>
</html>

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
*/

View File

@ -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;
}
}

View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

View File

@ -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);

View File

@ -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);
})();
*/

View File

@ -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);
}
},
});

View File

@ -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,
});

View File

@ -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();
});

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="expert_theme.ExpertHomeTemplate" owl="1">
<div class="expert-home-container">
<!-- Theme Colors Button -->
<div class="expert-theme-controls">
<button class="btn btn-primary expert-theme-btn"
t-on-click="() => this.openThemeColors()">
🎨 Customize Theme Colors
</button>
</div>
<div t-if="state.loading" class="expert-loading">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
<p>Loading your modules...</p>
</div>
<div t-if="state.error" class="expert-error">
<div class="alert alert-danger">
<strong>Error:</strong> <t t-esc="state.error"/>
</div>
</div>
<div t-if="!state.loading and !state.error" class="expert-modules-grid">
<div t-foreach="state.modules" t-as="module" t-key="module.id"
class="expert-module-card"
t-on-click="() => this.openModule(module)">
<div class="expert-module-icon">
<img t-att-src="this.getModuleIcon(module.web_icon)"
t-att-alt="module.name"
class="module-icon"/>
</div>
<div class="expert-module-info">
<h3 t-esc="module.name"/>
<p>Click to open</p>
</div>
</div>
</div>
<div t-if="!state.loading and !state.error and state.modules.length === 0"
class="expert-no-modules">
<div class="alert alert-info">
<strong>No installed modules found.</strong>
<p>This could mean:</p>
<ul>
<li>No modules are currently installed</li>
<li>The module detection is not working properly</li>
<li>Check the browser console for debugging information</li>
</ul>
<p>Try refreshing the page or check if modules are properly installed in Odoo.</p>
</div>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Custom Home Page View -->
<record id="view_expert_home" model="ir.ui.view">
<field name="name">expert.theme.home</field>
<field name="model">ir.ui.menu</field>
<field name="type">qweb</field>
<field name="arch" type="xml">
<t t-name="expert_theme.home">
<div class="expert-home-container">
<div class="expert-home-header">
<h1>Welcome to Expert Theme</h1>
<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>
</field>
</record>
<!-- Action for Expert Home -->
<record id="action_expert_home" model="ir.actions.client">
<field name="name">Expert Home</field>
<field name="tag">expert_home</field>
<field name="target">current</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,233 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Expert Login Template Form View -->
<record id="view_expert_login_template_form" model="ir.ui.view">
<field name="name">expert.login.template.form</field>
<field name="model">expert.login.template</field>
<field name="arch" type="xml">
<form string="Login Page Template">
<header>
<button string="Save" special="save"
class="btn-primary"
icon="fa-save"/>
<button name="toggle_active" string="Activate" type="object"
class="btn-primary"
invisible="active"/>
<button name="action_duplicate" string="Duplicate" type="object"
class="btn-secondary"/>
<field name="active" widget="boolean_toggle"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Template Name"/>
</h1>
</div>
<div class="alert alert-warning" role="alert" invisible="not id">
<strong>Editing Existing Template:</strong> You are editing an existing template.
To create a NEW template, go back to the list and click "New" button.
</div>
<group>
<group string="Template Design">
<field name="template_type"
widget="radio"
options="{'horizontal': true}"
required="1"/>
<div class="alert alert-info" role="alert" col="2">
<strong>Template Types:</strong><br/>
<strong>Default:</strong> Standard Odoo login page<br/>
<strong>Modern:</strong> Card design with gradient background<br/>
<strong>Minimal:</strong> Clean minimal design<br/>
<strong>Corporate:</strong> Professional dark theme
</div>
</group>
<group string="Template Settings">
<field name="sequence"/>
<field name="is_default" readonly="1"/>
<field name="background_color" widget="color"
string="Background Color"
help="Background color for the login page (used for CSS customization)"/>
<div class="alert alert-info" role="alert" col="2">
<strong>Note:</strong> Only one template can be active at a time.
Activating this template will deactivate all others.
Each template has a completely different HTML structure and design.
</div>
</group>
</group>
<group>
<group string="Description">
<field name="description" placeholder="Describe this template..." nolabel="1"/>
</group>
</group>
<notebook>
<page string="Template Images &amp; Logo" name="template_images">
<group>
<group string="Modern Template" invisible="template_type != 'modern'">
<field name="modern_template_logo" widget="image" class="oe_avatar"
options="{'size': [100, 100]}"
help="Upload your company logo (recommended size: 40x40px). This will replace the default 'E' icon."/>
<field name="modern_template_image" widget="image" class="oe_avatar"
options="{'size': [600, 600]}"
help="Upload an image to display on the right side of Modern login, signup, and reset password pages"/>
<div class="alert alert-info" role="alert" col="2">
<strong>Note:</strong> Logo and image will be used across all three pages (Login, Signup, Reset Password) when Modern template is active.
</div>
</group>
<group string="Minimal Template" invisible="template_type != 'minimal'">
<field name="minimal_template_logo" widget="image" class="oe_avatar"
options="{'size': [100, 100]}"
help="Upload your company logo (recommended size: 40x40px). This will replace the default 'E' icon."/>
<field name="minimal_template_image" widget="image" class="oe_avatar"
options="{'size': [600, 600]}"
help="Upload an image to display on the right side of Minimal login, signup, and reset password pages"/>
<div class="alert alert-info" role="alert" col="2">
<strong>Note:</strong> Logo and image will be used across all three pages (Login, Signup, Reset Password) when Minimal template is active.
</div>
</group>
<group string="Corporate Template" invisible="template_type != 'corporate'">
<field name="corporate_template_logo" widget="image" class="oe_avatar"
options="{'size': [100, 100]}"
help="Upload your company logo (recommended size: 40x40px). This will replace the default 'E' icon."/>
<field name="corporate_template_image" widget="image" class="oe_avatar"
options="{'size': [600, 600]}"
help="Upload an image to display on the right side of Corporate login, signup, and reset password pages"/>
<div class="alert alert-info" role="alert" col="2">
<strong>Note:</strong> Logo and image will be used across all three pages (Login, Signup, Reset Password) when Corporate template is active.
</div>
</group>
</group>
</page>
<page string="Modern Template Text" name="modern_template_text" invisible="template_type != 'modern'">
<group>
<group string="Login Page">
<field name="modern_login_title" placeholder="e.g., Welcome to Expert 👋"/>
<field name="modern_login_subtitle" placeholder="e.g., Kindly fill in your details below to sign in to your account"/>
<field name="modern_login_button_text" placeholder="e.g., Sign In"/>
<field name="modern_login_button_bg_color" widget="color"/>
<field name="modern_login_button_text_color" widget="color"/>
<field name="modern_login_button_bg_hover" widget="color"/>
<field name="modern_login_button_text_hover" widget="color"/>
</group>
<group string="Signup Page">
<field name="modern_signup_title" placeholder="e.g., Create an account"/>
<field name="modern_signup_subtitle" placeholder="e.g., Join us today and get started"/>
<field name="modern_signup_button_text" placeholder="e.g., Create an account"/>
<field name="modern_signup_button_bg_color" widget="color"/>
<field name="modern_signup_button_text_color" widget="color"/>
<field name="modern_signup_button_bg_hover" widget="color"/>
<field name="modern_signup_button_text_hover" widget="color"/>
</group>
</group>
<group>
<group string="Reset Password Page">
<field name="modern_reset_title" placeholder="e.g., Reset your password"/>
<field name="modern_reset_subtitle" placeholder="e.g., Enter your email to receive reset instructions"/>
</group>
<group></group>
</group>
</page>
<page string="Minimal Template Text" name="minimal_template_text" invisible="template_type != 'minimal'">
<group>
<group string="Login Page">
<field name="minimal_login_title" placeholder="e.g., Welcome to Expert 👋"/>
<field name="minimal_login_subtitle" placeholder="e.g., Kindly fill in your details below to sign in to your account"/>
<field name="minimal_login_button_text" placeholder="e.g., Sign In"/>
<field name="minimal_login_button_bg_color" widget="color"/>
<field name="minimal_login_button_text_color" widget="color"/>
<field name="minimal_login_button_bg_hover" widget="color"/>
<field name="minimal_login_button_text_hover" widget="color"/>
</group>
<group string="Signup Page">
<field name="minimal_signup_title" placeholder="e.g., Create an account"/>
<field name="minimal_signup_subtitle" placeholder="e.g., Join us today and get started"/>
<field name="minimal_signup_button_text" placeholder="e.g., Create an account"/>
<field name="minimal_signup_button_bg_color" widget="color"/>
<field name="minimal_signup_button_text_color" widget="color"/>
<field name="minimal_signup_button_bg_hover" widget="color"/>
<field name="minimal_signup_button_text_hover" widget="color"/>
</group>
</group>
<group>
<group string="Reset Password Page">
<field name="minimal_reset_title" placeholder="e.g., Reset your password"/>
<field name="minimal_reset_subtitle" placeholder="e.g., Enter your email to receive reset instructions"/>
</group>
<group></group>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Expert Login Template List View -->
<record id="view_expert_login_template_tree" model="ir.ui.view">
<field name="name">expert.login.template.tree</field>
<field name="model">expert.login.template</field>
<field name="arch" type="xml">
<list string="Login Page Templates" default_order="sequence">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="template_type"/>
<field name="background_color" widget="color"/>
<field name="active" widget="boolean_toggle" context="{'from_list_view': True}"/>
<field name="is_default"/>
<field name="description" invisible="1"/>
<button name="toggle_active" string="Activate" type="object"
class="btn-primary"
invisible="active"
icon="fa-check"
confirm="Are you sure you want to activate this template? It will deactivate all other templates."/>
<button name="action_duplicate" string="Duplicate" type="object"
class="btn-secondary"
icon="fa-copy"/>
</list>
</field>
</record>
<!-- Expert Login Template Search View -->
<record id="view_expert_login_template_search" model="ir.ui.view">
<field name="name">expert.login.template.search</field>
<field name="model">expert.login.template</field>
<field name="arch" type="xml">
<search string="Search Login Templates">
<field name="name"/>
<filter string="Active" name="active" domain="[('active', '=', True)]"/>
<filter string="Inactive" name="inactive" domain="[('active', '=', False)]"/>
<filter string="Default" name="default" domain="[('is_default', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_active" context="{'group_by': 'active'}"/>
</group>
</search>
</field>
</record>
<!-- Expert Login Template Action -->
<record id="action_expert_login_template" model="ir.actions.act_window">
<field name="name">Login Page Templates</field>
<field name="res_model">expert.login.template</field>
<field name="view_mode">list,form</field>
<field name="domain">[]</field>
<field name="context">{'active_test': False}</field>
<field name="search_view_id" ref="view_expert_login_template_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first login page template!
</p>
<p>
Configure different login page templates and switch between them.
Each template can have different colors, backgrounds, and styles.
</p>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<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"/>
</data>
</odoo>

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Expert Theme Configuration Form View -->
<record id="view_expert_theme_config_form" model="ir.ui.view">
<field name="name">expert.theme.config.form</field>
<field name="model">expert.theme.config</field>
<field name="arch" type="xml">
<form string="Expert Theme Configuration">
<header>
<button name="apply_theme" string="Apply Theme" type="object" class="btn-primary"/>
<field name="active" widget="boolean_toggle"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Theme Name"/>
</h1>
</div>
<group>
<group string="Primary Colors">
<field name="primary_color" widget="color"/>
<field name="primary_hover" widget="color"/>
<field name="primary_light" widget="color"/>
</group>
<group string="Secondary Colors">
<field name="secondary_color" widget="color"/>
<field name="secondary_hover" widget="color"/>
</group>
</group>
<group>
<group string="Button Colors">
<field name="btn_primary" widget="color"/>
<field name="btn_primary_hover" widget="color"/>
<field name="btn_secondary" widget="color"/>
<field name="btn_secondary_hover" widget="color"/>
<field name="btn_light" widget="color"/>
<field name="btn_light_hover" widget="color"/>
<field name="btn_light_text" widget="color"/>
</group>
<group string="Background Colors">
<field name="bg_primary" widget="color"/>
<field name="bg_secondary" widget="color"/>
<field name="bg_content" widget="color"/>
<field name="bg_gradient_start" widget="color"/>
<field name="bg_gradient_end" widget="color"/>
</group>
</group>
<group>
<group string="Text Colors">
<field name="text_primary" widget="color"/>
<field name="text_secondary" widget="color"/>
<field name="text_light" widget="color"/>
</group>
<group string="Border Colors">
<field name="border_color" widget="color"/>
<field name="border_light" widget="color"/>
</group>
</group>
<group>
<group string="Status Colors">
<field name="success_color" widget="color"/>
<field name="warning_color" widget="color"/>
<field name="danger_color" widget="color"/>
<field name="info_color" widget="color"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Expert Theme Configuration List View -->
<record id="view_expert_theme_config_tree" model="ir.ui.view">
<field name="name">expert.theme.config.list</field>
<field name="model">expert.theme.config</field>
<field name="arch" type="xml">
<list string="Expert Theme Configurations">
<field name="name"/>
<field name="active"/>
<field name="primary_color" widget="color"/>
<field name="secondary_color" widget="color"/>
<field name="bg_gradient_start" widget="color"/>
<field name="bg_gradient_end" widget="color"/>
</list>
</field>
</record>
<!-- Expert Theme Configuration Search View -->
<record id="view_expert_theme_config_search" model="ir.ui.view">
<field name="name">expert.theme.config.search</field>
<field name="model">expert.theme.config</field>
<field name="arch" type="xml">
<search string="Search Theme Configurations">
<field name="name"/>
<filter string="Active" name="active" domain="[('active', '=', True)]"/>
<filter string="Inactive" name="inactive" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_active" context="{'group_by': 'active'}"/>
</group>
</search>
</field>
</record>
<!-- Expert Theme Configuration Action -->
<record id="action_expert_theme_config" model="ir.actions.act_window">
<field name="name">Theme Colors</field>
<field name="res_model">expert.theme.config</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_expert_theme_config_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first theme configuration!
</p>
<p>
Customize the colors of your Expert Theme by creating a new configuration.
You can set primary colors, button colors, backgrounds, and more.
</p>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,888 @@
<?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"/>
<style>
body.bg-100 {
background-color: <t t-if="login_template and login_template.background_color"><t t-esc="login_template.background_color"/></t><t t-else="">#19181F</t> !important;
}
</style>
</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 expert-modern-login-btn">
<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>
<!-- Button Styles -->
<style>
.expert-modern-login-btn {
background-color: <t t-if="login_template and login_template.modern_login_button_bg_color"><t t-esc="login_template.modern_login_button_bg_color"/></t><t t-else="">#007bff</t> !important;
color: <t t-if="login_template and login_template.modern_login_button_text_color"><t t-esc="login_template.modern_login_button_text_color"/></t><t t-else="">#FFFFFF</t> !important;
border: none !important;
}
.expert-modern-login-btn:hover {
background-color: <t t-if="login_template and login_template.modern_login_button_bg_hover"><t t-esc="login_template.modern_login_button_bg_hover"/></t><t t-else="">#0056b3</t> !important;
color: <t t-if="login_template and login_template.modern_login_button_text_hover"><t t-esc="login_template.modern_login_button_text_hover"/></t><t t-else="">#FFFFFF</t> !important;
}
</style>
<!-- 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"/>
<style>
body.bg-100 {
background-color: <t t-if="login_template and login_template.background_color"><t t-esc="login_template.background_color"/></t><t t-else="">#19181F</t> !important;
}
</style>
</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 expert-modern-signup-btn">
<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>
<!-- Button Styles -->
<style>
.expert-modern-signup-btn {
background-color: <t t-if="login_template and login_template.modern_signup_button_bg_color"><t t-esc="login_template.modern_signup_button_bg_color"/></t><t t-else="">#28a745</t> !important;
color: <t t-if="login_template and login_template.modern_signup_button_text_color"><t t-esc="login_template.modern_signup_button_text_color"/></t><t t-else="">#FFFFFF</t> !important;
border: none !important;
}
.expert-modern-signup-btn:hover {
background-color: <t t-if="login_template and login_template.modern_signup_button_bg_hover"><t t-esc="login_template.modern_signup_button_bg_hover"/></t><t t-else="">#218838</t> !important;
color: <t t-if="login_template and login_template.modern_signup_button_text_hover"><t t-esc="login_template.modern_signup_button_text_hover"/></t><t t-else="">#FFFFFF</t> !important;
}
</style>
<!-- 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"/>
<style>
body.bg-100 {
background-color: <t t-if="login_template and login_template.background_color"><t t-esc="login_template.background_color"/></t><t t-else="">#19181F</t> !important;
}
</style>
</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"/>
<style>
body.bg-100 {
background-color: <t t-if="login_template and login_template.background_color"><t t-esc="login_template.background_color"/></t><t t-else="">#FFFFFF</t> !important;
}
</style>
</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 expert-minimal-login-btn">
<t t-if="login_template and login_template.minimal_login_button_text">
<t t-esc="login_template.minimal_login_button_text"/>
</t>
<t t-else="">
Sign In
</t>
</button>
</form>
<!-- Button Styles -->
<style>
.expert-minimal-login-btn {
background-color: <t t-if="login_template and login_template.minimal_login_button_bg_color"><t t-esc="login_template.minimal_login_button_bg_color"/></t><t t-else="">#E5E5E5</t> !important;
color: <t t-if="login_template and login_template.minimal_login_button_text_color"><t t-esc="login_template.minimal_login_button_text_color"/></t><t t-else="">#000000</t> !important;
border: none !important;
}
.expert-minimal-login-btn:hover {
background-color: <t t-if="login_template and login_template.minimal_login_button_bg_hover"><t t-esc="login_template.minimal_login_button_bg_hover"/></t><t t-else="">#D0D0D0</t> !important;
color: <t t-if="login_template and login_template.minimal_login_button_text_hover"><t t-esc="login_template.minimal_login_button_text_hover"/></t><t t-else="">#000000</t> !important;
}
</style>
<!-- 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"/>
<style>
body.bg-100 {
background-color: <t t-if="login_template and login_template.background_color"><t t-esc="login_template.background_color"/></t><t t-else="">#FFFFFF</t> !important;
}
</style>
</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 expert-minimal-signup-btn">
<t t-if="login_template and login_template.minimal_signup_button_text">
<t t-esc="login_template.minimal_signup_button_text"/>
</t>
<t t-else="">
Create an account
</t>
</button>
</form>
<!-- Button Styles -->
<style>
.expert-minimal-signup-btn {
background-color: <t t-if="login_template and login_template.minimal_signup_button_bg_color"><t t-esc="login_template.minimal_signup_button_bg_color"/></t><t t-else="">#000000</t> !important;
color: <t t-if="login_template and login_template.minimal_signup_button_text_color"><t t-esc="login_template.minimal_signup_button_text_color"/></t><t t-else="">#FFFFFF</t> !important;
border: none !important;
}
.expert-minimal-signup-btn:hover {
background-color: <t t-if="login_template and login_template.minimal_signup_button_bg_hover"><t t-esc="login_template.minimal_signup_button_bg_hover"/></t><t t-else="">#333333</t> !important;
color: <t t-if="login_template and login_template.minimal_signup_button_text_hover"><t t-esc="login_template.minimal_signup_button_text_hover"/></t><t t-else="">#FFFFFF</t> !important;
}
</style>
<!-- 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"/>
<style>
body.bg-100 {
background-color: <t t-if="login_template and login_template.background_color"><t t-esc="login_template.background_color"/></t><t t-else="">#FFFFFF</t> !important;
}
</style>
</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">
<t t-set="head">
<style>
body.bg-100 {
background-color: <t t-if="login_template and login_template.background_color"><t t-esc="login_template.background_color"/></t><t t-else="">#1a1a1a</t> !important;
}
</style>
</t>
<div class="oe_login_form" t-attf-style="background: {{login_template.background_color if login_template and login_template.background_color else '#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

@ -0,0 +1,785 @@
<?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>