Curso odoo (Módulos)

  • dominios
  • campos calculados
  • valores por defecto
  • función onchange
  • restricciones

📌 ¿Qué son los dominios (domains) en Odoo?

En Odoo, los dominios (domains) son filtros que permiten seleccionar un subconjunto de registros de un modelo basándose en ciertas condiciones.

Un dominio se define como una lista de tripletas donde cada condición tiene:

  1. Nombre del campo (ejemplo: "state")
  2. Operador de comparación (ejemplo: "=", ">", "in")
  3. Valor con el que se compara (ejemplo: "draft", 1000)

🚀 ¿Por qué son útiles los dominios en Odoo?

Permiten filtrar registros en campos Many2one, One2many y Many2many.
Optimizan la experiencia del usuario, mostrando solo datos relevantes.
Se pueden usar en modelos, vistas XML y acciones de búsqueda.

Actualiza el fichero models/ViajeModel.py

1....
2
3    # En la relación many2one me permite elegir un partner/cliente o crear uno directamente
4    # Tener en cuenta que solo aparecerán los partners/clientes que sean de tipo conductor
5    conductor_id = fields.Many2one('res.partner', string="Conductor", domain=[('conductor', '=', True)])
6
7    # En la relación many2one me permite elegir un vehiculo o crear uno directamente
8    vehiculo_id = fields.Many2one('viajes.vehiculo',
9        ondelete='cascade', string="Vehiculo", required=True)
10    
11    # puedo elegir uno(partner/cliente) que ya existe o crear uno nuevo
12    pasajeros_ids = fields.Many2many('res.partner', string="Pasajeros")

🔄 Reiniciar el módulo en Odoo y actualizar aplicación

Reinicia el servidor odoo y actualiza la aplicación de viajes para aplicar los cambios.

📌 Campos Calculados en Odoo (Computed Fields)

En Odoo, los campos calculados son aquellos cuyo valor no se almacena directamente en la base de datos, sino que se calcula dinámicamente mediante un método.


🔹 ¿Cómo se define un campo calculado en Odoo?

Para crear un campo calculado, se deben seguir estos pasos:

  1. Definir el campo con compute="nombre_del_método".
  2. Crear el método que calcula el valor.
  3. Asignar el valor del campo a self dentro del método.

Actualiza el fichero models/ViajeModel.py

1....
2
3    # En la relación many2one me permite elegir un partner/cliente o crear uno directamente
4    # Tener en cuenta que solo aparecerán los partners/clientes que sean de tipo conductor
5    conductor_id = fields.Many2one('res.partner', string="Conductor", domain=[('conductor', '=', True)])
6
7    # En la relación many2one me permite elegir un vehiculo o crear uno directamente
8    vehiculo_id = fields.Many2one('viajes.vehiculo',
9        ondelete='cascade', string="Vehiculo", required=True)
10    
11    # puedo elegir uno(partner/cliente) que ya existe o crear uno nuevo
12    pasajeros_ids = fields.Many2many('res.partner', string="Pasajeros")
13
14    plazas_ocupadas = fields.Float(string="Plazas ocupadas", compute='_get_plazas_ocupadas')
15
16    @api.depends('plazas', 'pasajeros_ids')
17    def _get_plazas_ocupadas(self):
18        for r in self:
19            if not r.plazas:
20                r.plazas_ocupadas = 0.0
21            else:
22                r.plazas_ocupadas = 100.0 * len(r.pasajeros_ids) / r.plazas

Actualiza la vista de formulario del fichero views/viaje.xml

1.....
2<record model="ir.ui.view" id="viaje_form_view">
3    <field name="name">viaje.form</field>
4    <field name="model">viajes.viaje</field>
5    <field name="arch" type="xml">
6        <form string="Formulario de Viajes">
7            <sheet>
8                <group>
9                    <group string="General">
10                        <field name="vehiculo_id"/>
11                        <field name="titulo"/>
12                        <field name="conductor_id"/>
13                        <field name="estado" />
14                    </group>
15                    <group string="Calendario">
16                        <field name="fecha_inicio"/>
17                        <field name="duracion"/>
18                        <field name="plazas"/>
19                        <field name="plazas_ocupadas" widget="progressbar"/>
20                    </group>
21                </group>
22                <label for="pasajeros_ids"/>
23                <field name="pasajeros_ids"/>
24            </sheet>
25        </form>
26    </field>
27</record>
28.....

Actualiza vista de lista del fichero views/viaje.xml

