724 lines
40 KiB
Python
724 lines
40 KiB
Python
# backend/data_manager.py (v2 - reescrito completo)
|
|
"""Capa de acceso a datos del módulo LEADS."""
|
|
import os
|
|
import requests
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
# ── Mapa de campañas (frase -> programa/sede/rango fechas), igual que el PBI ──
|
|
CAMPANIAS = [
|
|
('📚 Me interesa el', 'TERC', '200', 'Arequipa', 'Domingo', 'Pauta_wsp', '2025-10-01', '2025-12-31'),
|
|
('📚 Me interesa el', 'TEAC', '227', 'Lima', 'Domingo', 'Pauta_wsp', '2026-01-15', '2026-03-30'),
|
|
('Hola! 🚨', 'TEAC', '191', 'Piura', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 💯 Me interesa', 'TERC', '193', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 💼', 'TEAC', '195', 'Trujillo', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
|
|
('Hola! 💸', 'TEAC', '197', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
|
|
('Hola! ⚡', 'TEAC', '198', 'Trujillo', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
|
|
('Hola! 🦺', 'TERC', '199', 'Piura', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
|
|
('Hola! 🍾', 'TEAC', '201', 'Arequipa', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
|
|
('¡Hola! 📚', 'TERC', '200', 'Arequipa', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
|
|
('Hola! 🥶', 'TEAC', '202', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🎯', 'TEAC', '203', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
|
|
('Hola! 🌞', 'TERC', '204', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🗿', 'TERC', '205', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
|
|
('Hola! 😎', 'TEAC', '206', 'Piura', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-04-01'),
|
|
('Hola! 🎓', 'TEAC', '207', 'Arequipa', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-01-31'),
|
|
('Hola! 👀', 'TEAC', '208', 'Trujillo', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🎮', 'TERC', '209', 'Piura', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-04-30'),
|
|
('Hola! 👾', 'TERC', '210', 'Arequipa', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-02-25'),
|
|
('Hola! 🕹️', 'TERC', '211', 'Trujillo', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-04-01'),
|
|
('Hola! 🚀', 'TEAC', '212', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🛸', 'TERC', '213', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🦾', 'Amoniaco', '63a', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-04-15'),
|
|
('Hola! 🛠️', 'Diseño Chillers', '64a', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-04-30'),
|
|
('¡Hola! 🥽', 'VRF', '65a', 'Lima', '-', 'Pauta_wsp', '2025-09-01', '2026-02-28'),
|
|
('¡Hola! ❄️ Estoy interesado Seminario', 'Cámaras', '66a', 'Lima', '-', 'Pauta_wsp', '2026-02-01', '2026-05-28'),
|
|
('¡Hola! 🤩', 'Diseño de Sistemas', '67a', 'Lima', '-', 'Pauta_wsp', '2026-02-01', '2026-12-31'),
|
|
('¡Hola! 🙌🏼', 'Metrado y Costeo', '68a', 'Lima', '-', 'Pauta_wsp', '2026-02-01', '2026-12-31'),
|
|
('¡Hola! 🥽', 'VRF', '69a', 'Lima', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
|
|
(' 🧊 Más información del Seminario', 'VRF', '71a', 'Arequipa', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
|
|
(' 📐 Más información del Seminario', 'VRF', '72a', 'Trujillo', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
|
|
(' 👾 Más información del Seminario en Instalación', 'VRF', '73a', 'Piura', '-', 'Pauta_wsp', '2026-03-01', '2026-06-30'),
|
|
(' 🖥️ Estoy interesado en sus Seminarios', 'Seminarios', '74a', 'Lima', '-', 'Pauta_wsp', '2026-04-01', '2026-07-30'),
|
|
(' 🙋 Deseo más info. del Diplomado ', 'Diplomado REF', '75a', 'Lima', '-', 'Pauta_wsp', '2026-04-01', '2026-07-30'),
|
|
('💸 Deseo más info. del Seminario', 'CAD', '76a', 'Lima', '-', 'Pauta_wsp', '2026-04-01', '2026-07-30'),
|
|
('🦾 Estoy interesado Instalación de Sistemas', 'VRF', '77a', 'Lima', '-', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
|
|
('🙀 Estoy interesado Seminario', 'SUPER.OBRAS', '78a', 'Lima', '-', 'Pauta_wsp', '2026-05-15', '2026-07-30'),
|
|
('🛠️ más info del Seminario Diseño de Chillers', 'Diseño', '79a', 'Lima', '-', 'Pauta_wsp', '2026-05-15', '2026-07-30'),
|
|
('👾Estoy interesado en el Seminario Refrigeración', 'CO2', '80a', 'Lima', '-', 'Pauta_wsp', '2026-05-15', '2026-07-30'),
|
|
('📱 Estoy interesado Instalación', 'VRF', '81a', 'Lima', '-', 'Pauta_wsp', '2026-06-25', '2026-08-30'),
|
|
('🧊 Más información del Seminario', 'VRF', '82a', 'Arequipa', '-', 'Pauta_wsp', '2026-07-01', '2026-08-30'),
|
|
('📐 Más información del Seminario', 'VRF', '83a', 'Trujillo', '-', 'Pauta_wsp', '2026-07-01', '2026-08-30'),
|
|
('👾 Más información del Seminario', 'VRF', '84a', 'Piura', '-', 'Pauta_wsp', '2026-07-01', '2026-08-30'),
|
|
('🛠️ más info del Seminario Virtual ', 'Diseño de Chillers', '85a', 'Lima', '-', 'Pauta_wsp', '2026-07-01', '2026-09-30'),
|
|
('Hola! 👉', 'TEAC', '214', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🤝', 'TEAC', '215', 'Lima', 'Domingo', 'Pauta_wsp', '2025-12-01', '2026-03-15'),
|
|
('🆕 Me interesa', 'TERC', '217', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🌬️', 'TEAC', '218', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🌍', 'TERC', '219', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🎟️', 'TEAC', '220', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-03-31'),
|
|
('Hola! 🌈', 'TERC', '221', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-04-15'),
|
|
('¡Hola! ❄️ Me interesa el programa', 'TEAC', '222', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('Hola! 🔝', 'TERC', '223', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('🔋 Me interesa', 'TEAC', '224', 'Lima', 'Sábado', 'Pauta_wsp', '2025-09-01', '2026-04-15'),
|
|
('¡Hola! 🌡️', 'TEAC', '225', 'Lima', 'Domingo', 'Pauta_wsp', '2025-09-01', '2026-12-31'),
|
|
('¡Hola! ☃️', 'TERC', '226', 'Lima', 'Sábado', 'Pauta_wsp', '2026-01-01', '2026-04-15'),
|
|
('¡Hola! 📚', 'TERC', '227', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-10'),
|
|
('¡Hola! 🗺️', 'TEAC', '228', 'Arequipa', 'Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-05'),
|
|
('¡Hola! 🌤️', 'TEAC', '229', 'Piura', 'Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-30'),
|
|
('¡Hola! ⚡', 'TEAC', '230', 'Trujillo', 'Semipresencial', 'Pauta_wsp', '2026-02-01', '2026-05-07'),
|
|
('¡Hola! 🎤', 'TERC', '231', 'Lima', 'Domingo', 'Pauta_wsp', '2026-02-01', '2026-05-30'),
|
|
('¡Hola! 👾 Me interesa el programa', 'TEAC', '232', 'Lima', 'Domingo', 'Pauta_wsp', '2026-02-26', '2026-05-30'),
|
|
('¡Hola! 🍨', 'TEAC', '233', 'Lima', 'Sábado', 'Pauta_wsp', '2026-02-01', '2026-05-30'),
|
|
('¡Hola! 🐶 Me interesa', 'TEAC', '234', 'Piura', 'Sábado', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
|
|
('¡Hola! 💸 Me interesa', 'TERC', '235', 'Arequipa', 'Semipresencial', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
|
|
('¡Hola! 🗿 Me interesa', 'TERC', '236', 'Trujillo', 'Sábado', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
|
|
('¡Hola! 🦺 Me interesa', 'TEAC', '237', 'Lima', 'Sábado', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
|
|
('¡Hola! 🍾 Me interesa', 'TEAC', '238', 'Lima', 'Domingo', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
|
|
('💡 deseo más info del programa', 'TERC', '239', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-03-01', '2026-05-30'),
|
|
('¡Hola! 🎯 Me interesa el programa', 'TERC', '240', 'Lima', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-05-30'),
|
|
('¡Hola! 🤝 Me interesa el programa', 'TERC', '241', 'Lima', 'Sábado', 'Pauta_wsp', '2026-04-01', '2026-05-30'),
|
|
('🪐 Me interesa el programa', 'TERC', '242', 'Piura', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-05-30'),
|
|
('🎓 Me interesa el programa', 'TEAC', '243', 'Arequipa', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-06-30'),
|
|
('💼 Me interesa el programa ', 'TEAC', '244', 'Trujillo', 'Domingo', 'Pauta_wsp', '2026-04-01', '2026-06-30'),
|
|
('☃️ Me interesa el programa', 'TEAC', '245', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
|
|
('🔋 Me interesa el programa', 'TERC', '246', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
|
|
('😎 Me interesa el programa', 'TEAC', '247', 'Arequipa', 'Domingo', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
|
|
('🌈Me interesa el programa', 'TEAC', '248', 'Lima', 'Sabado', 'Pauta_wsp', '2026-04-16', '2026-07-30'),
|
|
('⭐ Me interesa el programa', 'TEAC', '249', 'Lima', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
|
|
('🕵️ Me interesa el programa', 'TEAC', '250', 'Piura', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
|
|
('📈 Me interesa el programa', 'TEAC', '251', 'Trujillo', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
|
|
('🗺️ Me interesa el programa', 'TEAC', '252', 'Piura', 'Sabado', 'Pauta_wsp', '2026-05-06', '2026-07-30'),
|
|
('🎮 Me interesa el programa', 'TEAC', '253', 'Trujillo', 'Sabado', 'Pauta_wsp', '2026-05-01', '2026-07-30'),
|
|
('🏞️ Me interesa el programa', 'TERC', '254', 'Arequipa', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
|
|
('⚙️ Me interesa el programa', 'TERC', '255', 'Piura', 'Domingo', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
|
|
('⚡ Me interesa el programa', 'TERC', '256', 'Trujillo', 'Domingo', 'Pauta_wsp', '2026-05-08', '2026-12-31'),
|
|
('🎟️ Me interesa el programa', 'TERC', '257', 'Trujillo', 'Sabado', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
|
|
('🕑 Me interesa el', 'TERC', '258', 'Piura', 'Sabado', 'Pauta_wsp', '2026-05-01', '2026-12-31'),
|
|
('📚 Me interesa el programa', 'TEAC', '259', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-05-11', '2026-12-31'),
|
|
('🤗 Me interesa el programa', 'TEAC', '260', 'Lima', 'Semipresencial', 'Pauta_wsp', '2026-05-11', '2026-12-31'),
|
|
('🕹️ Me interesa el programa', 'TERC', '261', 'Lima', 'Domingo', 'Pauta_wsp', '2026-05-11', '2026-12-31'),
|
|
('🐶 Me interesa el programa', 'TEAC', '262', 'Piura', 'Sabado', 'Pauta_wsp', '2026-05-11', '2026-08-30'),
|
|
('🍨 Me interesa el programa', 'TERC', '263', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
|
|
('⛱️ Me interesa el programa de Aire Acondicionado', 'TEAC', '264', 'Lima', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
|
|
('🌤️ Me interesa el programa', 'TEAC', '265', 'Arequipa', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
|
|
('⛱️ Me interesa el programa de Refrigeración comercial', 'TERC', '266', 'Lima', 'Sabado', 'Pauta_wsp', '2026-05-15', '2026-08-30'),
|
|
('¡Hola! Vengo de su web, quiero saber', 'TEAC', '0', 'Lima', '-', 'Web_Whatsapp', '2025-11-01', '2026-12-31'),
|
|
]
|
|
|
|
|
|
def _values_sql():
|
|
"""Construye el bloque VALUES (...) del mapa de campañas para el JOIN."""
|
|
filas = []
|
|
for frase, cargo, codigo, sede, dia, origen, fi, ff in CAMPANIAS:
|
|
f = frase.replace("'", "''")
|
|
filas.append(f"('{f}','{cargo}','{codigo}','{sede}','{dia}','{origen}','{fi}','{ff}')")
|
|
return ",\n".join(filas)
|
|
|
|
|
|
class DataManager:
|
|
def __init__(self):
|
|
self.pg_host = os.getenv("PG_HOST", "191.98.134.81")
|
|
self.pg_db = os.getenv("PG_DATABASE", "chatwoot_production")
|
|
self.pg_user = os.getenv("PG_USER", "postgres")
|
|
self.pg_pass = os.getenv("PG_PASSWORD", "")
|
|
self.pg_port = os.getenv("PG_PORT", "5432")
|
|
self.sql_server = os.getenv("SQL_SERVER", "191.98.134.80")
|
|
self.sql_db = os.getenv("SQL_DATABASE", "BDUS_CK000040_0001")
|
|
self.sql_user = os.getenv("SQL_USERNAME", "")
|
|
self.sql_pass = os.getenv("SQL_PASSWORD", "")
|
|
self.sql_driver = os.getenv("SQL_DRIVER", "{SQL Server}")
|
|
|
|
def pg_conn(self):
|
|
import psycopg2
|
|
return psycopg2.connect(
|
|
host=self.pg_host, dbname=self.pg_db, user=self.pg_user,
|
|
password=self.pg_pass, port=self.pg_port,
|
|
)
|
|
|
|
def sql_conn(self):
|
|
import pyodbc
|
|
conn_str = (
|
|
f"DRIVER={self.sql_driver};SERVER={self.sql_server};"
|
|
f"DATABASE={self.sql_db};UID={self.sql_user};PWD={self.sql_pass}"
|
|
)
|
|
return pyodbc.connect(conn_str)
|
|
|
|
# ── CHATWOOT: leads de pauta (mapa de campañas, dedup por mensaje) ──
|
|
def traer_leads_chatwoot(self):
|
|
sql = f"""
|
|
SELECT DISTINCT ON (m.id)
|
|
-- Normalización IGUAL que el PBI: quitar '+51' y '+', quedando el número
|
|
REPLACE(REPLACE(c.phone_number, '+51', ''), '+', '') AS telefono,
|
|
u.name AS asesor,
|
|
m.created_at AS fecha_creada,
|
|
cv.cached_label_list AS etiquetas,
|
|
map.cargo AS programa, map.sede AS sede,
|
|
map.codigo AS codigo, map.origen AS origen
|
|
FROM messages m
|
|
JOIN conversations cv ON m.conversation_id = cv.id
|
|
JOIN contacts c ON cv.contact_id = c.id
|
|
LEFT JOIN users u ON cv.assignee_id = u.id
|
|
JOIN (VALUES
|
|
{_values_sql()}
|
|
) AS map(frase_busqueda, cargo, codigo, sede, dia, origen, fecha_inicio, fecha_fin)
|
|
ON m.content LIKE '%' || map.frase_busqueda || '%'
|
|
AND m.created_at >= CAST(map.fecha_inicio AS TIMESTAMP)
|
|
AND m.created_at <= CAST(map.fecha_fin AS TIMESTAMP) + INTERVAL '1 day'
|
|
WHERE m.sender_type = 'Contact'
|
|
ORDER BY m.id ASC
|
|
"""
|
|
conn = self.pg_conn()
|
|
cur = conn.cursor()
|
|
cur.execute(sql)
|
|
cols = [d[0] for d in cur.description]
|
|
filas = [dict(zip(cols, row)) for row in cur.fetchall()]
|
|
conn.close()
|
|
return filas
|
|
|
|
# ── SQL SERVER: cursos ──
|
|
def traer_cursos(self):
|
|
sql = """
|
|
SELECT rp.num_indice, rp.dsc_det_programa, p.dsc_programa,
|
|
rp.cod_frecuencia, rp.fch_inicio, rp.cod_estado,
|
|
(SELECT COUNT(*) FROM sgeca_matricula m
|
|
WHERE m.cod_periodo = rp.cod_detalle AND m.num_indice = rp.num_indice
|
|
AND m.cod_estado NOT IN ('ANU')) AS Inscritos_Totales,
|
|
(SELECT COUNT(*) FROM sgeca_matricula m
|
|
WHERE m.cod_periodo = rp.cod_detalle AND m.num_indice = rp.num_indice
|
|
AND m.cod_estado = 'RET') AS Inscritos_Retirados,
|
|
(SELECT COUNT(*) FROM sgeca_matricula m
|
|
WHERE m.cod_periodo = rp.cod_detalle AND m.num_indice = rp.num_indice
|
|
AND m.cod_estado NOT IN ('ANU','RET','SUS')) AS Inscritos_Activos
|
|
FROM sgede_RP_programa rp
|
|
INNER JOIN sgeca_programa p ON rp.cod_programa = p.cod_programa
|
|
WHERE YEAR(rp.fch_inicio) IN (2025, 2026)
|
|
ORDER BY rp.fch_inicio ASC
|
|
"""
|
|
conn = self.sql_conn()
|
|
cur = conn.cursor()
|
|
cur.execute(sql)
|
|
cols = [d[0] for d in cur.description]
|
|
filas = [dict(zip(cols, row)) for row in cur.fetchall()]
|
|
conn.close()
|
|
return filas
|
|
|
|
# ── SUPABASE: mapa num_indice → pauta (tabla basebi_programacion) ──
|
|
def traer_pauta_cursos(self):
|
|
"""Devuelve dict {str(num_indice): {pauta, estado, contar}} desde Supabase.
|
|
Fuente: tabla BaseBI_Programacion de SharePoint, subida a Supabase."""
|
|
url = os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
tabla = os.getenv("SUPABASE_TABLA_PAUTA", "basebi_programacion")
|
|
if not url or not key:
|
|
return {}
|
|
try:
|
|
r = requests.get(
|
|
f"{url}/rest/v1/{tabla}",
|
|
params={"select": "num_indice,pauta_codigo,estado,contar"},
|
|
headers={"apikey": key, "Authorization": f"Bearer {key}"},
|
|
timeout=30,
|
|
)
|
|
r.raise_for_status()
|
|
mapa = {}
|
|
for row in r.json():
|
|
ni = row.get("num_indice")
|
|
if ni is None:
|
|
continue
|
|
pa = row.get("pauta_codigo")
|
|
mapa[str(ni)] = {
|
|
"pauta": (str(pa).strip() if pa not in (None, "") else None),
|
|
"estado": (str(row.get("estado") or "").strip().upper()),
|
|
"contar": (str(row.get("contar") or "").strip().upper()),
|
|
}
|
|
return mapa
|
|
except Exception as e:
|
|
print(f"[traer_pauta_cursos] {e}")
|
|
return {}
|
|
|
|
# ── SUPABASE: valores de programa/sede no identificados (para la campanita) ──
|
|
def traer_no_identificados(self):
|
|
"""Compara cartera_junta vs alias_normalizacion y devuelve los valores de
|
|
programa/sede que NO están en el diccionario (correcto).
|
|
Retorna {'programa': [{'valor','veces'}], 'sede': [...]}."""
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t_cart = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta")
|
|
t_alias = os.getenv("SUPABASE_TABLA_ALIAS", "alias_normalizacion")
|
|
vacio = {"programa": [], "sede": []}
|
|
if not url or not key:
|
|
return vacio
|
|
headers = {"apikey": key, "Authorization": f"Bearer {key}"}
|
|
try:
|
|
# 1) valores conocidos (correcto) por tipo
|
|
r = requests.get(f"{url}/rest/v1/{t_alias}",
|
|
params={"select": "tipo,correcto"}, headers=headers, timeout=30)
|
|
r.raise_for_status()
|
|
conocidos = {"PROGRAMA": set(), "SEDE": set()}
|
|
for row in r.json():
|
|
tipo = str(row.get("tipo") or "").upper()
|
|
corr = " ".join(str(row.get("correcto") or "").upper().split())
|
|
if tipo in conocidos:
|
|
conocidos[tipo].add(corr)
|
|
|
|
# 2) traer de cartera_junta los valores que NO estén en los conocidos
|
|
def _desconocidos(columna, tipo):
|
|
conj = conocidos.get(tipo, set())
|
|
if not conj:
|
|
return []
|
|
lista = ",".join('"' + v.replace('"', '') + '"' for v in conj)
|
|
rr = requests.get(
|
|
f"{url}/rest/v1/{t_cart}",
|
|
params={"select": columna, columna: f"not.in.({lista})", "limit": "5000"},
|
|
headers=headers, timeout=30)
|
|
rr.raise_for_status()
|
|
cont = {}
|
|
for row in rr.json():
|
|
v = " ".join(str(row.get(columna) or "").upper().split())
|
|
cont[v] = cont.get(v, 0) + 1
|
|
return [{"valor": k, "veces": n}
|
|
for k, n in sorted(cont.items(), key=lambda x: -x[1])]
|
|
|
|
return {"programa": _desconocidos("programa", "PROGRAMA"),
|
|
"sede": _desconocidos("sede", "SEDE")}
|
|
except Exception as e:
|
|
print(f"[traer_no_identificados] {e}")
|
|
return vacio
|
|
|
|
# ── SUPABASE: diccionario alias (para normalizar campanias como la cartera) ──
|
|
def traer_alias_map(self):
|
|
"""{'PROGRAMA': {alias:correcto}, 'SEDE': {alias:correcto}}."""
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_ALIAS", "alias_normalizacion")
|
|
out = {"PROGRAMA": {}, "SEDE": {}}
|
|
if not url or not key:
|
|
return out
|
|
try:
|
|
r = requests.get(f"{url}/rest/v1/{t}", params={"select": "tipo,alias,correcto"},
|
|
headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30)
|
|
r.raise_for_status()
|
|
for row in r.json():
|
|
tipo = str(row.get("tipo") or "").upper()
|
|
al = " ".join(str(row.get("alias") or "").upper().split())
|
|
co = str(row.get("correcto") or "")
|
|
if tipo in out:
|
|
out[tipo][al] = co
|
|
except Exception as e:
|
|
print(f"[traer_alias_map] {e}")
|
|
return out
|
|
|
|
# ── SUPABASE: mapa codigo → (sede, programa) desde campanias (normalizado) ──
|
|
def traer_campanias_map(self):
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias")
|
|
out = {}
|
|
if not url or not key:
|
|
return out
|
|
alias = self.traer_alias_map()
|
|
|
|
def _norm(v, tipo):
|
|
s = " ".join(str(v or "").upper().split())
|
|
return alias.get(tipo, {}).get(s, s)
|
|
|
|
try:
|
|
r = requests.get(f"{url}/rest/v1/{t}", params={"select": "codigo,sede,cargo"},
|
|
headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30)
|
|
r.raise_for_status()
|
|
for row in r.json():
|
|
cod = " ".join(str(row.get("codigo") or "").upper().split())
|
|
if not cod:
|
|
continue
|
|
out[cod] = (_norm(row.get("sede"), "SEDE"), _norm(row.get("cargo"), "PROGRAMA"))
|
|
except Exception as e:
|
|
print(f"[traer_campanias_map] {e}")
|
|
return out
|
|
|
|
# ── SUPABASE: filas de cartera_junta para contar (telefono, sede, programa, fecha) ──
|
|
def traer_cartera_rows(self):
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_CARTERA", "cartera_junta")
|
|
out = []
|
|
if not url or not key:
|
|
return out
|
|
headers = {"apikey": key, "Authorization": f"Bearer {key}"}
|
|
paso, desde = 1000, 0
|
|
try:
|
|
while True:
|
|
r = requests.get(
|
|
f"{url}/rest/v1/{t}",
|
|
params={"select": "telefono,sede,programa,fecha_creada,asesor,es_origen,canal",
|
|
"offset": str(desde), "limit": str(paso)},
|
|
headers=headers, timeout=60)
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
if not data:
|
|
break
|
|
out.extend(data)
|
|
if len(data) < paso:
|
|
break
|
|
desde += paso
|
|
except Exception as e:
|
|
print(f"[traer_cartera_rows] {e}")
|
|
return out
|
|
|
|
# ── SUPABASE: mapa pauta → [conjuntos de anuncios] (tabla conjunto_pauta) ──
|
|
def traer_conjunto_pauta(self):
|
|
"""Devuelve dict {pauta(str): [conjunto, ...]} desde la tabla conjunto_pauta.
|
|
Una pauta puede tener varios conjuntos de anuncios."""
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_CONJUNTO", "conjunto_pauta")
|
|
out = {}
|
|
if not url or not key:
|
|
return out
|
|
try:
|
|
r = requests.get(f"{url}/rest/v1/{t}",
|
|
params={"select": "conjunto,pauta"},
|
|
headers={"apikey": key, "Authorization": f"Bearer {key}"},
|
|
timeout=30)
|
|
r.raise_for_status()
|
|
for row in r.json():
|
|
pa = str(row.get("pauta") or "").strip()
|
|
co = " ".join(str(row.get("conjunto") or "").split())
|
|
if not pa or not co:
|
|
continue
|
|
out.setdefault(pa, []).append(co)
|
|
except Exception as e:
|
|
print(f"[traer_conjunto_pauta] {e}")
|
|
return out
|
|
|
|
# ── GOOGLE SHEET (CSV publicado): importe gastado por conjunto de anuncios ──
|
|
def traer_meta_importe(self):
|
|
"""Lee el CSV publicado de Meta_Ads_Adsets y devuelve una lista de dicts:
|
|
[{conjunto, importe, inicio}, ...] (una por fila del CSV, inicio = 'Inicio del informe').
|
|
Se conserva la fecha para poder filtrar por periodo (Importe Pauta en el Mes)."""
|
|
import csv, io
|
|
url = os.getenv("META_CSV_URL", "")
|
|
out = []
|
|
if not url:
|
|
return out
|
|
try:
|
|
r = requests.get(url, timeout=30)
|
|
r.raise_for_status()
|
|
r.encoding = "utf-8"
|
|
rows = list(csv.reader(io.StringIO(r.text)))
|
|
if not rows:
|
|
return out
|
|
enc = [" ".join(str(h or "").split()).lower() for h in rows[0]]
|
|
|
|
def _idx(*claves):
|
|
for k in claves:
|
|
for i, h in enumerate(enc):
|
|
if k in h:
|
|
return i
|
|
return -1
|
|
|
|
# OJO: buscar "nombre del conjunto..." primero (hay tambien "ID del conjunto...").
|
|
i_conj = _idx("nombre del conjunto de anuncios", "nombre del conjunto")
|
|
i_imp = _idx("importe gastado")
|
|
i_ini = _idx("inicio del informe")
|
|
i_res = _idx("resultados")
|
|
if i_conj < 0 or i_imp < 0:
|
|
print(f"[traer_meta_importe] columnas no encontradas: {enc}")
|
|
return out
|
|
for fila in rows[1:]:
|
|
if len(fila) <= max(i_conj, i_imp):
|
|
continue
|
|
co = " ".join(str(fila[i_conj] or "").split())
|
|
if not co:
|
|
continue
|
|
raw = str(fila[i_imp] or "").strip().replace(",", "")
|
|
try:
|
|
val = float(raw) if raw else 0.0
|
|
except ValueError:
|
|
val = 0.0
|
|
rraw = str(fila[i_res] or "").strip().replace(",", "") if i_res >= 0 and len(fila) > i_res else ""
|
|
try:
|
|
res = float(rraw) if rraw else 0.0
|
|
except ValueError:
|
|
res = 0.0
|
|
ini = str(fila[i_ini] or "").strip() if i_ini >= 0 and len(fila) > i_ini else ""
|
|
out.append({"conjunto": co, "importe": val, "resultados": res, "inicio": ini})
|
|
except Exception as e:
|
|
print(f"[traer_meta_importe] {e}")
|
|
return out
|
|
|
|
# ── CHATWOOT: leads asignados (mensaje "Asignado a..."/auto-asignado, ultimo por telefono) ──
|
|
def traer_leads_asignados(self):
|
|
"""Devuelve [{telefono, fecha_asignada(date, -5h), user_name}] tomando el
|
|
mensaje de asignacion mas reciente por telefono."""
|
|
sql = """
|
|
SELECT * FROM (
|
|
SELECT DISTINCT ON (c.phone_number)
|
|
REPLACE(REPLACE(c.phone_number, '+51', ''), '+', '') AS telefono,
|
|
m.id AS message_id,
|
|
(m.created_at - INTERVAL '5 hours') AS created_at,
|
|
COALESCE(u.name, 'Sin Asesor') AS user_name
|
|
FROM messages m
|
|
JOIN conversations cv ON m.conversation_id = cv.id
|
|
JOIN contacts c ON cv.contact_id = c.id
|
|
LEFT JOIN users u ON cv.assignee_id = u.id
|
|
WHERE (
|
|
m.content LIKE 'Asignado a %' OR
|
|
m.content LIKE '% auto-asignado%'
|
|
)
|
|
ORDER BY c.phone_number, m.id DESC
|
|
) AS t
|
|
ORDER BY message_id DESC
|
|
"""
|
|
conn = self.pg_conn()
|
|
cur = conn.cursor()
|
|
cur.execute(sql)
|
|
cols = [d[0] for d in cur.description]
|
|
filas = [dict(zip(cols, row)) for row in cur.fetchall()]
|
|
conn.close()
|
|
return filas
|
|
|
|
# ── SUPABASE: actualizar/insertar la pauta de un conjunto en conjunto_pauta ──
|
|
def upsert_conjunto_pauta(self, conjunto, pauta):
|
|
"""Si el conjunto ya existe en conjunto_pauta, actualiza su pauta; si no, lo crea.
|
|
Si pauta viene vacia, elimina el vinculo (deja el conjunto sin pauta)."""
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_CONJUNTO", "conjunto_pauta")
|
|
h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
|
co = str(conjunto).strip(); pa = str(pauta or "").strip()
|
|
# ¿existe el conjunto?
|
|
r = requests.get(f"{url}/rest/v1/{t}",
|
|
params={"select": "conjunto", "conjunto": f"eq.{co}"},
|
|
headers=h, timeout=30)
|
|
r.raise_for_status()
|
|
existe = len(r.json()) > 0
|
|
if not pa:
|
|
# sin pauta -> borrar vinculo si existia
|
|
if existe:
|
|
requests.delete(f"{url}/rest/v1/{t}", params={"conjunto": f"eq.{co}"},
|
|
headers=h, timeout=30).raise_for_status()
|
|
return {"conjunto": co, "pauta": None}
|
|
if existe:
|
|
requests.patch(f"{url}/rest/v1/{t}", params={"conjunto": f"eq.{co}"},
|
|
headers=h, json={"pauta": pa}, timeout=30).raise_for_status()
|
|
else:
|
|
requests.post(f"{url}/rest/v1/{t}", headers=h,
|
|
json={"conjunto": co, "pauta": pa}, timeout=30).raise_for_status()
|
|
return {"conjunto": co, "pauta": pa}
|
|
|
|
# ── SUPABASE: conjuntos de anuncios SIN pauta conectada (del Sheet, no en conjunto_pauta) ──
|
|
def traer_conjuntos_sin_pauta(self):
|
|
"""Lista de conjuntos del Google Sheet que NO estan en conjunto_pauta."""
|
|
conj_pauta = self.traer_conjunto_pauta() # {pauta: [conjuntos]}
|
|
conectados = set()
|
|
for cjs in conj_pauta.values():
|
|
for c in cjs:
|
|
conectados.add(" ".join(str(c).split()).upper())
|
|
vistos = {}
|
|
for f in self.traer_meta_importe():
|
|
nombre = " ".join(str(f.get("conjunto")).split())
|
|
if not nombre:
|
|
continue
|
|
if " ".join(nombre.split()).upper() not in conectados:
|
|
vistos[nombre] = True
|
|
return sorted(vistos.keys())
|
|
|
|
# ── SUPABASE: programa_pautas -> {num_indice: [pautas]} (muchos-a-muchos) ──
|
|
def traer_programa_pautas(self):
|
|
url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_PROGRAMA_PAUTAS", "programa_pautas")
|
|
out = {}
|
|
if not url or not key:
|
|
return out
|
|
try:
|
|
r = requests.get(f"{url}/rest/v1/{t}", params={"select": "num_indice,pauta"},
|
|
headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30)
|
|
r.raise_for_status()
|
|
for row in r.json():
|
|
ni = str(row.get("num_indice") or "").strip()
|
|
pa = str(row.get("pauta") or "").strip()
|
|
if ni and pa:
|
|
out.setdefault(ni, []).append(pa)
|
|
except Exception as e:
|
|
print(f"[traer_programa_pautas] {e}")
|
|
return out
|
|
|
|
# ── SUPABASE: agregar un vinculo num_indice<->pauta en programa_pautas (upsert) ──
|
|
def agregar_programa_pauta(self, num_indice, pauta):
|
|
url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_PROGRAMA_PAUTAS", "programa_pautas")
|
|
h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
|
ni = str(num_indice).strip(); pa = str(pauta).strip()
|
|
# ¿ya existe ese par?
|
|
r = requests.get(f"{url}/rest/v1/{t}",
|
|
params={"select": "id", "num_indice": f"eq.{ni}", "pauta": f"eq.{pa}"},
|
|
headers=h, timeout=30)
|
|
r.raise_for_status()
|
|
if len(r.json()) > 0:
|
|
return {"num_indice": ni, "pauta": pa, "ya_existia": True}
|
|
rr = requests.post(f"{url}/rest/v1/{t}", headers=h,
|
|
json={"num_indice": ni, "pauta": pa}, timeout=30)
|
|
rr.raise_for_status()
|
|
return {"num_indice": ni, "pauta": pa, "ya_existia": False}
|
|
|
|
# ── SUPABASE: num_indices que YA usan una pauta en basebi_programacion ──
|
|
def num_indices_de_pauta(self, pauta):
|
|
url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
tabla = os.getenv("SUPABASE_TABLA_PAUTA", "basebi_programacion")
|
|
try:
|
|
r = requests.get(f"{url}/rest/v1/{tabla}",
|
|
params={"select": "num_indice", "pauta_codigo": f"eq.{str(pauta).strip()}"},
|
|
headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30)
|
|
r.raise_for_status()
|
|
return [str(x.get("num_indice")).strip() for x in r.json() if x.get("num_indice") is not None]
|
|
except Exception as e:
|
|
print(f"[num_indices_de_pauta] {e}")
|
|
return []
|
|
|
|
# ── SUPABASE: guardar pauta y/o contar de un num_indice en basebi_programacion (upsert) ──
|
|
def guardar_pauta_basebi(self, num_indice, pauta, contar=None):
|
|
url = os.getenv("SUPABASE_PAUTA_URL", ""); key = os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
tabla = os.getenv("SUPABASE_TABLA_PAUTA", "basebi_programacion")
|
|
h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
|
ni = str(num_indice).strip(); pa = str(pauta or "").strip()
|
|
campos = {}
|
|
if pa:
|
|
campos["pauta_codigo"] = pa # solo actualizar pauta si viene con valor
|
|
if contar is not None:
|
|
campos["contar"] = str(contar).strip().upper() # "SI" / "NO"
|
|
if not campos:
|
|
return {"num_indice": ni, "sin_cambios": True}
|
|
r = requests.get(f"{url}/rest/v1/{tabla}",
|
|
params={"select": "num_indice", "num_indice": f"eq.{ni}"},
|
|
headers=h, timeout=30)
|
|
r.raise_for_status()
|
|
existe = len(r.json()) > 0
|
|
if existe:
|
|
rr = requests.patch(f"{url}/rest/v1/{tabla}",
|
|
params={"num_indice": f"eq.{ni}"},
|
|
headers=h, json=campos, timeout=30)
|
|
else:
|
|
rr = requests.post(f"{url}/rest/v1/{tabla}",
|
|
headers=h, json={"num_indice": ni, **campos}, timeout=30)
|
|
rr.raise_for_status()
|
|
return {"num_indice": ni, "pauta": pa, "contar": campos.get("contar"), "creado": not existe}
|
|
|
|
# ── SUPABASE: vincular un conjunto de anuncios a una pauta (insert en conjunto_pauta) ──
|
|
def guardar_conjunto_pauta(self, conjunto, pauta):
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_CONJUNTO", "conjunto_pauta")
|
|
h = {"apikey": key, "Authorization": f"Bearer {key}", "Content-Type": "application/json"}
|
|
rr = requests.post(f"{url}/rest/v1/{t}", headers=h,
|
|
json={"conjunto": str(conjunto).strip(), "pauta": str(pauta).strip()},
|
|
timeout=30)
|
|
rr.raise_for_status()
|
|
return {"conjunto": conjunto, "pauta": pauta}
|
|
|
|
# ── CHATWOOT: plantillas enviadas (para matriz de plantillas cobradas) ──
|
|
def traer_plantillas(self):
|
|
"""Devuelve [{plantilla, telefono, user_name, created_at_peru,
|
|
hora_anterior_contacto, siguiente_mensaje_1}] de mensajes de plantilla
|
|
con content_attributes vacio (ISBLANK)."""
|
|
sql = """
|
|
SELECT
|
|
m.additional_attributes -> 'template_params' ->> 'name' AS plantilla,
|
|
REPLACE(REPLACE(c.phone_number, '+51', ''), '+', '') AS telefono,
|
|
u.name AS user_name,
|
|
(m.created_at - INTERVAL '5 hours') AS created_at_peru,
|
|
(SELECT (mp.created_at - INTERVAL '5 hours') FROM messages mp
|
|
WHERE mp.conversation_id = m.conversation_id AND mp.id < m.id
|
|
AND mp.sender_type = 'Contact' ORDER BY mp.id DESC LIMIT 1) AS hora_anterior_contacto,
|
|
(SELECT m2.content FROM messages m2
|
|
WHERE m2.conversation_id = m.conversation_id AND m2.id > m.id
|
|
AND m2.sender_type = 'Contact' ORDER BY m2.id ASC LIMIT 1) AS siguiente_mensaje_1
|
|
FROM messages m
|
|
JOIN conversations cv ON m.conversation_id = cv.id
|
|
JOIN contacts c ON cv.contact_id = c.id
|
|
LEFT JOIN users u ON cv.assignee_id = u.id
|
|
WHERE m.sender_type = 'User'
|
|
AND m.additional_attributes -> 'template_params' ->> 'name' IS NOT NULL
|
|
AND (m.content_attributes IS NULL
|
|
OR m.content_attributes::text = '{}'
|
|
OR m.content_attributes::text = 'null')
|
|
"""
|
|
conn = self.pg_conn()
|
|
cur = conn.cursor()
|
|
cur.execute(sql)
|
|
cols = [d[0] for d in cur.description]
|
|
filas = [dict(zip(cols, row)) for row in cur.fetchall()]
|
|
conn.close()
|
|
return filas
|
|
|
|
# ── SUPABASE: campanias -> {codigo(pauta): {sede, cargo}} (crudo, sin normalizar) ──
|
|
def traer_campanias_sede_cargo(self):
|
|
url = os.getenv("CARTERA_URL") or os.getenv("SUPABASE_PAUTA_URL", "")
|
|
key = os.getenv("CARTERA_KEY") or os.getenv("SUPABASE_PAUTA_KEY", "")
|
|
t = os.getenv("SUPABASE_TABLA_CAMPANIAS", "campanias")
|
|
out = {}
|
|
if not url or not key:
|
|
return out
|
|
try:
|
|
r = requests.get(f"{url}/rest/v1/{t}", params={"select": "codigo,sede,cargo"},
|
|
headers={"apikey": key, "Authorization": f"Bearer {key}"}, timeout=30)
|
|
r.raise_for_status()
|
|
for row in r.json():
|
|
cod = str(row.get("codigo") or "").strip()
|
|
if cod:
|
|
out[cod] = {"sede": str(row.get("sede") or "").strip(),
|
|
"cargo": str(row.get("cargo") or "").strip()}
|
|
except Exception as e:
|
|
print(f"[traer_campanias_sede_cargo] {e}")
|
|
return out
|
|
|
|
# ── SQL SERVER: matrículas ──
|
|
def traer_matriculas(self):
|
|
sql = """
|
|
SELECT
|
|
sgeca_matricula.num_matricula, sgeca_matricula.num_indice,
|
|
sgeca_matricula.fch_matricula,
|
|
sgeca_matricula.cod_estado AS estado_matricula,
|
|
REPLACE(REPLACE(REPLACE(sgema_alumno.dsc_telefono_1,' ',''),'+51',''),'+','') AS dsc_telefono_1,
|
|
REPLACE(REPLACE(REPLACE(sgema_alumno.dsc_telefono_2,' ',''),'+51',''),'+','') AS dsc_telefono_2,
|
|
sgeca_matricula.cod_moneda,
|
|
ISNULL((SELECT SUM(c.imp_total - ISNULL(c.imp_dscto,0))
|
|
FROM sgede_cronograma_matricula c
|
|
WHERE c.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND c.num_matricula = sgeca_matricula.num_matricula
|
|
AND c.num_refinanciamiento = 1), 0) AS INV_NETA,
|
|
(SELECT rhuma_trabajador.dsc_apellido_paterno + ' ' +
|
|
SUBSTRING(rhuma_trabajador.dsc_apellido_materno, 1, 1) + '. ' +
|
|
rhuma_trabajador.dsc_nombres
|
|
FROM rhuma_trabajador
|
|
WHERE rhuma_trabajador.cod_trabajador = sgeca_matricula.cod_vendedor) AS dsc_vendedor,
|
|
sgeca_programa.dsc_programa,
|
|
sgede_RP_programa.dsc_det_programa AS dsc_promocion,
|
|
ISNULL((SELECT sgede_cronograma_matricula.imp_saldo FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
AND sgede_cronograma_matricula.num_cuota = 0), 0) AS imp_saldo_matricula,
|
|
ISNULL((SELECT sgede_cronograma_matricula.imp_saldo FROM sgede_cronograma_matricula
|
|
WHERE sgede_cronograma_matricula.cod_localidad = sgeca_matricula.cod_localidad
|
|
AND sgede_cronograma_matricula.num_matricula = sgeca_matricula.num_matricula
|
|
AND sgede_cronograma_matricula.num_refinanciamiento = 1
|
|
AND sgede_cronograma_matricula.num_cuota = 1), 0) AS imp_saldo_cuota1
|
|
FROM sgeca_matricula
|
|
INNER JOIN sgeca_programa ON sgeca_matricula.cod_programa = sgeca_programa.cod_programa
|
|
LEFT JOIN sgede_RP_programa ON sgeca_matricula.cod_periodo = sgede_RP_programa.cod_detalle
|
|
AND sgeca_matricula.cod_programa = sgede_RP_programa.cod_programa
|
|
AND sgeca_matricula.num_indice = sgede_RP_programa.num_indice
|
|
INNER JOIN sgema_alumno ON sgeca_matricula.cod_alumno = sgema_alumno.cod_alumno
|
|
WHERE sgeca_matricula.cod_localidad LIKE 'SCENT'
|
|
AND sgeca_matricula.fch_matricula BETWEEN '01-11-2024 00:00:00.000' AND '31-12-2026 23:59:00.000'
|
|
AND sgeca_matricula.cod_estado <> 'ANU'
|
|
"""
|
|
conn = self.sql_conn()
|
|
cur = conn.cursor()
|
|
cur.execute(sql)
|
|
cols = [d[0] for d in cur.description]
|
|
rows = cur.fetchall()
|
|
conn.close()
|
|
return [dict(zip(cols, r)) for r in rows]
|