From b4db4e4ef5aea5c3835db9fa11de7cd7e382bd73 Mon Sep 17 00:00:00 2001 From: egutierrez Date: Tue, 5 May 2026 18:29:26 +0200 Subject: [PATCH] feat(metabase): smartscalar KPI builders (sql + payload + dimension tag) 3 helpers puros para construir KPIs con display=smartscalar y comparacion vs n-1 sin que Metabase v0.59 pida breakout temporal. Replican el patron del dashboard Informe Lean (UNION ALL de 2 filas periodo/valor) y rellenan la firma exacta de template-tags que el frontend MBQL5 acepta. Co-Authored-By: Claude Opus 4.7 (1M context) --- python/functions/metabase/__init__.py | 2 + .../metabase_smartscalar_dimension_tag.md | 74 +++++ .../metabase_smartscalar_kpi_payload.md | 96 ++++++ .../metabase/metabase_smartscalar_kpi_sql.md | 77 +++++ python/functions/metabase/smartscalar.py | 282 ++++++++++++++++++ 5 files changed, 531 insertions(+) create mode 100644 python/functions/metabase/metabase_smartscalar_dimension_tag.md create mode 100644 python/functions/metabase/metabase_smartscalar_kpi_payload.md create mode 100644 python/functions/metabase/metabase_smartscalar_kpi_sql.md create mode 100644 python/functions/metabase/smartscalar.py diff --git a/python/functions/metabase/__init__.py b/python/functions/metabase/__init__.py index 53a028ee..7d1fa75b 100644 --- a/python/functions/metabase/__init__.py +++ b/python/functions/metabase/__init__.py @@ -13,6 +13,7 @@ from .setup import metabase_setup from .maintenance import metabase_fix_null_ratio, metabase_pair_n_n1_columns from .metabase_mbql_validate import metabase_mbql_validate from .metabase_update_dashboard_safe import metabase_update_dashboard_safe +from .smartscalar import metabase_smartscalar_kpi_sql, metabase_smartscalar_dimension_tag, metabase_smartscalar_kpi_payload __all__ = [ "MetabaseClient", @@ -38,4 +39,5 @@ __all__ = [ "metabase_pair_n_n1_columns", "metabase_mbql_validate", "metabase_update_dashboard_safe", + "metabase_smartscalar_kpi_sql", "metabase_smartscalar_dimension_tag", "metabase_smartscalar_kpi_payload", ] diff --git a/python/functions/metabase/metabase_smartscalar_dimension_tag.md b/python/functions/metabase/metabase_smartscalar_dimension_tag.md new file mode 100644 index 00000000..20d35ef4 --- /dev/null +++ b/python/functions/metabase/metabase_smartscalar_dimension_tag.md @@ -0,0 +1,74 @@ +--- +name: metabase_smartscalar_dimension_tag +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: pure +signature: "def metabase_smartscalar_dimension_tag(*, name: str, field_id: int, base_type: str = 'type/Text', widget_type: str = 'string/=', display_name: str = '') -> dict" +description: "Construye un template-tag de tipo dimension con la firma exacta que Metabase v0.59 espera (incluyendo lib/uuid, base-type, effective-type). Sin estos flags Metabase devuelve PUT 200 pero descarta silenciosamente el dataset_query." +tags: [metabase, smartscalar, template-tag, field-filter, kpi, pure, builder] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: + - "import uuid" +params: + - name: name + desc: "Identificador snake_case del template-tag; tambien se usa en el SQL como [[AND {{name}}]]" + - name: field_id + desc: "ID numerico del Field de Metabase al que apunta el filtro; consultable via GET /api/field/ o el data model" + - name: base_type + desc: "Tipo base del Field. Valores comunes: type/Text, type/Date, type/Integer, type/Decimal, type/DateTime" + - name: widget_type + desc: "Widget de filtro en el dashboard. Comunes: string/= (multivalor texto), date/all-options, number/=, string/contains" + - name: display_name + desc: "Etiqueta legible mostrada en la UI; vacio = autogenerada title-casing del name" +output: "dict con la estructura {name, id, display-name, type, widget-type, dimension: ['field', {base-type, effective-type, lib/uuid}, field_id]} listo para usar como entrada del map template-tags de un dataset_query nativo" +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/metabase/smartscalar.py" +--- + +## Por que existe + +Metabase v0.59 valida silenciosamente la estructura de los ``template-tags`` +en cards nativas: si falta ``lib/uuid``, ``base-type`` o ``effective-type`` +en el ``dimension`` array, el ``PUT /api/card/:id`` devuelve 200 pero al +releer la card el ``dataset_query`` aparece vacio (``stages: [{}]``) y la +card no se puede ejecutar. + +Este helper rellena la firma exacta y ademas genera el ``lib/uuid`` +deterministicamente con ``uuid5`` sobre el ``name``, garantizando que la +funcion sea pura: dos llamadas con los mismos argumentos producen el mismo +dict bit a bit. + +## Ejemplo + +```python +tags = { + "fecha": metabase_smartscalar_dimension_tag( + name="fecha", field_id=322392, + base_type="type/Date", widget_type="date/all-options", + display_name="Fecha", + ), + "categoria": metabase_smartscalar_dimension_tag( + name="categoria", field_id=270702, + widget_type="string/=", + ), +} +# tags["fecha"]["dimension"] == ["field", {"base-type": "type/Date", ...}, 322392] +``` + +## Notas + +- El ``id`` interno se rellena con ``f"{name}_tag"`` por consistencia con las + cards de la suite Lean. +- Para campos calculados (expresiones MBQL) o template-tags de tipo ``text``, + ``snippet`` o ``card``, NO usar este helper — fueron disenados solo para + dimension/field-filters. +- Si la card se va a guardar una vez y editar a mano luego, los UUIDs + deterministicos no causan colisiones porque solo importan dentro de la card. diff --git a/python/functions/metabase/metabase_smartscalar_kpi_payload.md b/python/functions/metabase/metabase_smartscalar_kpi_payload.md new file mode 100644 index 00000000..a668a8d4 --- /dev/null +++ b/python/functions/metabase/metabase_smartscalar_kpi_payload.md @@ -0,0 +1,96 @@ +--- +name: metabase_smartscalar_kpi_payload +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: pure +signature: "def metabase_smartscalar_kpi_payload(*, name: str, database_id: int, sql: str, template_tags: dict | None = None, description: str = '', collection_id: int = 0, currency: bool = False, currency_code: str = 'EUR', decimals: int = 0, comparison_label: str = 'vs n-1', extra_visualization_settings: dict | None = None) -> dict" +description: "Construye el payload completo para POST /api/card de un KPI smartscalar con formato y comparacion previousValue. Combina SQL nativo (idealmente generado por metabase_smartscalar_kpi_sql) con template-tags y visualization_settings predefinidos. Listo para metabase_create_card_raw." +tags: [metabase, smartscalar, kpi, card, payload, pure, builder] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: name + desc: "Nombre de la card mostrado en el dashboard" + - name: database_id + desc: "ID de la database de Metabase contra la que se ejecuta el SQL" + - name: sql + desc: "Query nativa que devuelve columnas periodo (DATE) y valor (numero) con 2 filas (n-1, actual). Generable con metabase_smartscalar_kpi_sql" + - name: template_tags + desc: "Dict {tag_name: tag_dict} para field-filters; None para card sin filtros. Construir con metabase_smartscalar_dimension_tag" + - name: description + desc: "Descripcion mostrada en la card; vacio = autorrellenada" + - name: collection_id + desc: "ID de la coleccion destino; 0 = Our analytics (root)" + - name: currency + desc: "Si True formatea valor como moneda con simbolo currency_code" + - name: currency_code + desc: "Codigo ISO de moneda. Default EUR. Solo aplica si currency=True" + - name: decimals + desc: "Decimales mostrados en valor" + - name: comparison_label + desc: "Etiqueta del bloque de comparacion bajo el numero principal. Default 'vs n-1'" + - name: extra_visualization_settings + desc: "Settings adicionales fusionados (top-level override) en visualization_settings; util para anadir scalar.title, card.title.alignment, etc." +output: "dict con payload completo (name, description, type=question, display=smartscalar, dataset_query nativo, visualization_settings con scalar.field/scalar.comparisons/column_settings, [collection_id]) directamente posteable a /api/card" +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/metabase/smartscalar.py" +--- + +## Por que existe + +Encapsular la combinacion exacta de campos que Metabase v0.59 necesita para +un KPI ``smartscalar`` con comparacion ``previousValue``: ``scalar.field``, +``scalar.comparisons`` con ``id``, ``type`` y ``label``, y ``column_settings`` +con la clave de columna serializada como JSON ``'["name","valor"]'``. + +Cualquier desviacion (clave de column_settings sin escapar, ausencia de +``scalar.field``, ``type`` distinto de ``previousValue/anotherColumn/staticNumber``) +hace que el frontend muestre el numero pero descarta silenciosamente la +comparacion o pide breakout. + +## Ejemplo + +```python +sql = metabase_smartscalar_kpi_sql( + act_expr="ROUND(SUM(v.venta_n), 2)", + n1_expr="ROUND(SUM(v.venta_n1), 2)", + body_sql=ventas_cte_sql, + date_expr="MIN(v.fecha)", +) +tags = { + "fecha": metabase_smartscalar_dimension_tag( + name="fecha", field_id=322392, + base_type="type/Date", widget_type="date/all-options", + ), +} +payload = metabase_smartscalar_kpi_payload( + name="Venta total", + database_id=6, + sql=sql, + template_tags=tags, + currency=True, decimals=0, + description="Venta del periodo seleccionado vs ano anterior.", + collection_id=583, +) +card = metabase_create_card_raw(client, payload) +print(card["id"]) +``` + +## Notas + +- ``visualization_settings.column_settings`` usa la clave con formato JSON + exacto ``'["name","valor"]'`` (sin espacios entre comillas y coma) — Metabase + no normaliza variaciones. +- Si necesitas la card como ``model`` o ``metric`` en lugar de ``question``, + override via ``extra_visualization_settings`` no aplica — clona el resultado + y modifica ``payload["type"]`` antes de enviar. +- Para reemplazar una card existente usa ``metabase_update_card`` con los + mismos campos del payload (display, dataset_query, visualization_settings). diff --git a/python/functions/metabase/metabase_smartscalar_kpi_sql.md b/python/functions/metabase/metabase_smartscalar_kpi_sql.md new file mode 100644 index 00000000..6736174f --- /dev/null +++ b/python/functions/metabase/metabase_smartscalar_kpi_sql.md @@ -0,0 +1,77 @@ +--- +name: metabase_smartscalar_kpi_sql +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: pure +signature: "def metabase_smartscalar_kpi_sql(*, act_expr: str, n1_expr: str, body_sql: str, date_expr: str = 'MIN(fecha)') -> str" +description: "Envuelve agregaciones actual+n-1 en el patron de 2 filas (periodo, valor) que el display smartscalar de Metabase v0.59 requiere para mostrar comparacion vs ano anterior sin pedir breakout temporal. Genera SQL nativo BigQuery con UNION ALL d_min/d_min-52w." +tags: [metabase, smartscalar, kpi, sql, bigquery, pure, builder] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: act_expr + desc: "Expresion SQL de agregado para el periodo actual, ej. 'ROUND(SUM(v.venta_n), 2)' o 'SAFE_DIVIDE(SUM(a), NULLIF(SUM(b),0))'" + - name: n1_expr + desc: "Expresion SQL para el mismo agregado del ano anterior, ej. 'ROUND(SUM(v.venta_n1), 2)'" + - name: body_sql + desc: "Cuerpo SQL desde 'FROM' que define las tablas/CTEs y filtros con template-tags Metabase ([[AND {{tag}}]])" + - name: date_expr + desc: "Expresion para extraer la fecha minima del periodo; usada como periodo en la fila actual y ancla para DATE_SUB de la fila n-1. Default 'MIN(fecha)'" +output: "string con SQL nativo BigQuery que devuelve 2 filas con columnas periodo (DATE) y valor (numero); fila 1 = periodo n-1, fila 2 = periodo actual" +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/metabase/smartscalar.py" +--- + +## Por que existe + +El display ``smartscalar`` de Metabase v0.59.4 con ``previousValue`` requiere +una serie temporal para mostrar comparacion. Si el query devuelve un solo +numero el frontend muestra el error "Agrupa solo por un campo de tiempo para +ver como ha cambiado con el tiempo". + +El truco probado en el dashboard ``Informe Lean`` (cards 9340-9373) es +producir 2 filas sinteticas: una para el periodo actual (``d_min``, ``act``) +y otra para n-1 (``d_min - 52 weeks``, ``n1``). Smartscalar lo interpreta como +serie de 2 puntos y compara naturalmente. + +Esta funcion automatiza ese patron: el caller solo provee el cuerpo SQL con +sus filtros y las dos expresiones de agregado. + +## Ejemplo + +```python +body = """ +FROM `proj.ds.base_margenes_aa` `proj.ds.base_margenes_aa` v +LEFT JOIN `proj.ds.Objeto_productos` p ON p.nav_id = v.prod_nav_id +WHERE 1=1 + [[AND {{fecha}}]] + [[AND {{categoria}}]] + [[AND {{tipo}}]] +""" + +sql = metabase_smartscalar_kpi_sql( + act_expr="ROUND(SUM(v.venta_n), 2)", + n1_expr="ROUND(SUM(v.venta_n1), 2)", + body_sql=body, + date_expr="MIN(v.fecha)", +) +# sql tiene UNION ALL con periodo y valor +``` + +## Notas + +- Por convencion los template-tags estan envueltos en ``[[ ]]`` para que sean + opcionales: si el dashboard no aplica el filtro la clausula desaparece. +- Para que los field-filters de Metabase resuelvan correctamente las tablas + dentro de CTEs, alias cada tabla con la cadena ``schema.table`` exacta que + Metabase usa al expandir el template-tag (ej. ``\`proj.ds.tabla\` \`ds.tabla\``). +- El SQL generado usa ``DATE_SUB(d_min, INTERVAL 52 WEEK)`` (BigQuery dialect). + Para Postgres adaptar a ``d_min - INTERVAL '52 weeks'``. diff --git a/python/functions/metabase/smartscalar.py b/python/functions/metabase/smartscalar.py new file mode 100644 index 00000000..3937550f --- /dev/null +++ b/python/functions/metabase/smartscalar.py @@ -0,0 +1,282 @@ +"""Helpers puros para construir KPIs con display=smartscalar en Metabase. + +El display ``smartscalar`` de Metabase v0.59 muestra un numero grande con +comparacion (% variacion + flecha verde/roja) cuando el query devuelve una serie +temporal y ``scalar.comparisons`` esta configurado. Para mostrar "total del +periodo seleccionado vs mismo periodo del ano anterior" sin que el motor pida +un breakout temporal, el truco probado en el dashboard ``Informe Lean`` es +producir 2 filas: ``(d_min - 52 semanas, n-1_total)`` y ``(d_min, total_actual)``, +con columnas ``periodo`` y ``valor``. Smartscalar lo interpreta como serie de +2 puntos y compara natural con ``previousValue``. +""" + +from __future__ import annotations + +import uuid + + +_TAG_NAMESPACE = uuid.UUID("c0ffee00-6e75-4b14-9e8e-bea7ed5ca1ab") + + +def metabase_smartscalar_kpi_sql( + *, + act_expr: str, + n1_expr: str, + body_sql: str, + date_expr: str = "MIN(fecha)", +) -> str: + """Envuelve una agregacion en el patron de 2 filas que smartscalar requiere. + + Recibe las expresiones de agregado para periodo actual y n-1, mas el cuerpo + SQL (CTEs + JOINs + WHERE con template-tags) que produce la tabla a agregar, + y devuelve la query nativa completa lista para incrustar en una card. La + salida tiene siempre el mismo shape: 2 filas con columnas ``periodo`` + (DATE) y ``valor`` (numero). + + Args: + act_expr: Expresion SQL para el total del periodo actual. Tipicamente + ``SUM(...)`` o ``SAFE_DIVIDE(SUM(a), NULLIF(SUM(b), 0))``. Debe poder + evaluarse contra el alias del cuerpo (por ejemplo ``v.venta_n``). + n1_expr: Equivalente para el mismo periodo del ano anterior. + body_sql: Cuerpo SQL que define las tablas/CTEs y termina con un + ``FROM ... [JOIN ...] [WHERE ...]`` referenciable como ``ventas v``, + o equivalente. Ejemplo en docstring de salida. + date_expr: Expresion para extraer la fecha minima del periodo, usada + como ``periodo`` en la fila actual y como ancla para calcular la + fecha n-1 con ``DATE_SUB(..., INTERVAL 52 WEEK)``. Default + ``MIN(fecha)``. Para queries con alias usar ``MIN(v.fecha)``. + + Returns: + SQL nativo (BigQuery dialect por defecto) con la estructura: + + WITH agg AS ( + SELECT COALESCE({date_expr}, CURRENT_DATE()) AS d_min, + {act_expr} AS act, + {n1_expr} AS n1 + {body_sql} + ) + SELECT DATE_SUB(d_min, INTERVAL 52 WEEK) AS periodo, n1 AS valor FROM agg + UNION ALL + SELECT d_min, act FROM agg + ORDER BY periodo + + Example: + >>> body = ''' + ... FROM `proj.ds.base_margenes_aa` v + ... WHERE 1=1 [[AND {{fecha}}]] [[AND {{tipo}}]]''' + >>> sql = metabase_smartscalar_kpi_sql( + ... act_expr="ROUND(SUM(v.venta_n), 2)", + ... n1_expr="ROUND(SUM(v.venta_n1), 2)", + ... body_sql=body, + ... date_expr="MIN(v.fecha)", + ... ) + >>> assert "UNION ALL" in sql + >>> assert "periodo" in sql and "valor" in sql + """ + inner_select = ( + f" SELECT\n" + f" COALESCE({date_expr}, CURRENT_DATE()) AS d_min,\n" + f" {act_expr} AS act,\n" + f" {n1_expr} AS n1\n" + f" {body_sql.strip()}" + ) + return ( + "WITH agg AS (\n" + f"{inner_select}\n" + ")\n" + "SELECT DATE_SUB(d_min, INTERVAL 52 WEEK) AS periodo, n1 AS valor FROM agg\n" + "UNION ALL\n" + "SELECT d_min, act FROM agg\n" + "ORDER BY periodo" + ) + + +def metabase_smartscalar_dimension_tag( + *, + name: str, + field_id: int, + base_type: str = "type/Text", + widget_type: str = "string/=", + display_name: str = "", +) -> dict: + """Construye un template-tag de tipo ``dimension`` listo para Metabase v0.59. + + Las cards nativas con filtros (field-filters) requieren un dict por + template-tag con la firma exacta que espera el frontend MBQL5. Esta funcion + rellena ``id``, ``display-name``, ``type``, ``widget-type`` y ``dimension`` + con los flags ``base-type``, ``effective-type`` y ``lib/uuid`` que sin los + cuales Metabase silenciosamente descarta el query al guardar (PUT 200 sin + persistir el ``dataset_query``). + + El ``lib/uuid`` se deriva deterministicamente del nombre del tag con + ``uuid5`` para que la funcion sea pura: dos llamadas con el mismo ``name`` + producen el mismo UUID. Esto es seguro porque ``lib/uuid`` solo necesita ser + unico dentro de la misma card. + + Args: + name: Identificador del template-tag (snake_case). Tambien sirve como + nombre del filtro en el SQL: ``[[AND {{name}}]]``. + field_id: ID numerico del Field de Metabase al que apunta el filtro. + Se obtiene con ``GET /api/field/`` o navegando el data model. + base_type: Tipo base del Field (ej. ``type/Text``, ``type/Date``, + ``type/Integer``, ``type/Decimal``). + widget_type: Tipo de widget en el dashboard. Comunes: + ``string/=`` (multivalor texto), ``date/all-options`` (fecha), + ``number/=``, ``string/contains``. + display_name: Etiqueta legible mostrada en la UI cuando alguien + ejecuta la card directamente. Vacio = se autogenera title-casing + del ``name``. + + Returns: + Dict con la estructura ``{"name", "id", "display-name", "type", + "widget-type", "dimension": ["field", {...}, field_id]}``. + + Example: + >>> tag = metabase_smartscalar_dimension_tag( + ... name="fecha", field_id=322392, + ... base_type="type/Date", widget_type="date/all-options", + ... display_name="Fecha", + ... ) + >>> assert tag["dimension"][2] == 322392 + >>> assert tag["widget-type"] == "date/all-options" + >>> # UUID determinista: misma llamada produce mismo dict + >>> assert tag == metabase_smartscalar_dimension_tag( + ... name="fecha", field_id=322392, + ... base_type="type/Date", widget_type="date/all-options", + ... display_name="Fecha", + ... ) + """ + label = display_name or name.replace("_", " ").title() + tag_uuid = str(uuid.uuid5(_TAG_NAMESPACE, name)) + return { + "name": name, + "id": f"{name}_tag", + "display-name": label, + "type": "dimension", + "widget-type": widget_type, + "dimension": [ + "field", + { + "base-type": base_type, + "effective-type": base_type, + "lib/uuid": tag_uuid, + }, + field_id, + ], + } + + +def metabase_smartscalar_kpi_payload( + *, + name: str, + database_id: int, + sql: str, + template_tags: dict | None = None, + description: str = "", + collection_id: int = 0, + currency: bool = False, + currency_code: str = "EUR", + decimals: int = 0, + comparison_label: str = "vs n-1", + extra_visualization_settings: dict | None = None, +) -> dict: + """Construye el payload para POST /api/card de un KPI smartscalar. + + Combina el SQL nativo (idealmente generado por + ``metabase_smartscalar_kpi_sql``) con la configuracion de visualizacion + smartscalar que muestra ``valor`` con comparacion ``previousValue`` y + formato numero/divisa. El payload resultante puede pasarse directamente + a ``metabase_create_card_raw``. + + Args: + name: Nombre de la card. + database_id: ID de la database de Metabase contra la que se ejecuta. + sql: Query nativa que devuelve columnas ``periodo`` (DATE) y ``valor`` + (numero) con 2 filas (n-1, actual). Generable con + ``metabase_smartscalar_kpi_sql``. + template_tags: Dict de template-tags para field-filters + (``{tag_name: tag_dict}``). Construir con + ``metabase_smartscalar_dimension_tag``. None = sin filtros. + description: Descripcion mostrada en la card. Vacio se autorrellena. + collection_id: ID de la coleccion destino. 0 = root. + currency: Si True formatea ``valor`` como moneda con ``currency_code``. + currency_code: Codigo ISO de moneda. Default EUR. + decimals: Decimales mostrados en ``valor``. + comparison_label: Etiqueta del bloque de comparacion. Default "vs n-1". + extra_visualization_settings: Settings adicionales a fusionar (top-level + override) en ``visualization_settings``. Util para anadir + ``scalar.title``, ``card.title.alignment``, etc. + + Returns: + Dict con estructura: + + { + "name": ..., "description": ..., "type": "question", + "display": "smartscalar", + "dataset_query": {"database": ..., "type": "native", + "native": {"query": ..., "template-tags": ...}}, + "visualization_settings": { + "scalar.field": "valor", + "scalar.comparisons": [ + {"id": "cmp_n1", "type": "previousValue", "label": ...}], + "column_settings": {'["name","valor"]': {...formato...}} + }, + ["collection_id"]: ... + } + + Example: + >>> sql = metabase_smartscalar_kpi_sql( + ... act_expr="ROUND(SUM(v.venta_n), 2)", + ... n1_expr="ROUND(SUM(v.venta_n1), 2)", + ... body_sql="FROM ventas v", + ... date_expr="MIN(v.fecha)", + ... ) + >>> tags = {"fecha": metabase_smartscalar_dimension_tag( + ... name="fecha", field_id=322392, + ... base_type="type/Date", widget_type="date/all-options", + ... )} + >>> payload = metabase_smartscalar_kpi_payload( + ... name="Venta total", database_id=6, sql=sql, + ... template_tags=tags, currency=True, decimals=0, + ... ) + >>> assert payload["display"] == "smartscalar" + >>> assert payload["visualization_settings"]["scalar.field"] == "valor" + """ + fmt: dict = {"decimals": decimals} + if currency: + fmt.update( + { + "number_style": "currency", + "currency": currency_code, + "currency_in_header": False, + } + ) + viz: dict = { + "scalar.field": "valor", + "scalar.comparisons": [ + {"id": "cmp_n1", "type": "previousValue", "label": comparison_label} + ], + "column_settings": {'["name","valor"]': fmt}, + } + if extra_visualization_settings: + viz.update(extra_visualization_settings) + + payload: dict = { + "name": name, + "description": description + or f"KPI {name} con comparacion vs mismo periodo del ano anterior.", + "type": "question", + "display": "smartscalar", + "dataset_query": { + "database": database_id, + "type": "native", + "native": { + "query": sql, + "template-tags": template_tags or {}, + }, + }, + "visualization_settings": viz, + } + if collection_id > 0: + payload["collection_id"] = collection_id + return payload