Guía de desarrollo de temas de Odoo: cree temas personalizados para Odoo 17/18
El ecosistema de temas de Odoo genera más de $12 millones anualmente en la App Store de Odoo, sin embargo, menos del 8% de los temas reciben una calificación de 5 estrellas. La diferencia entre un tema olvidable y un éxito de ventas se reduce a comprender el proceso de renderizado de Odoo, la herencia de plantillas QWeb y la arquitectura de arrastrar y soltar del creador de sitios web.
Esta guía cubre cada capa del desarrollo de temas de Odoo, desde la estructuración del proyecto hasta la arquitectura SCSS avanzada, desde la creación de fragmentos personalizados hasta hacer que su tema funcione a la perfección con el creador de sitios web de Odoo.
Conclusiones clave
- Los temas de Odoo son módulos estándar con una estructura de directorio específica: amplían el módulo
websitey registran plantillas, SCSS y JavaScript a través del sistema de agrupación de activos. - QWeb es el motor de plantillas del lado del servidor de Odoo: utiliza directivas XML (t-if, t-foreach, t-call, t-inherit) que se compilan en HTML. Comprender la herencia de plantillas con XPath es esencial para personalizar cualquier parte del sitio web.
- El proceso SCSS se compila a través del administrador de activos de Odoo: las variables personalizadas anulan los valores predeterminados de Bootstrap 5 y
_variables.scssdebe cargarse antes que el marco. - La integración del creador de sitios web requiere definir opciones de fragmentos, bloques de creación personalizados y bloques de contenido dinámico que los usuarios no técnicos pueden configurar a través del editor de arrastrar y soltar.
- El diseño responsivo en Odoo aprovecha el sistema grid de Bootstrap 5 con utilidades específicas de Odoo: pruebe en todos los puntos de interrupción y considere los lenguajes RTL para implementaciones internacionales.
1. Estructura del módulo temático
Un tema de Odoo sigue la estructura del módulo estándar con adiciones específicas del sitio web:
theme_ecosire/
├── __init__.py
├── __manifest__.py
├── data/
│ ├── theme_data.xml # Default pages, menus
│ └── images.xml # Pre-loaded image library
├── static/
│ ├── description/
│ │ ├── icon.png # Module icon (128x128)
│ │ └── icon.svg # SVG version
│ ├── src/
│ │ ├── img/ # Theme images
│ │ ├── fonts/ # Custom fonts
│ │ ├── scss/
│ │ │ ├── _variables.scss # Bootstrap variable overrides
│ │ │ ├── _mixins.scss # Custom SCSS mixins
│ │ │ ├── _typography.scss # Font stacks, sizes
│ │ │ ├── _header.scss # Header styles
│ │ │ ├── _footer.scss # Footer styles
│ │ │ ├── _snippets.scss # Snippet-specific styles
│ │ │ └── primary.scss # Main entry point
│ │ ├── js/
│ │ │ ├── snippets/ # Snippet JS (animations, interactions)
│ │ │ └── theme.js # Theme-wide JS
│ │ └── xml/
│ │ └── snippets.xml # OWL snippet templates
│ └── img/ # Public images (backgrounds, icons)
├── views/
│ ├── assets.xml # Asset bundle registration
│ ├── layout.xml # Header/footer templates
│ ├── pages.xml # Pre-built page templates
│ └── snippets/
│ ├── options.xml # Snippet options (color pickers, etc.)
│ └── s_custom_block.xml # Custom snippet definitions
└── tests/
└── test_theme.py # Theme installation tests
El manifiesto temático
# __manifest__.py
{
'name': 'Theme Ecosire',
'description': 'Modern, performance-optimized theme for Odoo Website',
'category': 'Theme',
'version': '18.0.1.0.0',
'author': 'ECOSIRE Private Limited',
'website': 'https://ecosire.com',
'license': 'LGPL-3',
'depends': ['website'],
'data': [
'views/assets.xml',
'views/layout.xml',
'views/pages.xml',
'views/snippets/options.xml',
'views/snippets/s_custom_block.xml',
],
'assets': {
'web.assets_frontend': [
# Variables MUST come before Bootstrap
('prepend', 'theme_ecosire/static/src/scss/_variables.scss'),
'theme_ecosire/static/src/scss/primary.scss',
'theme_ecosire/static/src/js/theme.js',
],
'website.assets_wysiwyg': [
'theme_ecosire/static/src/js/snippets/*.js',
],
},
'images': [
'static/description/banner.png',
'static/description/theme_screenshot.jpg',
],
'snippet_lists': {
'website.snippets': [
('replace', 'theme_ecosire.s_custom_hero'),
],
},
'installable': True,
'application': False,
'auto_install': False,
}
Nota crítica sobre la clave assets: En Odoo 17+, el diccionario assets en __manifest__.py es la forma preferida de registrar archivos estáticos. El antiguo enfoque views/assets.xml que utiliza <template inherit_id="web.assets_frontend"> todavía funciona, pero se está eliminando gradualmente. Utilice ambos para obtener la máxima compatibilidad.
2. Análisis profundo de las plantillas QWeb
QWeb es el motor de plantillas basado en XML de Odoo. Procesa directivas con el prefijo t- y genera HTML. Comprender QWeb no es negociable para el desarrollo de temas.
Directivas básicas
<!-- Conditional rendering -->
<div t-if="website.is_publisher()">
<span>Edit mode controls here</span>
</div>
<div t-elif="request.env.user._is_internal()">
<span>Internal user view</span>
</div>
<div t-else="">
<span>Public visitor view</span>
</div>
<!-- Loops -->
<ul>
<t t-foreach="website.menu_id.child_id" t-as="menu_item">
<li t-attf-class="nav-item #{('active' if menu_item.is_active else '')}">
<a t-att-href="menu_item.url" t-out="menu_item.name"/>
</li>
</t>
</ul>
<!-- Variable setting -->
<t t-set="company" t-value="res_company"/>
<t t-set="primary_color" t-value="'#FF6B00'"/>
<!-- Output (auto-escaped) -->
<span t-out="partner.name"/>
<!-- Raw HTML output (use with caution) -->
<div t-raw="sanitized_html_content"/>
<!-- Dynamic attributes -->
<div t-att-id="'section-' + str(section.id)"
t-attf-class="container #{extra_class}"
t-att-data-section="section.sequence"/>
<!-- Template calling -->
<t t-call="theme_ecosire.hero_section">
<t t-set="title">Welcome to Our Store</t>
<t t-set="subtitle">Premium Odoo Themes</t>
</t>
Herencia de plantilla con XPath
Aquí es donde el desarrollo de temas se vuelve poderoso. Puedes modificar cualquier plantilla existente sin reemplazarla:
<!-- views/layout.xml -->
<template id="custom_header" inherit_id="website.layout" name="Custom Header">
<!-- Replace the entire header -->
<xpath expr="//header" position="replace">
<header id="top" class="ecosire-header">
<nav class="navbar navbar-expand-lg">
<div class="container">
<a class="navbar-brand" t-att-href="'/'">
<img t-if="website.logo"
t-att-src="website.image_url(website, 'logo')"
alt="Logo" class="img-fluid" style="max-height: 50px;"/>
</a>
<!-- Custom mega menu -->
<t t-call="theme_ecosire.mega_menu"/>
<!-- CTA button -->
<a href="/contactus" class="btn btn-primary ms-3">
Get Started
</a>
</div>
</nav>
</header>
</xpath>
<!-- Add a top bar before the header -->
<xpath expr="//header[@id='top']" position="before">
<div class="ecosire-topbar bg-dark text-white py-1">
<div class="container d-flex justify-content-between">
<span t-out="res_company.phone"/>
<span t-out="res_company.email"/>
</div>
</div>
</xpath>
<!-- Modify footer: add a newsletter section -->
<xpath expr="//footer" position="inside">
<div class="ecosire-newsletter bg-primary text-white py-5">
<div class="container text-center">
<h3>Stay Updated</h3>
<t t-call="website.s_newsletter_block"/>
</div>
</div>
</xpath>
</template>
Referencia de posición XPath
| Posición | Efecto |
|---|---|
| CÓDIGO0 | Reemplace el elemento coincidente por completo |
| CÓDIGO0 | Agregar dentro del elemento coincidente |
| CÓDIGO0 | Insertar antes del elemento coincidente |
| CÓDIGO0 | Insertar después del elemento coincidente |
| CÓDIGO0 | Modificar atributos del elemento coincidente |
<!-- Modify attributes example -->
<xpath expr="//div[@id='wrapwrap']" position="attributes">
<attribute name="class" add="ecosire-theme" separator=" "/>
</xpath>
<!-- Remove an element -->
<xpath expr="//div[hasclass('o_not_editable')]" position="replace"/>
3. Arquitectura SCSS
La canalización SCSS de Odoo se compila a través de su administrador de activos, lo que significa que sus anulaciones de variables deben cargarse antes que los valores predeterminados de Bootstrap. Utilice la directiva 'prepend' en el diccionario de activos de su manifiesto para asegurarse de que su archivo _variables.scss tenga prioridad. Este único archivo controla los colores, las fuentes, el espaciado, el radio del borde y cualquier otro token de diseño Bootstrap en su tema.
Anulaciones de variables (_variables.scss)
// _variables.scss — loaded BEFORE Bootstrap
// Override Bootstrap 5 defaults
// Brand Colors
$o-brand-primary: #FF6B00;
$o-brand-secondary: #1A1F36;
// Bootstrap color overrides
$primary: $o-brand-primary;
$secondary: $o-brand-secondary;
$success: #00C853;
$info: #2196F3;
$warning: #FFB300;
$danger: #FF1744;
// Typography
$font-family-sans-serif: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
$font-family-monospace: 'JetBrains Mono', 'Fira Code', monospace;
$font-size-base: 1rem;
$headings-font-weight: 700;
$headings-line-height: 1.2;
// Spacing
$spacer: 1rem;
// Border radius
$border-radius: 0.5rem;
$border-radius-lg: 1rem;
$border-radius-pill: 50rem;
// Shadows
$box-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
$box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
$box-shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
// Navbar
$navbar-padding-y: 1rem;
$navbar-brand-font-size: 1.5rem;
// Cards
$card-border-radius: $border-radius-lg;
$card-border-color: transparent;
$card-box-shadow: $box-shadow;
Hoja de estilo principal (primary.scss)
// primary.scss — Main entry point
@import 'variables'; // Already prepended, but import for IDE support
@import 'mixins';
@import 'typography';
@import 'header';
@import 'footer';
@import 'snippets';
// ============================================
// Global Overrides
// ============================================
#wrapwrap.ecosire-theme {
// Smooth scrolling
scroll-behavior: smooth;
// Better text rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// ============================================
// Section Spacing
// ============================================
.s_section {
padding: 5rem 0;
@media (max-width: 768px) {
padding: 3rem 0;
}
}
// ============================================
// Button Styles
// ============================================
.btn-primary {
background: linear-gradient(135deg, $primary, darken($primary, 10%));
border: none;
padding: 0.75rem 2rem;
font-weight: 600;
letter-spacing: 0.02em;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba($primary, 0.4);
}
}
// ============================================
// Card Component
// ============================================
.ecosire-card {
background: white;
border-radius: $border-radius-lg;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: $box-shadow-lg;
}
&__image {
aspect-ratio: 16 / 9;
object-fit: cover;
width: 100%;
}
&__body {
padding: 1.5rem;
}
}
Carga de fuentes personalizadas
// _typography.scss
@font-face {
font-family: 'Inter';
src: url('/theme_ecosire/static/src/fonts/Inter-Variable.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Inter';
src: url('/theme_ecosire/static/src/fonts/Inter-Variable-Italic.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
font-style: italic;
}
// Heading hierarchy
h1, .h1 { font-size: 2.5rem; font-weight: 800; }
h2, .h2 { font-size: 2rem; font-weight: 700; }
h3, .h3 { font-size: 1.5rem; font-weight: 600; }
h4, .h4 { font-size: 1.25rem; font-weight: 600; }
4. Fragmentos personalizados (bloques de construcción)
Los fragmentos son los bloques de construcción de arrastrar y soltar en el creador de sitios web de Odoo. Crear fragmentos personalizados es lo que hace que un tema sea realmente valioso.
Definición de una plantilla de fragmento
<!-- views/snippets/s_custom_block.xml -->
<odoo>
<!-- The snippet template -->
<template id="s_ecosire_hero" name="Ecosire Hero Section">
<section class="s_ecosire_hero pt-5 pb-5 o_cc o_cc1">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="display-4 fw-bold">
Transform Your Business
</h1>
<p class="lead text-muted mt-3">
Enterprise solutions that scale with your ambitions.
</p>
<div class="mt-4">
<a href="/contactus" class="btn btn-primary btn-lg me-3">
Get Started
</a>
<a href="/about" class="btn btn-outline-secondary btn-lg">
Learn More
</a>
</div>
</div>
<div class="col-lg-6">
<img src="/theme_ecosire/static/src/img/hero-illustration.svg"
alt="Hero" class="img-fluid" loading="lazy"/>
</div>
</div>
</div>
</section>
</template>
<!-- Register snippet in the builder panel -->
<template id="snippets" inherit_id="website.snippets">
<xpath expr="//div[@id='snippet_structure']//t[@t-snippet]/.."
position="inside">
<t t-snippet="theme_ecosire.s_ecosire_hero"
t-thumbnail="/theme_ecosire/static/src/img/snippets/hero_thumb.png"/>
</xpath>
</template>
</odoo>
Opciones de fragmentos (personalización del usuario)
<!-- views/snippets/options.xml -->
<odoo>
<template id="s_ecosire_hero_options" inherit_id="website.snippet_options">
<xpath expr="." position="inside">
<div data-js="EcosireHeroOptions"
data-selector=".s_ecosire_hero">
<!-- Layout selector -->
<we-select string="Layout">
<we-button data-select-class="s_hero_left">Text Left</we-button>
<we-button data-select-class="s_hero_center">Centered</we-button>
<we-button data-select-class="s_hero_right">Text Right</we-button>
</we-select>
<!-- Background style -->
<we-select string="Background">
<we-button data-select-class="bg-white">White</we-button>
<we-button data-select-class="bg-dark text-white">Dark</we-button>
<we-button data-select-class="bg-gradient">Gradient</we-button>
</we-select>
<!-- Animation toggle -->
<we-checkbox string="Enable Animation"
data-toggle-class="s_hero_animated"/>
</div>
</xpath>
</template>
</odoo>
Fragmento de JavaScript (animaciones)
// static/src/js/snippets/s_ecosire_hero.js
/** @odoo-module */
import publicWidget from '@web/legacy/js/public/public_widget';
const EcosireHero = publicWidget.Widget.extend({
selector: '.s_ecosire_hero.s_hero_animated',
disabledInEditableMode: true,
start() {
this._super(...arguments);
this._initAnimation();
return Promise.resolve();
},
_initAnimation() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.2 }
);
this.el.querySelectorAll('.animate-target').forEach(el => {
observer.observe(el);
});
},
destroy() {
this._super(...arguments);
},
});
publicWidget.registry.EcosireHero = EcosireHero;
export default EcosireHero;
5. Mejores prácticas de diseño responsivo
SCSS móvil primero
// _responsive.scss
// Mobile-first breakpoints (Bootstrap 5)
// xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px
.ecosire-header {
// Mobile default
padding: 0.5rem 0;
.navbar-brand img {
max-height: 35px;
}
.nav-link {
padding: 0.75rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
// Tablet and up
@include media-breakpoint-up(md) {
padding: 0.75rem 0;
.nav-link {
padding: 0.5rem 1rem;
border-bottom: none;
}
}
// Desktop
@include media-breakpoint-up(lg) {
padding: 1rem 0;
.navbar-brand img {
max-height: 50px;
}
}
}
// Touch-friendly elements
@media (hover: none) {
.btn {
min-height: 44px;
min-width: 44px;
}
.nav-link {
min-height: 44px;
display: flex;
align-items: center;
}
}
Soporte RTL
// _rtl.scss — Critical for Arabic, Hebrew, Urdu markets
html[dir="rtl"] {
.ecosire-header {
.navbar-brand {
margin-left: 1rem;
margin-right: 0;
}
.btn + .btn {
margin-right: 0.75rem;
margin-left: 0;
}
}
.ecosire-card__body {
text-align: right;
}
// Use logical properties where possible
.ms-3 { margin-inline-start: 1rem; }
.me-3 { margin-inline-end: 1rem; }
}
6. Optimización del rendimiento
Optimización de imagen
<!-- Use responsive images with srcset -->
<img t-att-src="'/web/image/ir.attachment/%s/datas/800x400' % image.id"
t-att-srcset="'/web/image/ir.attachment/%s/datas/400x200 400w, /web/image/ir.attachment/%s/datas/800x400 800w, /web/image/ir.attachment/%s/datas/1200x600 1200w' % (image.id, image.id, image.id)"
sizes="(max-width: 576px) 400px, (max-width: 992px) 800px, 1200px"
loading="lazy"
decoding="async"
alt="Descriptive alt text"/>
Integración CSS crítica
<!-- views/layout.xml -->
<template id="critical_css" inherit_id="website.layout">
<xpath expr="//head" position="inside">
<style type="text/css">
/* Inline critical above-the-fold CSS */
.ecosire-header { min-height: 72px; }
.s_ecosire_hero { min-height: 60vh; }
.navbar-brand img { max-height: 50px; }
</style>
</xpath>
</template>
Estrategia de carga de activos
# __manifest__.py
'assets': {
# Critical CSS — loaded in head
'web.assets_frontend': [
('prepend', 'theme_ecosire/static/src/scss/_variables.scss'),
'theme_ecosire/static/src/scss/primary.scss',
],
# Non-critical JS — deferred
'web.assets_frontend_lazy': [
'theme_ecosire/static/src/js/animations.js',
'theme_ecosire/static/src/js/parallax.js',
],
# Editor-only assets
'website.assets_wysiwyg': [
'theme_ecosire/static/src/js/snippets/*.js',
'theme_ecosire/static/src/scss/_editor.scss',
],
},
7. Integración del creador de sitios web
Bloques de contenido dinámico
<!-- A snippet that fetches products dynamically -->
<template id="s_featured_products" name="Featured Products Grid">
<section class="s_featured_products pt-5 pb-5">
<div class="container">
<h2 class="text-center mb-5">Featured Products</h2>
<div class="row" t-if="products">
<t t-foreach="products[:8]" t-as="product">
<div class="col-md-3 mb-4">
<div class="ecosire-card h-100">
<img t-att-src="product.image_url"
t-att-alt="product.name"
class="ecosire-card__image" loading="lazy"/>
<div class="ecosire-card__body">
<h5 t-out="product.name"/>
<p class="text-primary fw-bold"
t-out="product.list_price"
t-options='{"widget": "monetary"}'/>
<a t-att-href="product.website_url"
class="btn btn-outline-primary btn-sm">
View Details
</a>
</div>
</div>
</div>
</t>
</div>
</div>
</section>
</template>
Plantillas de página
<!-- Pre-built page templates that users can select -->
<template id="landing_page_template" name="Landing Page">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<t t-call="theme_ecosire.s_ecosire_hero"/>
<t t-call="theme_ecosire.s_features_grid"/>
<t t-call="theme_ecosire.s_testimonials"/>
<t t-call="theme_ecosire.s_cta_banner"/>
</div>
</t>
</template>
8. Probando tu tema
# tests/test_theme.py
from odoo.tests.common import HttpCase, tagged
@tagged('post_install', '-at_install')
class TestThemeEcosire(HttpCase):
def test_01_theme_installation(self):
"""Theme installs without errors."""
module = self.env['ir.module.module'].search([
('name', '=', 'theme_ecosire')
])
self.assertEqual(module.state, 'installed')
def test_02_homepage_loads(self):
"""Homepage renders with theme applied."""
response = self.url_open('/')
self.assertEqual(response.status_code, 200)
self.assertIn('ecosire-theme', response.text)
def test_03_snippets_available(self):
"""Custom snippets appear in the builder."""
self.start_tour('/web#action=website.action_website',
'theme_ecosire_snippet_tour', login='admin')
def test_04_responsive_meta(self):
"""Viewport meta tag is present."""
response = self.url_open('/')
self.assertIn('viewport', response.text)
9. Publicación en la tienda de aplicaciones Odoo
| Requisito | Detalles |
|---|---|
| Capturas de pantalla | 3-5 capturas de pantalla de alta calidad (1920x1080) |
| Icono | 128x128 PNG en static/description/ |
| Descripción | Descripción HTML en static/description/index.html |
| Licencia | LGPL-3 para temas (práctica estándar) |
| Versión | Serie Match Odoo (18.0.x.y.z) |
| Pruebas | Pasar las comprobaciones automáticas de linting de Odoo |
| Datos de demostración | Incluir datos de demostración para probar antes de comprar |
Preguntas frecuentes
¿Puedo usar Tailwind CSS en un tema de Odoo?
Aunque es técnicamente posible, el uso de Tailwind CSS junto con el Bootstrap 5 integrado de Odoo crea conflictos y aumenta significativamente el tamaño del paquete. El enfoque recomendado es utilizar las utilidades Bootstrap 5 y las propiedades personalizadas SCSS, que se integran de forma nativa con la canalización de activos y el creador de sitios web de Odoo. Si necesita clases de utilidades que Bootstrap no proporciona, cree utilidades SCSS personalizadas.
¿Cómo hago para que mi tema sea compatible con Odoo 17 y 18?
Cree ramas separadas para cada versión de Odoo. Odoo 17 usa Bootstrap 5.1 mientras que Odoo 18 usa Bootstrap 5.3 con soporte para variables CSS. El registro de activos también cambió: Odoo 18 prefiere la clave de activos manifiestos, mientras que Odoo 17 usa la herencia de plantillas XML. Mantenga parciales SCSS compartidos y archivos de integración específicos de la versión.
¿Por qué no funcionan las anulaciones de mis variables SCSS?
La causa más común es el orden de carga. Su _variables.scss debe cargarse antes de que se compile Bootstrap. Utilice la directiva 'anteponer' en su manifiesto: ('anteponer', 'nombre_tema/static/src/scss/_variables.scss'). También asegúrese de anular los nombres de variables correctos: Odoo envuelve algunas variables de Bootstrap con versiones con el prefijo o como $o-brand-primary.
¿Cómo funcionan los fragmentos personalizados con el creador de sitios web?
Los fragmentos personalizados son plantillas QWeb registradas en el panel de fragmentos a través de la herencia de plantillas de website.snippets. Cada fragmento necesita una plantilla para su estructura HTML, opciones opcionales definidas en website.snippet_options para el panel de personalización y JavaScript opcional que extiende publicWidget.Widget para interactividad. El creador de sitios web maneja automáticamente las funciones de arrastrar y soltar, editar en línea y deshacer/rehacer los fragmentos registrados.
¿Cuál es la mejor manera de agregar fuentes personalizadas a un tema de Odoo?
Coloque los archivos de fuentes WOFF2 en static/src/fonts/ y defina reglas @font-face en su SCSS. Utilice font-display: intercambie para mejorar el rendimiento. Luego anule la variable Bootstrap $font-family-sans-serif en su _variables.scss. Evite cargar fuentes desde CDN externas, ya que agrega una búsqueda de DNS y puede fallar en redes restringidas. Subconjunto de fuentes para incluir solo los rangos de caracteres necesarios.
¿Cómo puedo admitir el modo oscuro en mi tema de Odoo?
Odoo 18 introdujo soporte nativo para el modo oscuro a través de propiedades personalizadas de CSS. Anula las variables del modo oscuro en tu tema usando el selector [data-bs-theme="dark"]. Defina variantes claras y oscuras de sus colores, fondos y sombras personalizados. El interruptor del creador de sitios web maneja el cambio: su tema solo necesita proporcionar los valores de variable CSS correctos para ambos modos.
Próximos pasos
El desarrollo de temas es donde el diseño se encuentra con la ingeniería. Un tema de Odoo bien construido puede servir a cientos de clientes y generar importantes ingresos recurrentes en la App Store de Odoo.
Para guías relacionadas:
- Guía de desarrollo de Odoo Python — Fundamentos del desarrollo backend
- Desarrollo de módulo personalizado de Odoo — Descripción general de la arquitectura del módulo
- Tutorial de API REST de Odoo — Patrones de integración de API
¿Necesita un tema Odoo personalizado o un rediseño de sitio web? El equipo de personalización de Odoo de ECOSIRE diseña y crea temas de nivel empresarial con integración completa del creador de sitios web, diseño responsivo en todos los puntos de interrupción y soporte RTL para implementaciones internacionales. Hemos entregado temas personalizados para clientes en más de 40 países. Contáctenos para una consulta de diseño gratuita.
Escrito por
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
Artículos relacionados
Segmentación de clientes impulsada por IA: del RFM a la agrupación predictiva
Descubra cómo la IA transforma la segmentación de clientes desde el análisis RFM estático hasta la agrupación predictiva dinámica. Guía de implementación con Python, Odoo y datos reales de ROI.
IA para la optimización de la cadena de suministro: visibilidad, predicción y automatización
Transforme las operaciones de la cadena de suministro con IA: detección de demanda, calificación de riesgos de proveedores, optimización de rutas, automatización de almacenes y predicción de interrupciones. Guía 2026.
Estrategia de comercio electrónico B2B: cree un negocio mayorista en línea en 2026
Domine el comercio electrónico B2B con estrategias de precios mayoristas, gestión de cuentas, condiciones de crédito, catálogos perforados y configuración del portal Odoo B2B.