Files
aurgi/reports/0001-2026-06-10-plan-dashboard-tickets-venta-ticketmedio.md
T
2026-06-14 11:27:02 +02:00

184 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Report 0001 — Plan: Dashboard de progresión de Tickets, Venta y Ticket Medio (total y por categorías)
- **Fecha:** 10/06/2026
- **Autor:** Claude (sesión meta_bigq)
- **Ámbito:** Metabase `reports.autingo.es` (DB 6 `DCBigQuery`) + BigQuery `autingo-159109.psql_dcpublic`
- **Estado:** plan (no ejecutado — pendiente de validar decisiones abiertas)
## Resumen
Dashboard para vigilar la **evolución temporal** de tres KPIs de venta del negocio aurgi/autingo
(recambios y servicios de automoción, multi-centro + ecommerce + call center) y, sobre todo, para
**detectar caídas**: cuándo baja la venta, cuándo baja el número de tickets y cuándo baja el ticket
medio, tanto a nivel total como desglosado por categoría de producto. El objetivo no es solo "ver
los números" sino responder *dónde y cuándo se está cayendo y por qué* (menos tickets vs. ticket más
bajo vs. una categoría concreta arrastrando al total).
## Contexto descubierto (fuente de verdad del modelo)
El contexto se ha reconstruido clonando los análisis del proyecto aurgi alojados en Gitea
(`dataforge/venta_web`, `dataforge/sale_prices_comprobation`, `dataforge/presupuestos_callcenter`).
No existe un repo paraguas `dataforge/aurgi`; cada análisis es su propio sub-repo. El código de la
web del negocio (GitHub) no es necesario: el modelo analítico vive en BigQuery y en las cards de
Metabase ya construidas.
**Plataforma:**
- Metabase: `https://reports.autingo.es`, API key en `pass metabase/aurgi-api-key`.
- Database analítica: id **6** (`DCBigQuery`, engine bigquery-cloud-sdk).
- Proyecto/Dataset BigQuery: `autingo-159109.psql_dcpublic` (réplica del Postgres del TPV/ERP).
**Tablas núcleo de venta (dataset `psql_dcpublic`):**
| Tabla | Rol en el modelo |
|---|---|
| `tpv_orders_order` | Cabecera de pedido: `id`, `customer_id`, `vehicle_id`, `terminal_id`, `total_cost`, `created_at` |
| `tpv_orders_invoice` | Factura (venta consumada). Match con order por `order_id` |
| `tpv_orders_orderitem` | Líneas de pedido: `order_id`, `product_id`, importe, `tax` → base del desglose por categoría |
| `tpv_orders_quote` | Presupuesto (no es venta; relevante para conversión, fuera de alcance v1) |
| `products` | Catálogo de producto: `id`, `nav_id`, `channel_id`, `show_on_ecommerce`, referencia normalizada |
| `ecommerce_categories` | Catálogo de categorías (candidato a fuente de "categoría") |
| `channels` | Canal de venta (`id`, `name`) — p. ej. ecommerce vs. TPV |
| `centers` + `tpv_terminals` | Centro físico (terminal → center). **159 y 162 = CALL CENTER** (excluir o segmentar) |
| `companies`, `tpv_customers`, `tpv_vehicles_vehicle` | Cliente / empresa / vehículo (matrícula) |
**Patrón de dashboard ya probado en este Metabase** (`presupuestos_callcenter/create_dashboard_v2.py`):
- Cards SQL nativo con **field-filters parametrizados** `[[AND {{filtro}}]]` compartidos por todo el tab.
- Colección de trabajo (presupuestos usó `collection_id 559`); dashboard de KPIs ya existente (nº 999).
- Field-ids ya identificados que reutilizaremos donde apliquen: `quote.created_at=16588`,
`center.id=17327`, `tpvuser.id=17965`, `company.id=17157`, `product.id=16698`.
## Definiciones de métricas (propuesta — a confirmar)
- **Ticket** = una **venta facturada** = 1 fila de `tpv_orders_invoice` (o `tpv_orders_order` con
factura asociada). Nº de tickets = `COUNT(DISTINCT invoice.id)`.
- **Venta (€)** = importe facturado. Propuesta: `SUM(tpv_orders_order.total_cost)` a nivel total;
para el desglose por categoría se suma a nivel **línea** (`tpv_orders_orderitem`). Hay que fijar si
el importe es **con o sin IVA** (existe campo `tax`) — debe ser coherente entre total y categorías.
- **Ticket medio (€)** = `Venta / Nº tickets`.
- **Caída** = variación negativa de cualquiera de los tres KPIs frente al **periodo anterior
comparable** (mes vs. mes anterior, y mismo mes año anterior — interanual, por la estacionalidad
fuerte del recambio: ITV, neumáticos por temporada, balizas V16, etc.).
## Arquitectura del dashboard
Un único dashboard, **3 secciones** (filtros globales compartidos arriba):
**Filtros globales (field-filters):** Rango de fechas (sobre la fecha de venta), Centro, Canal,
Categoría, Compañía. Todos mapeados a todas las cards vía el patrón `[[AND {{...}}]]`.
### Sección A — KPIs cabecera (fila de scalars con variación)
Tres `smartscalar` (o `scalar` + comparación): **Venta total**, **Nº tickets**, **Ticket medio**,
cada uno con su **variación % vs. periodo anterior** y un mini-sparkline. Aquí se ve "de un vistazo"
si hay caída global.
### Sección B — Progresión temporal (series)
- **Línea/área**: Venta por mes (con línea del año anterior superpuesta para comparar).
- **Línea**: Nº tickets por mes.
- **Línea**: Ticket medio por mes.
- **Barras + línea combinada**: Venta (barras) y Ticket medio (línea) en el mismo eje temporal —
permite ver si una caída de venta viene de menos tickets o de ticket más bajo.
### Sección C — Desglose por categoría (dónde está la caída)
- **Barras horizontales**: Venta por categoría en el periodo, ordenada desc.
- **Tabla con variación**: por categoría → Venta, Nº tickets, Ticket medio, **Δ% vs. periodo
anterior**, con formato condicional (rojo = caída). Esta es la tabla clave para "analizar caídas".
- **Heatmap / pivot** (mes × categoría) de la venta o del Δ% — detecta qué categoría empezó a caer y
cuándo.
- **Opcional**: mismo desglose por **centro** y por **canal** (misma estructura), si se quiere cruzar
la caída por ubicación/canal.
## SQL base (esqueleto parametrizado)
Una sola CTE de "ventas" reutilizada por casi todas las cards, con los field-filters inyectados. La
tabla cuya columna alimenta un field-filter va **sin alias** (convención del patrón existente):
```sql
WITH ventas AS (
SELECT
`psql_dcpublic.tpv_orders_invoice`.id AS ticket_id,
`psql_dcpublic.tpv_orders_order`.total_cost AS importe,
DATE_TRUNC(DATE(`psql_dcpublic.tpv_orders_order`.created_at), MONTH) AS mes,
`psql_dcpublic.centers`.id AS centro_id
-- categoria_id / canal_id segun la fuente que se confirme
FROM `psql_dcpublic.tpv_orders_order`
JOIN `psql_dcpublic.tpv_orders_invoice`
ON `psql_dcpublic.tpv_orders_invoice`.order_id = `psql_dcpublic.tpv_orders_order`.id
LEFT JOIN `psql_dcpublic.tpv_terminals`
ON `psql_dcpublic.tpv_orders_order`.terminal_id = `psql_dcpublic.tpv_terminals`.id
LEFT JOIN `psql_dcpublic.centers`
ON `psql_dcpublic.tpv_terminals`.center_id = `psql_dcpublic.centers`.id
WHERE 1=1
-- AND COALESCE(`psql_dcpublic.centers`.id,0) NOT IN (159,162) -- excluir call center si procede
[[AND {{fecha}}]]
[[AND {{centro}}]]
[[AND {{canal}}]]
[[AND {{categoria}}]]
)
SELECT mes, SUM(importe) AS venta, COUNT(DISTINCT ticket_id) AS tickets,
SAFE_DIVIDE(SUM(importe), COUNT(DISTINCT ticket_id)) AS ticket_medio
FROM ventas
GROUP BY mes ORDER BY mes;
```
Para el desglose por categoría se baja el grano a `tpv_orders_orderitem` (JOIN a `products` y a la
fuente de categoría) y se agrupa por `categoria`. **Gotcha de doble conteo**: al unir líneas, el
`COUNT(DISTINCT ticket_id)` sigue siendo correcto, pero la venta por categoría debe sumarse a nivel
línea, no de cabecera, para no duplicar `total_cost`.
## Detección de caídas (lo que de verdad pide el encargo)
1. **Variación vs. periodo anterior** en cada KPI (smartscalar con `previousValue` sobre breakout
mensual; ver gotcha smartscalar más abajo).
2. **Tabla por categoría con Δ%** y formato condicional rojo para Δ% < 0 → ranking de "quién cae".
3. **Comparativa interanual** (mismo mes del año pasado) como segunda serie en las líneas, por la
estacionalidad del sector.
4. **Descomposición de la caída**: al poner Venta, Tickets y Ticket medio juntos en el tiempo, se
distingue si la venta cae por *menos clientes* (tickets) o por *gastan menos* (ticket medio).
5. *(Futuro, opcional)* alerta por email/Slack de Metabase cuando Δ% mensual de venta o de una
categoría top supere un umbral negativo.
## Plan de ejecución por fases
1. **Exploración (barata, sin coste BQ relevante)** — confirmar contra `INFORMATION_SCHEMA` y el
metadata de Metabase DB6: (a) campo de fecha de venta correcto (`order.created_at` vs.
`invoice.created_at`), (b) si importe lleva IVA, (c) fuente real de "categoría" (`ecommerce_categories`
vs. familia NAV vs. `CASE` manual ya usado en `venta_web`), (d) field-ids de los filtros nuevos.
2. **Cards base** — crear las cards SQL parametrizadas (KPIs, series, desglose) en una colección
propia del dashboard. Validar cada query con `dry_run`/límite antes de guardar.
3. **Dashboard** — crear dashboard, añadir filtros globales, colocar cards en las 3 secciones,
copiar los `parameter_mappings` (patrón `metabase_dashboard_append_row` / donante).
4. **Variaciones y formato** — smartscalars con comparación, formato condicional de la tabla por
categoría, formato moneda EUR y porcentaje.
5. **Validación** — cuadrar el total del dashboard contra un número de control conocido (p. ej. venta
de un mes ya cerrado) y revisar visualmente las caídas detectadas.
## Decisiones abiertas (necesito tu confirmación antes de construir)
1. **¿Ticket = factura (`invoice`) o pedido (`order`)?** ¿Cuentan los pedidos sin factura?
2. **¿Importe con o sin IVA?** ¿`total_cost` es el bueno o hay que sumar líneas/base imponible?
3. **¿Qué es "categoría"?** ¿`ecommerce_categories`, la familia NAV del producto, o la clasificación
manual tipo `CASE` que ya aparece en `venta_web` (Balizas V16, etc.)?
4. **Call center (centros 159/162):** ¿se excluyen, se incluyen, o van como segmento aparte?
5. **Canales:** ¿separar ecommerce vs. TPV físico, o todo junto con filtro?
6. **Granularidad temporal:** ¿mensual (propuesto), o también semanal/diaria?
7. **¿Dashboard nuevo o ampliar uno existente** (p. ej. el 999 de presupuestos)? Recomiendo nuevo.
## Gaps / riesgos / gotchas
- **Sin confirmar todavía**: el join exacto producto→categoría y el campo de fecha/IVA (fase 1). El
SQL de arriba es esqueleto, no definitivo.
- **Coste BigQuery**: las tablas TPV son grandes. Usar `dry_run` antes de cada query nueva,
particionar por fecha en el `WHERE`, y evitar `SELECT *`. Metabase cachea, pero el primer render de
cada card paga su escaneo.
- **Smartscalar v0.59**: la comparación `anotherColumn` falla si la query devuelve varias filas;
para "vs. periodo anterior" hace falta breakout temporal + `previousValue` (documentado en el skill
meta_bigq). Alternativa: card SQL de 2 filas (actual/anterior) con `metabase_smartscalar_kpi_*`.
- **Doble conteo** al desglosar por categoría (líneas vs. cabecera) — sumar venta a nivel línea.
- **Definición de "caída"**: sin acordar el periodo de comparación (MoM vs. interanual) el dashboard
puede dar falsas alarmas por estacionalidad. Propuesta: mostrar ambas.
## Próximo paso
En cuanto confirmes las **decisiones abiertas** (sobre todo 1, 2 y 3), arranco la **fase 1
(exploración barata)** para fijar campos exactos y luego construyo cards + dashboard.