Files
presupuestos_callcenter/create_dashboard_v2.py
2026-05-21 18:26:30 +02:00

281 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()