Business Intelligence sobre Odoo con Metabase: del dato bruto al dashboard ejecutivo

Cómo conectar Metabase al PostgreSQL de Odoo, modelar KPIs con dbt y construir dashboards ejecutivos de ventas, margen, cashflow y cohortes que dirección realmente usa.

Por qué el reporting nativo de Odoo no es Business Intelligence

Odoo tiene un módulo de informes razonablemente bueno para la gestión operativa del día a día: ventas del mes, facturas pendientes, movimientos de stock. Pero cuando la dirección necesita responder preguntas estratégicas —¿cuál es mi margen real por familia de producto? ¿qué cohorte de clientes tiene mejor LTV? ¿en qué semana del trimestre se concentra el 40% del cashflow?— el módulo de informes de Odoo se queda corto por diseño.

El problema no es Odoo. El problema es confundir un ERP con una plataforma analítica. Odoo está optimizado para registrar y procesar transacciones. Esa base de datos transaccional (OLTP) no está diseñada para consultas analíticas complejas que cruzan decenas de tablas, agregan millones de filas y deben responder en menos de dos segundos. Si lanzas una query analítica pesada directamente contra el PostgreSQL de producción de Odoo, estás compitiendo por recursos con los usuarios activos del ERP.

La solución es una arquitectura de datos en dos capas: Odoo como fuente de verdad transaccional y una réplica de solo lectura (o un data warehouse ligero) como base analítica, con Metabase como capa de visualización y Gobierno del dato. Este artículo describe exactamente cómo montar esa arquitectura.

Arquitectura general: réplica de solo lectura + Metabase + dbt

La arquitectura que recomendamos para PYMEs y medianas empresas tiene tres componentes:

  • Réplica PostgreSQL de solo lectura (streaming replication desde el primario de Odoo). Las consultas analíticas van aquí, nunca al primario.
  • dbt (data build tool) corriendo sobre esa réplica o sobre un esquema analítico separado. dbt transforma el esquema de Odoo —complejo, con decenas de tablas normalizadas— en modelos semánticos limpios y documentados que los analistas pueden usar sin conocer el modelo de datos interno de Odoo.
  • Metabase conectado a esos modelos dbt. Metabase es open source, se despliega en Docker en minutos, y su interfaz permite que cualquier persona de dirección construya sus propias preguntas sin SQL.
                  ┌─────────────────────────────────────┐
                  │   Odoo (producción)                 │
                  │   PostgreSQL primario (R/W)          │
                  └──────────────┬──────────────────────┘
                                 │ streaming replication
                  ┌──────────────▼──────────────────────┐
                  │   PostgreSQL réplica (solo lectura)  │
                  │   schema: public (tablas Odoo)       │
                  └──────────────┬──────────────────────┘
                                 │
                  ┌──────────────▼──────────────────────┐
                  │   dbt (transforma esquema Odoo)      │
                  │   schema: analytics                  │
                  │   modelos: dim_*, fct_*, mart_*      │
                  └──────────────┬──────────────────────┘
                                 │
                  ┌──────────────▼──────────────────────┐
                  │   Metabase (open source)             │
                  │   Dashboards, alertas, email reports │
                  └─────────────────────────────────────┘

Paso 1: Configurar la réplica de solo lectura de PostgreSQL

Si ya tienes Patroni o replicación streaming configurada (como se describe en el artículo sobre alta disponibilidad), simplemente usar el puerto de réplica (5433 vía HAProxy) para las conexiones de Metabase. Si partes de cero y quieres una réplica sencilla solo para analítica:

# En el servidor réplica — postgresql.conf
primary_conninfo = 'host=10.0.1.11 port=5432 user=replicator password=<PASSWORD> application_name=analytics_replica'
hot_standby = on
hot_standby_feedback = on   # evita que el primario limpie filas que la réplica todavía necesita
max_standby_streaming_delay = 30s

# Crear el fichero de señal de standby
touch /var/lib/postgresql/16/main/standby.signal

# En el primario — pg_hba.conf, añadir:
host replication replicator 10.0.1.20/32 scram-sha-256

Una vez activa la réplica, crear un usuario de solo lectura para Metabase con acceso únicamente al esquema analítico:

