Odoo · Vistas · Kanban
La kanban es la vista que más ha cambiado en las últimas versiones de Odoo, y la que más código antiguo arrastra: plantillas kanban-box, clases oe_kanban_global_click, menús desplegables montados a mano. En Odoo 19 la estructura es más simple, pero tiene dos trampas con algo en común: el servidor acepta la vista sin quejarse y el fallo solo aparece al renderizarla en el navegador.
Estructura
Una kanban mínima en Odoo 19
La tarjeta se define en una plantilla llamada card; el menú de los tres puntos, en otra llamada menu. Dentro de la tarjeta puedes usar <aside> para una imagen lateral, <main> para el contenido y <footer> para la fila inferior. Abrir el registro al hacer clic ya es el comportamiento por defecto.
<record id="fleet_trip_view_kanban" model="ir.ui.view">
<field name="name">fleet.trip.view.kanban</field>
<field name="model">fleet.trip</field>
<field name="arch" type="xml">
<kanban default_group_by="state">
<field name="is_late"/> <!-- solo se usa en t-if: hay que declararlo -->
<templates>
<t t-name="menu">
<a role="menuitem" type="object" name="action_close"
class="dropdown-item">Close</a>
</t>
<t t-name="card" class="flex-row">
<aside>
<field name="vehicle_id" widget="image"
options="{'preview_image': 'image_128'}"/>
</aside>
<main class="ms-2">
<field name="name" class="fw-bold"/>
<span t-if="record.is_late.raw_value"
class="badge text-bg-danger">Late</span>
<footer>
<field name="date_start"/>
<field name="user_id" widget="many2one_avatar_user"
class="ms-auto"/>
</footer>
</main>
</t>
</templates>
</kanban>
</field>
</record>
Trampa 1
kanban-box ya no existe
Hasta Odoo 16 la tarjeta se llamaba kanban-box. En Odoo 19 ese nombre no se reconoce, pero nada lo valida en el servidor: la vista se guarda, get_view() la devuelve y los tests que comprueban el arch pasan. Al abrir el menú, el cliente lanza OwlError: Missing 'card' template y el usuario ve la pantalla de error.
t-name="kanban-box" en todo el repositorio. Es un cambio de una línea, pero cada vista que se te escape es un menú roto en producción.
Trampa 2
record.campo sin <field>
El cliente solo carga los campos que aparecen como <field> en el arch. Si usas record.is_late.raw_value en un t-if pero is_late no aparece como <field> ni dentro de la tarjeta ni como hijo directo de <kanban>, el render del primer registro falla con Cannot read properties of undefined (reading 'raw_value'). Como con la trampa anterior, el servidor no se entera. Un test que parsea el arch y cruza ambas listas lo detecta antes del despliegue:
import re
from lxml import etree
def test_kanban_record_refs_are_declared(self):
arch = self.env["fleet.trip"].get_view(view_type="kanban")["arch"]
root = etree.fromstring(arch)
declared = {f.get("name") for f in root.iter("field")}
used = set(re.findall(r"record\.(\w+)\.", arch))
self.assertFalse(used - declared, "record.X sin <field> en la kanban")
Arquitectura
Una kanban no es un dashboard
Es tentador crear una kanban «especial» con contadores y badges para usarla como panel en una acción concreta. El problema: una vista kanban registrada para un modelo se ofrece en el selector de vistas de todas las acciones de ese modelo que incluyan el modo kanban, no solo en la que pensabas. Si el panel necesita datos agregados o un diseño propio, lo correcto es una acción de cliente con un componente OWL (ver OWL en Odoo 19) que pida los datos a un método del modelo.
| Antes (≤ 16) | Odoo 19 |
|---|---|
<t t-name="kanban-box"> | <t t-name="card"> |
Dropdown montado a mano con o_dropdown_kanban | <t t-name="menu"> con dropdown-item |
oe_kanban_global_click | Por defecto; ya no hace falta |
Divs con clases o_kanban_record_* | <aside>, <main>, <footer> |
kanban_image() en el template | <field widget="image"> |
Resumen
En Odoo 19 la tarjeta es card y el menú es menu; todo campo leído con record.X debe aparecer como <field>; y un panel con datos agregados es una acción de cliente, no una kanban. Ninguno de los dos errores sale en los tests de servidor: abre la vista en el navegador al menos una vez antes de dar el trabajo por terminado.