mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 13:15:16 +00:00
feat: Implement detailed onboarding data capture, refactor HR request date processing, and enhance bot command menu.
This commit is contained in:
24
Readme.md
24
Readme.md
@@ -1,6 +1,6 @@
|
|||||||
# 🤖 Vanessa Bot – Asistente de RH para Vanity
|
# 🤖 Vanessa Bot – Asistente de RH para Vanity
|
||||||
|
|
||||||
Vanessa es un bot de Telegram escrito en Python que automatiza procesos internos de Recursos Humanos en Vanity. Su objetivo es eliminar fricción operativa: onboarding, solicitudes de RH e impresión de documentos, todo orquestado desde Telegram y conectado a flujos de n8n o servicios de correo.
|
Vanessa es un bot de Telegram escrito en Python que automatiza procesos internos de Recursos Humanos en Vanity. Su objetivo es eliminar fricción operativa: onboarding y solicitudes de RH, todo orquestado desde Telegram y conectado a flujos de n8n o servicios de correo.
|
||||||
|
|
||||||
Este repositorio está pensado como **proyecto Python profesional**, modular y listo para correr 24/7 en producción.
|
Este repositorio está pensado como **proyecto Python profesional**, modular y listo para correr 24/7 en producción.
|
||||||
|
|
||||||
@@ -11,11 +11,10 @@ Este repositorio está pensado como **proyecto Python profesional**, modular y l
|
|||||||
Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
|
Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
|
||||||
|
|
||||||
- Onboarding completo de nuevas socias (`/welcome`)
|
- Onboarding completo de nuevas socias (`/welcome`)
|
||||||
- Envío de archivos a impresión por correo electrónico (`/print`)
|
|
||||||
- Solicitud de vacaciones (`/vacaciones`)
|
- Solicitud de vacaciones (`/vacaciones`)
|
||||||
- Solicitud de permisos por horas (`/permiso`)
|
- Solicitud de permisos por horas (`/permiso`)
|
||||||
|
|
||||||
Cada flujo es un módulo independiente, y los datos se envían a **webhooks de n8n** o se procesan directamente, como en el caso de la impresión.
|
Cada flujo es un módulo independiente, y los datos se envían a **webhooks de n8n**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ vanity_bot/
|
|||||||
├── __init__.py
|
├── __init__.py
|
||||||
├── database.py # Módulo de conexión a la base de datos
|
├── database.py # Módulo de conexión a la base de datos
|
||||||
├── onboarding.py # Flujo /welcome (onboarding RH)
|
├── onboarding.py # Flujo /welcome (onboarding RH)
|
||||||
├── printer.py # Flujo /print (impresión por email)
|
|
||||||
└── rh_requests.py # /vacaciones y /permiso
|
└── rh_requests.py # /vacaciones y /permiso
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -52,8 +50,8 @@ TELEGRAM_TOKEN=TU_TOKEN_AQUI
|
|||||||
|
|
||||||
# --- WEBHOOKS N8N ---
|
# --- WEBHOOKS N8N ---
|
||||||
WEBHOOK_ONBOARDING=https://... # Alias aceptado: WEBHOOK_CONTRATO
|
WEBHOOK_ONBOARDING=https://... # Alias aceptado: WEBHOOK_CONTRATO
|
||||||
WEBHOOK_PRINT=https://...
|
|
||||||
WEBHOOK_VACACIONES=https://...
|
WEBHOOK_VACACIONES=https://...
|
||||||
|
WEBHOOK_PERMISOS=https://...
|
||||||
|
|
||||||
# --- DATABASE ---
|
# --- DATABASE ---
|
||||||
# Usado por el servicio de la base de datos en docker-compose.yml
|
# Usado por el servicio de la base de datos en docker-compose.yml
|
||||||
@@ -62,13 +60,6 @@ MYSQL_USER=user
|
|||||||
MYSQL_PASSWORD=password
|
MYSQL_PASSWORD=password
|
||||||
MYSQL_ROOT_PASSWORD=rootpassword
|
MYSQL_ROOT_PASSWORD=rootpassword
|
||||||
|
|
||||||
# --- SMTP PARA IMPRESIÓN ---
|
|
||||||
# Usado por el módulo de impresión para enviar correos
|
|
||||||
SMTP_SERVER=smtp.hostinger.com
|
|
||||||
SMTP_PORT=465
|
|
||||||
SMTP_USER=tu_email@dominio.com
|
|
||||||
SMTP_PASSWORD=tu_password_de_email
|
|
||||||
SMTP_RECIPIENT=email_destino@dominio.com # También puedes usar PRINTER_EMAIL
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -111,14 +102,12 @@ docker-compose down
|
|||||||
|
|
||||||
### modules/onboarding.py
|
### modules/onboarding.py
|
||||||
Flujo conversacional complejo que recolecta datos de nuevas empleadas y los envía a un webhook de n8n.
|
Flujo conversacional complejo que recolecta datos de nuevas empleadas y los envía a un webhook de n8n.
|
||||||
|
Incluye derivadas útiles: `num_ext_texto` (número en letras, con interior) y `numero_empleado` (primeras 4 del CURP + fecha de ingreso).
|
||||||
### modules/printer.py
|
|
||||||
- Recibe documentos o imágenes desde Telegram.
|
|
||||||
- Descarga el archivo de forma segura desde los servidores de Telegram.
|
|
||||||
- Se conecta a un servidor SMTP para enviar el archivo como un adjunto por correo electrónico a una dirección predefinida.
|
|
||||||
|
|
||||||
### modules/rh_requests.py
|
### modules/rh_requests.py
|
||||||
- Maneja solicitudes simples de RH (Vacaciones y Permisos) y las envía a un webhook de n8n.
|
- Maneja solicitudes simples de RH (Vacaciones y Permisos) y las envía a un webhook de n8n.
|
||||||
|
- Vacaciones: pregunta año (actual o siguiente), día/mes de inicio y fin, calcula métricas y aplica semáforo automático.
|
||||||
|
- Permisos: ofrece accesos rápidos (hoy/mañana/pasado) o fecha específica (año actual/siguiente, día/mes), pide horario, clasifica motivo con IA y envía al webhook.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,7 +117,6 @@ Flujo conversacional complejo que recolecta datos de nuevas empleadas y los env
|
|||||||
- **Python como cerebro**: Lógica de negocio y orquestación.
|
- **Python como cerebro**: Lógica de negocio y orquestación.
|
||||||
- **Docker para despliegue**: Entornos consistentes y portátiles.
|
- **Docker para despliegue**: Entornos consistentes y portátiles.
|
||||||
- **MySQL para persistencia**: Registro auditable de todas las interacciones.
|
- **MySQL para persistencia**: Registro auditable de todas las interacciones.
|
||||||
- **SMTP para acciones directas**: Integración con sistemas estándar como el correo.
|
|
||||||
- **Modularidad total**: Cada habilidad es un componente independiente.
|
- **Modularidad total**: Cada habilidad es un componente independiente.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
30
main.py
30
main.py
@@ -5,13 +5,12 @@ from dotenv import load_dotenv
|
|||||||
# Cargar variables de entorno antes de importar módulos que las usan
|
# Cargar variables de entorno antes de importar módulos que las usan
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
from telegram import Update, ReplyKeyboardMarkup
|
from telegram import Update, ReplyKeyboardMarkup, BotCommand
|
||||||
from telegram.constants import ParseMode
|
from telegram.constants import ParseMode
|
||||||
from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
|
from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
|
||||||
|
|
||||||
# --- IMPORTAR HABILIDADES ---
|
# --- IMPORTAR HABILIDADES ---
|
||||||
from modules.onboarding import onboarding_handler
|
from modules.onboarding import onboarding_handler
|
||||||
from modules.printer import print_handler
|
|
||||||
from modules.rh_requests import vacaciones_handler, permiso_handler
|
from modules.rh_requests import vacaciones_handler, permiso_handler
|
||||||
from modules.database import log_request
|
from modules.database import log_request
|
||||||
# from modules.finder import finder_handler (Si lo creas después)
|
# from modules.finder import finder_handler (Si lo creas después)
|
||||||
@@ -27,18 +26,38 @@ async def menu_principal(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
log_request(user.id, user.username, "start", update.message.text)
|
log_request(user.id, user.username, "start", update.message.text)
|
||||||
texto = (
|
texto = (
|
||||||
"👩💼 **Hola, soy Vanessa. ¿En qué puedo ayudarte hoy?**\n\n"
|
"👩💼 **Hola, soy Vanessa. ¿En qué puedo ayudarte hoy?**\n\n"
|
||||||
"Toca un comando en azul para lanzarlo rápido:"
|
"Comandos rápidos:\n"
|
||||||
|
"/welcome — Onboarding\n"
|
||||||
|
"/vacaciones — Solicitud de vacaciones\n"
|
||||||
|
"/permiso — Solicitud de permiso por horas\n\n"
|
||||||
|
"También tienes los botones rápidos abajo 👇"
|
||||||
)
|
)
|
||||||
teclado = ReplyKeyboardMarkup(
|
teclado = ReplyKeyboardMarkup(
|
||||||
[["/welcome", "/print"], ["/vacaciones", "/permiso"]],
|
[["/welcome"], ["/vacaciones", "/permiso"]],
|
||||||
resize_keyboard=True
|
resize_keyboard=True
|
||||||
)
|
)
|
||||||
await update.message.reply_text(texto, reply_markup=teclado)
|
await update.message.reply_text(texto, reply_markup=teclado)
|
||||||
|
|
||||||
|
async def post_init(application: Application):
|
||||||
|
# Mantén los comandos rápidos disponibles en el menú de Telegram
|
||||||
|
await application.bot.set_my_commands([
|
||||||
|
BotCommand("start", "Mostrar menú principal"),
|
||||||
|
BotCommand("welcome", "Iniciar onboarding"),
|
||||||
|
BotCommand("vacaciones", "Solicitar vacaciones"),
|
||||||
|
BotCommand("permiso", "Solicitar permiso por horas"),
|
||||||
|
BotCommand("cancelar", "Cancelar flujo actual"),
|
||||||
|
])
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Configuración Global
|
# Configuración Global
|
||||||
defaults = Defaults(parse_mode=ParseMode.MARKDOWN)
|
defaults = Defaults(parse_mode=ParseMode.MARKDOWN)
|
||||||
app = Application.builder().token(TOKEN).defaults(defaults).build()
|
app = (
|
||||||
|
Application.builder()
|
||||||
|
.token(TOKEN)
|
||||||
|
.defaults(defaults)
|
||||||
|
.post_init(post_init)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
# --- REGISTRO DE HABILIDADES ---
|
# --- REGISTRO DE HABILIDADES ---
|
||||||
|
|
||||||
@@ -48,7 +67,6 @@ def main():
|
|||||||
|
|
||||||
# 2. Habilidades Complejas (Conversaciones)
|
# 2. Habilidades Complejas (Conversaciones)
|
||||||
app.add_handler(onboarding_handler)
|
app.add_handler(onboarding_handler)
|
||||||
app.add_handler(print_handler)
|
|
||||||
app.add_handler(vacaciones_handler)
|
app.add_handler(vacaciones_handler)
|
||||||
app.add_handler(permiso_handler)
|
app.add_handler(permiso_handler)
|
||||||
# app.add_handler(finder_handler)
|
# app.add_handler(finder_handler)
|
||||||
|
|||||||
@@ -67,6 +67,55 @@ def limpiar_texto_general(texto: str) -> str:
|
|||||||
t = " ".join(texto.split())
|
t = " ".join(texto.split())
|
||||||
return "N/A" if t == "0" else t
|
return "N/A" if t == "0" else t
|
||||||
|
|
||||||
|
def _num_to_words_es_hasta_999(n: int) -> str:
|
||||||
|
"""Convierte un número (0-999) a texto en español sin acentos."""
|
||||||
|
if n < 0 or n > 999:
|
||||||
|
return str(n)
|
||||||
|
unidades = ["cero", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]
|
||||||
|
especiales = {
|
||||||
|
10: "diez", 11: "once", 12: "doce", 13: "trece", 14: "catorce", 15: "quince",
|
||||||
|
20: "veinte", 30: "treinta", 40: "cuarenta", 50: "cincuenta",
|
||||||
|
60: "sesenta", 70: "setenta", 80: "ochenta", 90: "noventa",
|
||||||
|
100: "cien", 200: "doscientos", 300: "trescientos", 400: "cuatrocientos",
|
||||||
|
500: "quinientos", 600: "seiscientos", 700: "setecientos",
|
||||||
|
800: "ochocientos", 900: "novecientos"
|
||||||
|
}
|
||||||
|
if n < 10:
|
||||||
|
return unidades[n]
|
||||||
|
if n in especiales:
|
||||||
|
return especiales[n]
|
||||||
|
if n < 20:
|
||||||
|
return "dieci" + unidades[n - 10]
|
||||||
|
if n < 30:
|
||||||
|
return "veinti" + unidades[n - 20]
|
||||||
|
if n < 100:
|
||||||
|
decenas = (n // 10) * 10
|
||||||
|
resto = n % 10
|
||||||
|
return f"{especiales[decenas]} y {unidades[resto]}"
|
||||||
|
centenas = (n // 100) * 100
|
||||||
|
resto = n % 100
|
||||||
|
if centenas == 100 and resto > 0:
|
||||||
|
prefijo = "ciento"
|
||||||
|
else:
|
||||||
|
prefijo = especiales.get(centenas, str(centenas))
|
||||||
|
if resto == 0:
|
||||||
|
return prefijo
|
||||||
|
return f"{prefijo} { _num_to_words_es_hasta_999(resto)}"
|
||||||
|
|
||||||
|
def numero_a_texto(num_ext: str, num_int: str) -> str:
|
||||||
|
"""Devuelve un resumen textual del numero exterior (+ interior si aplica)."""
|
||||||
|
import re
|
||||||
|
texto_base = limpiar_texto_general(num_ext)
|
||||||
|
interior = limpiar_texto_general(num_int)
|
||||||
|
m = re.match(r"(\d+)", texto_base)
|
||||||
|
if not m:
|
||||||
|
return texto_base
|
||||||
|
numero = int(m.group(1))
|
||||||
|
en_letras = _num_to_words_es_hasta_999(numero)
|
||||||
|
if interior and interior.upper() != "N/A":
|
||||||
|
return f"{en_letras}, interior {interior}".strip()
|
||||||
|
return en_letras
|
||||||
|
|
||||||
# --- 4. TECLADOS DINÁMICOS ---
|
# --- 4. TECLADOS DINÁMICOS ---
|
||||||
|
|
||||||
# Meses: Texto vs Valor
|
# Meses: Texto vs Valor
|
||||||
@@ -130,7 +179,8 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
"telegram_id": user.id,
|
"telegram_id": user.id,
|
||||||
"username": user.username or "N/A",
|
"username": user.username or "N/A",
|
||||||
"first_name": user.first_name,
|
"first_name": user.first_name,
|
||||||
"start_ts": datetime.now().timestamp()
|
"start_ts": datetime.now().timestamp(),
|
||||||
|
"msg_count": 0,
|
||||||
}
|
}
|
||||||
context.user_data["respuestas"] = {}
|
context.user_data["respuestas"] = {}
|
||||||
|
|
||||||
@@ -146,6 +196,9 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
async def manejar_flujo(update: Update, context: ContextTypes.DEFAULT_TYPE, estado_actual: int) -> int:
|
async def manejar_flujo(update: Update, context: ContextTypes.DEFAULT_TYPE, estado_actual: int) -> int:
|
||||||
texto_recibido = update.message.text
|
texto_recibido = update.message.text
|
||||||
respuesta_procesada = limpiar_texto_general(texto_recibido)
|
respuesta_procesada = limpiar_texto_general(texto_recibido)
|
||||||
|
meta = context.user_data.get("metadata", {})
|
||||||
|
meta["msg_count"] = meta.get("msg_count", 0) + 1
|
||||||
|
context.user_data["metadata"] = meta
|
||||||
|
|
||||||
# --- LÓGICA DE PROCESAMIENTO ESPECÍFICA POR ESTADO ---
|
# --- LÓGICA DE PROCESAMIENTO ESPECÍFICA POR ESTADO ---
|
||||||
|
|
||||||
@@ -233,6 +286,9 @@ async def manejar_flujo(update: Update, context: ContextTypes.DEFAULT_TYPE, esta
|
|||||||
async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
# Guardar última respuesta (Relación Emergencia)
|
# Guardar última respuesta (Relación Emergencia)
|
||||||
context.user_data["respuestas"][EMERGENCIA_RELACION] = limpiar_texto_general(update.message.text)
|
context.user_data["respuestas"][EMERGENCIA_RELACION] = limpiar_texto_general(update.message.text)
|
||||||
|
meta = context.user_data.get("metadata", {})
|
||||||
|
meta["msg_count"] = meta.get("msg_count", 0) + 1
|
||||||
|
context.user_data["metadata"] = meta
|
||||||
|
|
||||||
await update.message.reply_text("¡Perfecto! 📝 Guardando tu expediente en el sistema... dame un momento.")
|
await update.message.reply_text("¡Perfecto! 📝 Guardando tu expediente en el sistema... dame un momento.")
|
||||||
|
|
||||||
@@ -246,6 +302,10 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
except Exception:
|
except Exception:
|
||||||
fecha_nac = "ERROR_FECHA"
|
fecha_nac = "ERROR_FECHA"
|
||||||
fecha_ini = "ERROR_FECHA"
|
fecha_ini = "ERROR_FECHA"
|
||||||
|
# Derivados
|
||||||
|
num_ext_texto = numero_a_texto(r.get(NUM_EXTERIOR, ""), r.get(NUM_INTERIOR, ""))
|
||||||
|
curp_base = (r.get(CURP) or "").upper()
|
||||||
|
n_empleado = f"{(curp_base + 'XXXX')[:4]}{fecha_ini.replace('-', '')}"
|
||||||
|
|
||||||
# PAYLOAD ESTRUCTURADO PARA N8N
|
# PAYLOAD ESTRUCTURADO PARA N8N
|
||||||
payload = {
|
payload = {
|
||||||
@@ -267,6 +327,7 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
"calle": r.get(CALLE),
|
"calle": r.get(CALLE),
|
||||||
"num_ext": r.get(NUM_EXTERIOR),
|
"num_ext": r.get(NUM_EXTERIOR),
|
||||||
"num_int": r.get(NUM_INTERIOR),
|
"num_int": r.get(NUM_INTERIOR),
|
||||||
|
"num_ext_texto": num_ext_texto,
|
||||||
"colonia": r.get(COLONIA),
|
"colonia": r.get(COLONIA),
|
||||||
"cp": r.get(CODIGO_POSTAL),
|
"cp": r.get(CODIGO_POSTAL),
|
||||||
"ciudad": r.get(CIUDAD_RESIDENCIA),
|
"ciudad": r.get(CIUDAD_RESIDENCIA),
|
||||||
@@ -275,7 +336,8 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
"laboral": {
|
"laboral": {
|
||||||
"rol_id": r.get(ROL).lower(), # partner, manager...
|
"rol_id": r.get(ROL).lower(), # partner, manager...
|
||||||
"sucursal_id": r.get(SUCURSAL), # plaza_cima, plaza_o
|
"sucursal_id": r.get(SUCURSAL), # plaza_cima, plaza_o
|
||||||
"fecha_inicio": fecha_ini
|
"fecha_inicio": fecha_ini,
|
||||||
|
"numero_empleado": n_empleado
|
||||||
},
|
},
|
||||||
"referencias": [
|
"referencias": [
|
||||||
{"nombre": r.get(REF1_NOMBRE), "telefono": r.get(REF1_TELEFONO), "relacion": r.get(REF1_TIPO)},
|
{"nombre": r.get(REF1_NOMBRE), "telefono": r.get(REF1_TELEFONO), "relacion": r.get(REF1_TIPO)},
|
||||||
@@ -291,7 +353,9 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
"telegram_user": meta["username"],
|
"telegram_user": meta["username"],
|
||||||
"chat_id": meta["telegram_id"],
|
"chat_id": meta["telegram_id"],
|
||||||
"bot_version": "welcome2soul_v2",
|
"bot_version": "welcome2soul_v2",
|
||||||
"fecha_registro": datetime.now().isoformat()
|
"fecha_registro": datetime.now().isoformat(),
|
||||||
|
"duracion_segundos": round(datetime.now().timestamp() - meta.get("start_ts", datetime.now().timestamp()), 2),
|
||||||
|
"mensajes_totales": meta.get("msg_count", 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import requests
|
import requests
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
@@ -24,105 +23,224 @@ def _send_webhooks(urls: list, payload: dict):
|
|||||||
print(f"[webhook] Error enviando a {url}: {e}")
|
print(f"[webhook] Error enviando a {url}: {e}")
|
||||||
return enviados
|
return enviados
|
||||||
|
|
||||||
TIPO_SOLICITITUD, FECHAS, MOTIVO = range(3)
|
# Estados de conversación
|
||||||
|
(
|
||||||
|
VAC_ANIO,
|
||||||
|
INICIO_DIA,
|
||||||
|
INICIO_MES,
|
||||||
|
FIN_DIA,
|
||||||
|
FIN_MES,
|
||||||
|
PERMISO_CUANDO,
|
||||||
|
PERMISO_ANIO,
|
||||||
|
HORARIO,
|
||||||
|
MOTIVO,
|
||||||
|
) = range(9)
|
||||||
|
|
||||||
# Teclados de apoyo
|
# Teclados de apoyo
|
||||||
MESES = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
|
MESES = [
|
||||||
|
"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
|
||||||
|
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
|
||||||
|
]
|
||||||
TECLADO_MESES = ReplyKeyboardMarkup([MESES[i:i+3] for i in range(0, 12, 3)], one_time_keyboard=True, resize_keyboard=True)
|
TECLADO_MESES = ReplyKeyboardMarkup([MESES[i:i+3] for i in range(0, 12, 3)], one_time_keyboard=True, resize_keyboard=True)
|
||||||
TECLADO_PERMISO_RAPIDO = ReplyKeyboardMarkup(
|
MESES_MAP = {nombre.lower(): idx + 1 for idx, nombre in enumerate(MESES)}
|
||||||
[["Hoy 09:00-11:00", "Hoy 15:00-18:00"], ["Mañana 09:00-11:00", "Mañana 15:00-18:00"], ["Otra fecha/horario"]],
|
ANIO_ACTUAL = datetime.now().year
|
||||||
|
TECLADO_ANIOS = ReplyKeyboardMarkup([[str(ANIO_ACTUAL), str(ANIO_ACTUAL + 1)]], one_time_keyboard=True, resize_keyboard=True)
|
||||||
|
TECLADO_PERMISO_CUANDO = ReplyKeyboardMarkup(
|
||||||
|
[["Hoy", "Mañana"], ["Pasado mañana", "Fecha específica"]],
|
||||||
one_time_keyboard=True,
|
one_time_keyboard=True,
|
||||||
resize_keyboard=True,
|
resize_keyboard=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_vacation_metrics(date_string: str) -> dict:
|
def _parse_dia(texto: str) -> int:
|
||||||
"""
|
try:
|
||||||
Calcula métricas de vacaciones a partir de un texto.
|
dia = int(texto)
|
||||||
Asume un formato como "10 al 15 de Octubre".
|
if 1 <= dia <= 31:
|
||||||
"""
|
return dia
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _parse_mes(texto: str) -> int:
|
||||||
|
return MESES_MAP.get(texto.strip().lower(), 0)
|
||||||
|
|
||||||
|
def _parse_anio(texto: str) -> int:
|
||||||
|
try:
|
||||||
|
return int(texto)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _build_dates(datos: dict) -> dict:
|
||||||
|
"""Construye fechas ISO; si fin < inicio, se ajusta a inicio."""
|
||||||
|
year = datos.get("anio") or datetime.now().year
|
||||||
|
try:
|
||||||
|
inicio = date(year, datos["inicio_mes"], datos["inicio_dia"])
|
||||||
|
fin_dia = datos.get("fin_dia", datos.get("inicio_dia"))
|
||||||
|
fin_mes = datos.get("fin_mes", datos.get("inicio_mes"))
|
||||||
|
fin = date(year, fin_mes, fin_dia)
|
||||||
|
if fin < inicio:
|
||||||
|
fin = inicio
|
||||||
|
return {"inicio": inicio, "fin": fin}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _calculate_vacation_metrics_from_dates(fechas: dict) -> dict:
|
||||||
today = date.today()
|
today = date.today()
|
||||||
current_year = today.year
|
inicio = fechas.get("inicio")
|
||||||
|
fin = fechas.get("fin")
|
||||||
# Mapeo de meses en español a número
|
if not inicio or not fin:
|
||||||
meses = {
|
return {"dias_totales": 0, "dias_anticipacion": 0}
|
||||||
'enero': 1, 'febrero': 2, 'marzo': 3, 'abril': 4, 'mayo': 5, 'junio': 6,
|
dias_totales = (fin - inicio).days + 1
|
||||||
'julio': 7, 'agosto': 8, 'septiembre': 9, 'octubre': 10, 'noviembre': 11, 'diciembre': 12
|
dias_anticipacion = (inicio - today).days
|
||||||
|
return {
|
||||||
|
"dias_totales": dias_totales,
|
||||||
|
"dias_anticipacion": dias_anticipacion,
|
||||||
|
"fechas_calculadas": {"inicio": inicio.isoformat(), "fin": fin.isoformat()},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Regex para "10 al 15 de Octubre"
|
def _fmt_fecha(fecha_iso: str) -> str:
|
||||||
match = re.search(r'(\d{1,2})\s*al\s*(\d{1,2})\s*de\s*(\w+)', date_string, re.IGNORECASE)
|
if not fecha_iso:
|
||||||
|
return "N/A"
|
||||||
if not match:
|
|
||||||
return {"dias_totales": 0, "dias_anticipacion": 0}
|
|
||||||
|
|
||||||
start_day, end_day, month_str = match.groups()
|
|
||||||
start_day, end_day = int(start_day), int(end_day)
|
|
||||||
month = meses.get(month_str.lower())
|
|
||||||
|
|
||||||
if not month:
|
|
||||||
return {"dias_totales": 0, "dias_anticipacion": 0}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_date = date(current_year, month, start_day)
|
return fecha_iso.split("T")[0]
|
||||||
# Si la fecha ya pasó este año, asumir que es del próximo año
|
except Exception:
|
||||||
if start_date < today:
|
return fecha_iso
|
||||||
start_date = date(current_year + 1, month, start_day)
|
|
||||||
|
|
||||||
end_date = date(start_date.year, month, end_day)
|
|
||||||
|
|
||||||
dias_totales = (end_date - start_date).days + 1
|
|
||||||
dias_anticipacion = (start_date - today).days
|
|
||||||
|
|
||||||
return {"dias_totales": dias_totales, "dias_anticipacion": dias_anticipacion, "fechas_calculadas": {"inicio": start_date.isoformat(), "fin": end_date.isoformat()}}
|
|
||||||
except ValueError:
|
|
||||||
return {"dias_totales": 0, "dias_anticipacion": 0}
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- Vacaciones ---
|
||||||
async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
log_request(user.id, user.username, "vacaciones", update.message.text)
|
log_request(user.id, user.username, "vacaciones", update.message.text)
|
||||||
|
context.user_data.clear()
|
||||||
context.user_data['tipo'] = 'VACACIONES'
|
context.user_data['tipo'] = 'VACACIONES'
|
||||||
|
context.user_data["anio"] = ANIO_ACTUAL
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"🌴 **Solicitud de Vacaciones**\n\n¿Para qué fechas las necesitas?\nUsa el formato: `10 al 15 de Octubre`.",
|
"🌴 **Solicitud de Vacaciones**\n\nUsaré el año actual. ¿En qué *día* inicia tu descanso? (número, ej: 10)",
|
||||||
reply_markup=TECLADO_MESES,
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
)
|
)
|
||||||
return FECHAS
|
return INICIO_DIA
|
||||||
|
|
||||||
|
# --- Permiso ---
|
||||||
async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
log_request(user.id, user.username, "permiso", update.message.text)
|
log_request(user.id, user.username, "permiso", update.message.text)
|
||||||
|
context.user_data.clear()
|
||||||
context.user_data['tipo'] = 'PERMISO'
|
context.user_data['tipo'] = 'PERMISO'
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"⏱️ **Solicitud de Permiso**\n\nSelecciona o escribe el día y horario. Ej: `Jueves 15 09:00-11:00`.",
|
"⏱️ **Solicitud de Permiso**\n\n¿Para cuándo lo necesitas?",
|
||||||
reply_markup=TECLADO_PERMISO_RAPIDO,
|
reply_markup=TECLADO_PERMISO_CUANDO,
|
||||||
)
|
)
|
||||||
return FECHAS
|
return PERMISO_CUANDO
|
||||||
|
|
||||||
async def recibir_fechas(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
# --- Selección de año / cuando ---
|
||||||
texto_fechas = update.message.text
|
async def recibir_anio_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
es_vacaciones = context.user_data.get('tipo') == 'VACACIONES'
|
anio = _parse_anio(update.message.text)
|
||||||
|
if anio not in (ANIO_ACTUAL, ANIO_ACTUAL + 1):
|
||||||
|
await update.message.reply_text("Elige el año del teclado (actual o siguiente).", reply_markup=TECLADO_ANIOS)
|
||||||
|
return VAC_ANIO
|
||||||
|
context.user_data["anio"] = anio
|
||||||
|
await update.message.reply_text("¿Qué *día* termina?", reply_markup=ReplyKeyboardRemove())
|
||||||
|
return FIN_DIA
|
||||||
|
|
||||||
if es_vacaciones:
|
async def recibir_cuando_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
metrics = _calculate_vacation_metrics(texto_fechas)
|
texto = update.message.text.strip().lower()
|
||||||
if metrics["dias_totales"] == 0:
|
hoy = date.today()
|
||||||
|
offset_map = {"hoy": 0, "mañana": 1, "manana": 1, "pasado mañana": 2, "pasado manana": 2}
|
||||||
|
if texto in offset_map:
|
||||||
|
delta = offset_map[texto]
|
||||||
|
fecha = hoy.fromordinal(hoy.toordinal() + delta)
|
||||||
|
context.user_data["anio"] = fecha.year
|
||||||
|
context.user_data["inicio_dia"] = fecha.day
|
||||||
|
context.user_data["inicio_mes"] = fecha.month
|
||||||
|
context.user_data["fin_dia"] = fecha.day
|
||||||
|
context.user_data["fin_mes"] = fecha.month
|
||||||
|
await update.message.reply_text("¿Cuál es el horario? Ej: `09:00-11:00` o `Todo el día`.", reply_markup=ReplyKeyboardRemove())
|
||||||
|
return HORARIO
|
||||||
|
if "fecha" in texto:
|
||||||
|
context.user_data["anio"] = ANIO_ACTUAL
|
||||||
|
await update.message.reply_text("¿En qué *día* inicia el permiso? (número, ej: 12)", reply_markup=ReplyKeyboardRemove())
|
||||||
|
return INICIO_DIA
|
||||||
|
await update.message.reply_text("Elige una opción: Hoy, Mañana, Pasado mañana o Fecha específica.", reply_markup=TECLADO_PERMISO_CUANDO)
|
||||||
|
return PERMISO_CUANDO
|
||||||
|
|
||||||
|
async def recibir_anio_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
anio = _parse_anio(update.message.text)
|
||||||
|
if anio not in (ANIO_ACTUAL, ANIO_ACTUAL + 1):
|
||||||
|
await update.message.reply_text("Elige el año del teclado (actual o siguiente).", reply_markup=TECLADO_ANIOS)
|
||||||
|
return PERMISO_ANIO
|
||||||
|
context.user_data["anio"] = anio
|
||||||
|
await update.message.reply_text("¿Qué *día* termina?", reply_markup=ReplyKeyboardRemove())
|
||||||
|
return FIN_DIA
|
||||||
|
|
||||||
|
# --- Captura de fechas ---
|
||||||
|
async def recibir_inicio_dia(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
dia = _parse_dia(update.message.text)
|
||||||
|
if not dia:
|
||||||
|
await update.message.reply_text("Necesito un número de día válido (1-31). Intenta de nuevo.")
|
||||||
|
return INICIO_DIA
|
||||||
|
context.user_data["inicio_dia"] = dia
|
||||||
|
await update.message.reply_text("¿De qué *mes* inicia?", reply_markup=TECLADO_MESES)
|
||||||
|
return INICIO_MES
|
||||||
|
|
||||||
|
async def recibir_inicio_mes(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
mes = _parse_mes(update.message.text)
|
||||||
|
if not mes:
|
||||||
|
await update.message.reply_text("Elige un mes del teclado o escríbelo igual que aparece.", reply_markup=TECLADO_MESES)
|
||||||
|
return INICIO_MES
|
||||||
|
context.user_data["inicio_mes"] = mes
|
||||||
|
context.user_data.setdefault("anio", ANIO_ACTUAL)
|
||||||
|
|
||||||
|
try:
|
||||||
|
inicio_candidato = date(context.user_data["anio"], mes, context.user_data["inicio_dia"])
|
||||||
|
if inicio_candidato < date.today():
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"No entendí las fechas. Usa un formato como `10 al 15 de Octubre`.",
|
"Esa fecha ya pasó este año. ¿Para qué año la agendamos?",
|
||||||
reply_markup=TECLADO_MESES,
|
reply_markup=TECLADO_ANIOS
|
||||||
)
|
)
|
||||||
return FECHAS
|
return VAC_ANIO if context.user_data.get("tipo") == "VACACIONES" else PERMISO_ANIO
|
||||||
# Si se entiende, guardamos también las métricas preliminares
|
except Exception:
|
||||||
context.user_data['metricas_preliminares'] = metrics
|
pass
|
||||||
|
|
||||||
|
await update.message.reply_text("¿Qué *día* termina?", reply_markup=ReplyKeyboardRemove())
|
||||||
|
return FIN_DIA
|
||||||
|
|
||||||
|
async def recibir_fin_dia(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
dia = _parse_dia(update.message.text)
|
||||||
|
if not dia:
|
||||||
|
await update.message.reply_text("Día inválido. Dame un número de 1 a 31.")
|
||||||
|
return FIN_DIA
|
||||||
|
context.user_data["fin_dia"] = dia
|
||||||
|
await update.message.reply_text("¿De qué *mes* termina?", reply_markup=TECLADO_MESES)
|
||||||
|
return FIN_MES
|
||||||
|
|
||||||
|
async def recibir_fin_mes(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
mes = _parse_mes(update.message.text)
|
||||||
|
if not mes:
|
||||||
|
await update.message.reply_text("Elige un mes válido.", reply_markup=TECLADO_MESES)
|
||||||
|
return FIN_MES
|
||||||
|
context.user_data["fin_mes"] = mes
|
||||||
|
|
||||||
|
if context.user_data.get("tipo") == "PERMISO":
|
||||||
|
await update.message.reply_text("¿Cuál es el horario? Ej: `09:00-11:00` o `Todo el día`.", reply_markup=ReplyKeyboardRemove())
|
||||||
|
return HORARIO
|
||||||
|
|
||||||
context.user_data['fechas'] = texto_fechas
|
|
||||||
await update.message.reply_text("Entendido. ¿Cuál es el motivo o comentario adicional?", reply_markup=ReplyKeyboardRemove())
|
await update.message.reply_text("Entendido. ¿Cuál es el motivo o comentario adicional?", reply_markup=ReplyKeyboardRemove())
|
||||||
return MOTIVO
|
return MOTIVO
|
||||||
|
|
||||||
|
async def recibir_horario(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
context.user_data["horario"] = update.message.text.strip()
|
||||||
|
await update.message.reply_text("Entendido. ¿Cuál es el motivo o comentario adicional?", reply_markup=ReplyKeyboardRemove())
|
||||||
|
return MOTIVO
|
||||||
|
|
||||||
|
# --- Motivo y cierre ---
|
||||||
async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
motivo = update.message.text
|
motivo = update.message.text
|
||||||
datos = context.user_data
|
datos = context.user_data
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
|
|
||||||
|
fechas = _build_dates(datos)
|
||||||
|
if not fechas:
|
||||||
|
await update.message.reply_text("🤔 No entendí las fechas. Por favor, inicia otra vez con /vacaciones o /permiso.")
|
||||||
|
return ConversationHandler.END
|
||||||
|
|
||||||
# Generar payload base
|
|
||||||
payload = {
|
payload = {
|
||||||
"record_id": str(uuid.uuid4()),
|
"record_id": str(uuid.uuid4()),
|
||||||
"solicitante": {
|
"solicitante": {
|
||||||
@@ -131,7 +249,10 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
"username": user.username
|
"username": user.username
|
||||||
},
|
},
|
||||||
"tipo_solicitud": datos['tipo'],
|
"tipo_solicitud": datos['tipo'],
|
||||||
"fechas_texto_original": datos['fechas'],
|
"fechas": {
|
||||||
|
"inicio": fechas.get("inicio").isoformat() if fechas else None,
|
||||||
|
"fin": fechas.get("fin").isoformat() if fechas else None,
|
||||||
|
},
|
||||||
"motivo_usuario": motivo,
|
"motivo_usuario": motivo,
|
||||||
"created_at": datetime.now().isoformat()
|
"created_at": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
@@ -141,11 +262,12 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
webhooks = _get_webhook_list("WEBHOOK_PERMISOS")
|
webhooks = _get_webhook_list("WEBHOOK_PERMISOS")
|
||||||
categoria = classify_reason(motivo)
|
categoria = classify_reason(motivo)
|
||||||
payload["categoria_detectada"] = categoria
|
payload["categoria_detectada"] = categoria
|
||||||
|
payload["horario"] = datos.get("horario", "N/A")
|
||||||
await update.message.reply_text(f"Categoría detectada → **{categoria}** 🚨")
|
await update.message.reply_text(f"Categoría detectada → **{categoria}** 🚨")
|
||||||
|
|
||||||
elif datos['tipo'] == 'VACACIONES':
|
elif datos['tipo'] == 'VACACIONES':
|
||||||
webhooks = _get_webhook_list("WEBHOOK_VACACIONES")
|
webhooks = _get_webhook_list("WEBHOOK_VACACIONES")
|
||||||
metrics = datos.get('metricas_preliminares') or _calculate_vacation_metrics(datos['fechas'])
|
metrics = _calculate_vacation_metrics_from_dates(fechas)
|
||||||
|
|
||||||
if metrics["dias_totales"] > 0:
|
if metrics["dias_totales"] > 0:
|
||||||
payload["metricas"] = metrics
|
payload["metricas"] = metrics
|
||||||
@@ -154,27 +276,49 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
if dias <= 5:
|
if dias <= 5:
|
||||||
status = "RECHAZADO"
|
status = "RECHAZADO"
|
||||||
mensaje = f"🔴 {dias} días es un periodo muy corto. Las vacaciones deben ser de al menos 6 días."
|
mensaje = f"🔴 {dias} días es un periodo muy corto. Las vacaciones deben ser de al menos 6 días."
|
||||||
|
elif dias > 30:
|
||||||
|
status = "RECHAZADO"
|
||||||
|
mensaje = "🔴 Las vacaciones no pueden exceder 30 días. Ajusta tus fechas, por favor."
|
||||||
elif 6 <= dias <= 11:
|
elif 6 <= dias <= 11:
|
||||||
status = "REVISION_MANUAL"
|
status = "REVISION_MANUAL"
|
||||||
mensaje = f"🟡 Solicitud de {dias} días recibida. Tu manager la revisará pronto."
|
mensaje = f"🟡 Solicitud de {dias} días recibida. Tu manager la revisará pronto."
|
||||||
else: # 12+
|
else: # 12-30
|
||||||
status = "PRE_APROBADO"
|
status = "PRE_APROBADO"
|
||||||
mensaje = f"🟢 ¡Excelente planeación! Tu solicitud de {dias} días ha sido pre-aprobada."
|
mensaje = f"🟢 ¡Excelente planeación! Tu solicitud de {dias} días ha sido pre-aprobada (ideal: 12 días)."
|
||||||
|
|
||||||
payload["status_inicial"] = status
|
payload["status_inicial"] = status
|
||||||
await update.message.reply_text(mensaje)
|
await update.message.reply_text(mensaje)
|
||||||
else:
|
else:
|
||||||
# Si no se pudieron parsear las fechas
|
|
||||||
payload["status_inicial"] = "ERROR_FECHAS"
|
payload["status_inicial"] = "ERROR_FECHAS"
|
||||||
await update.message.reply_text("🤔 No entendí las fechas. Por favor, usa un formato como '10 al 15 de Octubre'.")
|
await update.message.reply_text("🤔 No entendí las fechas. Por favor, comparte día y mes otra vez con /vacaciones.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
enviados = _send_webhooks(webhooks, payload) if webhooks else 0
|
enviados = _send_webhooks(webhooks, payload) if webhooks else 0
|
||||||
tipo_solicitud_texto = "Permiso" if datos['tipo'] == 'PERMISO' else 'Vacaciones'
|
tipo_solicitud_texto = "Permiso" if datos['tipo'] == 'PERMISO' else 'Vacaciones'
|
||||||
if enviados > 0:
|
inicio_txt = _fmt_fecha(payload["fechas"]["inicio"])
|
||||||
await update.message.reply_text(f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.")
|
fin_txt = _fmt_fecha(payload["fechas"]["fin"])
|
||||||
|
if datos['tipo'] == 'PERMISO':
|
||||||
|
resumen = (
|
||||||
|
"📝 Resumen enviado:\n"
|
||||||
|
f"- Fecha: {inicio_txt} a {fin_txt}\n"
|
||||||
|
f"- Horario: {payload.get('horario', 'N/A')}\n"
|
||||||
|
f"- Categoría: {payload.get('categoria_detectada', 'N/A')}\n"
|
||||||
|
f"- Motivo: {motivo}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text("⚠️ No hay webhook configurado o falló el envío. RH lo revisará.")
|
m = payload.get("metricas", {})
|
||||||
|
resumen = (
|
||||||
|
"📝 Resumen enviado:\n"
|
||||||
|
f"- Inicio: {inicio_txt}\n"
|
||||||
|
f"- Fin: {fin_txt}\n"
|
||||||
|
f"- Días totales: {m.get('dias_totales', 'N/A')}\n"
|
||||||
|
f"- Anticipación: {m.get('dias_anticipacion', 'N/A')} días\n"
|
||||||
|
f"- Estatus inicial: {payload.get('status_inicial', 'N/A')}"
|
||||||
|
)
|
||||||
|
if enviados > 0:
|
||||||
|
await update.message.reply_text(f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.\n\n{resumen}")
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(f"⚠️ No hay webhook configurado o falló el envío. RH lo revisará.\n\n{resumen}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error enviando webhook: {e}")
|
print(f"Error enviando webhook: {e}")
|
||||||
await update.message.reply_text("⚠️ Error enviando la solicitud.")
|
await update.message.reply_text("⚠️ Error enviando la solicitud.")
|
||||||
@@ -190,7 +334,11 @@ async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
vacaciones_handler = ConversationHandler(
|
vacaciones_handler = ConversationHandler(
|
||||||
entry_points=[CommandHandler("vacaciones", start_vacaciones)],
|
entry_points=[CommandHandler("vacaciones", start_vacaciones)],
|
||||||
states={
|
states={
|
||||||
FECHAS: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fechas)],
|
VAC_ANIO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_anio_vacaciones)],
|
||||||
|
INICIO_DIA: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_inicio_dia)],
|
||||||
|
INICIO_MES: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_inicio_mes)],
|
||||||
|
FIN_DIA: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fin_dia)],
|
||||||
|
FIN_MES: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fin_mes)],
|
||||||
MOTIVO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_motivo_fin)]
|
MOTIVO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_motivo_fin)]
|
||||||
},
|
},
|
||||||
fallbacks=[CommandHandler("cancelar", cancelar)]
|
fallbacks=[CommandHandler("cancelar", cancelar)]
|
||||||
@@ -199,7 +347,13 @@ vacaciones_handler = ConversationHandler(
|
|||||||
permiso_handler = ConversationHandler(
|
permiso_handler = ConversationHandler(
|
||||||
entry_points=[CommandHandler("permiso", start_permiso)],
|
entry_points=[CommandHandler("permiso", start_permiso)],
|
||||||
states={
|
states={
|
||||||
FECHAS: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fechas)],
|
PERMISO_CUANDO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_cuando_permiso)],
|
||||||
|
PERMISO_ANIO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_anio_permiso)],
|
||||||
|
INICIO_DIA: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_inicio_dia)],
|
||||||
|
INICIO_MES: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_inicio_mes)],
|
||||||
|
FIN_DIA: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fin_dia)],
|
||||||
|
FIN_MES: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fin_mes)],
|
||||||
|
HORARIO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_horario)],
|
||||||
MOTIVO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_motivo_fin)]
|
MOTIVO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_motivo_fin)]
|
||||||
},
|
},
|
||||||
fallbacks=[CommandHandler("cancelar", cancelar)]
|
fallbacks=[CommandHandler("cancelar", cancelar)]
|
||||||
|
|||||||
Reference in New Issue
Block a user