-- En la réplica, conectado como superusuario:
CREATE ROLE metabase_ro WITH LOGIN PASSWORD '<PASSWORD_SEGURO>';
GRANT CONNECT ON DATABASE odoo_prod TO metabase_ro;
GRANT USAGE ON SCHEMA analytics TO metabase_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO metabase_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT SELECT ON TABLES TO metabase_ro;
-- NO dar acceso al schema public (tablas brutas de Odoo) desde Metabase

Paso 2: Instalar Metabase con Docker Compose

Metabase se despliega en minutos. La configuración mínima de producción usa su propia base de datos (PostgreSQL preferiblemente, no H2) para almacenar dashboards, usuarios y preguntas guardadas:

# docker-compose.metabase.yml
version: '3.8'
services:
  metabase:
    image: metabase/metabase:v0.50.0
    container_name: metabase
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      MB_DB_TYPE: postgres
      MB_DB_HOST: metabase-db
      MB_DB_PORT: 5432
      MB_DB_DBNAME: metabase
      MB_DB_USER: metabase
      MB_DB_PASS: <METABASE_DB_PASSWORD>
      MB_SITE_URL: https://bi.tuempresa.com
      MB_EMAIL_SMTP_HOST: smtp.tuempresa.com
      MB_EMAIL_SMTP_PORT: 587
      MB_EMAIL_SMTP_USERNAME: alertas@tuempresa.com
      MB_EMAIL_SMTP_PASSWORD: <SMTP_PASSWORD>
      MB_EMAIL_SMTP_SECURITY: starttls
      JAVA_TIMEZONE: Europe/Madrid
    depends_on:
      - metabase-db

  metabase-db:
    image: postgres:16-alpine
    container_name: metabase-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: metabase
      POSTGRES_USER: metabase
      POSTGRES_PASSWORD: <METABASE_DB_PASSWORD>
    volumes:
      - metabase_db_data:/var/lib/postgresql/data

volumes:
  metabase_db_data:

Poner Nginx delante de Metabase con SSL (Certbot) antes de exponerlo a Internet. Metabase en puerto 3000 nunca debe ser accesible directamente desde fuera.

Paso 3: Modelar el esquema Odoo con dbt

El esquema de PostgreSQL de Odoo es complejo: más de 400 tablas con nombres crípticos (account_move, stock_quant, sale_order_line). dbt permite crear vistas y tablas materializadas limpias sobre ese esquema, con documentación, tests de calidad de datos y linaje automático.

Estructura de proyecto dbt recomendada para Odoo

odoo_analytics/
├── models/
│   ├── staging/          # tablas brutas de Odoo, renombradas y tipadas
│   │   ├── stg_sale_orders.sql
│   │   ├── stg_invoices.sql
│   │   ├── stg_products.sql
│   │   └── stg_partners.sql
│   ├── intermediate/     # joins y cálculos intermedios
│   │   ├── int_order_lines_enriched.sql
│   │   └── int_invoice_lines_enriched.sql
│   └── marts/            # modelos finales para Metabase
│       ├── mart_sales.sql
│       ├── mart_margin.sql
│       ├── mart_cashflow.sql
│       └── mart_customer_cohorts.sql
├── tests/
├── macros/
└── dbt_project.yml

Modelo de staging: pedidos de venta

-- models/staging/stg_sale_orders.sql
-- {{ config(materialized='view') }}
SELECT
    so.id                          AS order_id,
    so.name                        AS order_ref,
    so.date_order::date            AS order_date,
    so.state                       AS order_state,
    rp.name                        AS customer_name,
    rp.id                          AS customer_id,
    rc.name                        AS customer_country,
    so.amount_untaxed               AS amount_net,
    so.amount_tax                   AS amount_tax,
    so.amount_total                 AS amount_total,
    so.currency_id,
    su.name                        AS salesperson,
    so.team_id,
    so.company_id
FROM public.sale_order so
JOIN public.res_partner rp ON rp.id = so.partner_id
LEFT JOIN public.res_country rc ON rc.id = rp.country_id
LEFT JOIN public.res_users su ON su.id = so.user_id
WHERE so.state IN ('sale', 'done')

Modelo de mart: margen por producto

