chore: sync from fn-registry agent
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# JUPYTER HABILITADO EN ESTE ANALISIS
|
||||||
|
|
||||||
|
## Reglas OBLIGATORIAS para Claude
|
||||||
|
|
||||||
|
### 1. CODIGO INMUTABLE — NUNCA MODIFICAR CELDAS EXISTENTES
|
||||||
|
- **PROHIBIDO** usar NotebookEdit para reemplazar celdas existentes
|
||||||
|
- **SIEMPRE** anadir celdas NUEVAS al final del notebook
|
||||||
|
- Si hay un error en una celda, crear celda nueva con la correccion
|
||||||
|
- El historial de trabajo debe quedar intacto para trazabilidad
|
||||||
|
|
||||||
|
### 2. PROGRAMACION FUNCIONAL OBLIGATORIA
|
||||||
|
- **Funciones puras**: sin efectos secundarios, mismo input -> mismo output
|
||||||
|
- **Inmutabilidad**: nunca mutar datos, crear copias transformadas
|
||||||
|
- **Composicion**: funciones pequenas que se combinan
|
||||||
|
- Preferir: `map`, `filter`, `reduce`, list comprehensions
|
||||||
|
- Evitar: loops con mutacion, `global`, modificar argumentos in-place
|
||||||
|
|
||||||
|
### 3. SIEMPRE usar MCP jupyter para ejecutar codigo Python
|
||||||
|
- Las ejecuciones se ven en tiempo real en Jupyter Lab del usuario
|
||||||
|
- Compartimos variables y estado del kernel
|
||||||
|
- **NUNCA usar bash para ejecutar Python en este analisis**
|
||||||
|
|
||||||
|
### 4. Verificar Jupyter activo ANTES de ejecutar
|
||||||
|
- Si no esta activo: pedir al usuario que ejecute `./run-jupyter-lab.sh`
|
||||||
|
|
||||||
|
### 5. Gestion de notebooks
|
||||||
|
- Notebooks en la carpeta `notebooks/` o subcarpetas
|
||||||
|
- Si un notebook tiene >50 celdas, crear uno nuevo
|
||||||
|
- Nombrar descriptivamente: `01_exploracion.ipynb`, `02_limpieza.ipynb`
|
||||||
|
|
||||||
|
### 6. Gestion de Python
|
||||||
|
- **SIEMPRE usar `uv`** para gestionar dependencias
|
||||||
|
- Anadir paquetes con `uv add nombre_paquete`
|
||||||
|
|
||||||
|
### 7. Acceso al fn_registry
|
||||||
|
- `FN_REGISTRY_ROOT` apunta a la raiz del registry
|
||||||
|
- Para importar funciones Python: `sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))`
|
||||||
|
- Para consultar registry.db: `sqlite3` o `import sqlite3` con la ruta `$FN_REGISTRY_ROOT/registry.db`
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -0,0 +1,100 @@
|
|||||||
|
"""
|
||||||
|
fn_registry kernel startup
|
||||||
|
Autoconfigura acceso al registry en cada notebook.
|
||||||
|
Generado por write_jupyter_registry_kernel (fn_registry).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ── FN_REGISTRY_ROOT ────────────────────────────────────────
|
||||||
|
# Prioridad: env var > path hardcoded > descubrimiento automatico
|
||||||
|
def _discover_registry_root():
|
||||||
|
if os.environ.get("FN_REGISTRY_ROOT"):
|
||||||
|
return Path(os.environ["FN_REGISTRY_ROOT"]).resolve()
|
||||||
|
hardcoded = Path("/home/egutierrez/fn_registry")
|
||||||
|
if (hardcoded / "registry.db").exists():
|
||||||
|
return hardcoded
|
||||||
|
# Subir desde CWD hasta encontrar registry.db
|
||||||
|
p = Path.cwd()
|
||||||
|
for _ in range(10):
|
||||||
|
if (p / "registry.db").exists():
|
||||||
|
return p
|
||||||
|
if p.parent == p:
|
||||||
|
break
|
||||||
|
p = p.parent
|
||||||
|
return hardcoded
|
||||||
|
|
||||||
|
FN_REGISTRY_ROOT = _discover_registry_root()
|
||||||
|
os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)
|
||||||
|
|
||||||
|
# ── sys.path: importar funciones Python del registry ────────
|
||||||
|
_python_functions = FN_REGISTRY_ROOT / "python" / "functions"
|
||||||
|
for _domain in sorted(_python_functions.iterdir()) if _python_functions.exists() else []:
|
||||||
|
if _domain.is_dir() and not _domain.name.startswith("_"):
|
||||||
|
_path = str(_domain)
|
||||||
|
if _path not in sys.path:
|
||||||
|
sys.path.insert(0, _path)
|
||||||
|
|
||||||
|
# Tambien el directorio padre para imports por dominio: from core import filter_list
|
||||||
|
_pf = str(_python_functions)
|
||||||
|
if _pf not in sys.path:
|
||||||
|
sys.path.insert(0, _pf)
|
||||||
|
|
||||||
|
# ── fn_query: consultar registry.db desde el notebook ───────
|
||||||
|
_REGISTRY_DB = FN_REGISTRY_ROOT / "registry.db"
|
||||||
|
|
||||||
|
def fn_query(sql, params=()):
|
||||||
|
"""Ejecuta una consulta SQL sobre registry.db y retorna las filas.
|
||||||
|
|
||||||
|
Ejemplos:
|
||||||
|
fn_query("SELECT id, description FROM functions WHERE domain = ?", ("finance",))
|
||||||
|
fn_query("SELECT id FROM functions_fts WHERE functions_fts MATCH ?", ("slice*",))
|
||||||
|
"""
|
||||||
|
if not _REGISTRY_DB.exists():
|
||||||
|
raise FileNotFoundError(f"registry.db no encontrado en {_REGISTRY_DB}")
|
||||||
|
con = sqlite3.connect(str(_REGISTRY_DB))
|
||||||
|
con.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
rows = con.execute(sql, params).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
def fn_search(term):
|
||||||
|
"""Busca funciones y tipos en el registry por nombre o descripcion.
|
||||||
|
|
||||||
|
Ejemplo:
|
||||||
|
fn_search("slice")
|
||||||
|
fn_search("finance")
|
||||||
|
"""
|
||||||
|
fts_term = f"name:{term}* OR description:{term}*"
|
||||||
|
functions = fn_query(
|
||||||
|
"SELECT id, kind, purity, lang, description FROM functions "
|
||||||
|
"WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?) "
|
||||||
|
"ORDER BY name", (fts_term,)
|
||||||
|
)
|
||||||
|
types = fn_query(
|
||||||
|
"SELECT id, algebraic, lang, description FROM types "
|
||||||
|
"WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?) "
|
||||||
|
"ORDER BY name", (fts_term,)
|
||||||
|
)
|
||||||
|
return {"functions": functions, "types": types}
|
||||||
|
|
||||||
|
def fn_code(function_id):
|
||||||
|
"""Retorna el codigo fuente de una funcion del registry.
|
||||||
|
|
||||||
|
Ejemplo:
|
||||||
|
print(fn_code("filter_list_py_core"))
|
||||||
|
"""
|
||||||
|
rows = fn_query("SELECT code FROM functions WHERE id = ?", (function_id,))
|
||||||
|
if not rows:
|
||||||
|
raise KeyError(f"Funcion no encontrada: {function_id}")
|
||||||
|
return rows[0]["code"]
|
||||||
|
|
||||||
|
# ── Mensaje de bienvenida ───────────────────────────────────
|
||||||
|
print(f"fn_registry conectado: {FN_REGISTRY_ROOT}")
|
||||||
|
print(f" registry.db: {'OK' if _REGISTRY_DB.exists() else 'NO ENCONTRADO'}")
|
||||||
|
print(f" Python functions: {_pf}")
|
||||||
|
print(f" Helpers: fn_query(), fn_search(), fn_code()")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
8888
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"577c4238-54a0-43dd-9392-9c50040ee0d2": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"created_at": "2026-05-21T10:39:30.038590+00:00",
|
||||||
|
"document_version": "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"jupyter": {
|
||||||
|
"command": "/home/egutierrez/fn_registry/projects/aurgi/analysis/presupuestos_callcenter/.venv/bin/python",
|
||||||
|
"args": ["-m", "jupyter_mcp_server.server"],
|
||||||
|
"env": {
|
||||||
|
"SERVER_URL": "http://localhost:8888",
|
||||||
|
"TOKEN": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# presupuestos_callcenter
|
||||||
|
|
||||||
|
Atribución de presupuestos generados por call_center → factura.
|
||||||
|
|
||||||
|
## Hipótesis
|
||||||
|
|
||||||
|
1. Agentes del call_center generan presupuestos (`tpv_orders_quote`) para un cliente identificado por `(customer_id, vehicle_id)` (matrícula + tlf vía joins).
|
||||||
|
2. El cliente luego acude a un centro físico. Allí:
|
||||||
|
- **Acepta el quote del call_center** → mismo `order_id` → invoice. CONVIERTE.
|
||||||
|
- **Genera quote nuevo en TPV centro** → otro `order_id` → invoice. REGENERA y convierte.
|
||||||
|
- **No vuelve** → quote sin invoice.
|
||||||
|
3. La facturación total a esos clientes en el centro suele ser mayor que el valor del quote inicial (productos/servicios adicionales que el técnico añade en recepción).
|
||||||
|
|
||||||
|
## Datasets
|
||||||
|
|
||||||
|
| Origen | Tabla / propósito |
|
||||||
|
|---|---|
|
||||||
|
| `psql_dcpublic.tpv_orders_quote` | Quote raw — `created_by_id`, `order_id`, `accepted`, `status` |
|
||||||
|
| `psql_dcpublic.tpv_authorization_tpvuser` | Users TPV. ID = `created_by_id` |
|
||||||
|
| `psql_dcpublic.tpv_authorization_tpvuser_centers` | Mapeo user ↔ centro (`dccenter_id`) |
|
||||||
|
| `psql_dcpublic.centers` | Catálogo centros. **id 159 (CALL CENTER AURGI) e id 162 (CALL CENTER)** son los centros call_center |
|
||||||
|
| `psql_dcpublic.tpv_orders_order` | Order. `customer_id`, `vehicle_id`, `terminal_id`, `total_cost` |
|
||||||
|
| `psql_dcpublic.tpv_terminals` | Mapeo `terminal_id → center_id` |
|
||||||
|
| `psql_dcpublic.tpv_orders_invoice` | Invoice. Match con order via `order_id` |
|
||||||
|
| `psql_dcpublic.tpv_customers.entity_phone_number` | Tlf cliente |
|
||||||
|
| `psql_dcpublic.tpv_vehicles_vehicle.license_plate` | Matrícula |
|
||||||
|
|
||||||
|
## Notebooks
|
||||||
|
|
||||||
|
1. **`01_exploracion.ipynb`** — schemas, sanity counts, conversion rate global.
|
||||||
|
2. **`02_metricas_3kpi.ipynb`** — A/B/C por centro:
|
||||||
|
- A = € facturados de quotes call_center que CONVIRTIERON (mismo order_id).
|
||||||
|
- B = € facturados al mismo `(customer_id, vehicle_id)` en centro (excluye call_center).
|
||||||
|
- C = € totales del centro.
|
||||||
|
3. **`03_regeneracion.ipynb`** — Q0 (call_center) vs Q1+ (centro). Centros que más regeneran, distribución temporal, impacto en conversión.
|
||||||
|
|
||||||
|
## Identidad cliente
|
||||||
|
|
||||||
|
Por defecto `(customer_id, vehicle_id)` (FK estables). Si necesitas normalizar via tlf+matrícula:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
JOIN `psql_dcpublic.tpv_customers` cus ON o.customer_id = cus.id
|
||||||
|
JOIN `psql_dcpublic.tpv_vehicles_vehicle` v ON o.vehicle_id = v.id
|
||||||
|
-- cus.entity_phone_number, v.license_plate
|
||||||
|
```
|
||||||
|
|
||||||
|
Ventana temporal default: 90 días sobre Q0, 60 días para detectar regeneración posterior. Editar `WINDOW_DAYS` / `REGEN_WINDOW_DAYS` arriba en cada notebook.
|
||||||
|
|
||||||
|
## Próximos pasos
|
||||||
|
|
||||||
|
- Validar que `created_by_id` no es null en todos los quotes call_center.
|
||||||
|
- Refinar identidad cliente con normalización de teléfono (la BI views `*_telefono_normalizado` existen).
|
||||||
|
- Si la métrica B es estable, candidatos a dashboard Metabase paralelo al 999.
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: presupuestos_callcenter
|
||||||
|
lang: py
|
||||||
|
domain: datascience
|
||||||
|
description: "Atribucion de presupuestos call_center -> factura: total convertido, valor mismo cliente en centro, facturacion total centro, regeneracion de quotes por centro"
|
||||||
|
tags: [aurgi, call_center, quotes, bigquery]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
framework: "jupyterlab"
|
||||||
|
entry_point: "notebooks/main.ipynb"
|
||||||
|
dir_path: "projects/aurgi/analysis/presupuestos_callcenter"
|
||||||
|
repo_url: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Atribucion de presupuestos call_center -> factura: total convertido, valor mismo cliente en centro, facturacion total centro, regeneracion de quotes por centro
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build Metabase dashboard 'Presupuestos Call Center → Facturación'.
|
||||||
|
|
||||||
|
5 dashboard filters: fecha (date/range), centro, agente CC, compañía, producto.
|
||||||
|
3 KPIs × 3 cards each = 9 scalar cards:
|
||||||
|
A — Presupuestos Call center (quote CC → invoice mismo order_id)
|
||||||
|
B — Presupuestos CallC regenerados (Q0 CC → Q1 en centro físico → invoice)
|
||||||
|
C — Total facturado (invoice en centros, excluye 159/162)
|
||||||
|
|
||||||
|
Métricas por KPI:
|
||||||
|
- Total facturado (€)
|
||||||
|
- # Facturas
|
||||||
|
- Ticket medio (€) = Total / #
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
DB_ID = 6
|
||||||
|
COLLECTION_ID = 559 # "Datos de call center"
|
||||||
|
|
||||||
|
# Field IDs (precomputados via /api/table/<id>/query_metadata)
|
||||||
|
F_INVOICE_CREATED_AT = 16235
|
||||||
|
F_CENTER_ID = 17327
|
||||||
|
F_TPVUSER_ID = 17965
|
||||||
|
F_COMPANY_ID = 17157
|
||||||
|
F_PRODUCT_ID = 16698
|
||||||
|
|
||||||
|
client = httpx.Client(base_url=BASE, headers={"x-api-key": API_KEY}, timeout=120)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Template tags ----------
|
||||||
|
def field_filter_tag(name: str, field_id: int, widget: str, display_name: str):
|
||||||
|
return {
|
||||||
|
"id": name + "-tag",
|
||||||
|
"name": name,
|
||||||
|
"display-name": display_name,
|
||||||
|
"type": "dimension",
|
||||||
|
"dimension": ["field", field_id, None],
|
||||||
|
"widget-type": widget,
|
||||||
|
"default": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tag set para A y B (5 filtros)
|
||||||
|
TAGS_AB = {
|
||||||
|
"date": field_filter_tag("date", F_INVOICE_CREATED_AT, "date/range", "Fecha"),
|
||||||
|
"centro": field_filter_tag("centro", F_CENTER_ID, "id", "Centro"),
|
||||||
|
"agente": field_filter_tag("agente", F_TPVUSER_ID, "id", "Agente CC"),
|
||||||
|
"compania": field_filter_tag("compania", F_COMPANY_ID, "id", "Compañía"),
|
||||||
|
"producto": field_filter_tag("producto", F_PRODUCT_ID, "id", "Producto"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tag set para C (4 filtros — sin agente)
|
||||||
|
TAGS_C = {k: v for k, v in TAGS_AB.items() if k != "agente"}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- SQL skeletons ----------
|
||||||
|
SQL_A = """
|
||||||
|
WITH
|
||||||
|
cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN cc_users ON `psql_dcpublic.tpv_orders_quote`.created_by_id = cc_users.user_id
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_authorization_tpvuser` ON `psql_dcpublic.tpv_orders_quote`.created_by_id = `psql_dcpublic.tpv_authorization_tpvuser`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
AND COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{agente}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_B = """
|
||||||
|
WITH
|
||||||
|
cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
q0 AS (
|
||||||
|
SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,
|
||||||
|
q.created_by_id AS cc_agent_id,
|
||||||
|
o.customer_id, o.vehicle_id
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote` q
|
||||||
|
JOIN cc_users cc ON q.created_by_id = cc.user_id
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` o ON q.order_id = o.id
|
||||||
|
WHERE q.deleted_at IS NULL
|
||||||
|
AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL
|
||||||
|
),
|
||||||
|
qN AS (
|
||||||
|
SELECT q.id AS qn_id, q.order_id AS qn_order, q.created_at AS qn_ts,
|
||||||
|
o.customer_id, o.vehicle_id, t.center_id
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote` q
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` o ON q.order_id = o.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_terminals` t ON o.terminal_id = t.id
|
||||||
|
WHERE q.deleted_at IS NULL
|
||||||
|
AND t.center_id IS NOT NULL AND t.center_id NOT IN (159, 162)
|
||||||
|
),
|
||||||
|
pairs AS (
|
||||||
|
SELECT DISTINCT q0.cc_agent_id, qN.qn_order
|
||||||
|
FROM q0
|
||||||
|
JOIN qN
|
||||||
|
ON q0.customer_id = qN.customer_id
|
||||||
|
AND q0.vehicle_id = qN.vehicle_id
|
||||||
|
AND qN.qn_ts > q0.q0_ts
|
||||||
|
AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL 60 DAY)
|
||||||
|
AND qN.qn_order != q0.q0_order
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM pairs
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON pairs.qn_order = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_authorization_tpvuser` ON pairs.cc_agent_id = `psql_dcpublic.tpv_authorization_tpvuser`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
WHERE 1=1
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{agente}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_C = """
|
||||||
|
WITH filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM `psql_dcpublic.tpv_orders_invoice`
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` 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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
WHERE COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
AGG = {
|
||||||
|
"total": "ROUND(SUM(total_cost), 2)",
|
||||||
|
"count": "COUNT(*)",
|
||||||
|
"ticket": "ROUND(SAFE_DIVIDE(SUM(total_cost), NULLIF(COUNT(*), 0)), 2)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_card(name: str, sql_skel: str, agg_key: str, tags: dict, display: str, currency: bool):
|
||||||
|
sql = sql_skel.replace("__AGG__", AGG[agg_key])
|
||||||
|
viz = {}
|
||||||
|
if currency:
|
||||||
|
viz = {
|
||||||
|
"column_settings": {
|
||||||
|
'["name","valor"]': {
|
||||||
|
"number_style": "currency",
|
||||||
|
"currency": "EUR",
|
||||||
|
"currency_style": "symbol",
|
||||||
|
"currency_in_header": False,
|
||||||
|
"decimals": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body = {
|
||||||
|
"name": name,
|
||||||
|
"type": "question",
|
||||||
|
"display": display,
|
||||||
|
"visualization_settings": viz,
|
||||||
|
"dataset_query": {
|
||||||
|
"type": "native",
|
||||||
|
"database": DB_ID,
|
||||||
|
"native": {"query": sql, "template-tags": tags},
|
||||||
|
},
|
||||||
|
"collection_id": COLLECTION_ID,
|
||||||
|
"description": name,
|
||||||
|
"result_metadata": None,
|
||||||
|
}
|
||||||
|
r = client.post("/api/card", json=body)
|
||||||
|
if r.status_code >= 400:
|
||||||
|
print(r.status_code, r.text[:500])
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Build 9 cards ----------
|
||||||
|
specs = [
|
||||||
|
# (id, name, skeleton, agg, tags, display, currency)
|
||||||
|
("a_total", "A · Presupuestos Call center — Total facturado", SQL_A, "total", TAGS_AB, "scalar", True),
|
||||||
|
("a_count", "A · Presupuestos Call center — # Facturas", SQL_A, "count", TAGS_AB, "scalar", False),
|
||||||
|
("a_ticket", "A · Presupuestos Call center — Ticket medio", SQL_A, "ticket", TAGS_AB, "scalar", True),
|
||||||
|
|
||||||
|
("b_total", "B · Presupuestos CallC regenerados — Total facturado", SQL_B, "total", TAGS_AB, "scalar", True),
|
||||||
|
("b_count", "B · Presupuestos CallC regenerados — # Facturas", SQL_B, "count", TAGS_AB, "scalar", False),
|
||||||
|
("b_ticket", "B · Presupuestos CallC regenerados — Ticket medio", SQL_B, "ticket", TAGS_AB, "scalar", True),
|
||||||
|
|
||||||
|
("c_total", "C · Total facturado — Total facturado", SQL_C, "total", TAGS_C, "scalar", True),
|
||||||
|
("c_count", "C · Total facturado — # Facturas", SQL_C, "count", TAGS_C, "scalar", False),
|
||||||
|
("c_ticket", "C · Total facturado — Ticket medio", SQL_C, "ticket", TAGS_C, "scalar", True),
|
||||||
|
]
|
||||||
|
|
||||||
|
cards = {}
|
||||||
|
for sid, name, skel, agg, tags, display, currency in specs:
|
||||||
|
print(f"creating {sid} ... {name}")
|
||||||
|
c = make_card(name, skel, agg, tags, display, currency)
|
||||||
|
cards[sid] = c
|
||||||
|
print(f" -> id={c['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Build dashboard ----------
|
||||||
|
print("\ncreating dashboard")
|
||||||
|
dash = client.post("/api/dashboard", json={
|
||||||
|
"name": "Call Center — Presupuestos · KPIs (90d)",
|
||||||
|
"collection_id": COLLECTION_ID,
|
||||||
|
"description": "A=Quotes CC facturados · B=CallC regenerados · C=Total facturado · Filtros: fecha/centro/agente/compañía/producto",
|
||||||
|
}).json()
|
||||||
|
DID = dash["id"]
|
||||||
|
print(f" dashboard id={DID}")
|
||||||
|
|
||||||
|
# Dashboard-level parameters
|
||||||
|
DPARAMS = [
|
||||||
|
{"id": "p_date", "name": "Fecha", "slug": "fecha", "type": "date/range", "sectionId": "date"},
|
||||||
|
{"id": "p_centro", "name": "Centro", "slug": "centro", "type": "id", "sectionId": "id"},
|
||||||
|
{"id": "p_agente", "name": "Agente CC", "slug": "agente", "type": "id", "sectionId": "id"},
|
||||||
|
{"id": "p_compania", "name": "Compañía", "slug": "compania", "type": "id", "sectionId": "id"},
|
||||||
|
{"id": "p_producto", "name": "Producto", "slug": "producto", "type": "id", "sectionId": "id"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Parameter mappings: each dashboard param → each card's template-tag target
|
||||||
|
def mapping_for_card(card_id: int, tag_keys: list[str]):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"parameter_id": f"p_{k}",
|
||||||
|
"card_id": card_id,
|
||||||
|
"target": ["dimension", ["template-tag", k]],
|
||||||
|
}
|
||||||
|
for k in tag_keys
|
||||||
|
]
|
||||||
|
|
||||||
|
# Grid: 24 wide. Columns 0/8/16 = 8 wide. Rows 0/4/8 by KPI band.
|
||||||
|
DASHCARDS = []
|
||||||
|
layout = [
|
||||||
|
# (sid, row, col, h)
|
||||||
|
("a_total", 0, 0, 4), ("a_count", 0, 8, 4), ("a_ticket", 0, 16, 4),
|
||||||
|
("b_total", 4, 0, 4), ("b_count", 4, 8, 4), ("b_ticket", 4, 16, 4),
|
||||||
|
("c_total", 8, 0, 4), ("c_count", 8, 8, 4), ("c_ticket", 8, 16, 4),
|
||||||
|
]
|
||||||
|
|
||||||
|
neg = -1
|
||||||
|
for sid, row, col, h in layout:
|
||||||
|
card = cards[sid]
|
||||||
|
tag_keys = list((TAGS_AB if sid[0] in "ab" else TAGS_C).keys())
|
||||||
|
DASHCARDS.append({
|
||||||
|
"id": neg,
|
||||||
|
"card_id": card["id"],
|
||||||
|
"row": row,
|
||||||
|
"col": col,
|
||||||
|
"size_x": 8,
|
||||||
|
"size_y": h,
|
||||||
|
"parameter_mappings": mapping_for_card(card["id"], tag_keys),
|
||||||
|
"visualization_settings": {},
|
||||||
|
})
|
||||||
|
neg -= 1
|
||||||
|
|
||||||
|
# Update dashboard with params + dashcards
|
||||||
|
r = client.put(f"/api/dashboard/{DID}", json={
|
||||||
|
"parameters": DPARAMS,
|
||||||
|
"dashcards": DASHCARDS,
|
||||||
|
})
|
||||||
|
if r.status_code >= 400:
|
||||||
|
print(r.status_code, r.text[:800])
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
print(f"\nDONE")
|
||||||
|
print(f"dashboard URL: {BASE}/dashboard/{DID}")
|
||||||
|
client.close()
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dashboard v2 — coherencia:
|
||||||
|
- Date filter en `tpv_orders_quote.created_at` (creacion de presupuesto) para A B C.
|
||||||
|
- A = quotes CC -> factura (mismo order_id).
|
||||||
|
- B = (A) ∪ (quotes regenerados en centro fisico desde Q0 CC dentro 60d) -> facturas.
|
||||||
|
- C = invoices de centros fisicos cuyo order tiene AL MENOS un quote en la ventana
|
||||||
|
(98.7% de invoices tienen quote, solo cae 3.7% del valor — aceptable).
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
DB_ID = 6
|
||||||
|
COLLECTION_ID = 559
|
||||||
|
|
||||||
|
F_QUOTE_CREATED_AT = 16588
|
||||||
|
F_CENTER_ID = 17327
|
||||||
|
F_TPVUSER_ID = 17965
|
||||||
|
F_COMPANY_ID = 17157
|
||||||
|
F_PRODUCT_ID = 16698
|
||||||
|
|
||||||
|
client = httpx.Client(base_url=BASE, headers={"x-api-key": API_KEY}, timeout=180)
|
||||||
|
|
||||||
|
|
||||||
|
def field_filter_tag(name: str, field_id: int, widget: str, display_name: str):
|
||||||
|
return {
|
||||||
|
"id": name + "-tag",
|
||||||
|
"name": name,
|
||||||
|
"display-name": display_name,
|
||||||
|
"type": "dimension",
|
||||||
|
"dimension": ["field", field_id, None],
|
||||||
|
"widget-type": widget,
|
||||||
|
"default": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
TAGS_AB = {
|
||||||
|
"date": field_filter_tag("date", F_QUOTE_CREATED_AT, "date/range", "Fecha presupuesto"),
|
||||||
|
"centro": field_filter_tag("centro", F_CENTER_ID, "id", "Centro"),
|
||||||
|
"agente": field_filter_tag("agente", F_TPVUSER_ID, "id", "Agente CC"),
|
||||||
|
"compania": field_filter_tag("compania", F_COMPANY_ID, "id", "Compañía"),
|
||||||
|
"producto": field_filter_tag("producto", F_PRODUCT_ID, "id", "Producto"),
|
||||||
|
}
|
||||||
|
TAGS_C = {k: v for k, v in TAGS_AB.items() if k != "agente"}
|
||||||
|
|
||||||
|
|
||||||
|
# ---- SQL ----
|
||||||
|
# Convention: tabla cuya columna se filtra via field-filter NO lleva alias.
|
||||||
|
# Demas tablas pueden usar alias o full ref.
|
||||||
|
|
||||||
|
SQL_A = """
|
||||||
|
WITH cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN cc_users ON `psql_dcpublic.tpv_orders_quote`.created_by_id = cc_users.user_id
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_authorization_tpvuser` ON `psql_dcpublic.tpv_orders_quote`.created_by_id = `psql_dcpublic.tpv_authorization_tpvuser`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
AND COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{agente}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
# B: union de Q-CC directos + Q1 regenerados (desde Q0-CC dentro 60d).
|
||||||
|
# Q1 vive en una CTE clon de la tabla quote para no chocar con field-filter de Q-CC.
|
||||||
|
SQL_B = """
|
||||||
|
WITH cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
quotes_q1 AS (
|
||||||
|
SELECT id, order_id, created_at, deleted_at
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
),
|
||||||
|
orders_q1 AS (
|
||||||
|
SELECT id, customer_id, vehicle_id, terminal_id
|
||||||
|
FROM `psql_dcpublic.tpv_orders_order`
|
||||||
|
),
|
||||||
|
terminals_q1 AS (
|
||||||
|
SELECT id, center_id FROM `psql_dcpublic.tpv_terminals`
|
||||||
|
),
|
||||||
|
-- Q0 (CC) anchor; field-filters (date/agente) aplican sobre la quote CC
|
||||||
|
cc_anchored AS (
|
||||||
|
SELECT
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.id AS q0_id,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.order_id AS q0_order,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.created_at AS q0_ts,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.created_by_id AS cc_agent_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.customer_id AS cust_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.vehicle_id AS veh_id
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN cc_users ON `psql_dcpublic.tpv_orders_quote`.created_by_id = cc_users.user_id
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_authorization_tpvuser` ON `psql_dcpublic.tpv_orders_quote`.created_by_id = `psql_dcpublic.tpv_authorization_tpvuser`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{agente}}]]
|
||||||
|
),
|
||||||
|
-- Orders incluidos: Q-CC directos + Q1 regenerados
|
||||||
|
b_orders AS (
|
||||||
|
SELECT q0_order AS order_id FROM cc_anchored
|
||||||
|
UNION DISTINCT
|
||||||
|
SELECT q1.order_id AS order_id
|
||||||
|
FROM cc_anchored a
|
||||||
|
JOIN quotes_q1 q1
|
||||||
|
ON q1.deleted_at IS NULL
|
||||||
|
AND q1.created_at > a.q0_ts
|
||||||
|
AND q1.created_at <= TIMESTAMP_ADD(a.q0_ts, INTERVAL 60 DAY)
|
||||||
|
AND q1.order_id != a.q0_order
|
||||||
|
JOIN orders_q1 o1 ON q1.order_id = o1.id
|
||||||
|
LEFT JOIN terminals_q1 t1 ON o1.terminal_id = t1.id
|
||||||
|
WHERE o1.customer_id = a.cust_id
|
||||||
|
AND o1.vehicle_id = a.veh_id
|
||||||
|
AND t1.center_id IS NOT NULL
|
||||||
|
AND t1.center_id NOT IN (159, 162)
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM b_orders
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON b_orders.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
WHERE COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
# C: total facturado en centros fisicos cuyos orders tienen al menos un quote en la ventana
|
||||||
|
SQL_C = """
|
||||||
|
WITH filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
AND COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
AGG = {
|
||||||
|
"total": "ROUND(SUM(total_cost), 2)",
|
||||||
|
"count": "COUNT(*)",
|
||||||
|
"ticket": "ROUND(SAFE_DIVIDE(SUM(total_cost), NULLIF(COUNT(*), 0)), 2)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_card(name, sql_skel, agg_key, tags, display, currency):
|
||||||
|
sql = sql_skel.replace("__AGG__", AGG[agg_key])
|
||||||
|
viz = {}
|
||||||
|
if currency:
|
||||||
|
viz = {"column_settings": {'["name","valor"]': {
|
||||||
|
"number_style": "currency", "currency": "EUR",
|
||||||
|
"currency_style": "symbol", "currency_in_header": False, "decimals": 0,
|
||||||
|
}}}
|
||||||
|
body = {
|
||||||
|
"name": name,
|
||||||
|
"type": "question",
|
||||||
|
"display": display,
|
||||||
|
"visualization_settings": viz,
|
||||||
|
"dataset_query": {
|
||||||
|
"type": "native", "database": DB_ID,
|
||||||
|
"native": {"query": sql, "template-tags": tags},
|
||||||
|
},
|
||||||
|
"collection_id": COLLECTION_ID,
|
||||||
|
"description": name,
|
||||||
|
"result_metadata": None,
|
||||||
|
}
|
||||||
|
r = client.post("/api/card", json=body)
|
||||||
|
if r.status_code >= 400:
|
||||||
|
print(r.status_code, r.text[:500]); r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
specs = [
|
||||||
|
("a_total", "A · Presupuestos Call center — Total facturado", SQL_A, "total", TAGS_AB, "scalar", True),
|
||||||
|
("a_count", "A · Presupuestos Call center — # Facturas", SQL_A, "count", TAGS_AB, "scalar", False),
|
||||||
|
("a_ticket", "A · Presupuestos Call center — Ticket medio", SQL_A, "ticket", TAGS_AB, "scalar", True),
|
||||||
|
|
||||||
|
("b_total", "B · Presupuestos CallC + Regenerados — Total facturado", SQL_B, "total", TAGS_AB, "scalar", True),
|
||||||
|
("b_count", "B · Presupuestos CallC + Regenerados — # Facturas", SQL_B, "count", TAGS_AB, "scalar", False),
|
||||||
|
("b_ticket", "B · Presupuestos CallC + Regenerados — Ticket medio", SQL_B, "ticket", TAGS_AB, "scalar", True),
|
||||||
|
|
||||||
|
("c_total", "C · Total facturado — Total facturado", SQL_C, "total", TAGS_C, "scalar", True),
|
||||||
|
("c_count", "C · Total facturado — # Facturas", SQL_C, "count", TAGS_C, "scalar", False),
|
||||||
|
("c_ticket", "C · Total facturado — Ticket medio", SQL_C, "ticket", TAGS_C, "scalar", True),
|
||||||
|
]
|
||||||
|
|
||||||
|
cards = {}
|
||||||
|
for sid, name, skel, agg, tags, display, currency in specs:
|
||||||
|
print(f"creating {sid} ... {name}")
|
||||||
|
c = make_card(name, skel, agg, tags, display, currency)
|
||||||
|
cards[sid] = c
|
||||||
|
print(f" -> id={c['id']}")
|
||||||
|
|
||||||
|
print("\ncreating dashboard v2")
|
||||||
|
dash = client.post("/api/dashboard", json={
|
||||||
|
"name": "Call Center — Presupuestos · KPIs (v2)",
|
||||||
|
"collection_id": COLLECTION_ID,
|
||||||
|
"description": "A=Quotes CC · B=CC + Regenerados · C=Total facturado (orders con quote). Filtros: fecha-presupuesto/centro/agente/compañía/producto",
|
||||||
|
}).json()
|
||||||
|
DID = dash["id"]
|
||||||
|
print(f" dashboard id={DID}")
|
||||||
|
|
||||||
|
DPARAMS = [
|
||||||
|
{"id": "p_date", "name": "Fecha presupuesto", "slug": "fecha", "type": "date/range", "sectionId": "date"},
|
||||||
|
{"id": "p_centro", "name": "Centro", "slug": "centro", "type": "id", "sectionId": "id"},
|
||||||
|
{"id": "p_agente", "name": "Agente CC", "slug": "agente", "type": "id", "sectionId": "id"},
|
||||||
|
{"id": "p_compania", "name": "Compañía", "slug": "compania", "type": "id", "sectionId": "id"},
|
||||||
|
{"id": "p_producto", "name": "Producto", "slug": "producto", "type": "id", "sectionId": "id"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def mapping_for_card(card_id, tag_keys):
|
||||||
|
return [{"parameter_id": f"p_{k}", "card_id": card_id,
|
||||||
|
"target": ["dimension", ["template-tag", k]]} for k in tag_keys]
|
||||||
|
|
||||||
|
DASHCARDS = []
|
||||||
|
layout = [
|
||||||
|
("a_total", 0, 0, 4), ("a_count", 0, 8, 4), ("a_ticket", 0, 16, 4),
|
||||||
|
("b_total", 4, 0, 4), ("b_count", 4, 8, 4), ("b_ticket", 4, 16, 4),
|
||||||
|
("c_total", 8, 0, 4), ("c_count", 8, 8, 4), ("c_ticket", 8, 16, 4),
|
||||||
|
]
|
||||||
|
neg = -1
|
||||||
|
for sid, row, col, h in layout:
|
||||||
|
card = cards[sid]
|
||||||
|
tag_keys = list((TAGS_AB if sid[0] in "ab" else TAGS_C).keys())
|
||||||
|
DASHCARDS.append({
|
||||||
|
"id": neg, "card_id": card["id"],
|
||||||
|
"row": row, "col": col, "size_x": 8, "size_y": h,
|
||||||
|
"parameter_mappings": mapping_for_card(card["id"], tag_keys),
|
||||||
|
"visualization_settings": {},
|
||||||
|
})
|
||||||
|
neg -= 1
|
||||||
|
|
||||||
|
r = client.put(f"/api/dashboard/{DID}", json={"parameters": DPARAMS, "dashcards": DASHCARDS})
|
||||||
|
if r.status_code >= 400:
|
||||||
|
print(r.status_code, r.text[:800]); r.raise_for_status()
|
||||||
|
|
||||||
|
print(f"\nDONE")
|
||||||
|
print(f"dashboard URL: {BASE}/dashboard/{DID}")
|
||||||
|
client.close()
|
||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create Metabase document with analysis results."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
HERE = Path(__file__).parent
|
||||||
|
RES = HERE / "data" / "results"
|
||||||
|
COLLECTION_ID = 559 # "Datos de call center"
|
||||||
|
|
||||||
|
client = httpx.Client(base_url=BASE, headers={"x-api-key": API_KEY}, timeout=120)
|
||||||
|
|
||||||
|
# ---- load results ----
|
||||||
|
totales = json.loads((RES / "totales_globales.json").read_text())
|
||||||
|
conv = json.loads((RES / "01_conversion_origen.json").read_text())
|
||||||
|
kpi = json.loads((RES / "02_kpi_3_por_centro.json").read_text())
|
||||||
|
regen = json.loads((RES / "03_regen_por_centro.json").read_text())
|
||||||
|
rvc = json.loads((RES / "04_regen_vs_conversion.json").read_text())
|
||||||
|
|
||||||
|
# ---- prosemirror builders ----
|
||||||
|
ALLOWED = {"doc","heading","paragraph","text","horizontalRule","blockquote",
|
||||||
|
"bulletList","listItem","codeBlock"}
|
||||||
|
|
||||||
|
def h(level, text):
|
||||||
|
return {"type": "heading", "attrs": {"level": level},
|
||||||
|
"content": [{"type": "text", "text": text}]}
|
||||||
|
|
||||||
|
def p(*parts):
|
||||||
|
content = []
|
||||||
|
for x in parts:
|
||||||
|
if isinstance(x, str):
|
||||||
|
content.append({"type": "text", "text": x})
|
||||||
|
else:
|
||||||
|
content.append(x)
|
||||||
|
return {"type": "paragraph", "content": content}
|
||||||
|
|
||||||
|
def bold(text):
|
||||||
|
return {"type": "text", "text": text, "marks": [{"type": "bold"}]}
|
||||||
|
|
||||||
|
def code(text):
|
||||||
|
return {"type": "text", "text": text, "marks": [{"type": "code"}]}
|
||||||
|
|
||||||
|
def bullet_list(items):
|
||||||
|
nodes = []
|
||||||
|
for it in items:
|
||||||
|
if isinstance(it, str):
|
||||||
|
paras = [p(it)]
|
||||||
|
elif isinstance(it, list):
|
||||||
|
paras = it
|
||||||
|
else:
|
||||||
|
paras = [it]
|
||||||
|
nodes.append({"type": "listItem", "content": paras})
|
||||||
|
return {"type": "bulletList", "content": nodes}
|
||||||
|
|
||||||
|
def hr():
|
||||||
|
return {"type": "horizontalRule"}
|
||||||
|
|
||||||
|
def code_block(text, lang="sql"):
|
||||||
|
return {"type": "codeBlock", "attrs": {"language": lang},
|
||||||
|
"content": [{"type": "text", "text": text}]}
|
||||||
|
|
||||||
|
def fmt_eur(n):
|
||||||
|
return f"{n:,.0f} €".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||||
|
|
||||||
|
def fmt_pct(n):
|
||||||
|
return f"{n*100:.2f}%".replace(".", ",")
|
||||||
|
|
||||||
|
# ---- build content ----
|
||||||
|
|
||||||
|
# KPI table emulated as bullet list (table not allowed)
|
||||||
|
kpi_rows = kpi["rows"]
|
||||||
|
top10_a = sorted(kpi_rows, key=lambda r: r[3], reverse=True)[:10]
|
||||||
|
top10_c = sorted(kpi_rows, key=lambda r: r[5], reverse=True)[:10]
|
||||||
|
regen_rows = regen["rows"]
|
||||||
|
top10_regen = regen_rows[:10]
|
||||||
|
|
||||||
|
conv_rows = conv["rows"]
|
||||||
|
conv_cc = next(r for r in conv_rows if r[0] == "call_center")
|
||||||
|
conv_otro = next(r for r in conv_rows if r[0] == "otro")
|
||||||
|
|
||||||
|
doc_content = [
|
||||||
|
h(1, "Presupuestos generados por Call Center → Facturación"),
|
||||||
|
p("Ventana: últimos 90 días. Datos: ", code("psql_dcpublic"), " en BigQuery ", code("autingo-159109"),
|
||||||
|
". Centros call_center excluidos del cómputo de facturación física: ",
|
||||||
|
code("159 CALL CENTER AURGI"), " y ", code("162 CALL CENTER"), "."),
|
||||||
|
|
||||||
|
h(2, "Metodología"),
|
||||||
|
p("Identificación del origen call_center vía ",
|
||||||
|
code("tpv_authorization_tpvuser_centers.dccenter_id IN (159, 162)"),
|
||||||
|
". El ", code("tpv_orders_quote.created_by_id"), " se cruza con ese conjunto de usuarios (249 usuarios call_center)."),
|
||||||
|
p("Cadena de joins:"),
|
||||||
|
code_block(
|
||||||
|
"tpv_authorization_tpvuser_centers (dccenter_id ∈ {159, 162})\n"
|
||||||
|
" └─ tpvuser_id ─► tpv_orders_quote.created_by_id\n"
|
||||||
|
" └─ order_id ─► tpv_orders_order ─► terminal_id ─► tpv_terminals.center_id ─► centers\n"
|
||||||
|
" └─ order_id ─► tpv_orders_invoice (convertido si fila existe)\n"
|
||||||
|
" └─ customer_id, vehicle_id (identidad cliente)",
|
||||||
|
"text"),
|
||||||
|
p("Identidad del cliente = ", code("(customer_id, vehicle_id)"), " del order. "
|
||||||
|
"Conversión a factura = existe fila en ", code("tpv_orders_invoice"), " con el mismo ", code("order_id"), "."),
|
||||||
|
|
||||||
|
hr(),
|
||||||
|
|
||||||
|
h(2, "Totales globales (90 días)"),
|
||||||
|
bullet_list([
|
||||||
|
[p(bold("A — € quotes call_center facturados (mismo order_id): "), fmt_eur(totales["A_quote_cc_eur"]))],
|
||||||
|
[p(bold("B — € facturados a esos mismos clientes en centros físicos: "), fmt_eur(totales["B_mismo_cliente_eur"]),
|
||||||
|
" (lift ", f"{totales['lift_B_vs_A']:.2f}×".replace(".", ","), " sobre A)")],
|
||||||
|
[p(bold("C — € totales facturados en centros físicos: "), fmt_eur(totales["C_total_centros_eur"]))],
|
||||||
|
[p(bold("A / C = "), fmt_pct(totales["A_sobre_C"]),
|
||||||
|
" — peso directo del call_center en facturación de centros")],
|
||||||
|
[p(bold("B / C = "), fmt_pct(totales["B_sobre_C"]),
|
||||||
|
" — peso de clientes tocados por call_center")],
|
||||||
|
[p(bold("Centros activos en ventana: "), str(totales["centros_activos"]))],
|
||||||
|
]),
|
||||||
|
p("Lectura: el call_center trae directamente ~21,6% de la facturación de centros, "
|
||||||
|
"pero sus clientes acaban facturando ~23,4% (1,08× más): el centro suele añadir "
|
||||||
|
"valor al ticket cuando el cliente acude. Brecha B−A = ",
|
||||||
|
fmt_eur(totales["B_mismo_cliente_eur"] - totales["A_quote_cc_eur"]), "."),
|
||||||
|
|
||||||
|
hr(),
|
||||||
|
|
||||||
|
h(2, "Conversión de quote → factura por origen"),
|
||||||
|
bullet_list([
|
||||||
|
[p(bold(f"Call center: "), f"{conv_cc[1]:,} quotes / 90d → {conv_cc[2]:,} facturas → ",
|
||||||
|
bold(fmt_pct(conv_cc[3])))],
|
||||||
|
[p(bold(f"Otros usuarios: "), f"{conv_otro[1]:,} quotes / 90d → {conv_otro[2]:,} facturas → ",
|
||||||
|
bold(fmt_pct(conv_otro[3])))],
|
||||||
|
]),
|
||||||
|
p("Brecha esperable de ~10 pp: el quote del centro se hace casi siempre con el "
|
||||||
|
"cliente delante; el del call_center es 'en frío' (vía teléfono)."),
|
||||||
|
|
||||||
|
hr(),
|
||||||
|
|
||||||
|
h(2, "Top 10 centros por A (más facturado vía quote call_center)"),
|
||||||
|
bullet_list([
|
||||||
|
[p(bold(r[1]), " — A=", fmt_eur(r[3]), " · B=", fmt_eur(r[4]), " · C=", fmt_eur(r[5]),
|
||||||
|
" · A/C=", fmt_pct(r[6]), " · lift B/A=", str(r[8]) if r[8] is not None else "—", "×")]
|
||||||
|
for r in top10_a
|
||||||
|
]),
|
||||||
|
|
||||||
|
h(2, "Top 10 centros por C (facturación total)"),
|
||||||
|
bullet_list([
|
||||||
|
[p(bold(r[1]), " — A=", fmt_eur(r[3]), " · B=", fmt_eur(r[4]), " · C=", fmt_eur(r[5]),
|
||||||
|
" · A/C=", fmt_pct(r[6]), " · B/C=", fmt_pct(r[7]))]
|
||||||
|
for r in top10_c
|
||||||
|
]),
|
||||||
|
|
||||||
|
hr(),
|
||||||
|
|
||||||
|
h(2, "Regeneración del presupuesto"),
|
||||||
|
p("Regeneración = un mismo par ", code("(customer_id, vehicle_id)"),
|
||||||
|
" con un Q0 abierto por call_center y un Q1 posterior abierto en un terminal de "
|
||||||
|
"centro físico no-call_center con ", code("order_id"), " distinto, dentro de los 60 días siguientes."),
|
||||||
|
p(bold("Volumen Q0 con / sin regeneración:")),
|
||||||
|
bullet_list([
|
||||||
|
[p(bold("No regenerado: "), f"{rvc['rows'][0][1]:,} Q0 — convierte ",
|
||||||
|
bold(fmt_pct(rvc['rows'][0][3])), " sobre su order_id original")],
|
||||||
|
[p(bold("Regenerado: "), f"{rvc['rows'][1][1]:,} Q0 — convierte ",
|
||||||
|
bold(fmt_pct(rvc['rows'][1][3])), " sobre su order_id original")],
|
||||||
|
]),
|
||||||
|
p("Es decir, ~34,2% de los Q0 del call_center entran en patrón de regeneración. "
|
||||||
|
"La conversión sobre el order_id original cae del 63,1% al 38,7% en ese segmento, "
|
||||||
|
"pero el cliente puede haber facturado por el order_id del centro — esa parte queda "
|
||||||
|
"capturada por el KPI B."),
|
||||||
|
|
||||||
|
h(2, "Top 10 centros que MÁS regeneran (90d / window 60d)"),
|
||||||
|
bullet_list([
|
||||||
|
[p(bold(r[1]), " — Q0 regenerados aquí: ", str(r[2]),
|
||||||
|
" · eventos: ", str(r[3]),
|
||||||
|
" · días promedio entre Q0 y regeneración: ", f"{r[4]:.1f}".replace(".", ","))]
|
||||||
|
for r in top10_regen
|
||||||
|
]),
|
||||||
|
|
||||||
|
hr(),
|
||||||
|
|
||||||
|
h(2, "Cuestiones abiertas"),
|
||||||
|
bullet_list([
|
||||||
|
"Filtrar invoices por status válido (excluir rectificativas/anuladas)",
|
||||||
|
"¿Sumar líneas (orderitem.total_price) en vez de order.total_cost? Captura descuentos finales mejor",
|
||||||
|
"Refinar identidad cliente con teléfono normalizado (BI views *_telefono_normalizado)",
|
||||||
|
"Validar si algún centro fuera de 159/162 actúa como call_center mixto",
|
||||||
|
]),
|
||||||
|
|
||||||
|
h(2, "Origen del análisis"),
|
||||||
|
p("Notebooks ejecutados: ",
|
||||||
|
code("projects/aurgi/analysis/presupuestos_callcenter/notebooks/"),
|
||||||
|
" en el repo ", code("fn_registry"),
|
||||||
|
". Datos crudos: ", code("data/results/*.csv"), "."),
|
||||||
|
]
|
||||||
|
|
||||||
|
doc = {"type": "doc", "content": doc_content}
|
||||||
|
|
||||||
|
# ---- validate before POST ----
|
||||||
|
def validate(node, path=""):
|
||||||
|
errs = []
|
||||||
|
if isinstance(node, dict):
|
||||||
|
typ = node.get("type", "?")
|
||||||
|
if typ not in ALLOWED:
|
||||||
|
errs.append(f"{path}: tipo no permitido '{typ}'")
|
||||||
|
for i, c in enumerate(node.get("content", []) or []):
|
||||||
|
errs += validate(c, f"{path}/{typ}[{i}]")
|
||||||
|
return errs
|
||||||
|
|
||||||
|
errs = validate(doc)
|
||||||
|
if errs:
|
||||||
|
print("INVALID DOC:")
|
||||||
|
for e in errs:
|
||||||
|
print(" ", e)
|
||||||
|
sys.exit(1)
|
||||||
|
print("doc valid, posting...")
|
||||||
|
|
||||||
|
# ---- POST ----
|
||||||
|
r = client.post("/api/document", json={
|
||||||
|
"name": "Call Center — Presupuestos → Facturación (90d)",
|
||||||
|
"collection_id": COLLECTION_ID,
|
||||||
|
"document": doc,
|
||||||
|
})
|
||||||
|
if r.status_code >= 400:
|
||||||
|
print(r.status_code, r.text)
|
||||||
|
sys.exit(1)
|
||||||
|
data = r.json()
|
||||||
|
print(f"created document id={data['id']}")
|
||||||
|
print(f"url: {BASE}/document/{data['id']}")
|
||||||
|
client.close()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
users_totales,users_activos
|
||||||
|
249,497
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"cols": [
|
||||||
|
"users_totales",
|
||||||
|
"users_activos"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
249,
|
||||||
|
497
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
origen,quotes,convertidos,conv_rate
|
||||||
|
call_center,62779,29576,0.4711
|
||||||
|
otro,477095,273764,0.5738
|
||||||
|
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"cols": [
|
||||||
|
"origen",
|
||||||
|
"quotes",
|
||||||
|
"convertidos",
|
||||||
|
"conv_rate"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
"call_center",
|
||||||
|
62779,
|
||||||
|
29576,
|
||||||
|
0.4711
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"otro",
|
||||||
|
477095,
|
||||||
|
273764,
|
||||||
|
0.5738
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
center_id,center_name,quotes_cc_facturados,A_quote_cc_eur,B_mismo_cliente_eur,C_total_centro_eur,A_sobre_C,B_sobre_C,lift_B_vs_A
|
||||||
|
144,Velez Malaga,158,21890.28,37700.63,653333.86,0.0335,0.0577,1.72
|
||||||
|
75,Store,607,150402.5,167424.36,644637.94,0.2333,0.2597,1.11
|
||||||
|
54,Leganes,516,62620.06,87800.04,637007.16,0.0983,0.1378,1.4
|
||||||
|
121,Villalba,316,41725.46,57526.94,524985.02,0.0795,0.1096,1.38
|
||||||
|
168,Goya GLASS,1773,414629.07,418220.06,497398.12,0.8336,0.8408,1.01
|
||||||
|
73,Malaga,379,64408.08,77124.76,489986.85,0.1314,0.1574,1.2
|
||||||
|
130,Alcorcon,210,29467.17,42686.28,487245.3,0.0605,0.0876,1.45
|
||||||
|
35,MT Sanchinarro,480,78658.96,87172.31,487013.5,0.1615,0.179,1.11
|
||||||
|
82,Vallecas,307,40691.79,57401.47,460004.1,0.0885,0.1248,1.41
|
||||||
|
146,Vaguada,384,52572.11,66427.6,453792.26,0.1159,0.1464,1.26
|
||||||
|
74,La Red,149,20090,28795.52,453685.44,0.0443,0.0635,1.43
|
||||||
|
86,Almeria,206,27675.26,41355.76,447008.24,0.0619,0.0925,1.49
|
||||||
|
160,Aurgi Web,0,0,32.76,434152.84,0,0.0001,
|
||||||
|
76,Barbera,146,20540.91,32027.62,428700.38,0.0479,0.0747,1.56
|
||||||
|
55,MT Pozuelo,148,29616.45,41182.26,428580,0.0691,0.0961,1.39
|
||||||
|
63,San Fernando,212,40205.6,47217.16,427069.15,0.0941,0.1106,1.17
|
||||||
|
171,Vallecas CRISTALES,1463,331148.6,333739.92,401405.53,0.825,0.8314,1.01
|
||||||
|
187,Santa Engracia CRISTALES,1348,329681.23,331496.25,392774.08,0.8394,0.844,1.01
|
||||||
|
125,Cornella,111,15571.48,21147.91,391400.35,0.0398,0.054,1.36
|
||||||
|
126,Alcala Henares,72,10900.34,15705.3,380211.91,0.0287,0.0413,1.44
|
||||||
|
7,Sant Cugat,58,7763.24,13564.45,378412.32,0.0205,0.0358,1.75
|
||||||
|
169,MT El Bercial CRISTALES,1308,330748.83,332453.44,376719.85,0.878,0.8825,1.01
|
||||||
|
72,Las Rozas,205,33158.64,47830.21,373018.48,0.0889,0.1282,1.44
|
||||||
|
81,Granada,299,47903.35,54525.44,371155.24,0.1291,0.1469,1.14
|
||||||
|
84,Cordoba,48,11288.3,17263.68,366709.5,0.0308,0.0471,1.53
|
||||||
|
129,Valdemoro,133,17213.85,25731.54,354756.19,0.0485,0.0725,1.49
|
||||||
|
131,San Juan,237,43924.29,50351.18,349943.56,0.1255,0.1439,1.15
|
||||||
|
17,Granollers,114,15622.97,24005.78,345773.76,0.0452,0.0694,1.54
|
||||||
|
127,Alcobendas,200,29518.42,40648.59,343632.11,0.0859,0.1183,1.38
|
||||||
|
136,Emilio Muñoz,187,24739.45,34694.08,341627.1,0.0724,0.1016,1.4
|
||||||
|
143,Majadahonda,192,31276.73,44971.82,340391.08,0.0919,0.1321,1.44
|
||||||
|
15,Islazul,126,21984.03,29037.25,339479.77,0.0648,0.0855,1.32
|
||||||
|
178,MT San Jose de Valderas CRISTALES,1161,281222.38,281715.46,318152.82,0.8839,0.8855,1
|
||||||
|
179,Leganes CRISTALES,987,265504.51,266191.45,317917.47,0.8351,0.8373,1
|
||||||
|
70,San Sebastian,172,25052.46,34852.71,316956.08,0.079,0.11,1.39
|
||||||
|
85,Gta. Cadiz,165,21422.85,33067.75,314647.04,0.0681,0.1051,1.54
|
||||||
|
4,Denia,51,8000.13,12828.23,306405.66,0.0261,0.0419,1.6
|
||||||
|
153,Gava,106,14083.57,21989.08,306073.49,0.046,0.0718,1.56
|
||||||
|
158,Roquetas,0,0,65.16,298977.66,0,0.0002,
|
||||||
|
177,Alcobendas CRISTALES,1051,269955.49,270601.04,298382.31,0.9047,0.9069,1
|
||||||
|
148,Cornella 2,592,96134.21,100573.91,296797.99,0.3239,0.3389,1.05
|
||||||
|
173,Las Rozas CRISTALES,888,245368.18,246898.16,292203.52,0.8397,0.845,1.01
|
||||||
|
135,Sabadell,101,13238.18,17443.95,290542.96,0.0456,0.06,1.32
|
||||||
|
172,Villalba CRISTALES,885,232394.1,232769.47,290040.42,0.8012,0.8025,1
|
||||||
|
68,MT Cornella,536,103189.78,105522.57,289781.5,0.3561,0.3641,1.02
|
||||||
|
176,Alcala Henares CRISTALES,873,246470.51,247304.4,289184.89,0.8523,0.8552,1
|
||||||
|
47,MT Campo de las Naciones,211,39703.45,50583.64,287865.64,0.1379,0.1757,1.27
|
||||||
|
128,Mostoles,133,21200.57,31128.67,287784.26,0.0737,0.1082,1.47
|
||||||
|
157,AUR ALICANTE AV. NOVELDA,145,20770.08,31319.51,286873.34,0.0724,0.1092,1.51
|
||||||
|
154,Torrevieja,36,4073.8,4560.73,278330.4,0.0146,0.0164,1.12
|
||||||
|
170,San Fernando CRISTALES,908,227844.83,229641.99,275826.67,0.826,0.8326,1.01
|
||||||
|
145,Fuengirola,123,17562.19,22214.43,264709.16,0.0663,0.0839,1.26
|
||||||
|
12,Torremolinos,93,12503.16,20883.84,255659.79,0.0489,0.0817,1.67
|
||||||
|
137,Avda. Toreros,191,24358.6,33144.91,252478.86,0.0965,0.1313,1.36
|
||||||
|
2,Vall D'Uixo,7,951.37,1177.09,242236.49,0.0039,0.0049,1.24
|
||||||
|
80,Sant Boi,86,14467.37,18441.03,241629.5,0.0599,0.0763,1.27
|
||||||
|
5,Aluche,25,5264.01,7271.62,240940.88,0.0218,0.0302,1.38
|
||||||
|
71,Pinto,78,11014.94,16229.78,237649.58,0.0463,0.0683,1.47
|
||||||
|
62,MT Siete Palmas,8,1529.55,1529.55,227455.75,0.0067,0.0067,1
|
||||||
|
196,San Sebastian CRISTALES,696,185155.98,185859.21,224569.04,0.8245,0.8276,1
|
||||||
|
52,MT Compostela,52,9256.22,10811.55,206916.08,0.0447,0.0523,1.17
|
||||||
|
151,Puerto de Santa Maria,37,5087.11,6051,206400.33,0.0246,0.0293,1.19
|
||||||
|
183,Aurgi Asociados Gruas,0,0,0,205234.47,0,0,
|
||||||
|
152,Villanueva de la Serena,44,5433.46,7159.23,201920.97,0.0269,0.0355,1.32
|
||||||
|
140,Marques Vadillo,112,13760.46,23911.84,198371.08,0.0694,0.1205,1.74
|
||||||
|
3,Xativa,36,4944.67,8936.41,196945.1,0.0251,0.0454,1.81
|
||||||
|
139,Olias del Rey,99,12674.41,16752.24,194246.83,0.0652,0.0862,1.32
|
||||||
|
16,Zaragoza,70,12267.94,16538.83,193574.84,0.0634,0.0854,1.35
|
||||||
|
175,MT Pozuelo CRISTALES,582,165660.74,165713.78,192385.99,0.8611,0.8614,1
|
||||||
|
49,MT San Jose de Valderas,61,14991.76,19566.14,190501.93,0.0787,0.1027,1.31
|
||||||
|
138,Arganda,46,8350.57,11300.86,189509.48,0.0441,0.0596,1.35
|
||||||
|
10,Badajoz,77,9368.53,12066.14,182832.34,0.0512,0.066,1.29
|
||||||
|
14,Huelva,138,15963.93,20659.18,181994.31,0.0877,0.1135,1.29
|
||||||
|
87,Linares,83,9545.41,12120.64,180444.4,0.0529,0.0672,1.27
|
||||||
|
9,Sant Celoni,31,5014.96,6555.14,179714.84,0.0279,0.0365,1.31
|
||||||
|
39,MT Xanadu,46,8257.93,10728.95,174160.49,0.0474,0.0616,1.3
|
||||||
|
65,MT Bahia de Malaga,230,54588.03,54898.26,173394.01,0.3148,0.3166,1.01
|
||||||
|
44,MT Alcala de Henares,45,7898.49,11414.08,172249.29,0.0459,0.0663,1.45
|
||||||
|
150,Alfafar,131,22094.69,28683.36,171669.07,0.1287,0.1671,1.3
|
||||||
|
114,Rivas,77,11415.44,15251.13,167509.18,0.0681,0.091,1.34
|
||||||
|
58,MT El Bercial,81,17010.34,23055.27,158228.81,0.1075,0.1457,1.36
|
||||||
|
185,MT Avenida de Francia CRISTALES,434,130993.68,130993.68,157172.93,0.8334,0.8334,1
|
||||||
|
69,MT Bahia de Santander,55,8303.33,10238.89,144137.82,0.0576,0.071,1.23
|
||||||
|
182,MT Monasterio CRISTALES,380,112996.56,113678.99,135043.75,0.8367,0.8418,1.01
|
||||||
|
186,Santa Engracia,66,9667.07,13594.88,130549.62,0.074,0.1041,1.41
|
||||||
|
20,MT Castellana,65,8613.86,11728.55,128307.4,0.0671,0.0914,1.36
|
||||||
|
56,MT Costa de Marbella,29,5393.21,6215.55,128083.53,0.0421,0.0485,1.15
|
||||||
|
64,MT Bahia de Cadiz,17,2565.2,2822.03,126955.96,0.0202,0.0222,1.1
|
||||||
|
195,Zaragoza CRISTALES,368,97169.88,98730.64,120766.79,0.8046,0.8175,1.02
|
||||||
|
51,MT Gijon,1,110.99,110.99,118236.41,0.0009,0.0009,1
|
||||||
|
155,Elche,54,8385.01,11009.52,111975.73,0.0749,0.0983,1.31
|
||||||
|
11,Finestrat,68,7470.14,9966.23,111270.18,0.0671,0.0896,1.33
|
||||||
|
199,Barbera CRISTALES,295,78373.14,78926.29,103207.81,0.7594,0.7647,1.01
|
||||||
|
188,San Juan CRISTALES,291,79172.97,79686.13,100873.29,0.7849,0.79,1.01
|
||||||
|
79,Mataro,24,3783.85,5451.63,98348.75,0.0385,0.0554,1.44
|
||||||
|
57,MT Bahia de Algeciras,46,8659.07,9742.71,92987.24,0.0931,0.1048,1.13
|
||||||
|
29,MT Ramon y Cajal,54,9549.58,9598.06,90904.79,0.1051,0.1056,1.01
|
||||||
|
37,MT Tres de Mayo,16,2601.63,2714.24,90660.63,0.0287,0.0299,1.04
|
||||||
|
34,MT Avenida de Francia,27,3381.63,4224.51,90103.94,0.0375,0.0469,1.25
|
||||||
|
190,Store CRISTALES,244,59349.38,59581.83,83733.78,0.7088,0.7116,1
|
||||||
|
32,MT San Juan de Aznalfarache,5,2141.5,3030.57,83374.16,0.0257,0.0363,1.42
|
||||||
|
48,MT Ronda de Cordoba,2,72.48,72.48,77640.25,0.0009,0.0009,1
|
||||||
|
41,MT Monasterio,32,5912.41,7227,77453.79,0.0763,0.0933,1.22
|
||||||
|
18,MT Goya,28,5333.72,6082.91,76017.66,0.0702,0.08,1.14
|
||||||
|
167,Autingo,0,0,0,75778.19,0,0,
|
||||||
|
193,MT Cornella CRISTALES,204,49304.4,49304.4,74662.85,0.6604,0.6604,1
|
||||||
|
40,MT Ctra de Madrid-Irun km. 236,13,2347.9,2575.04,73744.33,0.0318,0.0349,1.1
|
||||||
|
26,MT Malaga,44,4864.2,5052.17,71516.57,0.068,0.0706,1.04
|
||||||
|
43,MT Gran Casa,48,7443.89,8108.43,70587.48,0.1055,0.1149,1.09
|
||||||
|
59,MT Ciudad de Elche,18,2413.82,3232.7,69880.96,0.0345,0.0463,1.34
|
||||||
|
27,MT Nuevo Centro,38,7164.94,7578.46,68997.21,0.1038,0.1098,1.06
|
||||||
|
197,MT Ramon y Cajal CRISTALES,199,62096.18,62157.08,67476.67,0.9203,0.9212,1
|
||||||
|
156,MT Costa Mijas,4,556.5,852.39,65321.85,0.0085,0.013,1.53
|
||||||
|
60,MT Jaen,124,37246.93,37246.93,64164.78,0.5805,0.5805,1
|
||||||
|
200,Unidad Movil Madrid Glass,191,39717.04,39777.94,56923.41,0.6977,0.6988,1
|
||||||
|
31,MT Alexandre Rosello,101,35027.15,35411.35,53680.12,0.6525,0.6597,1.01
|
||||||
|
36,MT Conquistadores,4,618.56,618.56,52295.77,0.0118,0.0118,1
|
||||||
|
38,MT Jerez,15,1827.28,1827.28,52093.94,0.0351,0.0351,1
|
||||||
|
46,MT Puerto Venecia,21,3805.56,5082.72,51536.81,0.0738,0.0986,1.34
|
||||||
|
33,MT El Corte Ingles Cartagena,11,2309.83,3308.05,51368.02,0.045,0.0644,1.43
|
||||||
|
206,Arganda CRISTALES,172,47691.39,47691.39,47874.09,0.9962,0.9962,1
|
||||||
|
23,MT Avenida de la Libertad,12,2541.59,2749.56,45596.45,0.0557,0.0603,1.08
|
||||||
|
28,MT Nervion,29,3469.91,3504.9,41910.86,0.0828,0.0836,1.01
|
||||||
|
66,MT Paseo de Morella,12,2767.14,4367.52,40230.44,0.0688,0.1086,1.58
|
||||||
|
181,Huelva CRISTALES,90,29242.65,29242.65,37407.34,0.7817,0.7817,1
|
||||||
|
198,MT Compostela CRISTALES,87,28174.23,28174.23,36717.66,0.7673,0.7673,1
|
||||||
|
204,Implant Centauro Alicante,337,34975.59,34975.59,35324.21,0.9901,0.9901,1
|
||||||
|
194,MT Maria Auxiliadora CRISTALES,103,27493.09,27875.74,33957.08,0.8096,0.8209,1.01
|
||||||
|
61,MT Avenida de España,0,0,0,32895.15,0,0,
|
||||||
|
30,MT Alicante,12,2753.03,2949.78,32304.57,0.0852,0.0913,1.07
|
||||||
|
25,MT Vigo,11,3193.17,4321.84,32198.1,0.0992,0.1342,1.35
|
||||||
|
180,Puerto de Santa Maria CRISTALES,96,26934.72,27009.61,31691.8,0.8499,0.8523,1
|
||||||
|
53,MT Maria Auxiliadora,3,502.88,623.86,30068.91,0.0167,0.0207,1.24
|
||||||
|
202,Cordoba CRISTALES,76,17913.72,17913.72,29649.87,0.6042,0.6042,1
|
||||||
|
67,MT Talavera de la Reina,5,281.85,281.85,28310.59,0.01,0.01,1
|
||||||
|
203,San Fernando MOVIL,354,20543.85,20631.01,21780.37,0.9432,0.9472,1
|
||||||
|
192,MT Ctra de Madrid-Irun km. 236 CRISTALES,63,18083.01,18083.01,21574.22,0.8382,0.8382,1
|
||||||
|
161,MT Web,0,0,0,7576.48,0,0,
|
||||||
|
184,Aurgi Asociados,0,0,0,620,0,0,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
center_id,center_name,q0_regenerados_aqui,regen_events,dias_avg_regen
|
||||||
|
146,Vaguada,594,1371,4.9
|
||||||
|
54,Leganes,575,1246,5.7
|
||||||
|
75,Store,530,1167,8.1
|
||||||
|
35,MT Sanchinarro,508,1273,5.6
|
||||||
|
168,Goya GLASS,461,519,7.3
|
||||||
|
82,Vallecas,427,895,5.6
|
||||||
|
81,Granada,419,782,5.6
|
||||||
|
55,MT Pozuelo,410,1097,5.3
|
||||||
|
73,Malaga,397,833,5.0
|
||||||
|
121,Villalba,396,927,5.2
|
||||||
|
68,MT Cornella,376,714,14.1
|
||||||
|
130,Alcorcon,349,744,3.0
|
||||||
|
74,La Red,346,852,6.1
|
||||||
|
86,Almeria,327,735,3.6
|
||||||
|
148,Cornella 2,327,495,18.8
|
||||||
|
131,San Juan,326,731,5.7
|
||||||
|
63,San Fernando,325,668,3.6
|
||||||
|
143,Majadahonda,324,722,4.2
|
||||||
|
70,San Sebastian,324,901,2.9
|
||||||
|
72,Las Rozas,320,678,4.4
|
||||||
|
127,Alcobendas,306,765,3.8
|
||||||
|
47,MT Campo de las Naciones,303,730,4.9
|
||||||
|
85,Gta. Cadiz,288,667,4.6
|
||||||
|
136,Emilio Muñoz,282,589,4.5
|
||||||
|
144,Velez Malaga,280,583,5.0
|
||||||
|
137,Avda. Toreros,277,571,4.3
|
||||||
|
76,Barbera,257,786,3.8
|
||||||
|
58,MT El Bercial,236,564,3.5
|
||||||
|
15,Islazul,234,506,4.3
|
||||||
|
187,Santa Engracia CRISTALES,230,269,7.4
|
||||||
|
@@ -0,0 +1,221 @@
|
|||||||
|
{
|
||||||
|
"cols": [
|
||||||
|
"center_id",
|
||||||
|
"center_name",
|
||||||
|
"q0_regenerados_aqui",
|
||||||
|
"regen_events",
|
||||||
|
"dias_avg_regen"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
146,
|
||||||
|
"Vaguada",
|
||||||
|
594,
|
||||||
|
1371,
|
||||||
|
4.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
54,
|
||||||
|
"Leganes",
|
||||||
|
575,
|
||||||
|
1246,
|
||||||
|
5.7
|
||||||
|
],
|
||||||
|
[
|
||||||
|
75,
|
||||||
|
"Store",
|
||||||
|
530,
|
||||||
|
1167,
|
||||||
|
8.1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
35,
|
||||||
|
"MT Sanchinarro",
|
||||||
|
508,
|
||||||
|
1273,
|
||||||
|
5.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
168,
|
||||||
|
"Goya GLASS",
|
||||||
|
461,
|
||||||
|
519,
|
||||||
|
7.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
82,
|
||||||
|
"Vallecas",
|
||||||
|
427,
|
||||||
|
895,
|
||||||
|
5.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
81,
|
||||||
|
"Granada",
|
||||||
|
419,
|
||||||
|
782,
|
||||||
|
5.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
55,
|
||||||
|
"MT Pozuelo",
|
||||||
|
410,
|
||||||
|
1097,
|
||||||
|
5.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
73,
|
||||||
|
"Malaga",
|
||||||
|
397,
|
||||||
|
833,
|
||||||
|
5.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
121,
|
||||||
|
"Villalba",
|
||||||
|
396,
|
||||||
|
927,
|
||||||
|
5.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
68,
|
||||||
|
"MT Cornella",
|
||||||
|
376,
|
||||||
|
714,
|
||||||
|
14.1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
130,
|
||||||
|
"Alcorcon",
|
||||||
|
349,
|
||||||
|
744,
|
||||||
|
3.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
74,
|
||||||
|
"La Red",
|
||||||
|
346,
|
||||||
|
852,
|
||||||
|
6.1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
86,
|
||||||
|
"Almeria",
|
||||||
|
327,
|
||||||
|
735,
|
||||||
|
3.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
148,
|
||||||
|
"Cornella 2",
|
||||||
|
327,
|
||||||
|
495,
|
||||||
|
18.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
131,
|
||||||
|
"San Juan",
|
||||||
|
326,
|
||||||
|
731,
|
||||||
|
5.7
|
||||||
|
],
|
||||||
|
[
|
||||||
|
63,
|
||||||
|
"San Fernando",
|
||||||
|
325,
|
||||||
|
668,
|
||||||
|
3.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
143,
|
||||||
|
"Majadahonda",
|
||||||
|
324,
|
||||||
|
722,
|
||||||
|
4.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
70,
|
||||||
|
"San Sebastian",
|
||||||
|
324,
|
||||||
|
901,
|
||||||
|
2.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
72,
|
||||||
|
"Las Rozas",
|
||||||
|
320,
|
||||||
|
678,
|
||||||
|
4.4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
127,
|
||||||
|
"Alcobendas",
|
||||||
|
306,
|
||||||
|
765,
|
||||||
|
3.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
47,
|
||||||
|
"MT Campo de las Naciones",
|
||||||
|
303,
|
||||||
|
730,
|
||||||
|
4.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
85,
|
||||||
|
"Gta. Cadiz",
|
||||||
|
288,
|
||||||
|
667,
|
||||||
|
4.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
136,
|
||||||
|
"Emilio Mu\u00f1oz",
|
||||||
|
282,
|
||||||
|
589,
|
||||||
|
4.5
|
||||||
|
],
|
||||||
|
[
|
||||||
|
144,
|
||||||
|
"Velez Malaga",
|
||||||
|
280,
|
||||||
|
583,
|
||||||
|
5.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
137,
|
||||||
|
"Avda. Toreros",
|
||||||
|
277,
|
||||||
|
571,
|
||||||
|
4.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
76,
|
||||||
|
"Barbera",
|
||||||
|
257,
|
||||||
|
786,
|
||||||
|
3.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
58,
|
||||||
|
"MT El Bercial",
|
||||||
|
236,
|
||||||
|
564,
|
||||||
|
3.5
|
||||||
|
],
|
||||||
|
[
|
||||||
|
15,
|
||||||
|
"Islazul",
|
||||||
|
234,
|
||||||
|
506,
|
||||||
|
4.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
187,
|
||||||
|
"Santa Engracia CRISTALES",
|
||||||
|
230,
|
||||||
|
269,
|
||||||
|
7.4
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
bucket,q0_total,q0_facturado_propio,conv_q0_propio
|
||||||
|
no_regenerado,35507,22390,0.6306
|
||||||
|
regenerado,18488,7158,0.3872
|
||||||
|
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"cols": [
|
||||||
|
"bucket",
|
||||||
|
"q0_total",
|
||||||
|
"q0_facturado_propio",
|
||||||
|
"conv_q0_propio"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
[
|
||||||
|
"no_regenerado",
|
||||||
|
35507,
|
||||||
|
22390,
|
||||||
|
0.6306
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"regenerado",
|
||||||
|
18488,
|
||||||
|
7158,
|
||||||
|
0.3872
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"window_days": 90,
|
||||||
|
"A_quote_cc_eur": 6392965.08,
|
||||||
|
"B_mismo_cliente_eur": 6923203.81,
|
||||||
|
"C_total_centros_eur": 29635811.36,
|
||||||
|
"A_sobre_C": 0.2157,
|
||||||
|
"B_sobre_C": 0.2336,
|
||||||
|
"lift_B_vs_A": 1.08,
|
||||||
|
"centros_activos": 139
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Remove project prefix `autingo-159109.` from cards 10221-10229.
|
||||||
|
|
||||||
|
Reason: Metabase field-filter substitution emits `psql_dcpublic.<table>.<col>` (no project).
|
||||||
|
BigQuery rejects since the rest of the SQL uses `autingo-159109.psql_dcpublic.<table>`.
|
||||||
|
Mixed qualifiers in same query break.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
|
||||||
|
client = httpx.Client(base_url=BASE, headers={"x-api-key": API_KEY}, timeout=120)
|
||||||
|
|
||||||
|
for cid in range(10221, 10230):
|
||||||
|
card = client.get(f"/api/card/{cid}").json()
|
||||||
|
dq = card["dataset_query"]
|
||||||
|
# MBQL5 stages format: dq["stages"][0]["native"] is the string, "template-tags" lives at stage level
|
||||||
|
stage = dq["stages"][0]
|
||||||
|
sql = stage["native"]
|
||||||
|
new_sql = sql.replace("`autingo-159109.psql_dcpublic.", "`psql_dcpublic.")
|
||||||
|
if new_sql == sql:
|
||||||
|
print(f" {cid}: no change")
|
||||||
|
continue
|
||||||
|
# Rewrite as legacy native query (Metabase writes legacy on PUT)
|
||||||
|
new_dq = {
|
||||||
|
"type": "native",
|
||||||
|
"database": dq.get("database", 6),
|
||||||
|
"native": {
|
||||||
|
"query": new_sql,
|
||||||
|
"template-tags": stage.get("template-tags", {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r = client.put(f"/api/card/{cid}", json={"dataset_query": new_dq})
|
||||||
|
r.raise_for_status()
|
||||||
|
print(f" {cid}: updated")
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
print("done")
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""B = (A) UNION (regenerados): vuelve la union al SQL de cards B.
|
||||||
|
|
||||||
|
Mantiene los field-filters por NAME (multi-select dropdown) ya aplicados en rewire_filters_by_name.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
DB_ID = 6
|
||||||
|
|
||||||
|
F_QUOTE_CREATED_AT = 16588
|
||||||
|
F_CENTER_NAME = 17330
|
||||||
|
F_TPVUSER_NAME = 17958
|
||||||
|
F_COMPANY_NAME = 17158
|
||||||
|
F_PRODUCT_DESC = 16795
|
||||||
|
|
||||||
|
client = httpx.Client(base_url=BASE, headers={"x-api-key": API_KEY}, timeout=180)
|
||||||
|
|
||||||
|
|
||||||
|
def field_filter_tag(name, field_id, widget, display_name):
|
||||||
|
return {
|
||||||
|
"id": name + "-tag",
|
||||||
|
"name": name,
|
||||||
|
"display-name": display_name,
|
||||||
|
"type": "dimension",
|
||||||
|
"dimension": ["field", field_id, None],
|
||||||
|
"widget-type": widget,
|
||||||
|
"default": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TAGS_AB = {
|
||||||
|
"date": field_filter_tag("date", F_QUOTE_CREATED_AT, "date/range", "Fecha presupuesto"),
|
||||||
|
"centro": field_filter_tag("centro", F_CENTER_NAME, "string/=", "Centro"),
|
||||||
|
"agente": field_filter_tag("agente", F_TPVUSER_NAME, "string/=", "Agente CC"),
|
||||||
|
"compania": field_filter_tag("compania", F_COMPANY_NAME, "string/=", "Compañía"),
|
||||||
|
"producto": field_filter_tag("producto", F_PRODUCT_DESC, "string/contains", "Producto"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SQL_B = """
|
||||||
|
WITH cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
quotes_q1 AS (SELECT id, order_id, created_at, deleted_at FROM `psql_dcpublic.tpv_orders_quote`),
|
||||||
|
orders_q1 AS (SELECT id, customer_id, vehicle_id, terminal_id FROM `psql_dcpublic.tpv_orders_order`),
|
||||||
|
terminals_q1 AS (SELECT id, center_id FROM `psql_dcpublic.tpv_terminals`),
|
||||||
|
cc_anchored AS (
|
||||||
|
SELECT
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.id AS q0_id,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.order_id AS q0_order,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.created_at AS q0_ts,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.created_by_id AS cc_agent_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.customer_id AS cust_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.vehicle_id AS veh_id
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN cc_users ON `psql_dcpublic.tpv_orders_quote`.created_by_id = cc_users.user_id
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_authorization_tpvuser` ON `psql_dcpublic.tpv_orders_quote`.created_by_id = `psql_dcpublic.tpv_authorization_tpvuser`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{agente}}]]
|
||||||
|
),
|
||||||
|
b_orders AS (
|
||||||
|
-- A: orders directos del Q-CC
|
||||||
|
SELECT q0_order AS order_id FROM cc_anchored
|
||||||
|
UNION DISTINCT
|
||||||
|
-- Regenerados: Q1 dentro 60d desde Q0-CC, distinto order, centro NO-CC, mismo cliente+vehiculo
|
||||||
|
SELECT q1.order_id AS order_id
|
||||||
|
FROM cc_anchored a
|
||||||
|
JOIN quotes_q1 q1
|
||||||
|
ON q1.deleted_at IS NULL
|
||||||
|
AND q1.created_at > a.q0_ts
|
||||||
|
AND q1.created_at <= TIMESTAMP_ADD(a.q0_ts, INTERVAL 60 DAY)
|
||||||
|
AND q1.order_id != a.q0_order
|
||||||
|
JOIN orders_q1 o1 ON q1.order_id = o1.id
|
||||||
|
LEFT JOIN terminals_q1 t1 ON o1.terminal_id = t1.id
|
||||||
|
WHERE o1.customer_id = a.cust_id
|
||||||
|
AND o1.vehicle_id = a.veh_id
|
||||||
|
AND t1.center_id IS NOT NULL
|
||||||
|
AND t1.center_id NOT IN (159, 162)
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM b_orders
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON b_orders.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.products` ON `psql_dcpublic.tpv_orders_orderitem`.product_id = `psql_dcpublic.products`.id
|
||||||
|
WHERE COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
AGG = {
|
||||||
|
"total": "ROUND(SUM(total_cost), 2)",
|
||||||
|
"count": "COUNT(*)",
|
||||||
|
"ticket": "ROUND(SAFE_DIVIDE(SUM(total_cost), NULLIF(COUNT(*), 0)), 2)",
|
||||||
|
}
|
||||||
|
|
||||||
|
CARDS = {
|
||||||
|
10251: ("total", "B · Presupuestos CallC + Regenerados — Total facturado"),
|
||||||
|
10252: ("count", "B · Presupuestos CallC + Regenerados — # Facturas"),
|
||||||
|
10253: ("ticket", "B · Presupuestos CallC + Regenerados — Ticket medio"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for cid, (agg, name) in CARDS.items():
|
||||||
|
cur = client.get(f"/api/card/{cid}").json()
|
||||||
|
viz = cur.get("visualization_settings", {})
|
||||||
|
sql = SQL_B.replace("__AGG__", AGG[agg])
|
||||||
|
body = {
|
||||||
|
"name": name,
|
||||||
|
"description": name,
|
||||||
|
"dataset_query": {
|
||||||
|
"type": "native", "database": DB_ID,
|
||||||
|
"native": {"query": sql, "template-tags": TAGS_AB},
|
||||||
|
},
|
||||||
|
"visualization_settings": viz,
|
||||||
|
}
|
||||||
|
r = client.put(f"/api/card/{cid}", json=body)
|
||||||
|
r.raise_for_status()
|
||||||
|
print(f" card {cid} ({agg}) updated -> name='{name}'")
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
print("done")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from presupuestos-callcenter!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "2b3d1ae8",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 00 — Resultados ejecutados (vía Metabase, sin ADC)\n",
|
||||||
|
"\n",
|
||||||
|
"Resultados de la ejecución del script `run_via_metabase.py` (BigQuery `autingo-159109.psql_dcpublic`).\n",
|
||||||
|
"\n",
|
||||||
|
"Ventana: 90 días Q0, 60 días para detectar regeneración.\n",
|
||||||
|
"Centros call_center excluidos del cómputo: 159 (CALL CENTER AURGI), 162 (CALL CENTER)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "c39f6e2c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{'window_days': 90,\n",
|
||||||
|
" 'A_quote_cc_eur': 6392965.08,\n",
|
||||||
|
" 'B_mismo_cliente_eur': 6923203.81,\n",
|
||||||
|
" 'C_total_centros_eur': 29635811.36,\n",
|
||||||
|
" 'A_sobre_C': 0.2157,\n",
|
||||||
|
" 'B_sobre_C': 0.2336,\n",
|
||||||
|
" 'lift_B_vs_A': 1.08,\n",
|
||||||
|
" 'centros_activos': 139}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 1,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"import json\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"\n",
|
||||||
|
"BASE = Path('../data/results').resolve()\n",
|
||||||
|
"\n",
|
||||||
|
"def load(name):\n",
|
||||||
|
" return pd.read_csv(BASE / f'{name}.csv')\n",
|
||||||
|
"\n",
|
||||||
|
"totales = json.loads((BASE / 'totales_globales.json').read_text())\n",
|
||||||
|
"totales"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "33adaf14",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Q1 — Tasa de conversión por origen del usuario que generó la quote"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "24471a70",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"load('01_conversion_origen')"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "5847ffda",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"**Lectura:** \n",
|
||||||
|
"- Call center genera 62.8K quotes / 90d, convierte 47.1% (29.6K facturas con mismo `order_id`).\n",
|
||||||
|
"- Otros usuarios generan 477K quotes / 90d, convierten 57.4% (273.8K facturas).\n",
|
||||||
|
"- Brecha de ~10pp es esperable: el call_center genera quote en frío (cliente no presente), el TPV de centro genera quote casi siempre con el cliente ya en mostrador."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "3484eeb6",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Q2 — 3 KPI por centro\n",
|
||||||
|
"\n",
|
||||||
|
"- **A** = € facturados desde quotes creados por call_center (mismo order_id).\n",
|
||||||
|
"- **B** = € facturados a los mismos clientes (`customer_id` + `vehicle_id`) en centros físicos NO call_center.\n",
|
||||||
|
"- **C** = € facturados totales del centro (todos los clientes).\n",
|
||||||
|
"- `lift_B_vs_A`: 1.0 = solo factura el quote inicial; >1 = el centro factura más al cliente que sólo el quote."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "f2bc0458",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"df = load('02_kpi_3_por_centro')\n",
|
||||||
|
"df.head(15)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "201ac95f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Top 15 centros por valor A (más facturado vía call_center)\n",
|
||||||
|
"top = df.sort_values('A_quote_cc_eur', ascending=False).head(15).iloc[::-1]\n",
|
||||||
|
"fig, ax = plt.subplots(figsize=(10, 7))\n",
|
||||||
|
"y = range(len(top))\n",
|
||||||
|
"ax.barh(y, top.A_quote_cc_eur, label='A (cc -> factura)')\n",
|
||||||
|
"ax.barh(y, (top.B_mismo_cliente_eur - top.A_quote_cc_eur).clip(lower=0),\n",
|
||||||
|
" left=top.A_quote_cc_eur, label='B-A (mismo cliente extra)')\n",
|
||||||
|
"ax.set_yticks(list(y)); ax.set_yticklabels(top.center_name)\n",
|
||||||
|
"ax.set_xlabel('€ facturados (90d)')\n",
|
||||||
|
"ax.legend(); ax.set_title('Top 15 centros — quotes call_center -> factura')\n",
|
||||||
|
"plt.tight_layout(); plt.show()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "291fa4af",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"print('TOTALES 90d (excluye centros call_center 159/162):')\n",
|
||||||
|
"for k, v in totales.items():\n",
|
||||||
|
" print(f' {k:25} {v}')"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "3d161868",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Q3 — Centros que MÁS regeneran el presupuesto\n",
|
||||||
|
"\n",
|
||||||
|
"Definición operativa: para un par `(customer_id, vehicle_id)` cuyo primer presupuesto (Q0) lo abrió el call_center, hay un Q1+ posterior con **distinto `order_id`** abierto en un terminal del centro físico dentro de 60 días."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "7831e9fe",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"regen = load('03_regen_por_centro')\n",
|
||||||
|
"regen"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "83758dc9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"top_regen = regen.head(15).iloc[::-1]\n",
|
||||||
|
"fig, ax = plt.subplots(figsize=(10, 6))\n",
|
||||||
|
"y = range(len(top_regen))\n",
|
||||||
|
"ax.barh(y, top_regen.q0_regenerados_aqui)\n",
|
||||||
|
"ax.set_yticks(list(y)); ax.set_yticklabels(top_regen.center_name)\n",
|
||||||
|
"ax.set_xlabel('# Q0 (de call_center) regenerados en este centro')\n",
|
||||||
|
"ax.set_title('Top centros regeneradores de presupuesto call_center (90d Q0, 60d window)')\n",
|
||||||
|
"plt.tight_layout(); plt.show()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "b0f6c55c",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Q4 — ¿Regenerar perjudica la conversión propia del Q0?\n",
|
||||||
|
"\n",
|
||||||
|
"Conversión del Q0 = el invoice se genera contra el MISMO order_id del Q0 (no contra el order_id regenerado en centro). Si regeneran, ese flujo cae."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "423b68cd",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"load('04_regen_vs_conversion')"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "4956e2d1",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"**Lectura:**\n",
|
||||||
|
"- 35.5K Q0 sin regeneración convierten al **63.1%** (sobre el order_id original).\n",
|
||||||
|
"- 18.5K Q0 con regeneración convierten al **38.7%** sobre el order_id original.\n",
|
||||||
|
"- Los 'regenerados' no se 'pierden' necesariamente — el cliente puede haberse facturado vía un order_id distinto (el del centro). Esa parte está capturada en el KPI **B** del cuadro anterior.\n",
|
||||||
|
"- Aprox **34.2% de los Q0 call_center** entran en patrón de regeneración (18488 / 53995)."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "07306d98",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 01 — Exploración: quotes ↔ call_center ↔ factura\n",
|
||||||
|
"\n",
|
||||||
|
"Mapa de tablas y joins en `psql_dcpublic` (BigQuery `autingo-159109`).\n",
|
||||||
|
"\n",
|
||||||
|
"## Cadena de joins\n",
|
||||||
|
"\n",
|
||||||
|
"```\n",
|
||||||
|
"tpv_authorization_tpvuser_centers (dccenter_id ∈ {159 CALL_CENTER_AURGI, 162 CALL_CENTER})\n",
|
||||||
|
" │ tpvuser_id\n",
|
||||||
|
" ▼\n",
|
||||||
|
"tpv_orders_quote.created_by_id ──► quote por agente call_center\n",
|
||||||
|
" │ order_id\n",
|
||||||
|
" ▼\n",
|
||||||
|
"tpv_orders_order ─► terminal_id ─► tpv_terminals.center_id ─► centers (centro real de facturación)\n",
|
||||||
|
" │ │ customer_id │ vehicle_id\n",
|
||||||
|
" ▼ ▼ ▼\n",
|
||||||
|
"tpv_orders_invoice (status convertido) tpv_customers (tlf) tpv_vehicles_vehicle (matrícula)\n",
|
||||||
|
"```\n",
|
||||||
|
"\n",
|
||||||
|
"Identidad cliente = `(customer_id, vehicle_id)` o (tlf, matrícula) según necesite normalización."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "d2b6d545",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import os, sys\n",
|
||||||
|
"from google.cloud import bigquery\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"PROJECT = \"autingo-159109\"\n",
|
||||||
|
"DATASET = \"psql_dcpublic\"\n",
|
||||||
|
"bq = bigquery.Client(project=PROJECT)\n",
|
||||||
|
"\n",
|
||||||
|
"def q(sql):\n",
|
||||||
|
" return bq.query(sql).to_dataframe()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "3238b92e",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 1. Usuarios call_center"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "34d656ad",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"cc_users = q(f\"\"\"\n",
|
||||||
|
"SELECT u.id, u.name, u.email, u.is_active, u.role_id,\n",
|
||||||
|
" STRING_AGG(CAST(uc.dccenter_id AS STRING)) AS centers\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser` u\n",
|
||||||
|
"JOIN `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers` uc\n",
|
||||||
|
" ON u.id = uc.tpvuser_id\n",
|
||||||
|
"WHERE uc.dccenter_id IN (159, 162)\n",
|
||||||
|
"GROUP BY 1,2,3,4,5\n",
|
||||||
|
"ORDER BY u.is_active DESC, u.id\n",
|
||||||
|
"\"\"\")\n",
|
||||||
|
"print(f\"Total usuarios call_center: {len(cc_users)} (activos: {cc_users.is_active.sum()})\")\n",
|
||||||
|
"cc_users.head(20)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "9ec102fb",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 2. Schema quote — campos clave"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "e5f94b52",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"SELECT column_name, data_type\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.INFORMATION_SCHEMA.COLUMNS`\n",
|
||||||
|
"WHERE table_name='tpv_orders_quote'\n",
|
||||||
|
"ORDER BY ordinal_position\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "32ffc1d2",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 3. Distribución `status` y `accepted` (últimos 90d)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "609022a9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"SELECT status, accepted, COUNT(*) n\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_orders_quote`\n",
|
||||||
|
"WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)\n",
|
||||||
|
" AND deleted_at IS NULL\n",
|
||||||
|
"GROUP BY status, accepted\n",
|
||||||
|
"ORDER BY n DESC\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "73c85198",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 4. Conversion quote → invoice (mismo order_id)\n",
|
||||||
|
"\n",
|
||||||
|
"Una quote convierte cuando existe `tpv_orders_invoice` con el mismo `order_id`. Ese invoice fija la facturación real (NAV-sync via `nav_id`)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "46378f84",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" COUNT(*) AS quotes,\n",
|
||||||
|
" COUNT(DISTINCT q.order_id) AS distinct_orders,\n",
|
||||||
|
" SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END) AS quotes_con_invoice,\n",
|
||||||
|
" SAFE_DIVIDE(SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END), COUNT(*)) AS conversion_rate\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
"LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i\n",
|
||||||
|
" ON q.order_id = i.order_id\n",
|
||||||
|
"WHERE q.created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "df8b402c",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 5. Sanity: quote por call_center vs otro\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "506744d3",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"WITH cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" CASE WHEN cc.user_id IS NOT NULL THEN 'call_center' ELSE 'otro' END AS origen_user,\n",
|
||||||
|
" COUNT(*) AS quotes,\n",
|
||||||
|
" SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END) AS convertidos,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END), COUNT(*)), 3) AS conv_rate\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
"LEFT JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
"LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON q.order_id = i.order_id\n",
|
||||||
|
"WHERE q.created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
"GROUP BY 1\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "36ae91c9",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 02 — 3 KPI principales\n",
|
||||||
|
"\n",
|
||||||
|
"Por **centro real de facturación** (`tpv_terminals.center_id` del invoice) y ventana temporal:\n",
|
||||||
|
"\n",
|
||||||
|
"1. **A — Valor facturado de quotes call_center que CONVIRTIERON** \n",
|
||||||
|
" Quote creado por usuario call_center + existe `tpv_orders_invoice` con el mismo `order_id`. Sumamos `tpv_orders_order.total_cost` o líneas. Centro = centro del invoice.\n",
|
||||||
|
"\n",
|
||||||
|
"2. **B — Valor facturado total a esos mismos clientes en centros** \n",
|
||||||
|
" Misma identidad cliente (`customer_id` y/o `vehicle_id` y/o `tlf`+`matricula` normalizados). Todas las facturas del cliente en ese centro en la misma ventana. Debe ser ≥ A.\n",
|
||||||
|
"\n",
|
||||||
|
"3. **C — Facturación total del centro** \n",
|
||||||
|
" Suma de invoices del centro en la ventana, todos los clientes."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "7938921f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from google.cloud import bigquery\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"PROJECT = \"autingo-159109\"\n",
|
||||||
|
"DATASET = \"psql_dcpublic\"\n",
|
||||||
|
"bq = bigquery.Client(project=PROJECT)\n",
|
||||||
|
"\n",
|
||||||
|
"WINDOW_DAYS = 90\n",
|
||||||
|
"\n",
|
||||||
|
"def q(sql):\n",
|
||||||
|
" return bq.query(sql).to_dataframe()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "c7cfa785",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Setup: CTEs base reutilizables\n",
|
||||||
|
"\n",
|
||||||
|
"Construimos una query maestra con CTEs para A, B, C juntos por centro."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "3e06df33",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_KPI = f\"\"\"\n",
|
||||||
|
"DECLARE window_days INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL window_days DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Quotes creados por call_center que TIENEN invoice (convertidos)\n",
|
||||||
|
"cc_converted AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q.id AS quote_id,\n",
|
||||||
|
" q.order_id,\n",
|
||||||
|
" q.created_at AS quote_ts,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id,\n",
|
||||||
|
" o.terminal_id,\n",
|
||||||
|
" t.center_id,\n",
|
||||||
|
" o.total_cost,\n",
|
||||||
|
" i.id AS invoice_id,\n",
|
||||||
|
" i.created_at AS invoice_ts\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON i.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.created_at >= t_start\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Clientes \"tocados\" por call_center (customer_id + vehicle_id)\n",
|
||||||
|
"cc_clients AS (\n",
|
||||||
|
" SELECT DISTINCT center_id, customer_id, vehicle_id\n",
|
||||||
|
" FROM cc_converted\n",
|
||||||
|
" WHERE customer_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Todas las facturas en la ventana, con centro real\n",
|
||||||
|
"all_invoices AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" i.id AS invoice_id,\n",
|
||||||
|
" i.order_id,\n",
|
||||||
|
" i.created_at AS invoice_ts,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id,\n",
|
||||||
|
" o.terminal_id,\n",
|
||||||
|
" t.center_id,\n",
|
||||||
|
" o.total_cost\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_invoice` i\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON i.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE i.created_at >= t_start\n",
|
||||||
|
"),\n",
|
||||||
|
"-- B: facturas de los mismos clientes en cualquier centro NO call_center\n",
|
||||||
|
"client_invoices_in_centers AS (\n",
|
||||||
|
" SELECT ai.*\n",
|
||||||
|
" FROM all_invoices ai\n",
|
||||||
|
" JOIN cc_clients cc ON ai.customer_id = cc.customer_id\n",
|
||||||
|
" WHERE ai.center_id NOT IN (159, 162) -- excluye los propios centros call_center\n",
|
||||||
|
"),\n",
|
||||||
|
"kpi_a AS (\n",
|
||||||
|
" SELECT center_id,\n",
|
||||||
|
" COUNT(DISTINCT quote_id) AS quotes_cc_convertidos,\n",
|
||||||
|
" COUNT(DISTINCT invoice_id) AS invoices_a,\n",
|
||||||
|
" SUM(total_cost) AS valor_a\n",
|
||||||
|
" FROM cc_converted\n",
|
||||||
|
" WHERE center_id IS NOT NULL\n",
|
||||||
|
" GROUP BY center_id\n",
|
||||||
|
"),\n",
|
||||||
|
"kpi_b AS (\n",
|
||||||
|
" SELECT center_id,\n",
|
||||||
|
" COUNT(DISTINCT invoice_id) AS invoices_b,\n",
|
||||||
|
" SUM(total_cost) AS valor_b\n",
|
||||||
|
" FROM client_invoices_in_centers\n",
|
||||||
|
" GROUP BY center_id\n",
|
||||||
|
"),\n",
|
||||||
|
"kpi_c AS (\n",
|
||||||
|
" SELECT center_id,\n",
|
||||||
|
" COUNT(DISTINCT invoice_id) AS invoices_c,\n",
|
||||||
|
" SUM(total_cost) AS valor_c\n",
|
||||||
|
" FROM all_invoices\n",
|
||||||
|
" WHERE center_id IS NOT NULL\n",
|
||||||
|
" GROUP BY center_id\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" c.id AS center_id,\n",
|
||||||
|
" c.name AS center_name,\n",
|
||||||
|
" COALESCE(a.quotes_cc_convertidos, 0) AS quotes_cc_convertidos,\n",
|
||||||
|
" ROUND(COALESCE(a.valor_a, 0), 2) AS A_valor_quote_cc_convertido,\n",
|
||||||
|
" ROUND(COALESCE(b.valor_b, 0), 2) AS B_valor_mismo_cliente_centro,\n",
|
||||||
|
" ROUND(COALESCE(c2.valor_c, 0), 2) AS C_valor_total_centro,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(COALESCE(a.valor_a, 0), c2.valor_c), 4) AS A_sobre_C,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(COALESCE(b.valor_b, 0), c2.valor_c), 4) AS B_sobre_C\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.centers` c\n",
|
||||||
|
"LEFT JOIN kpi_a a ON c.id = a.center_id\n",
|
||||||
|
"LEFT JOIN kpi_b b ON c.id = b.center_id\n",
|
||||||
|
"LEFT JOIN kpi_c c2 ON c.id = c2.center_id\n",
|
||||||
|
"WHERE c.id NOT IN (159, 162)\n",
|
||||||
|
" AND COALESCE(c2.valor_c, 0) > 0\n",
|
||||||
|
"ORDER BY C_valor_total_centro DESC\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"df = q(SQL_KPI)\n",
|
||||||
|
"print(f\"Centros con actividad ({WINDOW_DAYS}d): {len(df)}\")\n",
|
||||||
|
"df.head(30)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "724baf5c",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Totales globales"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "75c3297e",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"totals = df[[\"A_valor_quote_cc_convertido\", \"B_valor_mismo_cliente_centro\", \"C_valor_total_centro\"]].sum()\n",
|
||||||
|
"print(totals.to_string())\n",
|
||||||
|
"print()\n",
|
||||||
|
"print(f\"A/C global: {totals.A_valor_quote_cc_convertido / totals.C_valor_total_centro:.4f}\")\n",
|
||||||
|
"print(f\"B/C global: {totals.B_valor_mismo_cliente_centro / totals.C_valor_total_centro:.4f}\")\n",
|
||||||
|
"print(f\"Lift B vs A: {totals.B_valor_mismo_cliente_centro / totals.A_valor_quote_cc_convertido:.2f}x\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0fba60db",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Top 15 centros por A (valor traído por call_center)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "f90abe7e",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"\n",
|
||||||
|
"top = df.sort_values(\"A_valor_quote_cc_convertido\", ascending=False).head(15)\n",
|
||||||
|
"fig, ax = plt.subplots(figsize=(10, 6))\n",
|
||||||
|
"x = range(len(top))\n",
|
||||||
|
"ax.barh(x, top.A_valor_quote_cc_convertido, label=\"A (cc → factura)\")\n",
|
||||||
|
"ax.barh(x, top.B_valor_mismo_cliente_centro - top.A_valor_quote_cc_convertido,\n",
|
||||||
|
" left=top.A_valor_quote_cc_convertido, label=\"B−A (mismo cliente extra)\")\n",
|
||||||
|
"ax.set_yticks(x)\n",
|
||||||
|
"ax.set_yticklabels(top.center_name)\n",
|
||||||
|
"ax.invert_yaxis()\n",
|
||||||
|
"ax.set_xlabel(\"€ facturados\")\n",
|
||||||
|
"ax.legend()\n",
|
||||||
|
"ax.set_title(f\"Top 15 centros — quotes call_center ({WINDOW_DAYS}d)\")\n",
|
||||||
|
"plt.tight_layout()\n",
|
||||||
|
"plt.show()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "b090f346",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 03 — Regeneración de presupuestos\n",
|
||||||
|
"\n",
|
||||||
|
"**Hipótesis:** un mismo cliente (`customer_id` + `vehicle_id`) recibe N quotes antes de convertir. El centro \"regenera\" el presupuesto cuando descarta el de call_center y abre uno nuevo en TPV local.\n",
|
||||||
|
"\n",
|
||||||
|
"Definición operativa de regeneración:\n",
|
||||||
|
"- Existe quote call_center previa (Q0) para el par cliente+vehículo.\n",
|
||||||
|
"- Existe quote posterior (Q1...Qn) en un terminal de centro NO call_center, dentro de ventana D días.\n",
|
||||||
|
"- Q1 puede tener distinto `order_id` que Q0 (regenera de cero) o mismo (reescribe — menos común).\n",
|
||||||
|
"\n",
|
||||||
|
"Métricas pedidas:\n",
|
||||||
|
"1. Centros que MÁS regeneran (cuentan regeneraciones absolutas y % sobre quotes call_center recibidos).\n",
|
||||||
|
"2. Quotes call_center con regeneración vs sin regeneración."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5626c2cd",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from google.cloud import bigquery\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"PROJECT = \"autingo-159109\"\n",
|
||||||
|
"DATASET = \"psql_dcpublic\"\n",
|
||||||
|
"bq = bigquery.Client(project=PROJECT)\n",
|
||||||
|
"\n",
|
||||||
|
"WINDOW_DAYS = 90 # ventana de análisis sobre quote call_center\n",
|
||||||
|
"REGEN_WINDOW_DAYS = 60 # ventana para detectar regeneración posterior\n",
|
||||||
|
"\n",
|
||||||
|
"def q(sql):\n",
|
||||||
|
" return bq.query(sql).to_dataframe()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "17cff6ce",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_REGEN = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Q0: quotes generados por call_center\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q.id AS q0_id,\n",
|
||||||
|
" q.order_id AS q0_order,\n",
|
||||||
|
" q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL\n",
|
||||||
|
" AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Q1..Qn: quotes posteriores para mismo cliente+vehículo, en centro NO call_center\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q.id AS qn_id,\n",
|
||||||
|
" q.order_id AS qn_order,\n",
|
||||||
|
" q.created_at AS qn_ts,\n",
|
||||||
|
" q.created_by_id,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id,\n",
|
||||||
|
" o.terminal_id,\n",
|
||||||
|
" t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL\n",
|
||||||
|
" AND t.center_id NOT IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Empareja Q0 con Q1+ dentro de regen_win días\n",
|
||||||
|
"regen AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q0.q0_id,\n",
|
||||||
|
" q0.q0_order,\n",
|
||||||
|
" q0.customer_id,\n",
|
||||||
|
" q0.vehicle_id,\n",
|
||||||
|
" qN.qn_id,\n",
|
||||||
|
" qN.qn_order,\n",
|
||||||
|
" qN.center_id AS regen_center,\n",
|
||||||
|
" TIMESTAMP_DIFF(qN.qn_ts, q0.q0_ts, HOUR) / 24 AS dias_entre\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Para cada Q0, ¿hay al menos UNA regeneración?\n",
|
||||||
|
"q0_has_regen AS (\n",
|
||||||
|
" SELECT q0_id, COUNT(*) AS regen_count,\n",
|
||||||
|
" MIN(dias_entre) AS dias_a_regen,\n",
|
||||||
|
" APPROX_TOP_COUNT(regen_center, 1)[OFFSET(0)].value AS first_regen_center\n",
|
||||||
|
" FROM regen\n",
|
||||||
|
" GROUP BY q0_id\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"-- Vista por centro: cuántos Q0 regenera cada centro\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" c.id AS center_id,\n",
|
||||||
|
" c.name AS center_name,\n",
|
||||||
|
" COUNT(DISTINCT r.q0_id) AS q0_regenerados_aqui,\n",
|
||||||
|
" COUNT(*) AS regen_events,\n",
|
||||||
|
" ROUND(AVG(r.dias_entre), 1) AS dias_avg_regen\n",
|
||||||
|
"FROM regen r\n",
|
||||||
|
"JOIN `{PROJECT}.{DATASET}.centers` c ON r.regen_center = c.id\n",
|
||||||
|
"GROUP BY c.id, c.name\n",
|
||||||
|
"ORDER BY q0_regenerados_aqui DESC\n",
|
||||||
|
"LIMIT 30\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"df_centros = q(SQL_REGEN)\n",
|
||||||
|
"print(f\"Centros con eventos de regeneración: {len(df_centros)}\")\n",
|
||||||
|
"df_centros.head(30)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "43add847",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Totales: Q0 con regeneración vs sin regeneración"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "736158ba",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_TOT = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT q.order_id AS qn_order, q.created_at AS qn_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id, t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)\n",
|
||||||
|
"),\n",
|
||||||
|
"regen AS (\n",
|
||||||
|
" SELECT DISTINCT q0.q0_id\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" COUNT(*) AS q0_total,\n",
|
||||||
|
" COUNT(DISTINCT r.q0_id) AS q0_regenerados,\n",
|
||||||
|
" COUNT(*) - COUNT(DISTINCT r.q0_id) AS q0_no_regenerados,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(COUNT(DISTINCT r.q0_id), COUNT(*)), 4) AS pct_regenerados\n",
|
||||||
|
"FROM q0\n",
|
||||||
|
"LEFT JOIN regen r USING (q0_id)\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"q(SQL_TOT)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "c183a653",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Distribución días hasta regeneración"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "aab452ca",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_DIAS = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT q.order_id AS qn_order, q.created_at AS qn_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id, t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" TIMESTAMP_DIFF(qN.qn_ts, q0.q0_ts, HOUR)/24 AS dias_entre\n",
|
||||||
|
"FROM q0\n",
|
||||||
|
"JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"dias = q(SQL_DIAS)\n",
|
||||||
|
"print(dias.describe())\n",
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"dias[\"dias_entre\"].clip(upper=60).hist(bins=30)\n",
|
||||||
|
"plt.xlabel(\"Días entre Q0 (call_center) y Q1 (centro)\")\n",
|
||||||
|
"plt.ylabel(\"# eventos\")\n",
|
||||||
|
"plt.title(\"Distribución de regeneración temporal\")\n",
|
||||||
|
"plt.show()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0feaa9c1",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Cruzar regeneración con conversión a factura\n",
|
||||||
|
"\n",
|
||||||
|
"¿Los Q0 regenerados convierten MENOS que los Q0 no regenerados? (Hipótesis: el cliente prefiere lo que negocia el centro)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "376502d8",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_CONV = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT q.order_id AS qn_order, q.created_at AS qn_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id, t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)\n",
|
||||||
|
"),\n",
|
||||||
|
"regen AS (\n",
|
||||||
|
" SELECT DISTINCT q0.q0_id\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
"),\n",
|
||||||
|
"q0_inv AS (\n",
|
||||||
|
" SELECT q0.q0_id,\n",
|
||||||
|
" CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END AS q0_factura\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON i.order_id = q0.q0_order\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" CASE WHEN r.q0_id IS NOT NULL THEN 'regenerado' ELSE 'no_regenerado' END AS bucket,\n",
|
||||||
|
" COUNT(*) AS q0_total,\n",
|
||||||
|
" SUM(qi.q0_factura) AS q0_convertido_propio,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(SUM(qi.q0_factura), COUNT(*)), 4) AS conv_q0_propio\n",
|
||||||
|
"FROM q0_inv qi\n",
|
||||||
|
"LEFT JOIN regen r USING (q0_id)\n",
|
||||||
|
"GROUP BY bucket\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"q(SQL_CONV)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,214 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "07306d98",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 01 — Exploración: quotes ↔ call_center ↔ factura\n",
|
||||||
|
"\n",
|
||||||
|
"Mapa de tablas y joins en `psql_dcpublic` (BigQuery `autingo-159109`).\n",
|
||||||
|
"\n",
|
||||||
|
"## Cadena de joins\n",
|
||||||
|
"\n",
|
||||||
|
"```\n",
|
||||||
|
"tpv_authorization_tpvuser_centers (dccenter_id ∈ {159 CALL_CENTER_AURGI, 162 CALL_CENTER})\n",
|
||||||
|
" │ tpvuser_id\n",
|
||||||
|
" ▼\n",
|
||||||
|
"tpv_orders_quote.created_by_id ──► quote por agente call_center\n",
|
||||||
|
" │ order_id\n",
|
||||||
|
" ▼\n",
|
||||||
|
"tpv_orders_order ─► terminal_id ─► tpv_terminals.center_id ─► centers (centro real de facturación)\n",
|
||||||
|
" │ │ customer_id │ vehicle_id\n",
|
||||||
|
" ▼ ▼ ▼\n",
|
||||||
|
"tpv_orders_invoice (status convertido) tpv_customers (tlf) tpv_vehicles_vehicle (matrícula)\n",
|
||||||
|
"```\n",
|
||||||
|
"\n",
|
||||||
|
"Identidad cliente = `(customer_id, vehicle_id)` o (tlf, matrícula) según necesite normalización."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "d2b6d545",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import os, sys\n",
|
||||||
|
"from google.cloud import bigquery\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"PROJECT = \"autingo-159109\"\n",
|
||||||
|
"DATASET = \"psql_dcpublic\"\n",
|
||||||
|
"bq = bigquery.Client(project=PROJECT)\n",
|
||||||
|
"\n",
|
||||||
|
"def q(sql):\n",
|
||||||
|
" return bq.query(sql).to_dataframe()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "3238b92e",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 1. Usuarios call_center"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "34d656ad",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"cc_users = q(f\"\"\"\n",
|
||||||
|
"SELECT u.id, u.name, u.email, u.is_active, u.role_id,\n",
|
||||||
|
" STRING_AGG(CAST(uc.dccenter_id AS STRING)) AS centers\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser` u\n",
|
||||||
|
"JOIN `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers` uc\n",
|
||||||
|
" ON u.id = uc.tpvuser_id\n",
|
||||||
|
"WHERE uc.dccenter_id IN (159, 162)\n",
|
||||||
|
"GROUP BY 1,2,3,4,5\n",
|
||||||
|
"ORDER BY u.is_active DESC, u.id\n",
|
||||||
|
"\"\"\")\n",
|
||||||
|
"print(f\"Total usuarios call_center: {len(cc_users)} (activos: {cc_users.is_active.sum()})\")\n",
|
||||||
|
"cc_users.head(20)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "9ec102fb",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 2. Schema quote — campos clave"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "e5f94b52",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"SELECT column_name, data_type\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.INFORMATION_SCHEMA.COLUMNS`\n",
|
||||||
|
"WHERE table_name='tpv_orders_quote'\n",
|
||||||
|
"ORDER BY ordinal_position\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "32ffc1d2",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 3. Distribución `status` y `accepted` (últimos 90d)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "609022a9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"SELECT status, accepted, COUNT(*) n\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_orders_quote`\n",
|
||||||
|
"WHERE created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)\n",
|
||||||
|
" AND deleted_at IS NULL\n",
|
||||||
|
"GROUP BY status, accepted\n",
|
||||||
|
"ORDER BY n DESC\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "73c85198",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 4. Conversion quote → invoice (mismo order_id)\n",
|
||||||
|
"\n",
|
||||||
|
"Una quote convierte cuando existe `tpv_orders_invoice` con el mismo `order_id`. Ese invoice fija la facturación real (NAV-sync via `nav_id`)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "46378f84",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" COUNT(*) AS quotes,\n",
|
||||||
|
" COUNT(DISTINCT q.order_id) AS distinct_orders,\n",
|
||||||
|
" SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END) AS quotes_con_invoice,\n",
|
||||||
|
" SAFE_DIVIDE(SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END), COUNT(*)) AS conversion_rate\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
"LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i\n",
|
||||||
|
" ON q.order_id = i.order_id\n",
|
||||||
|
"WHERE q.created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "df8b402c",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## 5. Sanity: quote por call_center vs otro\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "506744d3",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"q(f\"\"\"\n",
|
||||||
|
"WITH cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" CASE WHEN cc.user_id IS NOT NULL THEN 'call_center' ELSE 'otro' END AS origen_user,\n",
|
||||||
|
" COUNT(*) AS quotes,\n",
|
||||||
|
" SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END) AS convertidos,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END), COUNT(*)), 3) AS conv_rate\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
"LEFT JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
"LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON q.order_id = i.order_id\n",
|
||||||
|
"WHERE q.created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
"GROUP BY 1\n",
|
||||||
|
"\"\"\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "36ae91c9",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 02 — 3 KPI principales\n",
|
||||||
|
"\n",
|
||||||
|
"Por **centro real de facturación** (`tpv_terminals.center_id` del invoice) y ventana temporal:\n",
|
||||||
|
"\n",
|
||||||
|
"1. **A — Valor facturado de quotes call_center que CONVIRTIERON** \n",
|
||||||
|
" Quote creado por usuario call_center + existe `tpv_orders_invoice` con el mismo `order_id`. Sumamos `tpv_orders_order.total_cost` o líneas. Centro = centro del invoice.\n",
|
||||||
|
"\n",
|
||||||
|
"2. **B — Valor facturado total a esos mismos clientes en centros** \n",
|
||||||
|
" Misma identidad cliente (`customer_id` y/o `vehicle_id` y/o `tlf`+`matricula` normalizados). Todas las facturas del cliente en ese centro en la misma ventana. Debe ser ≥ A.\n",
|
||||||
|
"\n",
|
||||||
|
"3. **C — Facturación total del centro** \n",
|
||||||
|
" Suma de invoices del centro en la ventana, todos los clientes."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "7938921f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from google.cloud import bigquery\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"PROJECT = \"autingo-159109\"\n",
|
||||||
|
"DATASET = \"psql_dcpublic\"\n",
|
||||||
|
"bq = bigquery.Client(project=PROJECT)\n",
|
||||||
|
"\n",
|
||||||
|
"WINDOW_DAYS = 90\n",
|
||||||
|
"\n",
|
||||||
|
"def q(sql):\n",
|
||||||
|
" return bq.query(sql).to_dataframe()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "c7cfa785",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Setup: CTEs base reutilizables\n",
|
||||||
|
"\n",
|
||||||
|
"Construimos una query maestra con CTEs para A, B, C juntos por centro."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "3e06df33",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_KPI = f\"\"\"\n",
|
||||||
|
"DECLARE window_days INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL window_days DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Quotes creados por call_center que TIENEN invoice (convertidos)\n",
|
||||||
|
"cc_converted AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q.id AS quote_id,\n",
|
||||||
|
" q.order_id,\n",
|
||||||
|
" q.created_at AS quote_ts,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id,\n",
|
||||||
|
" o.terminal_id,\n",
|
||||||
|
" t.center_id,\n",
|
||||||
|
" o.total_cost,\n",
|
||||||
|
" i.id AS invoice_id,\n",
|
||||||
|
" i.created_at AS invoice_ts\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON i.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.created_at >= t_start\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Clientes \"tocados\" por call_center (customer_id + vehicle_id)\n",
|
||||||
|
"cc_clients AS (\n",
|
||||||
|
" SELECT DISTINCT center_id, customer_id, vehicle_id\n",
|
||||||
|
" FROM cc_converted\n",
|
||||||
|
" WHERE customer_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Todas las facturas en la ventana, con centro real\n",
|
||||||
|
"all_invoices AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" i.id AS invoice_id,\n",
|
||||||
|
" i.order_id,\n",
|
||||||
|
" i.created_at AS invoice_ts,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id,\n",
|
||||||
|
" o.terminal_id,\n",
|
||||||
|
" t.center_id,\n",
|
||||||
|
" o.total_cost\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_invoice` i\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON i.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE i.created_at >= t_start\n",
|
||||||
|
"),\n",
|
||||||
|
"-- B: facturas de los mismos clientes en cualquier centro NO call_center\n",
|
||||||
|
"client_invoices_in_centers AS (\n",
|
||||||
|
" SELECT ai.*\n",
|
||||||
|
" FROM all_invoices ai\n",
|
||||||
|
" JOIN cc_clients cc ON ai.customer_id = cc.customer_id\n",
|
||||||
|
" WHERE ai.center_id NOT IN (159, 162) -- excluye los propios centros call_center\n",
|
||||||
|
"),\n",
|
||||||
|
"kpi_a AS (\n",
|
||||||
|
" SELECT center_id,\n",
|
||||||
|
" COUNT(DISTINCT quote_id) AS quotes_cc_convertidos,\n",
|
||||||
|
" COUNT(DISTINCT invoice_id) AS invoices_a,\n",
|
||||||
|
" SUM(total_cost) AS valor_a\n",
|
||||||
|
" FROM cc_converted\n",
|
||||||
|
" WHERE center_id IS NOT NULL\n",
|
||||||
|
" GROUP BY center_id\n",
|
||||||
|
"),\n",
|
||||||
|
"kpi_b AS (\n",
|
||||||
|
" SELECT center_id,\n",
|
||||||
|
" COUNT(DISTINCT invoice_id) AS invoices_b,\n",
|
||||||
|
" SUM(total_cost) AS valor_b\n",
|
||||||
|
" FROM client_invoices_in_centers\n",
|
||||||
|
" GROUP BY center_id\n",
|
||||||
|
"),\n",
|
||||||
|
"kpi_c AS (\n",
|
||||||
|
" SELECT center_id,\n",
|
||||||
|
" COUNT(DISTINCT invoice_id) AS invoices_c,\n",
|
||||||
|
" SUM(total_cost) AS valor_c\n",
|
||||||
|
" FROM all_invoices\n",
|
||||||
|
" WHERE center_id IS NOT NULL\n",
|
||||||
|
" GROUP BY center_id\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" c.id AS center_id,\n",
|
||||||
|
" c.name AS center_name,\n",
|
||||||
|
" COALESCE(a.quotes_cc_convertidos, 0) AS quotes_cc_convertidos,\n",
|
||||||
|
" ROUND(COALESCE(a.valor_a, 0), 2) AS A_valor_quote_cc_convertido,\n",
|
||||||
|
" ROUND(COALESCE(b.valor_b, 0), 2) AS B_valor_mismo_cliente_centro,\n",
|
||||||
|
" ROUND(COALESCE(c2.valor_c, 0), 2) AS C_valor_total_centro,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(COALESCE(a.valor_a, 0), c2.valor_c), 4) AS A_sobre_C,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(COALESCE(b.valor_b, 0), c2.valor_c), 4) AS B_sobre_C\n",
|
||||||
|
"FROM `{PROJECT}.{DATASET}.centers` c\n",
|
||||||
|
"LEFT JOIN kpi_a a ON c.id = a.center_id\n",
|
||||||
|
"LEFT JOIN kpi_b b ON c.id = b.center_id\n",
|
||||||
|
"LEFT JOIN kpi_c c2 ON c.id = c2.center_id\n",
|
||||||
|
"WHERE c.id NOT IN (159, 162)\n",
|
||||||
|
" AND COALESCE(c2.valor_c, 0) > 0\n",
|
||||||
|
"ORDER BY C_valor_total_centro DESC\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"df = q(SQL_KPI)\n",
|
||||||
|
"print(f\"Centros con actividad ({WINDOW_DAYS}d): {len(df)}\")\n",
|
||||||
|
"df.head(30)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "724baf5c",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Totales globales"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "75c3297e",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"totals = df[[\"A_valor_quote_cc_convertido\", \"B_valor_mismo_cliente_centro\", \"C_valor_total_centro\"]].sum()\n",
|
||||||
|
"print(totals.to_string())\n",
|
||||||
|
"print()\n",
|
||||||
|
"print(f\"A/C global: {totals.A_valor_quote_cc_convertido / totals.C_valor_total_centro:.4f}\")\n",
|
||||||
|
"print(f\"B/C global: {totals.B_valor_mismo_cliente_centro / totals.C_valor_total_centro:.4f}\")\n",
|
||||||
|
"print(f\"Lift B vs A: {totals.B_valor_mismo_cliente_centro / totals.A_valor_quote_cc_convertido:.2f}x\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0fba60db",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Top 15 centros por A (valor traído por call_center)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "f90abe7e",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"\n",
|
||||||
|
"top = df.sort_values(\"A_valor_quote_cc_convertido\", ascending=False).head(15)\n",
|
||||||
|
"fig, ax = plt.subplots(figsize=(10, 6))\n",
|
||||||
|
"x = range(len(top))\n",
|
||||||
|
"ax.barh(x, top.A_valor_quote_cc_convertido, label=\"A (cc → factura)\")\n",
|
||||||
|
"ax.barh(x, top.B_valor_mismo_cliente_centro - top.A_valor_quote_cc_convertido,\n",
|
||||||
|
" left=top.A_valor_quote_cc_convertido, label=\"B−A (mismo cliente extra)\")\n",
|
||||||
|
"ax.set_yticks(x)\n",
|
||||||
|
"ax.set_yticklabels(top.center_name)\n",
|
||||||
|
"ax.invert_yaxis()\n",
|
||||||
|
"ax.set_xlabel(\"€ facturados\")\n",
|
||||||
|
"ax.legend()\n",
|
||||||
|
"ax.set_title(f\"Top 15 centros — quotes call_center ({WINDOW_DAYS}d)\")\n",
|
||||||
|
"plt.tight_layout()\n",
|
||||||
|
"plt.show()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "b090f346",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 03 — Regeneración de presupuestos\n",
|
||||||
|
"\n",
|
||||||
|
"**Hipótesis:** un mismo cliente (`customer_id` + `vehicle_id`) recibe N quotes antes de convertir. El centro \"regenera\" el presupuesto cuando descarta el de call_center y abre uno nuevo en TPV local.\n",
|
||||||
|
"\n",
|
||||||
|
"Definición operativa de regeneración:\n",
|
||||||
|
"- Existe quote call_center previa (Q0) para el par cliente+vehículo.\n",
|
||||||
|
"- Existe quote posterior (Q1...Qn) en un terminal de centro NO call_center, dentro de ventana D días.\n",
|
||||||
|
"- Q1 puede tener distinto `order_id` que Q0 (regenera de cero) o mismo (reescribe — menos común).\n",
|
||||||
|
"\n",
|
||||||
|
"Métricas pedidas:\n",
|
||||||
|
"1. Centros que MÁS regeneran (cuentan regeneraciones absolutas y % sobre quotes call_center recibidos).\n",
|
||||||
|
"2. Quotes call_center con regeneración vs sin regeneración."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5626c2cd",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from google.cloud import bigquery\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"PROJECT = \"autingo-159109\"\n",
|
||||||
|
"DATASET = \"psql_dcpublic\"\n",
|
||||||
|
"bq = bigquery.Client(project=PROJECT)\n",
|
||||||
|
"\n",
|
||||||
|
"WINDOW_DAYS = 90 # ventana de análisis sobre quote call_center\n",
|
||||||
|
"REGEN_WINDOW_DAYS = 60 # ventana para detectar regeneración posterior\n",
|
||||||
|
"\n",
|
||||||
|
"def q(sql):\n",
|
||||||
|
" return bq.query(sql).to_dataframe()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "17cff6ce",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_REGEN = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Q0: quotes generados por call_center\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q.id AS q0_id,\n",
|
||||||
|
" q.order_id AS q0_order,\n",
|
||||||
|
" q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start\n",
|
||||||
|
" AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL\n",
|
||||||
|
" AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Q1..Qn: quotes posteriores para mismo cliente+vehículo, en centro NO call_center\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q.id AS qn_id,\n",
|
||||||
|
" q.order_id AS qn_order,\n",
|
||||||
|
" q.created_at AS qn_ts,\n",
|
||||||
|
" q.created_by_id,\n",
|
||||||
|
" o.customer_id,\n",
|
||||||
|
" o.vehicle_id,\n",
|
||||||
|
" o.terminal_id,\n",
|
||||||
|
" t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL\n",
|
||||||
|
" AND t.center_id NOT IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Empareja Q0 con Q1+ dentro de regen_win días\n",
|
||||||
|
"regen AS (\n",
|
||||||
|
" SELECT\n",
|
||||||
|
" q0.q0_id,\n",
|
||||||
|
" q0.q0_order,\n",
|
||||||
|
" q0.customer_id,\n",
|
||||||
|
" q0.vehicle_id,\n",
|
||||||
|
" qN.qn_id,\n",
|
||||||
|
" qN.qn_order,\n",
|
||||||
|
" qN.center_id AS regen_center,\n",
|
||||||
|
" TIMESTAMP_DIFF(qN.qn_ts, q0.q0_ts, HOUR) / 24 AS dias_entre\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
"),\n",
|
||||||
|
"-- Para cada Q0, ¿hay al menos UNA regeneración?\n",
|
||||||
|
"q0_has_regen AS (\n",
|
||||||
|
" SELECT q0_id, COUNT(*) AS regen_count,\n",
|
||||||
|
" MIN(dias_entre) AS dias_a_regen,\n",
|
||||||
|
" APPROX_TOP_COUNT(regen_center, 1)[OFFSET(0)].value AS first_regen_center\n",
|
||||||
|
" FROM regen\n",
|
||||||
|
" GROUP BY q0_id\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"-- Vista por centro: cuántos Q0 regenera cada centro\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" c.id AS center_id,\n",
|
||||||
|
" c.name AS center_name,\n",
|
||||||
|
" COUNT(DISTINCT r.q0_id) AS q0_regenerados_aqui,\n",
|
||||||
|
" COUNT(*) AS regen_events,\n",
|
||||||
|
" ROUND(AVG(r.dias_entre), 1) AS dias_avg_regen\n",
|
||||||
|
"FROM regen r\n",
|
||||||
|
"JOIN `{PROJECT}.{DATASET}.centers` c ON r.regen_center = c.id\n",
|
||||||
|
"GROUP BY c.id, c.name\n",
|
||||||
|
"ORDER BY q0_regenerados_aqui DESC\n",
|
||||||
|
"LIMIT 30\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"df_centros = q(SQL_REGEN)\n",
|
||||||
|
"print(f\"Centros con eventos de regeneración: {len(df_centros)}\")\n",
|
||||||
|
"df_centros.head(30)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "43add847",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Totales: Q0 con regeneración vs sin regeneración"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "736158ba",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_TOT = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT q.order_id AS qn_order, q.created_at AS qn_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id, t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)\n",
|
||||||
|
"),\n",
|
||||||
|
"regen AS (\n",
|
||||||
|
" SELECT DISTINCT q0.q0_id\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" COUNT(*) AS q0_total,\n",
|
||||||
|
" COUNT(DISTINCT r.q0_id) AS q0_regenerados,\n",
|
||||||
|
" COUNT(*) - COUNT(DISTINCT r.q0_id) AS q0_no_regenerados,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(COUNT(DISTINCT r.q0_id), COUNT(*)), 4) AS pct_regenerados\n",
|
||||||
|
"FROM q0\n",
|
||||||
|
"LEFT JOIN regen r USING (q0_id)\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"q(SQL_TOT)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "c183a653",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Distribución días hasta regeneración"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "aab452ca",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_DIAS = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT q.order_id AS qn_order, q.created_at AS qn_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id, t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" TIMESTAMP_DIFF(qN.qn_ts, q0.q0_ts, HOUR)/24 AS dias_entre\n",
|
||||||
|
"FROM q0\n",
|
||||||
|
"JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"dias = q(SQL_DIAS)\n",
|
||||||
|
"print(dias.describe())\n",
|
||||||
|
"import matplotlib.pyplot as plt\n",
|
||||||
|
"dias[\"dias_entre\"].clip(upper=60).hist(bins=30)\n",
|
||||||
|
"plt.xlabel(\"Días entre Q0 (call_center) y Q1 (centro)\")\n",
|
||||||
|
"plt.ylabel(\"# eventos\")\n",
|
||||||
|
"plt.title(\"Distribución de regeneración temporal\")\n",
|
||||||
|
"plt.show()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0feaa9c1",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Cruzar regeneración con conversión a factura\n",
|
||||||
|
"\n",
|
||||||
|
"¿Los Q0 regenerados convierten MENOS que los Q0 no regenerados? (Hipótesis: el cliente prefiere lo que negocia el centro)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "376502d8",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"SQL_CONV = f\"\"\"\n",
|
||||||
|
"DECLARE win INT64 DEFAULT {WINDOW_DAYS};\n",
|
||||||
|
"DECLARE regen_win INT64 DEFAULT {REGEN_WINDOW_DAYS};\n",
|
||||||
|
"DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL win DAY);\n",
|
||||||
|
"\n",
|
||||||
|
"WITH\n",
|
||||||
|
"cc_users AS (\n",
|
||||||
|
" SELECT DISTINCT tpvuser_id AS user_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`\n",
|
||||||
|
" WHERE dccenter_id IN (159, 162)\n",
|
||||||
|
"),\n",
|
||||||
|
"q0 AS (\n",
|
||||||
|
" SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN cc_users cc ON q.created_by_id = cc.user_id\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" WHERE q.created_at >= t_start AND q.deleted_at IS NULL\n",
|
||||||
|
" AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL\n",
|
||||||
|
"),\n",
|
||||||
|
"qN AS (\n",
|
||||||
|
" SELECT q.order_id AS qn_order, q.created_at AS qn_ts,\n",
|
||||||
|
" o.customer_id, o.vehicle_id, t.center_id\n",
|
||||||
|
" FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q\n",
|
||||||
|
" JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id\n",
|
||||||
|
" WHERE q.deleted_at IS NULL\n",
|
||||||
|
" AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)\n",
|
||||||
|
"),\n",
|
||||||
|
"regen AS (\n",
|
||||||
|
" SELECT DISTINCT q0.q0_id\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" JOIN qN\n",
|
||||||
|
" ON q0.customer_id = qN.customer_id\n",
|
||||||
|
" AND q0.vehicle_id = qN.vehicle_id\n",
|
||||||
|
" AND qN.qn_ts > q0.q0_ts\n",
|
||||||
|
" AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL regen_win DAY)\n",
|
||||||
|
" AND qN.qn_order != q0.q0_order\n",
|
||||||
|
"),\n",
|
||||||
|
"q0_inv AS (\n",
|
||||||
|
" SELECT q0.q0_id,\n",
|
||||||
|
" CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END AS q0_factura\n",
|
||||||
|
" FROM q0\n",
|
||||||
|
" LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON i.order_id = q0.q0_order\n",
|
||||||
|
")\n",
|
||||||
|
"SELECT\n",
|
||||||
|
" CASE WHEN r.q0_id IS NOT NULL THEN 'regenerado' ELSE 'no_regenerado' END AS bucket,\n",
|
||||||
|
" COUNT(*) AS q0_total,\n",
|
||||||
|
" SUM(qi.q0_factura) AS q0_convertido_propio,\n",
|
||||||
|
" ROUND(SAFE_DIVIDE(SUM(qi.q0_factura), COUNT(*)), 4) AS conv_q0_propio\n",
|
||||||
|
"FROM q0_inv qi\n",
|
||||||
|
"LEFT JOIN regen r USING (q0_id)\n",
|
||||||
|
"GROUP BY bucket\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"q(SQL_CONV)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[project]
|
||||||
|
name = "presupuestos-callcenter"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"google-cloud-bigquery>=3.41.0",
|
||||||
|
"jupyter>=1.1.1",
|
||||||
|
"jupyter-collaboration>=4.4.0",
|
||||||
|
"jupyter-mcp-server>=1.0.2",
|
||||||
|
"jupyterlab>=4.5.7",
|
||||||
|
"matplotlib>=3.10.9",
|
||||||
|
"numpy>=2.4.6",
|
||||||
|
"pandas>=3.0.3",
|
||||||
|
"seaborn>=0.13.2",
|
||||||
|
]
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-wire dashboard 1019 filters to use NAME columns (searchable + multi-select dropdown).
|
||||||
|
|
||||||
|
Reason: ID PK fields tienen has_field_values=none -> no dropdown.
|
||||||
|
Name columns: has_field_values=list (companies) o search (centers/tpvuser/products).
|
||||||
|
Multi-select via string/= con isMultiSelect=True.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
DB_ID = 6
|
||||||
|
DASHBOARD_ID = 1019
|
||||||
|
|
||||||
|
# Field IDs (name/description columns)
|
||||||
|
F_QUOTE_CREATED_AT = 16588
|
||||||
|
F_CENTER_NAME = 17330 # centers.name
|
||||||
|
F_TPVUSER_NAME = 17958 # tpv_authorization_tpvuser.name
|
||||||
|
F_COMPANY_NAME = 17158 # companies.name
|
||||||
|
F_PRODUCT_DESC = 16795 # products.description
|
||||||
|
|
||||||
|
client = httpx.Client(base_url=BASE, headers={"x-api-key": API_KEY}, timeout=180)
|
||||||
|
|
||||||
|
|
||||||
|
def field_filter_tag(name, field_id, widget, display_name):
|
||||||
|
return {
|
||||||
|
"id": name + "-tag",
|
||||||
|
"name": name,
|
||||||
|
"display-name": display_name,
|
||||||
|
"type": "dimension",
|
||||||
|
"dimension": ["field", field_id, None],
|
||||||
|
"widget-type": widget,
|
||||||
|
"default": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TAGS_AB = {
|
||||||
|
"date": field_filter_tag("date", F_QUOTE_CREATED_AT, "date/range", "Fecha presupuesto"),
|
||||||
|
"centro": field_filter_tag("centro", F_CENTER_NAME, "string/=", "Centro"),
|
||||||
|
"agente": field_filter_tag("agente", F_TPVUSER_NAME, "string/=", "Agente CC"),
|
||||||
|
"compania": field_filter_tag("compania", F_COMPANY_NAME, "string/=", "Compañía"),
|
||||||
|
"producto": field_filter_tag("producto", F_PRODUCT_DESC, "string/contains", "Producto"),
|
||||||
|
}
|
||||||
|
TAGS_C = {k: v for k, v in TAGS_AB.items() if k != "agente"}
|
||||||
|
|
||||||
|
|
||||||
|
# SQL must reference products table now (join via orderitem.product_id)
|
||||||
|
SQL_A = """
|
||||||
|
WITH cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN cc_users ON `psql_dcpublic.tpv_orders_quote`.created_by_id = cc_users.user_id
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_authorization_tpvuser` ON `psql_dcpublic.tpv_orders_quote`.created_by_id = `psql_dcpublic.tpv_authorization_tpvuser`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.products` ON `psql_dcpublic.tpv_orders_orderitem`.product_id = `psql_dcpublic.products`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
AND COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{agente}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_B = """
|
||||||
|
WITH cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
quotes_q1 AS (SELECT id, order_id, created_at, deleted_at FROM `psql_dcpublic.tpv_orders_quote`),
|
||||||
|
orders_q1 AS (SELECT id, customer_id, vehicle_id, terminal_id FROM `psql_dcpublic.tpv_orders_order`),
|
||||||
|
terminals_q1 AS (SELECT id, center_id FROM `psql_dcpublic.tpv_terminals`),
|
||||||
|
cc_anchored AS (
|
||||||
|
SELECT
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.id AS q0_id,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.order_id AS q0_order,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.created_at AS q0_ts,
|
||||||
|
`psql_dcpublic.tpv_orders_quote`.created_by_id AS cc_agent_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.customer_id AS cust_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.vehicle_id AS veh_id
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN cc_users ON `psql_dcpublic.tpv_orders_quote`.created_by_id = cc_users.user_id
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_authorization_tpvuser` ON `psql_dcpublic.tpv_orders_quote`.created_by_id = `psql_dcpublic.tpv_authorization_tpvuser`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{agente}}]]
|
||||||
|
),
|
||||||
|
b_orders AS (
|
||||||
|
-- Solo Q1 regenerados (sin Q-CC directos)
|
||||||
|
SELECT q1.order_id AS order_id
|
||||||
|
FROM cc_anchored a
|
||||||
|
JOIN quotes_q1 q1
|
||||||
|
ON q1.deleted_at IS NULL
|
||||||
|
AND q1.created_at > a.q0_ts
|
||||||
|
AND q1.created_at <= TIMESTAMP_ADD(a.q0_ts, INTERVAL 60 DAY)
|
||||||
|
AND q1.order_id != a.q0_order
|
||||||
|
JOIN orders_q1 o1 ON q1.order_id = o1.id
|
||||||
|
LEFT JOIN terminals_q1 t1 ON o1.terminal_id = t1.id
|
||||||
|
WHERE o1.customer_id = a.cust_id
|
||||||
|
AND o1.vehicle_id = a.veh_id
|
||||||
|
AND t1.center_id IS NOT NULL
|
||||||
|
AND t1.center_id NOT IN (159, 162)
|
||||||
|
GROUP BY q1.order_id
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM b_orders
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON b_orders.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.products` ON `psql_dcpublic.tpv_orders_orderitem`.product_id = `psql_dcpublic.products`.id
|
||||||
|
WHERE COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
SQL_C = """
|
||||||
|
WITH filtered AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_orders_invoice`.id AS invoice_id,
|
||||||
|
`psql_dcpublic.tpv_orders_order`.total_cost AS total_cost
|
||||||
|
FROM `psql_dcpublic.tpv_orders_quote`
|
||||||
|
JOIN `psql_dcpublic.tpv_orders_order` ON `psql_dcpublic.tpv_orders_quote`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
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
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_customers` ON `psql_dcpublic.tpv_orders_order`.customer_id = `psql_dcpublic.tpv_customers`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.companies` ON `psql_dcpublic.tpv_customers`.company_id = `psql_dcpublic.companies`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.tpv_orders_orderitem` ON `psql_dcpublic.tpv_orders_orderitem`.order_id = `psql_dcpublic.tpv_orders_order`.id
|
||||||
|
LEFT JOIN `psql_dcpublic.products` ON `psql_dcpublic.tpv_orders_orderitem`.product_id = `psql_dcpublic.products`.id
|
||||||
|
WHERE `psql_dcpublic.tpv_orders_quote`.deleted_at IS NULL
|
||||||
|
AND COALESCE(`psql_dcpublic.centers`.id, 0) NOT IN (159, 162)
|
||||||
|
[[AND {{date}}]]
|
||||||
|
[[AND {{centro}}]]
|
||||||
|
[[AND {{compania}}]]
|
||||||
|
[[AND {{producto}}]]
|
||||||
|
)
|
||||||
|
SELECT __AGG__ AS valor FROM filtered
|
||||||
|
"""
|
||||||
|
|
||||||
|
AGG = {
|
||||||
|
"total": "ROUND(SUM(total_cost), 2)",
|
||||||
|
"count": "COUNT(*)",
|
||||||
|
"ticket": "ROUND(SAFE_DIVIDE(SUM(total_cost), NULLIF(COUNT(*), 0)), 2)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Card ids actuales del dashboard v2
|
||||||
|
CARDS = {
|
||||||
|
"a_total": (10248, SQL_A, "total"),
|
||||||
|
"a_count": (10249, SQL_A, "count"),
|
||||||
|
"a_ticket": (10250, SQL_A, "ticket"),
|
||||||
|
"b_total": (10251, SQL_B, "total"),
|
||||||
|
"b_count": (10252, SQL_B, "count"),
|
||||||
|
"b_ticket": (10253, SQL_B, "ticket"),
|
||||||
|
"c_total": (10254, SQL_C, "total"),
|
||||||
|
"c_count": (10255, SQL_C, "count"),
|
||||||
|
"c_ticket": (10256, SQL_C, "ticket"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_card(cid):
|
||||||
|
return client.get(f"/api/card/{cid}").json()
|
||||||
|
|
||||||
|
|
||||||
|
def put_card_sql(cid, sql, tags, viz):
|
||||||
|
body = {
|
||||||
|
"dataset_query": {
|
||||||
|
"type": "native", "database": DB_ID,
|
||||||
|
"native": {"query": sql, "template-tags": tags},
|
||||||
|
},
|
||||||
|
"visualization_settings": viz,
|
||||||
|
}
|
||||||
|
r = client.put(f"/api/card/{cid}", json=body)
|
||||||
|
if r.status_code >= 400:
|
||||||
|
print(r.status_code, r.text[:500])
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
print("rewiring cards...")
|
||||||
|
for sid, (cid, skel, agg) in CARDS.items():
|
||||||
|
tags = TAGS_AB if sid[0] in "ab" else TAGS_C
|
||||||
|
sql = skel.replace("__AGG__", AGG[agg])
|
||||||
|
# preserve current viz settings
|
||||||
|
cur = get_card(cid)
|
||||||
|
viz = cur.get("visualization_settings", {})
|
||||||
|
put_card_sql(cid, sql, tags, viz)
|
||||||
|
print(f" {sid} card={cid} updated")
|
||||||
|
|
||||||
|
|
||||||
|
print("\nupdating dashboard parameters to multi-select string filters...")
|
||||||
|
new_params = [
|
||||||
|
{"id": "p_date", "name": "Fecha presupuesto", "slug": "fecha", "sectionId": "date", "type": "date/range"},
|
||||||
|
{"id": "p_centro", "name": "Centro", "slug": "centro", "sectionId": "string", "type": "string/=",
|
||||||
|
"isMultiSelect": True},
|
||||||
|
{"id": "p_agente", "name": "Agente CC", "slug": "agente", "sectionId": "string", "type": "string/=",
|
||||||
|
"isMultiSelect": True},
|
||||||
|
{"id": "p_compania", "name": "Compañía", "slug": "compania", "sectionId": "string", "type": "string/=",
|
||||||
|
"isMultiSelect": True},
|
||||||
|
{"id": "p_producto", "name": "Producto", "slug": "producto", "sectionId": "string", "type": "string/contains",
|
||||||
|
"isMultiSelect": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Para que el dropdown se llene con los valores del field-filter dimension,
|
||||||
|
# Metabase usa has_field_values del field destino automaticamente. No requiere config extra.
|
||||||
|
|
||||||
|
# Re-fetch dashcards para mantener layout + remap parameter_mappings (tags actuales)
|
||||||
|
dash = client.get(f"/api/dashboard/{DASHBOARD_ID}").json()
|
||||||
|
|
||||||
|
# Map current dashcards: card_id -> tag_keys (depende si A/B o C)
|
||||||
|
def tag_keys_for_card(card_id):
|
||||||
|
name = next((c["name"] for c in [client.get(f"/api/card/{cid}").json() for cid in [card_id]]), "")
|
||||||
|
return list(TAGS_C.keys()) if name.startswith("C ·") else list(TAGS_AB.keys())
|
||||||
|
|
||||||
|
new_dashcards = []
|
||||||
|
for dc in dash["dashcards"]:
|
||||||
|
cname = dc["card"]["name"]
|
||||||
|
tag_keys = list(TAGS_C.keys()) if cname.startswith("C ·") else list(TAGS_AB.keys())
|
||||||
|
new_dashcards.append({
|
||||||
|
"id": dc["id"],
|
||||||
|
"card_id": dc["card_id"],
|
||||||
|
"row": dc["row"], "col": dc["col"],
|
||||||
|
"size_x": dc["size_x"], "size_y": dc["size_y"],
|
||||||
|
"visualization_settings": dc.get("visualization_settings", {}),
|
||||||
|
"parameter_mappings": [
|
||||||
|
{"parameter_id": f"p_{k}", "card_id": dc["card_id"],
|
||||||
|
"target": ["dimension", ["template-tag", k]]}
|
||||||
|
for k in tag_keys
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
r = client.put(f"/api/dashboard/{DASHBOARD_ID}", json={
|
||||||
|
"parameters": new_params,
|
||||||
|
"dashcards": new_dashcards,
|
||||||
|
})
|
||||||
|
if r.status_code >= 400:
|
||||||
|
print(r.status_code, r.text[:800])
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
print("done")
|
||||||
|
print(f"URL: {BASE}/dashboard/{DASHBOARD_ID}")
|
||||||
|
client.close()
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Jupyter Lab — modo colaborativo con autodeteccion de puerto
|
||||||
|
# Generado por write_jupyter_launcher (fn_registry)
|
||||||
|
|
||||||
|
find_free_port() {
|
||||||
|
for port in 8888 8889 8890 8891 8892 8893 8894 8895 8896 8897 8898 8899; do
|
||||||
|
if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
|
||||||
|
! lsof -i:"$port" >/dev/null 2>&1; then
|
||||||
|
echo $port
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo 8888
|
||||||
|
}
|
||||||
|
|
||||||
|
PORT=${1:-$(find_free_port)}
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo $PORT > .jupyter-port
|
||||||
|
|
||||||
|
source .venv/bin/activate 2>/dev/null || true
|
||||||
|
|
||||||
|
# IPython startup: cargar .ipython/ local (FN_REGISTRY_ROOT, helpers, sys.path)
|
||||||
|
if [ -d "$(pwd)/.ipython" ]; then
|
||||||
|
export IPYTHONDIR="$(pwd)/.ipython"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! python -c "import jupyter_collaboration" 2>/dev/null; then
|
||||||
|
echo "ERROR: jupyter-collaboration no esta instalado"
|
||||||
|
echo "Instala con: uv add jupyter-collaboration"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo " Jupyter Lab + Colaboracion en puerto $PORT"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
echo " Abre: http://localhost:$PORT"
|
||||||
|
echo " Ctrl+C para detener"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
jupyter lab \
|
||||||
|
--port=$PORT \
|
||||||
|
--no-browser \
|
||||||
|
--ServerApp.token='' \
|
||||||
|
--ServerApp.password='' \
|
||||||
|
--ServerApp.disable_check_xsrf=True \
|
||||||
|
--ServerApp.allow_origin='*' \
|
||||||
|
--ServerApp.root_dir="$(pwd)" \
|
||||||
|
--collaborative
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Execute the analysis queries against BigQuery via Metabase (db=6).
|
||||||
|
|
||||||
|
Saves results as CSV + JSON in data/results/ and prints summary numbers.
|
||||||
|
ADC-free: uses Metabase service account credentials.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.environ.get("FN_REGISTRY_ROOT", "/home/egutierrez/fn_registry"), "python", "functions"))
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
DB_ID = 6
|
||||||
|
HERE = Path(__file__).parent
|
||||||
|
OUT = HERE / "data" / "results"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
PROJECT = "autingo-159109"
|
||||||
|
DATASET = "psql_dcpublic"
|
||||||
|
WINDOW_DAYS = 90
|
||||||
|
REGEN_WINDOW_DAYS = 60
|
||||||
|
|
||||||
|
client = httpx.Client(
|
||||||
|
base_url=BASE,
|
||||||
|
headers={"x-api-key": API_KEY, "Content-Type": "application/json"},
|
||||||
|
timeout=300.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_sql(sql: str) -> tuple[list[str], list[list]]:
|
||||||
|
payload = {
|
||||||
|
"type": "native",
|
||||||
|
"database": DB_ID,
|
||||||
|
"native": {"query": sql},
|
||||||
|
}
|
||||||
|
r = client.post("/api/dataset", json=payload)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()["data"]
|
||||||
|
cols = [c["display_name"] for c in data["cols"]]
|
||||||
|
rows = data["rows"]
|
||||||
|
return cols, rows
|
||||||
|
|
||||||
|
|
||||||
|
def save(name: str, cols: list[str], rows: list[list]) -> None:
|
||||||
|
csv_path = OUT / f"{name}.csv"
|
||||||
|
with csv_path.open("w", newline="") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(cols)
|
||||||
|
w.writerows(rows)
|
||||||
|
json_path = OUT / f"{name}.json"
|
||||||
|
with json_path.open("w") as f:
|
||||||
|
json.dump({"cols": cols, "rows": rows}, f, indent=2, default=str)
|
||||||
|
print(f" -> {csv_path.name} ({len(rows)} rows)")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# QUERY 0 — Sanity: usuarios call_center
|
||||||
|
# =====================================================================
|
||||||
|
print("\n[Q0] usuarios call_center")
|
||||||
|
cols, rows = run_sql(f"""
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT u.id) AS users_totales,
|
||||||
|
SUM(CASE WHEN u.is_active THEN 1 ELSE 0 END) AS users_activos
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser` u
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers` uc
|
||||||
|
ON u.id = uc.tpvuser_id
|
||||||
|
WHERE uc.dccenter_id IN (159, 162)
|
||||||
|
""")
|
||||||
|
save("00_users_callcenter", cols, rows)
|
||||||
|
print(f" {dict(zip(cols, rows[0]))}")
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# QUERY 1 — Conversion rate global por origen
|
||||||
|
# =====================================================================
|
||||||
|
print(f"\n[Q1] conversion global ({WINDOW_DAYS}d)")
|
||||||
|
cols, rows = run_sql(f"""
|
||||||
|
WITH cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
CASE WHEN cc.user_id IS NOT NULL THEN 'call_center' ELSE 'otro' END AS origen,
|
||||||
|
COUNT(*) AS quotes,
|
||||||
|
SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END) AS convertidos,
|
||||||
|
ROUND(SAFE_DIVIDE(SUM(CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END), COUNT(*)), 4) AS conv_rate
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q
|
||||||
|
LEFT JOIN cc_users cc ON q.created_by_id = cc.user_id
|
||||||
|
LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON q.order_id = i.order_id
|
||||||
|
WHERE q.created_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {WINDOW_DAYS} DAY)
|
||||||
|
AND q.deleted_at IS NULL
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 1
|
||||||
|
""")
|
||||||
|
save("01_conversion_origen", cols, rows)
|
||||||
|
for r in rows:
|
||||||
|
print(f" {dict(zip(cols, r))}")
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# QUERY 2 — 3 KPI por centro (A, B, C)
|
||||||
|
# =====================================================================
|
||||||
|
print(f"\n[Q2] KPI A/B/C por centro ({WINDOW_DAYS}d)")
|
||||||
|
cols, rows = run_sql(f"""
|
||||||
|
DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {WINDOW_DAYS} DAY);
|
||||||
|
|
||||||
|
WITH
|
||||||
|
cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
cc_converted AS (
|
||||||
|
SELECT
|
||||||
|
q.id AS quote_id, q.order_id, o.customer_id, o.vehicle_id,
|
||||||
|
o.terminal_id, t.center_id, o.total_cost
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q
|
||||||
|
JOIN cc_users cc ON q.created_by_id = cc.user_id
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON i.order_id = o.id
|
||||||
|
LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id
|
||||||
|
WHERE q.created_at >= t_start AND q.deleted_at IS NULL
|
||||||
|
),
|
||||||
|
cc_clients AS (
|
||||||
|
SELECT DISTINCT customer_id, vehicle_id
|
||||||
|
FROM cc_converted
|
||||||
|
WHERE customer_id IS NOT NULL
|
||||||
|
),
|
||||||
|
all_invoices AS (
|
||||||
|
SELECT
|
||||||
|
i.id AS invoice_id, i.order_id,
|
||||||
|
o.customer_id, o.vehicle_id, o.terminal_id,
|
||||||
|
t.center_id, o.total_cost
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_orders_invoice` i
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON i.order_id = o.id
|
||||||
|
LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id
|
||||||
|
WHERE i.created_at >= t_start
|
||||||
|
),
|
||||||
|
client_invoices AS (
|
||||||
|
SELECT ai.*
|
||||||
|
FROM all_invoices ai
|
||||||
|
JOIN cc_clients cc
|
||||||
|
ON ai.customer_id = cc.customer_id
|
||||||
|
AND ai.vehicle_id = cc.vehicle_id
|
||||||
|
WHERE ai.center_id NOT IN (159, 162)
|
||||||
|
),
|
||||||
|
kpi_a AS (
|
||||||
|
SELECT center_id,
|
||||||
|
COUNT(DISTINCT quote_id) AS quotes_cc_facturados,
|
||||||
|
ROUND(SUM(total_cost), 2) AS A_eur
|
||||||
|
FROM cc_converted
|
||||||
|
WHERE center_id IS NOT NULL AND center_id NOT IN (159,162)
|
||||||
|
GROUP BY center_id
|
||||||
|
),
|
||||||
|
kpi_b AS (
|
||||||
|
SELECT center_id,
|
||||||
|
COUNT(DISTINCT invoice_id) AS invoices_b,
|
||||||
|
ROUND(SUM(total_cost), 2) AS B_eur
|
||||||
|
FROM client_invoices
|
||||||
|
GROUP BY center_id
|
||||||
|
),
|
||||||
|
kpi_c AS (
|
||||||
|
SELECT center_id,
|
||||||
|
COUNT(DISTINCT invoice_id) AS invoices_c,
|
||||||
|
ROUND(SUM(total_cost), 2) AS C_eur
|
||||||
|
FROM all_invoices
|
||||||
|
WHERE center_id IS NOT NULL AND center_id NOT IN (159,162)
|
||||||
|
GROUP BY center_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
c.id AS center_id,
|
||||||
|
c.name AS center_name,
|
||||||
|
COALESCE(a.quotes_cc_facturados, 0) AS quotes_cc_facturados,
|
||||||
|
COALESCE(a.A_eur, 0) AS A_quote_cc_eur,
|
||||||
|
COALESCE(b.B_eur, 0) AS B_mismo_cliente_eur,
|
||||||
|
COALESCE(c2.C_eur, 0) AS C_total_centro_eur,
|
||||||
|
ROUND(SAFE_DIVIDE(COALESCE(a.A_eur, 0), c2.C_eur), 4) AS A_sobre_C,
|
||||||
|
ROUND(SAFE_DIVIDE(COALESCE(b.B_eur, 0), c2.C_eur), 4) AS B_sobre_C,
|
||||||
|
ROUND(SAFE_DIVIDE(COALESCE(b.B_eur, 0), NULLIF(a.A_eur, 0)), 2) AS lift_B_vs_A
|
||||||
|
FROM `{PROJECT}.{DATASET}.centers` c
|
||||||
|
LEFT JOIN kpi_a a ON c.id = a.center_id
|
||||||
|
LEFT JOIN kpi_b b ON c.id = b.center_id
|
||||||
|
LEFT JOIN kpi_c c2 ON c.id = c2.center_id
|
||||||
|
WHERE COALESCE(c2.C_eur, 0) > 0
|
||||||
|
ORDER BY C_total_centro_eur DESC
|
||||||
|
""")
|
||||||
|
save("02_kpi_3_por_centro", cols, rows)
|
||||||
|
print(f" centros activos: {len(rows)}")
|
||||||
|
print(f" top5:")
|
||||||
|
for r in rows[:5]:
|
||||||
|
print(f" {r[1]:30} A={r[3]:>12,.0f} B={r[4]:>12,.0f} C={r[5]:>12,.0f}")
|
||||||
|
|
||||||
|
# Totales globales
|
||||||
|
A_total = sum(r[3] for r in rows)
|
||||||
|
B_total = sum(r[4] for r in rows)
|
||||||
|
C_total = sum(r[5] for r in rows)
|
||||||
|
print(f"\n TOTALES ({WINDOW_DAYS}d, centros sin call_center 159/162):")
|
||||||
|
print(f" A (€ quote cc facturados): {A_total:>15,.2f}")
|
||||||
|
print(f" B (€ mismo cliente centro): {B_total:>15,.2f}")
|
||||||
|
print(f" C (€ total centros): {C_total:>15,.2f}")
|
||||||
|
print(f" A/C = {A_total/C_total:.4f} B/C = {B_total/C_total:.4f} lift B/A = {B_total/A_total:.2f}x")
|
||||||
|
with (OUT / "totales_globales.json").open("w") as f:
|
||||||
|
json.dump({
|
||||||
|
"window_days": WINDOW_DAYS,
|
||||||
|
"A_quote_cc_eur": round(A_total, 2),
|
||||||
|
"B_mismo_cliente_eur": round(B_total, 2),
|
||||||
|
"C_total_centros_eur": round(C_total, 2),
|
||||||
|
"A_sobre_C": round(A_total / C_total, 4),
|
||||||
|
"B_sobre_C": round(B_total / C_total, 4),
|
||||||
|
"lift_B_vs_A": round(B_total / A_total, 2),
|
||||||
|
"centros_activos": len(rows),
|
||||||
|
}, f, indent=2)
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# QUERY 3 — Regeneración por centro
|
||||||
|
# =====================================================================
|
||||||
|
print(f"\n[Q3] regeneración por centro ({WINDOW_DAYS}d Q0, {REGEN_WINDOW_DAYS}d window)")
|
||||||
|
cols, rows = run_sql(f"""
|
||||||
|
DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {WINDOW_DAYS} DAY);
|
||||||
|
|
||||||
|
WITH
|
||||||
|
cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
q0 AS (
|
||||||
|
SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,
|
||||||
|
o.customer_id, o.vehicle_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q
|
||||||
|
JOIN cc_users cc ON q.created_by_id = cc.user_id
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id
|
||||||
|
WHERE q.created_at >= t_start AND q.deleted_at IS NULL
|
||||||
|
AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL
|
||||||
|
),
|
||||||
|
qN AS (
|
||||||
|
SELECT q.id AS qn_id, q.order_id AS qn_order, q.created_at AS qn_ts,
|
||||||
|
o.customer_id, o.vehicle_id, t.center_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id
|
||||||
|
LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id
|
||||||
|
WHERE q.deleted_at IS NULL
|
||||||
|
AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)
|
||||||
|
),
|
||||||
|
regen AS (
|
||||||
|
SELECT q0.q0_id, q0.q0_order, q0.customer_id, q0.vehicle_id,
|
||||||
|
qN.qn_id, qN.qn_order, qN.center_id AS regen_center,
|
||||||
|
TIMESTAMP_DIFF(qN.qn_ts, q0.q0_ts, HOUR) / 24 AS dias_entre
|
||||||
|
FROM q0
|
||||||
|
JOIN qN
|
||||||
|
ON q0.customer_id = qN.customer_id
|
||||||
|
AND q0.vehicle_id = qN.vehicle_id
|
||||||
|
AND qN.qn_ts > q0.q0_ts
|
||||||
|
AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL {REGEN_WINDOW_DAYS} DAY)
|
||||||
|
AND qN.qn_order != q0.q0_order
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
c.id AS center_id,
|
||||||
|
c.name AS center_name,
|
||||||
|
COUNT(DISTINCT r.q0_id) AS q0_regenerados_aqui,
|
||||||
|
COUNT(*) AS regen_events,
|
||||||
|
ROUND(AVG(r.dias_entre), 1) AS dias_avg_regen
|
||||||
|
FROM regen r
|
||||||
|
JOIN `{PROJECT}.{DATASET}.centers` c ON r.regen_center = c.id
|
||||||
|
GROUP BY c.id, c.name
|
||||||
|
ORDER BY q0_regenerados_aqui DESC
|
||||||
|
LIMIT 30
|
||||||
|
""")
|
||||||
|
save("03_regen_por_centro", cols, rows)
|
||||||
|
print(f" centros con regeneración: {len(rows)}")
|
||||||
|
print(f" top5:")
|
||||||
|
for r in rows[:5]:
|
||||||
|
print(f" {r[1]:30} q0={r[2]:>5} events={r[3]:>5} dias_avg={r[4]}")
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# QUERY 4 — Totales Q0 con / sin regeneración
|
||||||
|
# =====================================================================
|
||||||
|
print(f"\n[Q4] Q0 con/sin regeneración")
|
||||||
|
cols, rows = run_sql(f"""
|
||||||
|
DECLARE t_start TIMESTAMP DEFAULT TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {WINDOW_DAYS} DAY);
|
||||||
|
|
||||||
|
WITH
|
||||||
|
cc_users AS (
|
||||||
|
SELECT DISTINCT tpvuser_id AS user_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_authorization_tpvuser_centers`
|
||||||
|
WHERE dccenter_id IN (159, 162)
|
||||||
|
),
|
||||||
|
q0 AS (
|
||||||
|
SELECT q.id AS q0_id, q.order_id AS q0_order, q.created_at AS q0_ts,
|
||||||
|
o.customer_id, o.vehicle_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q
|
||||||
|
JOIN cc_users cc ON q.created_by_id = cc.user_id
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id
|
||||||
|
WHERE q.created_at >= t_start AND q.deleted_at IS NULL
|
||||||
|
AND o.customer_id IS NOT NULL AND o.vehicle_id IS NOT NULL
|
||||||
|
),
|
||||||
|
qN AS (
|
||||||
|
SELECT q.order_id AS qn_order, q.created_at AS qn_ts,
|
||||||
|
o.customer_id, o.vehicle_id, t.center_id
|
||||||
|
FROM `{PROJECT}.{DATASET}.tpv_orders_quote` q
|
||||||
|
JOIN `{PROJECT}.{DATASET}.tpv_orders_order` o ON q.order_id = o.id
|
||||||
|
LEFT JOIN `{PROJECT}.{DATASET}.tpv_terminals` t ON o.terminal_id = t.id
|
||||||
|
WHERE q.deleted_at IS NULL
|
||||||
|
AND t.center_id IS NOT NULL AND t.center_id NOT IN (159,162)
|
||||||
|
),
|
||||||
|
regen AS (
|
||||||
|
SELECT DISTINCT q0.q0_id
|
||||||
|
FROM q0
|
||||||
|
JOIN qN
|
||||||
|
ON q0.customer_id = qN.customer_id
|
||||||
|
AND q0.vehicle_id = qN.vehicle_id
|
||||||
|
AND qN.qn_ts > q0.q0_ts
|
||||||
|
AND qN.qn_ts <= TIMESTAMP_ADD(q0.q0_ts, INTERVAL {REGEN_WINDOW_DAYS} DAY)
|
||||||
|
AND qN.qn_order != q0.q0_order
|
||||||
|
),
|
||||||
|
q0_inv AS (
|
||||||
|
SELECT q0.q0_id,
|
||||||
|
CASE WHEN i.id IS NOT NULL THEN 1 ELSE 0 END AS q0_factura
|
||||||
|
FROM q0
|
||||||
|
LEFT JOIN `{PROJECT}.{DATASET}.tpv_orders_invoice` i ON i.order_id = q0.q0_order
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
CASE WHEN r.q0_id IS NOT NULL THEN 'regenerado' ELSE 'no_regenerado' END AS bucket,
|
||||||
|
COUNT(*) AS q0_total,
|
||||||
|
SUM(qi.q0_factura) AS q0_facturado_propio,
|
||||||
|
ROUND(SAFE_DIVIDE(SUM(qi.q0_factura), COUNT(*)), 4) AS conv_q0_propio
|
||||||
|
FROM q0_inv qi
|
||||||
|
LEFT JOIN regen r USING (q0_id)
|
||||||
|
GROUP BY bucket
|
||||||
|
ORDER BY bucket
|
||||||
|
""")
|
||||||
|
save("04_regen_vs_conversion", cols, rows)
|
||||||
|
for r in rows:
|
||||||
|
print(f" {dict(zip(cols, r))}")
|
||||||
|
|
||||||
|
print("\nDONE — resultados en data/results/")
|
||||||
|
client.close()
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Configure dashboard 1019 filters as multi-select dropdowns.
|
||||||
|
|
||||||
|
PKs no tienen has_field_values=list, asi que Metabase no auto-lista.
|
||||||
|
Solucion: crear 3 cards "lookup" (id, name) y usarlas como values_source de cada parametro.
|
||||||
|
|
||||||
|
Producto (2.7M rows) -> dropdown search (no card-source, busqueda en vivo).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
API_KEY = subprocess.check_output(["pass", "show", "metabase/aurgi-api-key"], text=True).strip().splitlines()[0]
|
||||||
|
BASE = "https://reports.autingo.es"
|
||||||
|
COLLECTION_ID = 559
|
||||||
|
DASHBOARD_ID = 1019
|
||||||
|
|
||||||
|
client = httpx.Client(base_url=BASE, headers={"x-api-key": API_KEY}, timeout=180)
|
||||||
|
|
||||||
|
|
||||||
|
# Lookup cards: id, name (orden alfabetico)
|
||||||
|
LOOKUPS = {
|
||||||
|
"centers": """
|
||||||
|
SELECT
|
||||||
|
`psql_dcpublic.centers`.id AS id,
|
||||||
|
`psql_dcpublic.centers`.name AS name
|
||||||
|
FROM `psql_dcpublic.centers`
|
||||||
|
WHERE `psql_dcpublic.centers`.id NOT IN (159, 162)
|
||||||
|
ORDER BY name
|
||||||
|
""",
|
||||||
|
"cc_users": """
|
||||||
|
SELECT DISTINCT
|
||||||
|
`psql_dcpublic.tpv_authorization_tpvuser`.id AS id,
|
||||||
|
`psql_dcpublic.tpv_authorization_tpvuser`.name AS name
|
||||||
|
FROM `psql_dcpublic.tpv_authorization_tpvuser`
|
||||||
|
JOIN `psql_dcpublic.tpv_authorization_tpvuser_centers`
|
||||||
|
ON `psql_dcpublic.tpv_authorization_tpvuser`.id = `psql_dcpublic.tpv_authorization_tpvuser_centers`.tpvuser_id
|
||||||
|
WHERE `psql_dcpublic.tpv_authorization_tpvuser_centers`.dccenter_id IN (159, 162)
|
||||||
|
AND `psql_dcpublic.tpv_authorization_tpvuser`.is_active
|
||||||
|
ORDER BY name
|
||||||
|
""",
|
||||||
|
"companies": """
|
||||||
|
SELECT
|
||||||
|
`psql_dcpublic.companies`.id AS id,
|
||||||
|
`psql_dcpublic.companies`.name AS name
|
||||||
|
FROM `psql_dcpublic.companies`
|
||||||
|
ORDER BY name
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_lookup_card(slug: str, sql: str) -> int:
|
||||||
|
name = f"_lookup_{slug}"
|
||||||
|
body = {
|
||||||
|
"name": name,
|
||||||
|
"description": f"Lookup table for dashboard filter dropdown ({slug})",
|
||||||
|
"type": "question",
|
||||||
|
"display": "table",
|
||||||
|
"visualization_settings": {},
|
||||||
|
"dataset_query": {
|
||||||
|
"type": "native", "database": 6,
|
||||||
|
"native": {"query": sql.strip(), "template-tags": {}},
|
||||||
|
},
|
||||||
|
"collection_id": COLLECTION_ID,
|
||||||
|
"result_metadata": None,
|
||||||
|
}
|
||||||
|
r = client.post("/api/card", json=body)
|
||||||
|
r.raise_for_status()
|
||||||
|
cid = r.json()["id"]
|
||||||
|
print(f" lookup card {slug:12} -> {cid}")
|
||||||
|
# Run query to populate result_metadata
|
||||||
|
client.post(f"/api/card/{cid}/query")
|
||||||
|
return cid
|
||||||
|
|
||||||
|
|
||||||
|
print("creating lookup cards...")
|
||||||
|
lookup_ids = {slug: make_lookup_card(slug, sql) for slug, sql in LOOKUPS.items()}
|
||||||
|
|
||||||
|
# Re-read each card to pick up result_metadata field-ids (needed for value_field/label_field by name)
|
||||||
|
def field_ref(card_id, col_name):
|
||||||
|
"""Reference column inside a native source card. Metabase accepts:
|
||||||
|
['field', '<name>', {'base-type': '...'}]
|
||||||
|
"""
|
||||||
|
c = client.get(f"/api/card/{card_id}").json()
|
||||||
|
for f in c.get("result_metadata", []) or []:
|
||||||
|
if f["name"] == col_name:
|
||||||
|
base = f.get("base_type", "type/Integer" if col_name == "id" else "type/Text")
|
||||||
|
return ["field", col_name, {"base-type": base}]
|
||||||
|
# fallback
|
||||||
|
return ["field", col_name, {"base-type": "type/Integer" if col_name == "id" else "type/Text"}]
|
||||||
|
|
||||||
|
|
||||||
|
print("\nupdating dashboard parameters...")
|
||||||
|
new_params = [
|
||||||
|
{"id": "p_date", "name": "Fecha presupuesto", "slug": "fecha",
|
||||||
|
"sectionId": "date", "type": "date/range"},
|
||||||
|
{"id": "p_centro", "name": "Centro", "slug": "centro",
|
||||||
|
"sectionId": "id", "type": "id",
|
||||||
|
"values_query_type": "list",
|
||||||
|
"values_source_type": "card",
|
||||||
|
"values_source_config": {
|
||||||
|
"card_id": lookup_ids["centers"],
|
||||||
|
"value_field": field_ref(lookup_ids["centers"], "id"),
|
||||||
|
"label_field": field_ref(lookup_ids["centers"], "name"),
|
||||||
|
},
|
||||||
|
"isMultiSelect": True},
|
||||||
|
{"id": "p_agente", "name": "Agente CC", "slug": "agente",
|
||||||
|
"sectionId": "id", "type": "id",
|
||||||
|
"values_query_type": "list",
|
||||||
|
"values_source_type": "card",
|
||||||
|
"values_source_config": {
|
||||||
|
"card_id": lookup_ids["cc_users"],
|
||||||
|
"value_field": field_ref(lookup_ids["cc_users"], "id"),
|
||||||
|
"label_field": field_ref(lookup_ids["cc_users"], "name"),
|
||||||
|
},
|
||||||
|
"isMultiSelect": True},
|
||||||
|
{"id": "p_compania", "name": "Compañía", "slug": "compania",
|
||||||
|
"sectionId": "id", "type": "id",
|
||||||
|
"values_query_type": "list",
|
||||||
|
"values_source_type": "card",
|
||||||
|
"values_source_config": {
|
||||||
|
"card_id": lookup_ids["companies"],
|
||||||
|
"value_field": field_ref(lookup_ids["companies"], "id"),
|
||||||
|
"label_field": field_ref(lookup_ids["companies"], "name"),
|
||||||
|
},
|
||||||
|
"isMultiSelect": True},
|
||||||
|
{"id": "p_producto", "name": "Producto", "slug": "producto",
|
||||||
|
"sectionId": "id", "type": "id",
|
||||||
|
"values_query_type": "search",
|
||||||
|
"values_source_type": None,
|
||||||
|
"values_source_config": {},
|
||||||
|
"isMultiSelect": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
r = client.put(f"/api/dashboard/{DASHBOARD_ID}", json={"parameters": new_params})
|
||||||
|
r.raise_for_status()
|
||||||
|
print("dashboard updated")
|
||||||
|
print(f" URL: {BASE}/dashboard/{DASHBOARD_ID}")
|
||||||
|
client.close()
|
||||||
Reference in New Issue
Block a user