Per què el reporting natiu d'Odoo no és Business Intelligence
Odoo té un mòdul d'informes raonablement bo per a la gestió operativa del dia a dia: vendes del mes, factures pendents, moviments d'estoc. Però quan la direcció necessita respondre preguntes estratègiques —quin és el meu marge real per família de producte? quina cohort de clients té millor LTV? en quina setmana del trimestre es concentra el 40% del cashflow?— el mòdul d'informes d'Odoo es queda curt per disseny.
El problema no és Odoo. El problema és confondre un ERP amb una plataforma analítica. Odoo està optimitzat per registrar i processar transaccions. Aquesta base de dades transaccional (OLTP) no està dissenyada per a consultes analítiques complexes que creuen desenes de taules, agreguen milions de files i han de respondre en menys de dos segons. Si llances una consulta analítica pesada directament contra el PostgreSQL de producció d'Odoo, estàs competint per recursos amb els usuaris actius de l'ERP.
La solució és una arquitectura de dades en dues capes: Odoo com a font de veritat transaccional i una rèplica de només lectura (o un data warehouse lleuger) com a base analítica, amb Metabase com a capa de visualització i govern de la dada. Aquest article descriu exactament com muntar aquesta arquitectura.
Arquitectura general: rèplica de només lectura + Metabase + dbt
L'arquitectura que recomanem per a pimes i empreses mitjanes té tres components:
- Rèplica PostgreSQL de només lectura (streaming replication des del primari d'Odoo). Les consultes analítiques van aquí, mai al primari.
- dbt (data build tool) funcionant sobre aquesta rèplica o sobre un esquema analític separat. dbt transforma l'esquema d'Odoo —complex, amb desenes de taules normalitzades— en models semàntics nets i documentats que els analistes poden usar sense conèixer el model de dades intern d'Odoo.
- Metabase connectat a aquests models dbt. Metabase és open source, es desplega a Docker en minuts, i la seva interfície permet que qualsevol persona de direcció construeixi les seves pròpies preguntes sense 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 │
└─────────────────────────────────────┘
Pas 1: Configurar la rèplica de només lectura de PostgreSQL
Si ja tens Patroni o replicació streaming configurada (com es descriu a l'article sobre alta disponibilitat), simplement usa el port de rèplica (5433 via HAProxy) per a les connexions de Metabase. Si parteixes de zero i vols una rèplica senzilla només per a 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
Un cop activa la rèplica, crear un usuari de només lectura per a Metabase amb accés únicament a l'esquema analític:
-- 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
Pas 2: Instal·lar Metabase amb Docker Compose
Metabase es desplega en minuts. La configuració mínima de producció utilitza la seva pròpia base de dades (PostgreSQL preferiblement, no H2) per emmagatzemar dashboards, usuaris i preguntes desades:
# 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:
Posar Nginx davant de Metabase amb SSL (Certbot) abans d'exposar-lo a Internet. Metabase al port 3000 mai ha de ser accessible directament des de fora.
Pas 3: Modelar l'esquema Odoo amb dbt
L'esquema de PostgreSQL d'Odoo és complex: més de 400 taules amb noms críptics (account_move, stock_quant, sale_order_line). dbt permet crear vistes i taules materialitzades netes sobre aquest esquema, amb documentació, tests de qualitat de dades i llinatge automàtic.
Estructura de projecte dbt recomanada per a 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
Model de staging: comandes de venda
-- 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')
Model de mart: marge per producte
-- 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
Executar dbt amb un cron diari (o cada hora per a dades més fresques):
# 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
Pas 4: KPIs executius que realment importen
Un cop els models dbt estan disponibles a Metabase, els dashboards executius que més valor aporten en el context d'una pime amb Odoo són:
Dashboard 1: Vendes i pipeline
- Vendes del mes actual vs. mateix mes de l'any anterior (variació %)
- Vendes acumulades de l'any vs. objectiu
- Top 10 clients per facturació en els últims 90 dies
- Pipeline de pressupostos oberts (estat
draft/sentasale_order) amb probabilitat ponderada - Taxa de conversió pressupost → comanda confirmada per comercial
Dashboard 2: Marge i rendibilitat
- Marge brut per família de producte (%) — mapa de calor mensual
- Evolució del marge mitjà dels últims 12 mesos
- Productes amb marge negatiu o per sota del llindar (alerta automàtica)
- Contribució de cada categoria al marge total (stacked bar)
Dashboard 3: Cashflow operatiu
-- 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: Cohorts de clients
L'anàlisi de cohorts és potser el KPI més infrautilitzat en pimes amb Odoo. Permet respondre: els clients que vam captar al Q1 2025 continuen comprant als 6 mesos al mateix ritme que els del 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
Pas 5: Informes automàtics per correu i alertes
Metabase inclou un sistema de pulses i informes programats que permet enviar dashboards per correu electrònic automàticament. La configuració recomanada per a direcció:
- Dilluns 08:00: resum setmanal de vendes, marge i cobraments pendents de la setmana
- Dia 1 de cada mes 07:00: tancament del mes anterior amb comparativa vs. mes anterior i mateix mes de l'any passat
- Alerta en temps real: si el marge de qualsevol línia de comanda cau per sota del llindar definit (configurable per família de producte)
- Alerta de cashflow: si el saldo de cobraments esperats en els propers 30 dies cau per sota de X euros
Les alertes de Metabase es configuren des de la interfície (secció Alertes en qualsevol pregunta desada) i s'envien per correu o webhook. Per a alertes a Telegram o Slack, un webhook de Metabase apuntant a un bot de Telegram és la solució més directa.
Govern de la dada: l'element que més projectes ignoren
Una plataforma de BI sense govern de la dada es converteix en un caos de dashboards contradictoris en tres mesos. Les preguntes que maten la confiança en la dada són: per què aquest dashboard diu que vam vendre 450k i el de comercial diu 430k? La resposta sempre és: definicions inconsistents. Per evitar-ho:
- Definir les mètriques a dbt, no a Metabase: la definició de 'venda confirmada' (quins estats de
sale_order, si s'inclouen devolucions, si es compta en data de comanda o de factura) ha d'estar al model dbt, no a cada dashboard per separat. - Tests de dbt per a qualitat de dades: verificar que no hi ha
order_idnuls, que els imports són sempre positius, que cada factura apunta a un client existent. Si el test falla, el model no es materialitza i Metabase mostra les dades del dia anterior. - Documentació a dbt: cada model i cada columna té una descripció a
schema.yml. Metabase pot importar aquesta documentació i mostrar-la a l'usuari en passar el cursor sobre una columna. - Control d'accés a Metabase: la direcció veu tots els dashboards; els comercials veuen només les seves pròpies mètriques; la logística no veu marges. Els grups i permisos de Metabase mapegen exactament sobre els rols de negoci.
Costos i alternatives
| Component | Opció open source | Alternativa comercial | Cost aprox. |
|---|---|---|---|
| Visualització | Metabase OSS | Metabase Pro / Power BI | 0€ / desde 500€/mes |
| Transformació | dbt Core | dbt Cloud | 0€ / desde 100€/mes |
| Rèplica BBDD | PostgreSQL streaming | AWS RDS read replica | 0€ (self-hosted) / desde 80€/mes |
| Orquestació | cron + dbt | Airflow / Dagster | 0€ / coste de infraestructura |
Per a la majoria de pimes espanyoles amb Odoo, l'stack rèplica PG + dbt Core + Metabase OSS és suficient i el cost d'infraestructura addicional és de 30–80€/mes (un VPS petit per a Metabase i dbt). La inversió real és el temps de modelatge inicial: normalment entre 15 i 40 hores de consultoria tècnica depenent de la complexitat del negoci.
Errors freqüents en projectes BI sobre Odoo
- Connectar Metabase directament al PostgreSQL de producció: les consultes analítiques competeixen amb Odoo per CPU i connexions. Sempre usar rèplica o dades exportades.
- Construir dashboards abans de modelar la dada: el resultat són dashboards que ningú entén ni manté. Primer el model dbt, després el dashboard.
- Ignorar les dades de cost (
standard_price): sense costos, els dashboards de marge són una il·lusió. Verificar que l'equip de compres manté els costos actualitzats a Odoo. - No programar els informes automàtics: si el dashboard no arriba al correu de direcció, la direcció no l'utilitza. L'automatització és el que converteix el projecte en un hàbit.