-- models/marts/mart_margin.sql
-- {{ config(materialized='table') }}
WITH order_lines AS (
    SELECT
        sol.order_id,
        so.order_date,
        so.customer_id,
        so.customer_name,
        sol.product_id,
        pt.name                  AS product_name,
        pc.name                  AS product_category,
        sol.product_uom_qty      AS qty_sold,
        sol.price_unit           AS sale_price,
        sol.price_subtotal       AS revenue,
        COALESCE(pp.standard_price, 0) AS cost_price,
        (sol.price_subtotal - sol.product_uom_qty * COALESCE(pp.standard_price, 0)) AS gross_margin
    FROM public.sale_order_line sol
    JOIN public.sale_order so ON so.id = sol.order_id
    JOIN public.product_product pp ON pp.id = sol.product_id
    JOIN public.product_template pt ON pt.id = pp.product_tmpl_id
    LEFT JOIN public.product_category pc ON pc.id = pt.categ_id
    WHERE so.state IN ('sale', 'done')
        AND sol.product_id IS NOT NULL
)
SELECT
    order_date,
    DATE_TRUNC('month', order_date)  AS month,
    DATE_TRUNC('quarter', order_date) AS quarter,
    product_id,
    product_name,
    product_category,
    customer_id,
    customer_name,
    SUM(qty_sold)                    AS total_qty,
    SUM(revenue)                     AS total_revenue,
    SUM(cost_price * qty_sold)       AS total_cost,
    SUM(gross_margin)                AS total_margin,
    CASE WHEN SUM(revenue) > 0
         THEN ROUND(SUM(gross_margin) / SUM(revenue) * 100, 2)
         ELSE 0
    END                              AS margin_pct
FROM order_lines
GROUP BY 1, 2, 3, 4, 5, 6, 7, 8

Ejecutar dbt con un cron diario (o cada hora para datos más frescos):

# crontab -e
0 6 * * * cd /opt/odoo_analytics && dbt run --profiles-dir . --target prod >> /var/log/dbt/run.log 2>&1
30 6 * * * cd /opt/odoo_analytics && dbt test --profiles-dir . --target prod >> /var/log/dbt/test.log 2>&1

Paso 4: KPIs ejecutivos que realmente importan

Una vez los modelos dbt están disponibles en Metabase, los dashboards ejecutivos que más valor aportan en el contexto de una PYME con Odoo son:

Dashboard 1: Ventas y pipeline

  • Ventas del mes actual vs. mismo mes del año anterior (variación %)
  • Ventas acumuladas del año vs. objetivo
  • Top 10 clientes por facturación en los últimos 90 días
  • Pipeline de presupuestos abiertos (estado draft/sent en sale_order) con probabilidad ponderada
  • Tasa de conversión presupuesto → pedido confirmado por comercial

Dashboard 2: Margen y rentabilidad

  • Margen bruto por familia de producto (%) — mapa de calor mensual
  • Evolución del margen medio de los últimos 12 meses
  • Productos con margen negativo o por debajo del umbral (alerta automática)
  • Contribución de cada categoría al margen total (stacked bar)

Dashboard 3: Cashflow operativo

-- Cashflow semanal: cobros esperados vs. pagos previstos
SELECT
    DATE_TRUNC('week', am.invoice_date_due) AS week,
    am.move_type,
    SUM(am.amount_residual_signed)          AS pending_amount
FROM public.account_move am
WHERE am.state = 'posted'
  AND am.payment_state IN ('not_paid', 'partial')
  AND am.invoice_date_due BETWEEN CURRENT_DATE AND CURRENT_DATE + 90
GROUP BY 1, 2
ORDER BY 1, 2

Dashboard 4: Cohortes de clientes

El análisis de cohortes es quizás el KPI más infrautilizado en PYMEs con Odoo. Permite responder: ¿los clientes que captamos en Q1 2025 siguen comprando a los 6 meses al mismo ritmo que los de Q1 2024?

-- Cohorte de retención mensual
WITH first_order AS (
    SELECT
        partner_id,
        MIN(DATE_TRUNC('month', date_order)) AS cohort_month
    FROM public.sale_order
    WHERE state IN ('sale', 'done')
    GROUP BY 1
),
orders AS (
    SELECT
        so.partner_id,
        DATE_TRUNC('month', so.date_order) AS order_month
    FROM public.sale_order so
    WHERE so.state IN ('sale', 'done')
    GROUP BY 1, 2
)
SELECT
    fo.cohort_month,
    o.order_month,
    EXTRACT(MONTH FROM AGE(o.order_month, fo.cohort_month)) AS months_since_first,
    COUNT(DISTINCT o.partner_id) AS active_customers
