> For the complete documentation index, see [llms.txt](https://wedocs.gitbook.io/wedocs-5.1/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wedocs.gitbook.io/wedocs-5.1/wedocs-5.0-es_es/notas-de-la-version/notas-de-la-version-5.0.md).

# Notas de la Versión 5.0

## <mark style="color:blue;">Destacados</mark>

### <img src="/files/w76xBAY5NhszJ4p5J3co" alt="" data-size="line"> DuckDB como almacenamiento offline de dados

Ahora es posible utilizar **DuckDB** como alternativa a **MonetDB** para almacenar dados offline de metadados. [#duckdb-armazenamento-de-dados-offline](#duckdb-armazenamento-de-dados-offline "mention")

### <img src="/files/lpVA358VBo0IoWLdJhaC" alt="" data-size="line">  Metadatos vía script Python

A partir de la versión 5.0 será posible crear metadatos utilizando scripts en Python. Para ello, accede a la pantalla de creación o edición de metadatos en Tablas y selecciona la opción 'Script Python'. .[#criacao-de-metadado-via-script-python](#criacao-de-metadado-via-script-python "mention")

### &#x20;<img src="/files/DW2mlXjDv0N6O2Uy7g94" alt="" data-size="line"> Componente Python

Además de la creación de metadatos vía Python, ahora también es posible crear componentes con Python. En la pestaña Dashboard se añadió la opción de componente Python. [Broken mention](broken://pages/6f8f8e5daf4bf43c6d267e29962e965bbfdb360a#novo-componente-python)

## <mark style="color:blue;">Detalles</mark>

### DuckDB - Almacenamiento de datos offline

* Ahora es posible utilizar **DuckDB** como alternativa a **MonetDB** para almacenar datos offline de metadatos.

**Principales cambios:**

* **Nueva opción de base offline en el registro de metadatos:**\
  Al crear o editar un metadato, podrás elegir entre **DuckDB** (nuevo predeterminado) y **MonetDB**.
* **Migración facilitada entre bases:**\
  Al cambiar la base de datos de un metadato existente, el sistema ejecutará automáticamente la migración de los datos, con opción de eliminar los datos antiguos.
* **Soporte total para carga incremental y tablas virtuales:**\
  Si existen otras tablas virtuales vinculadas, será posible aplicar el cambio a todos los metadatos relacionados.
* **Entrada en vigor automática:**\
  El cambio de base solo se aplicará en la próxima carga de datos (programada o manual). Hasta entonces, los datos seguirán leyéndose normalmente desde la base anterior.
* **Explorador de datos con DuckDB:**\
  Cuando el explorador esté usando metadatos online con almacenamiento offline forzado, la base utilizada será siempre **DuckDB**.

Esta actualización aporta más ligereza, simplicidad y rendimiento, ya que DuckDB es una base embebida, ideal para escenarios locales y de lectura intensiva.

Con esto, también se añadió un servicio en el Administrador de Tareas responsable de la limpieza de la base DuckDB.

### Creación de metadato vía Script Python

* A partir de la versión 5.0 será posible crear metadatos utilizando scripts en Python. Para ello, accede a la pantalla de creación o edición de metadatos en Tablas y selecciona la opción 'Script Python'.

En el metadato por script Python es posible escribir cualquier script Python, **pero existe una regla obligatoria**:

El retorno/resultado de la ejecución del script debe asignarse a una variable llamada `result` y esta debe ser un objeto de la clase DataFrame del framework/biblioteca Pandas.

Ejemplo 1:

{% code title="Ejemplo 1" %}

```python
import pandas as pd

# Criar um DataFrame vazio
result = pd.DataFrame()

try:
    if fieldnames is None:
        fieldnames = []
except:
    fieldnames = []

# Adicionar as colunas com os tipos
if (len(fieldnames) == 0) or ('id' in fieldnames):
    result['id'] = pd.Series(dtype=pd.Int64Dtype())

if (len(fieldnames) == 0) or ('nome' in fieldnames):
    result['nome'] = pd.Series(dtype=pd.StringDtype())

myfilter = {{myfilter}}

# Inserir linhas manualmente
for i in range(5):
    id = i + 1
    nome = f'linha {i}'

    # Retornar apenas os registros que atendem aos filtros
    if myfilter is not None:
        if id != myfilter:
            continue

    # Retornar apenas a quantidade necessária de registros
    if i > recsmax - 1:
        break

    row = []

    if (len(fieldnames) == 0) or ('id' in fieldnames):
        row = row + [id]

    if (len(fieldnames) == 0) or ('nome' in fieldnames):
        row = row + [nome]

    result.loc[i] = row

if (len(fieldnames) == 0) or ('id' in fieldnames):
    result['id'] = result['id'].astype(pd.Int64Dtype())

if (len(fieldnames) == 0) or ('nome' in fieldnames):
    result['nome'] = result['nome'].astype(pd.StringDtype())
```

{% endcode %}

Ejemplo 2:

{% code title="Ejemplo 2" %}

```python
import sqlite3
import pandas as pd

conn = sqlite3.connect('c:/temp/sampleData1000.sdb')

cursor = conn.cursor()

# Devolver solo las columnas necesarias
try:
    if fieldnames is None:
        fieldnames = []
except:
    fieldnames = []

if len(fieldnames) == 0:
    query = 'select id, name from product_category'
else:
    query = 'select ' + ', '.join(fieldnames) + ' from product_category'

# Devolver solo los registros que cumplen los filtros
myfilter = {{myfilter}}

if myfilter is not None:
    query = query + ' where id = ' + str(myfilter)

# Devolver solo la cantidad necesaria de registros
if recsmax > 0:
    query = query + ' limit ' + str(recsmax)

cursor.execute(query)

result = pd.DataFrame(cursor.fetchall(), columns=[desc[0] for desc in cursor.description])

# Realizar la conversión después de llenar los datos para garantizar que los tipos sean correctos
if 'id' in fieldnames:
    result['id'] = result['id'].astype(pd.Int64Dtype())

if 'name' in fieldnames:
    result['name'] = result['name'].astype(pd.StringDtype())

conn.close()
```

{% endcode %}

Ejemplo 3: Recordando que todos los datos presentados são fictícios.

{% code title="Ejemplo 3" %}

```python
import pandas as pd
import random

# Listas de exemplo
hospitais = [f"Hospital {nome}" for nome in [
    "São José", "Santa Marta", "Vida Nova", "Esperança", "Central", "São Lucas", "UniVida", "Vital", "Bem Estar", "Coração"
]]
cidades = ["São Paulo", "Rio de Janeiro", "Belo Horizonte", "Curitiba", "Fortaleza", "Salvador", "Brasília", "Manaus", "Recife", "Porto Alegre"]
tipos = ["Público", "Privado"]

# Gerar dados fictícios
dados = []
for i in range(100):
    nome_hospital = random.choice(hospitais) + f" - Unidade {random.randint(1, 5)}"
    cidade = random.choice(cidades)
    tipo = random.choice(tipos)
    leitos = random.randint(50, 500)
    taxa_ocupacao = round(random.uniform(50, 100), 1)
    pacientes = random.randint(500, 10000)
    nota_avaliacao = round(random.uniform(2.5, 5.0), 1)

    dados.append({
        "hospital": nome_hospital,
        "cidade": cidade,
        "tipo": tipo,
        "leitos": leitos,
        "ocupacao_%": taxa_ocupacao,
        "pacientes_atendidos": pacientes,
        "avaliacao": nota_avaliacao
    })

# Criar DataFrame
df = pd.DataFrame(dados)

# Definir tipos de dados
df = df.astype({
    "hospital": "string",
    "cidade": "string",
    "tipo": "string",
    "leitos": "int",
    "ocupacao_%": "float",
    "pacientes_atendidos": "int",
    "avaliacao": "float"
})

# Aplicar filtros opcionais se existirem
if 'fieldnames' in globals() and fieldnames is not None:
    df = df[list(fieldnames)]

if 'recsmax' in globals() and recsmax > 0:
    df = df.head(recsmax)

# Resultado obrigatório
result = df
```

{% endcode %}

#### Información y consejos generales

{% hint style="info" %}
Devolver solo la cantidad de registros necesarios, solo las columnas necesarias, definir correctamente los tipos de datos y filtrar filas inválidas mejora el rendimiento y consumo de memoria.
{% endhint %}

{% hint style="warning" %}

* Para optimizar la performance y el consumo de memoria, prepare el script para retornar solo la cantidad necesaria de registros.
  * La variable `recsmax` indica el límite (0 = sin límite).
* Prepare el script para retornar solo las columnas necesarias.
  * La variable `fieldnames` (Tupla) contiene las columnas solicitadas.
* Defina los dtypes de las columnas (usar Pandas dtypes). Puede necesitar conversión via `astype()` después de llenar los datos.
* Filtre solo los registros válidos utilizando la sintaxis `{{NombreParametro}}` para acceder a valores de filtros/parámetros.
  {% endhint %}

Además de la creación, también es posible instalar nuevas bibliotecas Python. Para ello, haga clic en el botón Instalar biblioteca Python.

Tras hacer clic en 'Instalar biblioteca Python', se abrirá un nuevo menú solicitando el nombre de la biblioteca deseada. Informe el nombre y haga clic en 'Instalar'.

{% hint style="danger" %}
IMPORTANTE: Lembrando que este recurso está en fase experimental.
{% endhint %}

### Nuevo componente - Python

* Además de la creación de metadatos via Python, ahora también es posible crear componentes con Python. En la pestaña Dashboard, se añadió la opción de componente Python.

{% hint style="info" %}
Caso el componente no esté habilitado, acceda a Administración > Acesso ao Menu Principal, localice el componente Python y conceda permiso a los usuarios deseados.
{% endhint %}

#### Cómo crear un componente visual con Python

Puedes crear un componente visual escribiendo un script Python que genere una imagen en formato SVG para ser exibida en la pantalla.

{% stepper %}
{% step %}

### Reglas y convenciones obligatorias

* Resultado final: El script **debe terminar** asignando el contenido SVG a una variable llamada `result`.\
  Ejemplo: `result = svg.getvalue()`
* Formato: `result` **debe ser** una imagen **SVG** (Scalable Vector Graphics).
* Datos disponibles en el script:
  * `df_internalvalues`: datos originales retornados por el metadato (DataFrame Pandas).
  * `df_displayvalues`: datos formateados para visualización (DataFrame Pandas).
* Tamaño del área de visualización:
  * `height` y `width` son la altura y anchura en píxeles del espacio disponible.
  * Algunas bibliotecas usan pulgadas; convierta píxeles a pulgadas si es necesario:

    ```python
    fig_height = height / rcParams['figure.dpi']
    fig_width = width / rcParams['figure.dpi']
    ```

{% endstep %}

{% step %}

### Ejemplo práctico – Gráfico de líneas con Matplotlib

Este ejemplo supone un metadato con las columnas:

* `product_category_name` (eje X, formateado)
* `product_name_cnt` (eje Y, valor original)

```python
# 📚 Importa bibliotecas necesarias
from matplotlib import rcParams
from matplotlib.figure import Figure
from matplotlib.backends.backend_svg import FigureCanvasSVG
from io import StringIO

# 🎯 Paso 1 – Convertir pixels a pulgadas
fig_height = height / rcParams['figure.dpi']
fig_width = width / rcParams['figure.dpi']

# 🎯 Paso 2 – Crear figura y canvas para SVG
fig = Figure(figsize=(fig_width, fig_height))
canvas = FigureCanvasSVG(fig)

# 🎯 Paso 3 – Añadir y plotear los datos
ax = fig.add_subplot()
ax.plot(
    df_displayvalues['product_category_name'],  # Eje X (formatado)
    df_internalvalues['product_name_cnt']       # Eje Y (valor original)
)

# 🎯 Paso 4 – Exportar a SVG
svg = StringIO()
canvas.print_svg(svg)

# 🎯 Paso 5 – Retornar el SVG al sistema
result = svg.getvalue()
```

{% endstep %}

{% step %}

### Resumen del flujo

* Recibir los datos del metadato (`df_internalvalues` y `df_displayvalues`).
* Ajustar el tamaño de la imagen usando `height` y `width`.
* Crear y personalizar el gráfico usando la biblioteca elegida (Matplotlib, Plotly, Seaborn, etc.).
* Exportar el gráfico a SVG en memoria.
* Asignar el SVG a la variable `result`.
  {% endstep %}
  {% endstepper %}

En la creación o edición del componente, también es posible instalar nuevas bibliotecas.

Ejemplo de script Python utilizando el metadato del Ejemplo 3:

{% code title="Ejemplo - componente visual (SVG)" %}

```python
from matplotlib import rcParams
from matplotlib.figure import Figure
from matplotlib.backends.backend_svg import FigureCanvasSVG
from io import StringIO

# --- Busca el índice del hospital con mayor tasa de ocupación ---
idx = df_internalvalues['ocupacao_%'].idxmax()

# --- Recupera datos formateados (display) ---
hospital = df_displayvalues.loc[idx, 'hospital']
cidade = df_displayvalues.loc[idx, 'cidade']
tipo = df_displayvalues.loc[idx, 'tipo']

# --- Recupera datos originales (internal) ---
ocupacao = df_internalvalues.loc[idx, 'ocupacao_%']
leitos = df_internalvalues.loc[idx, 'leitos']
pacientes = df_internalvalues.loc[idx, 'pacientes_atendidos']
avaliacao = df_internalvalues.loc[idx, 'avaliacao']

# --- Define tamaño de la figura en pulgadas ---
fig_height = height / rcParams['figure.dpi']
fig_width = width / rcParams['figure.dpi']

# --- Crea figura y canvas ---
fig = Figure(figsize=(fig_width, fig_height))
canvas = FigureCanvasSVG(fig)
ax = fig.add_subplot()
ax.axis('off')  # Remove ejes
fig.patch.set_facecolor('white')

# --- Fondo colorido para destaque ---
ax.fill_between([0, 1], 0, 1, color='#f0f4f8')

# --- Título y informaciones ---
ax.text(0.05, 0.85, "Hospital com Maior Ocupação", fontsize=16, weight='bold', color='#2c3e50')
ax.text(0.05, 0.75, hospital, fontsize=14, weight='bold', color='#34495e')
ax.text(0.05, 0.70, f"Cidade: {cidade} | Tipo: {tipo}", fontsize=11, color='#7f8c8d')

# --- Indicadores principales ---
ax.text(0.05, 0.60, f"Leitos: {leitos}", fontsize=13, color='#2980b9')
ax.text(0.05, 0.53, f"Pacientes Atendidos: {pacientes:,}", fontsize=13, color='#27ae60')
ax.text(0.05, 0.46, f"Avaliação: {avaliacao:.2f} / 5", fontsize=13, color='#f39c12')
ax.text(0.05, 0.39, f"Ocupação: {ocupacao:.1f} %", fontsize=13, color='#c0392b')

# --- Fonte dos dados ---
ax.text(0.05, 0.10, "Fonte: Dados simulados do metadato", fontsize=8, color='gray')

# --- Exporta para SVG ---
svg = StringIO()
canvas.print_svg(svg)
result = svg.getvalue()
```

{% endcode %}

Resumen de lo que hace el script:

* Busca el hospital con mayor ocupación (%).
* Recupera nombre, ciudad, tipo, leitos, pacientes atendidos, evaluación y tasa de ocupación.
* Crea una imagen limpia en SVG, sin ejes, con fondo claro y textos organizados.

[Veja com mais detalhes no manual](broken://pages/4c6cf56f00c6393311da7c9fc4fec5733fc335d4)

{% hint style="danger" %}
IMPORTANTE: Los campos utilizados en el Script Python deben estar previamente configurados en la sección 'Campos' para que el script pueda ejecutarse correctamente.
{% endhint %}

### Validaciones para contêiner de filtros

Mejoras en el sistema de contêiner de filtros para controlar intervalos de fechas y opciones seleccionadas:

* Restringir intervalo de fechas (fecha mínima y máxima permitida).
* Definir fórmulas de validación para limitar selección múltiple (mínimo/máximo de opciones).
* Mostrar mensajes de error personalizados cuando los valores estén fuera de límites.

¿Qué es posible hacer ahora?

{% stepper %}
{% step %}

### Limitar intervalo máximo para filtros de fecha

Puedes configurar fórmulas para restringir el intervalo de fechas que el usuario puede seleccionar, por ejemplo, impedir periodos mayores a 1 año.

**Ejemplo de fórmula para filtro entre fechas:**

```sql
if(abs(:value2 - :value1) > 365, "O intervalo de pesquisa não pode ser maior que 365 dias")
```

Si el usuario selecciona un intervalo mayor a 365 días, verá el mensaje y no podrá proceder.
{% endstep %}

{% step %}

### Limitar selección en filtros de múltipla elección

Es posible definir límites para cuántas opciones el usuario puede elegir.

**Ejemplo de fórmula para selección múltiple:**

```sql
if(length(:value) > 3, "Selecione no máximo 3 opções")
```

Si el usuario selecciona más de 3 opciones, se mostrará un mensaje impidiendo continuar.
{% endstep %}
{% endstepper %}

Se añadió una nueva pestaña llamada **"Defina, opcionalmente, una fórmula de verificación"** para configurar validaciones de forma simple y flexible.

### Envío de alerta con componente planilha en formato .xlsx

* Ahora es posible **enviar componentes en formato de planilha (.XLSX o .CSV)** en tareas.

### Campos calculados por funciones de ventana pueden ser usados como filtros en componentes

* Ahora es posible usar campos derivados de funciones de ventana (por ejemplo `rank()`, `row_number()`) como filtros en los componentes de los dashboards. El mecanismo de visualización aplicará los filtros después del cálculo de las funciones de ventana.

En el rincón de "Filtros (pós resultado/janela)" hay un ícono "(?)" que muestra la fórmula usada para aplicar el filtro de ventana. La misma fórmula se presenta debajo.

```sql
select
    *
from
(
    select
        [colunas]
    from
        [script_do_metadado]
    where
        [filtros_por_registro]
    group by
        [colunas_de_agrupamento]
    having
        [filtros_agrupados]
)
where
    [filtros_pós_funções_janela]
```

Resumen funcional:

* Selecciona columnas del resultado interno.
* Obtiene datos desde `[script_do_metadado]`.
* Aplica filtros por registro.
* Agrupa por columnas seleccionadas.
* Filtra grupos (HAVING).
* Finalmente aplica filtros posteriores usando funciones de ventana.

Ahora es posible, por ejemplo, mostrar solo los diez primeros de un ranking o filtrar registros cuya suma acumulada supere un valor, directamente en los componentes.

### Uso de múltiples campos del metadato en fórmulas para series y colores personalizadas

* Las fórmulas en series personalizadas y reglas de color condicionadas ahora pueden referenciar valores de otros campos del metadato.

Por ejemplo: definir que el color de una barra varíe según un campo meta: si el valor real >= meta → verde; si no → rojo. Esto se hace mediante una fórmula que accede al campo de la meta.

### Configuración personalizada del ambiente Python

WK permite al usuario definir manualmente el directorio de instalación de Python que será usado por los módulos del sistema.

Cómo configurar:

1. Accede al **Servidor** o al **Gerenciador de Tarefas** del WK.
2. Haz clic en **Configurações**.
3. Localiza la opción **Python**.
4. En **Diretório instalação**, informa la ruta completa de la instalación de Python (ej: `C:\...\Python313\`).
5. Haz clic en **OK** para guardar.

Si el directorio contiene una instalación válida de Python, WK la usará como ambiente principal para ejecutar scripts Python. Si no se especifica, WK intentará usar una instalación estándar registrada en Windows.

Principales cambios:

* Configuración personalizada del Python desde la interfaz WK.
* Detección automática de instalaciones existentes en Windows.
* Instalación dedicada vía WK posible (instalador).
* Compatibilidad con múltiples instalaciones de Python sin conflictos.

### Cuenta web configurable automáticamente al cadastrar nueva cuenta

* Al agregar una nueva cuenta por el servidor, ahora se puede vincular automáticamente a la versión web sin editar `localConfigs` manualmente.

Al salvar, el sistema preguntará (imagen):

Al elegir 'Sí', se abrirá un menú solicitando nombre, puerto, ID y servidor (valores basados en una cuenta ya en `localConfigs`). Si elige 'No', la cuenta no se añade a `localConfigs` y deberá agregarse manualmente para acceso web.

Tras finalizar y hacer clic en 'OK', la cuenta estará disponible en la versión web.

Al eliminar una cuenta configurada, se pedirá confirmación antes de borrarla.

### Porta Apache configurable en el instalador

* En el instalador ahora es posible configurar las puertas HTTP y HTTPS del Apache mediante dos nuevos campos debajo del directorio de instalación (solo si Apache está seleccionado). Por defecto vienen 80 y 443. El sistema acepta enteros entre 1 y 65535, valida duplicidad y permite probar disponibilidad de la puerta con un enlace junto a cada campo.

[Veja com mais detalhes no manual](broken://pages/93a82aedec77d84e07ba79c500bbaa6bb53d9481)

### Migración para Angular2+

* En la versión 5.0 se implementó una actualización a Angular 2+ con el objetivo de mejorar el rendimiento del sistema.

{% hint style="info" %}
Para más información acceda a nuestro SAC:\
<https://atendimento.weknowhealthtech.com.br/kb> o escriba a <atendimento@weknowhealthtech.com.br>
{% endhint %}
