chore: sync from fn-registry agent
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user