FROM first_order fo
JOIN orders o ON o.partner_id = fo.partner_id
    AND o.order_month >= fo.cohort_month
GROUP BY 1, 2, 3
ORDER BY 1, 3

Paso 5: Reportes automáticos por email y alertas

Metabase incluye un sistema de pulses y reportes programados que permite enviar dashboards por email automáticamente. La configuración recomendada para dirección:

  • Lunes 08:00: resumen semanal de ventas, margen y cobros pendientes de la semana
  • Día 1 de cada mes 07:00: cierre del mes anterior con comparativa vs. mes anterior y mismo mes del año pasado
  • Alerta en tiempo real: si el margen de cualquier línea de pedido cae por debajo del umbral definido (configurable por familia de producto)
  • Alerta de cashflow: si el saldo de cobros esperados en los próximos 30 días cae por debajo de X euros

Las alertas de Metabase se configuran desde la interfaz (sección Alertas en cualquier pregunta guardada) y se envían por email o webhook. Para alertas en Telegram o Slack, un webhook de Metabase apuntando a un bot de Telegram es la solución más directa.

Gobierno del dato: el elemento que más proyectos ignoran

Una plataforma de BI sin gobierno del dato se convierte en un caos de dashboards contradictorios en tres meses. Las preguntas que matan la confianza en el dato son: ¿por qué este dashboard dice que vendimos 450k y el de comercial dice 430k? La respuesta siempre es: definiciones inconsistentes. Para evitarlo:

  • Definir las métricas en dbt, no en Metabase: la definición de “venta confirmada” (qué estados de sale_order, si se incluyen devoluciones, si se cuenta en fecha de pedido o de factura) debe estar en el modelo dbt, no en cada dashboard por separado.
  • Tests de dbt para calidad de datos: verificar que no hay order_id nulos, que los importes son siempre positivos, que cada factura apunta a un cliente existente. Si el test falla, el modelo no se materializa y Metabase muestra los datos del día anterior.
  • Documentación en dbt: cada modelo y cada columna tiene una descripción en schema.yml. Metabase puede importar esa documentación y mostrarla al usuario al hacer hover sobre una columna.
  • Control de acceso en Metabase: dirección ve todos los dashboards; comerciales ven solo sus propias métricas; logística no ve márgenes. Los grupos y permisos de Metabase mapean exactamente sobre los roles de negocio.

Costes y alternativas

ComponenteOpción open sourceAlternativa comercialCoste aprox.
VisualizaciónMetabase OSSMetabase Pro / Power BI0€ / desde 500€/mes
Transformacióndbt Coredbt Cloud0€ / desde 100€/mes
Réplica BBDDPostgreSQL streamingAWS RDS read replica0€ (self-hosted) / desde 80€/mes
Orquestacióncron + dbtAirflow / Dagster0€ / coste de infraestructura

Para la mayoría de PYMEs españolas con Odoo, el stack réplica PG + dbt Core + Metabase OSS es suficiente y el coste de infraestructura adicional es de 30-80€/mes (un VPS pequeño para Metabase y dbt). La inversión real es el tiempo de modelado inicial: normalmente entre 15 y 40 horas de consultoría técnica dependiendo de la complejidad del negocio.

Errores frecuentes en proyectos BI sobre Odoo

  • Conectar Metabase directamente al PostgreSQL de producción: las queries analíticas compiten con Odoo por CPU y conexiones. Siempre usar réplica o datos exportados.
  • Construir dashboards antes de modelar el dato: el resultado son dashboards que nadie entiende ni mantiene. Primero el modelo dbt, luego el dashboard.
  • Ignorar los datos de coste (standard_price): sin costes, los dashboards de margen son una ilusión. Verificar que el equipo de compras mantiene los costes actualizados en Odoo.
  • No programar los reportes automáticos: si el dashboard no llega al email de dirección, dirección no lo usa. La automatización es lo que convierte el proyecto en un hábito.

¿Quieres montar Business Intelligence real sobre tu Odoo?

Solicitar auditoría técnica gratuita

Cómo hacer una auditoría técnica de tu instancia Odoo en producción
Checklist completo para auditar rendimiento, seguridad, backups, módulos custom, deuda técnica e infraestructura de una instancia Odoo en producción. Con métricas reales y entregable.