1.....
2<record model="ir.ui.view" id="viaje_list_view">
3    <field name="name">viaje.list</field>
4    <field name="model">viajes.viaje</field>
5    <field name="arch" type="xml">
6        <list string="Viaje List">
7            <field name="titulo" />
8            <field name="duracion" />
9            <field name="vehiculo_id"/>
10            <field name="estado" />
11            <field name="plazas_ocupadas" widget="progressbar"/>
12        </list>
13    </field>
14</record>
15.....

🔄 Reiniciar el módulo en Odoo y actualizar aplicación

Reinicia el servidor odoo y actualiza la aplicación de viajes para aplicar los cambios.


Añadiendo mas campos calculados

Actualiza el fichero models/ViajeModel.py

1....
2
3    plazas_ocupadas = fields.Float(string="Plazas ocupadas", compute='_get_plazas_ocupadas')
4
5    @api.depends('plazas', 'pasajeros_ids')
6    def _get_plazas_ocupadas(self):
7        for r in self:
8            if not r.plazas:
9                r.plazas_ocupadas = 0.0
10            else:
11                r.plazas_ocupadas = 100.0 * len(r.pasajeros_ids) / r.plazas
12    
13    fecha_fin = fields.Date(compute="_compute_fecha_fin")
14
15    @api.depends('fecha_inicio', 'duracion')
16    def _compute_fecha_fin(self):
17        for record in self:
18            if record.fecha_inicio and record.duracion:
19                record.fecha_fin = record.fecha_inicio + timedelta(days=record.duracion)

Actualiza la vista de formulario del fichero views/viaje.xml

1.....
2<record model="ir.ui.view" id="viaje_form_view">
3    <field name="name">viaje.form</field>
4    <field name="model">viajes.viaje</field>
5    <field name="arch" type="xml">
6        <form string="Formulario de Viajes">
7            <sheet>
8                <group>
9                    <group string="General">
10                        <field name="vehiculo_id"/>
11                        <field name="titulo"/>
12                        <field name="conductor_id"/>
13                        <field name="estado" />
14                    </group>
15                    <group string="Calendario">
16                        <field name="fecha_inicio"/>
17                        <field name="fecha_fin"/>
18                        <field name="duracion"/>
19                        <field name="plazas"/>
20                        <field name="plazas_ocupadas" widget="progressbar"/>
21                    </group>
22                </group>
23                <label for="pasajeros_ids"/>
24                <field name="pasajeros_ids"/>
25            </sheet>
26        </form>
27    </field>
28</record>
29.....

🔹 Almacenar un Campo Calculado en la Base de Datos (store=True)

Si queremos guardar el valor en la base de datos, podemos añadir store=True:

1fecha_fin = fields.Date(compute="_compute_fecha_fin", store=True)

📌 Beneficio:

  • Reduce el tiempo de carga, ya que el valor se calcula una vez y se almacena.
  • Se puede usar en búsquedas y filtros en la interfaz de Odoo.

🔹 Campos Calculados con Formato de Texto

Actualiza el fichero models/ViajeModel.py

1....
2    plazas_ocupadas = fields.Float(string="Plazas ocupadas", compute='_get_plazas_ocupadas')
3
4    @api.depends('plazas', 'pasajeros_ids')
5    def _get_plazas_ocupadas(self):
6        for r in self:
7            if not r.plazas:
8                r.plazas_ocupadas = 0.0
9            else:
10                r.plazas_ocupadas = 100.0 * len(r.pasajeros_ids) / r.plazas
11
12    
13    fecha_fin = fields.Date(compute="_compute_fecha_fin")
14
15    @api.depends('fecha_inicio', 'duracion')
16    def _compute_fecha_fin(self):
17        for record in self:
18            if record.fecha_inicio and record.duracion:
19                record.fecha_fin = record.fecha_inicio + timedelta(days=record.duracion)
20
21
22    estado_display = fields.Char(compute="_compute_estado_display")
23
24    @api.depends('estado')
25    def _compute_estado_display(self):
26        estados_dict = {'planeado': '⏳ Planeado', 'en_curso': '🚀 En Curso', 'finalizado': '✅ Finalizado'}
27        for record in self:
28            record.estado_display = estados_dict.get(record.estado, 'Desconocido')

Actualiza la vista de formulario del fichero views/viaje.xml

