@login_required@permission_required("operaciones.view_centro_consulta", raise_exception=True)def fn_centro_consulta(request): """ Vista principal para el dashboard de Business Intelligence (BI). Renderiza el template estático inicialmente. """ context = {} return render(request, "operaciones/centro_consulta/centro_consulta.html", context)
Permission Required: Users must have the operaciones.view_centro_consulta permission to access the Query Center.
The Query Center uses dynamic SQL generation to handle complex multi-source queries:
operaciones/views/centro_consulta.py
def fn_obtener_subconsulta_origenes(lista_origenes): """ Arma dinámicamente la subconsulta con solo los bloques necesarios. """ bloque_pte = """ SELECT pd.id AS id_origen, 'PTE' AS tipo, COALESCE(ph.oficio_pte, 'SIN FOLIO') AS folio, COALESCE(c.descripcion, 'CLIENTE NO ASIGNADO') AS cliente, COALESCE(rp.descripcion, 'SIN LÍDER') AS lider, 'N/A' AS frente, NULL::integer AS id_sitio_oficial, 'NO APLICA' AS sitio_oficial, ... FROM pte_detalle pd INNER JOIN pte_header ph ON pd.id_pte_header_id = ph.id LEFT JOIN cliente c ON ph.id_cliente_id = c.id LEFT JOIN responsable_proyecto rp ON ph.id_responsable_proyecto_id = rp.id ... WHERE ph.estatus != 0 """ bloque_ot = """ SELECT od.id AS id_origen, 'OT' AS tipo, COALESCE(o.orden_trabajo, 'SIN OT') AS folio, ... FROM ot_detalle od INNER JOIN ot o ON od.id_ot_id = o.id ... WHERE o.estatus = 1 """ # Dynamically include only requested data sources subconsultas = [] origenes_validos = lista_origenes if lista_origenes else ["PTE", "OT"] if "PTE" in origenes_validos: subconsultas.append(bloque_pte) if "OT" in origenes_validos: subconsultas.append(bloque_ot) if "PROD" in origenes_validos: subconsultas.append(bloque_prod_reportes) subconsultas.append(bloque_prod_gpu) return " UNION ALL ".join(subconsultas)
Why Dynamic SQL?
Dynamic SQL construction provides:
Performance: Only queries selected data sources
Flexibility: Easily add new data sources
Maintainability: Clear separation of source logic
Optimization: Database can optimize smaller unions
The Query Center uses a utility function for executing parameterized SQL:
core/utils.py (reference)
def ejecutar_query_sql(sql, params): """Execute SQL with parameters and return results as list of dicts""" with connection.cursor() as cursor: cursor.execute(sql, params) columns = [col[0] for col in cursor.description] return [ dict(zip(columns, row)) for row in cursor.fetchall() ]def fn_ejecutar_query_sql_lotes(sql, params): """Execute SQL in batches for large result sets""" # Batch processing implementation for memory efficiency ...
SQL Injection Prevention: Always use parameterized queries with %(param_name)s placeholders. Never interpolate user input directly into SQL strings.
Work orders can have sites in different fields (patio, embarcación, plataforma) based on front type:
operaciones/views/centro_consulta.py (excerpt)
CASE WHEN o.id_frente_id = 1 -- Patio THEN o.id_patio WHEN o.id_frente_id = 2 -- Vessel THEN o.id_embarcacion WHEN o.id_frente_id = 4 -- Platform THEN o.id_plataforma ELSE NULLEND AS id_sitio_oficial,CASE WHEN o.id_frente_id = 1 THEN COALESCE(s_pat.descripcion, 'SIN PATIO') WHEN o.id_frente_id = 2 THEN COALESCE(s_emb.descripcion, 'SIN EMBARCACION') WHEN o.id_frente_id = 4 THEN COALESCE(s_plat.descripcion, 'SIN PLATAFORMA') ELSE 'SIN UBICACIÓN'END AS sitio_oficial
Documents are classified as delivered or pending based on file field content:
operaciones/views/centro_consulta.py
CASE WHEN T.archivo IS NOT NULL AND LENGTH(TRIM(T.archivo)) > 5 THEN 'ENTREGADO' WHEN T._fid_estatus_paso = 14 THEN 'NO APLICA' ELSE 'PENDIENTE'END AS "Estado Archivo"
File Validation: Files are considered valid if the archivo field has more than 5 characters (excluding whitespace), filtering out placeholder values.