
    ܞj%                     N    d Z ddlmZmZ ddlZd Zd Zd ZddZddZ	d	 Z
d
 Zy)u   
Acceso de solo lectura a la base de datos del sistema propio de Track Mar
("Utopia", MySQL). Consultas escritas contra el esquema real, relevado y
validado con Osvaldo el 2026-09-07 (ver config.py, sección 2, para el
detalle de cada decisión de mapeo).
    )create_enginetextNc                  @    t        t        j                         d      S )u   Crea la conexión de solo lectura. Requiere que la base sea alcanzable
    por red desde donde corra este script (ver sección 9 del documento .docx).T)pool_pre_ping)r   configget_connection_string     RC:\Users\OVA\Downloads\Agente_Seguimiento_TrackMar_codigo\agente_seguimiento\db.py
get_enginer      s     557tLLr
   c                 0   t        d      }| j                         5 }|j                  |t        j                  t        j
                  t        j                  d      D cg c]  }t        |j                         c}cddd       S c c}w # 1 sw Y   yxY w)ui  Eje 1: cotizaciones sin seguimiento, con esquema de 3 niveles definido
    junto con Osvaldo el 2026-09-07:

    - AGENDA_COTIZACIONES tiene UNIQUE KEY en COTIZACION (una fila por
      cotización, comment de la tabla: "SEGUIMIENTO DE COTIZACIONES
      REPUESTOS"). COMENTARIO es un historial concatenado (cada seguimiento
      nuevo se agrega al texto existente) y FECHA_REGISTRO se actualiza en
      cada agregado — así que FECHA_REGISTRO ya es, de por sí, la fecha del
      último toque, sin importar cuántas veces se tocó. COMENTARIO2,
      COMENTARIO3 y RESPUESTA son campos legados sin uso actual (confirmado:
      <0.2% de las filas, y las pocas cargadas son de 2014-2015) — no se
      usan acá.
    - VTA_CALZADA es el estado real de la cotización ('SEGUIMIENTO',
      'EVALUATIVA', 'PENDIENTE', 'PERDIDA...', 'POSTERGO INVERSION', etc.),
      no COTIZACIONES_EMITIDAS.ESTADO (que solo indica autorización
      interna). "Ya no se sigue más" = VTA_CALZADA empieza con 'PERDIDA' o
      es 'POSTERGO INVERSION'.
    - "Ya vendida" = tiene fila en VENTAS con ese COTIZACION.
    - Ventana de 5 semanas: solo cotizaciones creadas en los últimos 35 días
      (sin esto, el histórico completo desde 2002 hace la alerta
      inutilizable).
    - Excluye clientes con ACTIVIDAD=100 ("Orden de Taller") y el cliente
      256 (TRACK-MAR S.A.C.I., la propia empresa como cliente interno).
    - El vendedor/responsable es CLIENTES.VENDEDOR, no
      COTIZACIONES_EMITIDAS.EMPLEADO.

    Niveles de alerta (mutuamente excluyentes, por cotización):
    - 'nunca_contactada': sin ningún comentario en AGENDA_COTIZACIONES y
      pasaron más de UMBRAL_HORAS_COTIZACION_SIN_PRIMER_CONTACTO horas
      desde que se creó la cotización.
    - 'escalamiento': tiene comentario, pero pasaron más de
      UMBRAL_DIAS_COTIZACION_ESCALAMIENTO días desde el último
      FECHA_REGISTRO (gana por sobre 'sin_novedad' si se cumplen las dos).
    - 'sin_novedad': tiene comentario, pasaron más de
      UMBRAL_HORAS_COTIZACION_SIN_NOVEDAD horas (pero no llegó al umbral de
      escalamiento) desde el último FECHA_REGISTRO.
    aw  
        SELECT * FROM (
            SELECT c.NUMERO AS cotizacion_id, c.SUCURSAL AS sucursal_id,
                   c.CLIENTE AS cliente_id, cli.RAZON_SOCIAL AS cliente_nombre,
                   cli.WHATSAPP AS cliente_whatsapp, cli.VENDEDOR AS vendedor_id,
                   ag.VTA_CALZADA AS estado_seguimiento,
                   COALESCE(NULLIF(ag.FECHA_REGISTRO, '0000-00-00'), c.FECHA) AS fecha_ultima_actividad,
                   CASE
                       WHEN (ag.COMENTARIO IS NULL OR ag.COMENTARIO = '')
                            AND c.FECHA < NOW() - INTERVAL :horas_primer_contacto HOUR
                           THEN 'nunca_contactada'
                       WHEN (ag.COMENTARIO IS NOT NULL AND ag.COMENTARIO <> '')
                            AND COALESCE(NULLIF(ag.FECHA_REGISTRO, '0000-00-00'), c.FECHA) < NOW() - INTERVAL :dias_escalamiento DAY
                           THEN 'escalamiento'
                       WHEN (ag.COMENTARIO IS NOT NULL AND ag.COMENTARIO <> '')
                            AND COALESCE(NULLIF(ag.FECHA_REGISTRO, '0000-00-00'), c.FECHA) < NOW() - INTERVAL :horas_sin_novedad HOUR
                           THEN 'sin_novedad'
                       ELSE NULL
                   END AS nivel_alerta
            FROM COTIZACIONES_EMITIDAS c
            JOIN CLIENTES cli ON cli.CODIGO = c.CLIENTE
            LEFT JOIN VENTAS v ON v.COTIZACION = c.NUMERO
            LEFT JOIN AGENDA_COTIZACIONES ag ON ag.COTIZACION = c.NUMERO
            WHERE c.ESTADO = 'AUTORIZADA'
              AND v.NUMERO IS NULL
              AND cli.ACTIVIDAD <> 100
              AND cli.CODIGO <> 256
              AND c.FECHA > CURDATE() - INTERVAL 35 DAY
              AND (ag.VTA_CALZADA IS NULL OR ag.VTA_CALZADA = ''
                   OR (ag.VTA_CALZADA NOT LIKE 'PERDIDA%' AND ag.VTA_CALZADA <> 'POSTERGO INVERSION'))
        ) x
        WHERE x.nivel_alerta IS NOT NULL
    )horas_primer_contactohoras_sin_novedaddias_escalamientoN)	r   connectexecuter   +UMBRAL_HORAS_COTIZACION_SIN_PRIMER_CONTACTO#UMBRAL_HORAS_COTIZACION_SIN_NOVEDAD#UMBRAL_DIAS_COTIZACION_ESCALAMIENTOdict_mappingenginesqlconnrs       r   $obtener_cotizaciones_sin_seguimientor      s    L     	CB 
	T*.,,s%+%W%W!'!K!K!'!K!K=
 +  +QQZZ  +  
	 
	s   ABB;BBBc                     t        d      }| j                         5 }|j                  |      D cg c]  }t        |j                         c}cddd       S c c}w # 1 sw Y   yxY w)u=   Eje 1: ítems marcados como venta perdida sin motivo cargado.a  
        SELECT DISTINCT vp.COTIZACION AS cotizacion_id, c.SUCURSAL AS sucursal_id,
               c.EMPLEADO AS vendedor_id
        FROM VENTAS_PERDIDAS vp
        JOIN COTIZACIONES_EMITIDAS c ON c.NUMERO = vp.COTIZACION
        WHERE vp.MOTIVO IS NULL OR vp.MOTIVO = ''
    Nr   r   r   r   r   r   s       r   "obtener_ventas_perdidas_sin_motivor    `   sY    
  	C 
	T*.,,s*;<*;QQZZ *;< 
	< 
	   AAAAA&c                     |xs t         j                  }t        d      }| j                         5 }|j	                  |d|i      D cg c]  }t        |j                         c}cddd       S c c}w # 1 sw Y   yxY w)u  Eje 2 (redefinido junto con Osvaldo el 2026-09-07): giras planificadas
    en CLIENTES_GIRA cuya ventana [FECHA_INICIO, FECHA_CIERRE] ya venció, sin
    ningún CLIENTES_VISITAS de tipo visita (TIPO '01'/'04') para ese cliente
    en esa ventana. El umbral se aplica en días de calendario sobre
    FECHA_CIERRE (el cálculo de días hábiles queda para una iteración
    posterior, igual que en la v1 de este archivo).a  
        SELECT g.NRO_GIRA AS visita_id, g.CODIGO_CLIENTE AS cliente_id,
               cli.SUCURSAL AS sucursal_id, g.EMPLEADO AS vendedor_id,
               g.FECHA_CIERRE AS fecha
        FROM CLIENTES_GIRA g
        JOIN CLIENTES cli ON cli.CODIGO = g.CODIGO_CLIENTE
        WHERE g.FECHA_INICIO IS NOT NULL AND g.FECHA_INICIO <> '0000-00-00'
          AND g.FECHA_CIERRE IS NOT NULL AND g.FECHA_CIERRE <> '0000-00-00'
          AND g.FECHA_CIERRE < CURDATE() - INTERVAL :dias DAY
          AND NOT EXISTS (
              SELECT 1 FROM CLIENTES_VISITAS v
              WHERE v.CLIENTE = g.CODIGO_CLIENTE
                AND v.TIPO IN ('01', '04')
                AND v.FECHA_VISITA BETWEEN g.FECHA_INICIO AND g.FECHA_CIERRE
          )
    diasN)r   (UMBRAL_DIAS_HABILES_VISITA_SIN_RESULTADOr   r   r   r   r   )r   dias_habiles_umbralr   r   r   s        r   obtener_visitas_sin_resultador&   m   st     .`1`1`
  	C  
	T*.,,sVEX<Y*Z[*ZQQZZ *Z[ 
	[ 
	   A4A/#A4/A44A=c                     |xs t         j                  }t        d      }| j                         5 }|j	                  |d|i      D cg c]  }t        |j                         c}cddd       S c c}w # 1 sw Y   yxY w)u  Eje 3 (redefinido junto con Osvaldo el 2026-09-07): llamados
    (CLIENTES_VISITAS.TIPO = '02') sin ninguna cotización para ese mismo
    cliente dentro de la ventana de N horas posteriores al llamado. El campo
    CLIENTES_VISITAS.COTIZACION no se usa: en la base real está cargado en
    menos del 1% de los llamados y, cuando lo está, la fecha de la
    cotización vinculada no siempre coincide con la del llamado.aP  
        SELECT v.ID AS contacto_id, cli.SUCURSAL AS sucursal_id,
               v.CLIENTE AS cliente_id, cli.RAZON_SOCIAL AS cliente_nombre,
               cli.WHATSAPP AS cliente_whatsapp
        FROM CLIENTES_VISITAS v
        JOIN CLIENTES cli ON cli.CODIGO = v.CLIENTE
        WHERE v.TIPO = '02'
          AND v.FECHA_VISITA < NOW() - INTERVAL :horas HOUR
          AND NOT EXISTS (
              SELECT 1 FROM COTIZACIONES_EMITIDAS c
              WHERE c.CLIENTE = v.CLIENTE
                AND c.FECHA BETWEEN v.FECHA_VISITA AND v.FECHA_VISITA + INTERVAL :horas HOUR
          )
    horasN)r   )UMBRAL_HORAS_CONTACTO_FRIO_SIN_COTIZACIONr   r   r   r   r   )r   horas_umbralr   r   r   s        r   (obtener_contactos_en_frio_sin_cotizacionr,      sr      S6#S#SL
  	C 
	T*.,,sWl<S*TU*TQQZZ *TU 
	U 
	r'   c                     t        d      }| j                         5 }|j                  |      D cg c]  }t        |j                         c}cddd       S c c}w # 1 sw Y   yxY w)zEje 3: cantidad de llamados (TIPO='02') del mes en curso, por
    sucursal del cliente, para proyectar si se va a llegar al objetivo
    mensual. Excluye sucursales que no son de tipo 'REAL' (ej. el bucket
    interno "Transferencia").a  
        SELECT s.CODIGO AS sucursal_id, s.NOMBRE AS sucursal_nombre,
               COUNT(v.ID) AS contactos_mes_actual
        FROM SUCURSALES s
        LEFT JOIN CLIENTES cli ON cli.SUCURSAL = s.CODIGO
        LEFT JOIN CLIENTES_VISITAS v
               ON v.CLIENTE = cli.CODIGO
              AND v.TIPO = '02'
              AND v.FECHA_VISITA >= DATE_FORMAT(NOW(), '%Y-%m-01')
        WHERE s.TIPO = 'REAL'
        GROUP BY s.CODIGO, s.NOMBRE
    Nr   r   s       r   obtener_ritmo_contactos_mesr.      s[    
   	C 
	T*.,,s*;<*;QQZZ *;< 
	< 
	r!   c                     t        d      }| j                         5 }|j                  |      D cg c]  }t        |j                         c}cddd       S c c}w # 1 sw Y   yxY w)zSucursales reales (excluye buckets internos tipo 'TRANSITO'), con el
    email de alerta del encargado para rutear la alerta diaria.z
        SELECT CODIGO AS sucursal_id, NOMBRE AS sucursal_nombre, EMAIL_ALERTA AS email_alerta
        FROM SUCURSALES
        WHERE TIPO = 'REAL'
    Nr   r   s       r   obtener_sucursalesr0      s[       	C
 
	T*.,,s*;<*;QQZZ *;< 
	< 
	r!   )N)__doc__
sqlalchemyr   r   r   r   r   r    r&   r,   r.   r0   r	   r
   r   <module>r3      s:    + ML^
=\8V4=*	=r
   