1.....
2<record model="ir.ui.view" id="viaje_form_view">
3    <field name="name">viaje.form</field>
4    <field name="model">viajes.viaje</field>
5    <field name="arch" type="xml">
6        <form string="Formulario de Viajes">
7            <sheet>
8                <group>
9                    <group string="General">
10                        <field name="vehiculo_id"/>
11                        <field name="titulo"/>
12                        <field name="conductor_id"/>
13                        <field name="estado" />
14                        <field name="estado_display" />
15                    </group>
16                    <group string="Calendario">
17                        <field name="fecha_inicio"/>
18                        <field name="fecha_fin"/>
19                        <field name="duracion"/>
20                        <field name="plazas"/>
21                        <field name="plazas_ocupadas" widget="progressbar"/>
22                    </group>
23                </group>
24                <label for="pasajeros_ids"/>
25                <field name="pasajeros_ids"/>
26            </sheet>
27        </form>
28    </field>
29</record>
30.....

🚀 ¿Por qué son útiles los campos calculados en Odoo?

Permiten obtener valores dinámicos sin necesidad de almacenarlos.
Facilitan cálculos automáticos en tiempo real.
Pueden ser almacenados en la base de datos (store=True) si es necesario.
Se pueden usar en vistas, informes y filtros en Odoo.


🔄 Reiniciar el módulo en Odoo y actualizar aplicación

Reinicia el servidor odoo y actualiza la aplicación de viajes para aplicar los cambios.


📌 Valores por defecto en Odoo

Cualquier campo puede tener un valor por defecto. En la definición de campo, se agrega la opción default=X donde X es un valor literal Python (boolean, integer, float, string), o una función que toma un conjunto de registros y devuelve un valor:

Actualiza el fichero models/ViajeModel.py

1from odoo import models, fields, api
2from datetime import timedelta
3
4
5class Viaje(models.Model):
6    _name = 'viajes.viaje'
7    _description = "Viajes"
8
9    titulo = fields.Char(required=True)
10    fecha_inicio = fields.Date(default=fields.Date.today)
11    duracion = fields.Float(digits=(6, 2), help="Duracion en días")
12    plazas = fields.Integer(string="Numero de plazas disponibles")
13    finalizado = fields.Boolean(default=False)
14.....

Actualiza la vista de formulario del fichero views/viaje.xml

1.....
2<odoo>
3    <data>
4        <record model="ir.ui.view" id="viaje_form_view">
5            <field name="name">viaje.form</field>
6            <field name="model">viajes.viaje</field>
7            <field name="arch" type="xml">
8                <form string="Formulario de Viajes">
9                    <sheet>
10                        <group>
11                            <group string="General">
12                                <field name="vehiculo_id"/>
13                                <field name="titulo"/>
14                                <field name="conductor_id"/>
15                                <field name="estado" />
16                                <field name="estado_display" />
17                                <field name="finalizado"/>
18                            </group>
19                            <group string="Calendario">
20.....

📌 Función Onchange

El mecanismo "onchange" proporciona una forma para que la interfaz del cliente actualice los datos de un formulario cuando el usuario ha llenado un valor en un campo, sin guardar nada en la base de datos.

Por ejemplo, supóngase que un modelo tiene tres campos amount, unit_price y price, y se desea actualizar el precio en el formulario cuando cualquiera de los otros campos es modificado. Para ello, se define un método donde self representa el registro en la vista formulario y se decora con onchange() para especificar en qué campo se activará. Cualquier cambio que se realice en self será reflejado en el formulario.

En nuestro ejercicio vamos a añadir una función onchange para advertir sobre valores no válidos, como un número negativo de plazas o más pasajeros que plazas:

Actualiza el fichero models/ViajeModel.py

1.....
2    # Añade onchange explícito para advertir sobre valores no válidos, como un número negativo de plazas o más pasajeros que plazas
3    @api.onchange('plazas', 'pasajeros_ids')
4    def _checkearPlazasValidas(self):
5        if self.plazas < 0:
6            self.plazas = 0
7            return {
8                'warning': {
9                    'title': "Valor de 'plazas' incorrecto",
10                    'message': "El numero de plazas disponibles no puede ser negativo",
11                },
12            }
13        if self.plazas < len(self.pasajeros_ids):
14            return {
15                'warning': {
16                    'title': "Demasiados pasajeros",
17                    'message': "Incrementa el nº de plazas o elimina pasajeros",
18                },
19            }

📌 Restricciones / constraints

Odoo ofrece dos formas de configurar automáticamente invariantes: Python constraints y SQL constraints.

📌 Constraints Python

Una restricción de Python se define como un método decorado con constrains() y es invocado en un recordset. El decorador especifica los campos que están involucrados en la restricción, por lo que la restricción es automáticamente evaluada cuando uno de ellos es modificado. El método levantará una excepción si no se satisface su invariante

Actualiza el fichero models/ViajeModel.py

1from odoo.exceptions import ValidationError
2.....
3    @api.constrains('conductor_id', 'pasajeros_ids')
4    def _comprobarConductorNoEsPasajero(self):
5        for r in self:
6            if r.conductor_id and r.conductor_id in r.pasajeros_ids:
7                raise ValidationError("Un conductor no puede ser también pasajero")

Este código implementa una restricción (constraint) en Odoo para evitar que el conductor de un viaje también sea pasajero en el mismo viaje.

🔍 Desglose del código

🔹 1. @api.constrains('conductor_id', 'pasajeros_ids')

📌 ¿Qué hace?

  • Define una restricción en la base de datos para evitar inconsistencias.
  • Se activa automáticamente cuando cambian los campos conductor_id o pasajeros_ids.
  • 📌 Si la condición no se cumple, Odoo muestra un error y evita guardar el registro.

🔹 2. for r in self:

📌 ¿Qué hace?

  • Recorre cada registro (r) en self (puede haber varios si se crean o editan en lote).
  • Permite verificar que cada viaje cumple con la restricción.

🔹 3. if r.conductor_id and r.conductor_id in r.pasajeros_ids:

📌 ¿Qué hace?

  • Verifica si el conductor (conductor_id) también está en la lista de pasajeros (pasajeros_ids).
  • Si es así, se genera un error para evitar guardar el registro.

🔹 4. raise ValidationError("Un conductor no puede ser también pasajero")

📌 ¿Qué hace?

  • Si la condición se cumple, se genera un error de validación (ValidationError).
  • El usuario verá un mensaje de error en Odoo y no podrá guardar el registro.

🔹 Ejemplo del mensaje de error en la interfaz de Odoo:
"Un conductor no puede ser también pasajero"


🚀 ¿Por qué es útil este código?

Evita errores de datos en la base de datos.
Mejora la integridad del sistema al aplicar reglas de negocio.
Evita inconsistencias al asignar roles dentro del viaje.
Proporciona un mensaje claro al usuario cuando intenta guardar datos incorrectos.

Este código asegura que los datos sean correctos antes de ser guardados en la base de datos, mejorando la confiabilidad del módulo en Odoo. 🚗🛑📋


📌 Restricciones SQL

Los _sql_constraints en Odoo son restricciones a nivel de base de datos que permiten garantizar la integridad de los datos. Se pueden usar para validar condiciones (CHECK) o evitar duplicados (UNIQUE).

Actualiza el fichero models/VehiculoModel.py

1.....
2    # En la relacion one2many tengo que crear un coche nuevo al asociarlo
3    viaje_ids = fields.One2many(
4        'viajes.viaje', 'vehiculo_id', string="Viajes del coche")
5    
6    matricula = fields.Char()
7
8    _sql_constraints = [
9        ('matricula_descripcion_check',
10         'CHECK(matricula != descripcion)',
11         "La matricula del coche no puede ser igual que la descripción"),
12
13        ('nmatricula_unique',
14         'UNIQUE(matricula)',
15         "La matricula tiene que ser única"),
16    ]

Actualiza la vista de formulario del fichero views/vehiculo.xml

1.....
2<record model="ir.ui.view" id="vehiculo_form_view">
3    <field name="name">vehiculo.form</field>
4    <field name="model">viajes.vehiculo</field>
5    <field name="arch" type="xml">
6        <form string="Formulario de Vehiculo">
7            <sheet>
8                <group>
9                    <field name="modelo" />
10                    <field name="marca" />
11                    <field name="propietario_id"/>
12                        <field name="matricula"/>
13                </group>
14.....

Actualiza la vista de lista del fichero views/vehiculo.xml

1.....
2<record model="ir.ui.view" id="vehiculo_list_view">
3    <field name="name">vehiculo.list</field>
4    <field name="model">viajes.vehiculo</field>
5    <field name="arch" type="xml">
6        <list string="Vehiculo List">
7            <field name="modelo" />
8            <field name="marca" />
9            <field name="propietario_id"/>
10                <field name="matricula"/>
11        </list>
12    </field>
13</record>
14.....

🚀 ¿Por qué son útiles los _sql_constraints en Odoo?Mejoran la calidad de los datos al evitar errores y duplicados.
Son más eficientes que las validaciones en Python porque se ejecutan directamente en la base de datos.
Funcionan incluso cuando los datos se ingresan desde la API o importaciones masivas.
Evitan datos inconsistentes sin necesidad de escribir código extra en @api.constrains.

Estos _sql_constraints aseguran que los coches tengan matrículas únicas y descripciones distintas, garantizando la integridad del módulo de vehículos en Odoo. 🚗✅📋