Migra el servicio a arquitectura Docker persistente y mueve credenciales HANA a .env
- Reemplaza config_loader/config_model/hana_client/logger_utilities/outlook_utilities por core.py (pipeline funcional), hana_manager.py, logger_manager.py, scheduler.py y descargador_sovos.py (Playwright/Firefox) según la plantilla de servicios. - main.py pasa a loop continuo dirigido por scheduler.json, con manejo de SIGINT/SIGTERM. - Agrega Dockerfile, docker-compose.yml (restart unless-stopped) y certs/ para despliegue. - Agrega smb_transfer.py para la transferencia a carpeta compartida. - Saca las credenciales de HANA de config.json y las mueve a .env (HANA_HOST/PORT/USER/PASSWORD).
This commit is contained in:
1 parent
1503d6a2ec
commit
277d07dfeb
22 files changed
+1081
-750
No files matched your search
@@ -1,265 +1,56 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
"""main.py — entrada delgada. Bucle continuo dirigido por scheduler.json."""
|
||||
import signal
|
||||
import time
|
||||
import logger_utilities
|
||||
import hana_client
|
||||
from datetime import datetime, timedelta
|
||||
from config_loader import CONFIG
|
||||
import outlook_utilities
|
||||
import argparse
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from datetime import datetime
|
||||
|
||||
########################################################
|
||||
def descargar_comprobante(driver, wait, ruc, tipo, correlativo, monto, fecha, download_dir):
|
||||
url = "https://public-einvoicing.sovos.com/pe"
|
||||
driver.get(url)
|
||||
|
||||
# Esperar a que la aplicación cargue el input del RUC
|
||||
wait.until(EC.presence_of_element_located((By.NAME, "senderTaxId")))
|
||||
|
||||
# 1. Completar RUC
|
||||
ruc_input = driver.find_element(By.NAME, "senderTaxId")
|
||||
ruc_input.clear()
|
||||
ruc_input.send_keys(ruc)
|
||||
|
||||
# 2. Seleccionar Tipo Comprobante
|
||||
select_trigger = driver.find_element(By.ID, "mui-component-select-invoiceTypeCode")
|
||||
select_trigger.click()
|
||||
|
||||
# Esperar a que la opción esté disponible y seleccionarla
|
||||
option_xpath = f"//li[@role='option' and @data-value='{tipo}']"
|
||||
option_element = wait.until(EC.element_to_be_clickable((By.XPATH, option_xpath)))
|
||||
option_element.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# 3. Completar Folio / Correlativo
|
||||
correlative_input = driver.find_element(By.NAME, "correlative")
|
||||
correlative_input.clear()
|
||||
correlative_input.send_keys(correlativo)
|
||||
|
||||
# 4. Completar Monto Total
|
||||
amount_input = driver.find_element(By.NAME, "payableAmount")
|
||||
amount_input.clear()
|
||||
amount_input.send_keys(str(monto))
|
||||
|
||||
# 5. Completar Fecha de emisión
|
||||
date_input = driver.find_element(By.XPATH, "//input[contains(@placeholder, 'Ej: 202')]")
|
||||
driver.execute_script("arguments[0].click();", date_input)
|
||||
|
||||
# Seleccionar todo y reemplazar con la fecha
|
||||
date_input.send_keys(Keys.COMMAND, 'a')
|
||||
date_input.send_keys(Keys.BACKSPACE)
|
||||
date_input.send_keys(fecha)
|
||||
time.sleep(0.5)
|
||||
|
||||
# 6. Enviar Formulario
|
||||
submit_btn = driver.find_element(By.XPATH, "//button[contains(text(), 'Ver documento')]")
|
||||
if not submit_btn.is_enabled():
|
||||
raise ValueError("El botón 'Ver documento' está deshabilitado. Verifique que los campos sean válidos.")
|
||||
|
||||
driver.execute_script("arguments[0].click();", submit_btn)
|
||||
|
||||
# Esperar a que la página de resultados cargue o muestre error de "Document not found"
|
||||
download_btn_xpath = "//*[@data-testid='btn-download-document']"
|
||||
error_xpath = "//*[contains(@class, 'sovosSnackbar--error') or contains(text(), 'Document not found')]"
|
||||
|
||||
download_btn_present = False
|
||||
error_present = False
|
||||
|
||||
for _ in range(30): # Esperar hasta 15 segundos máximo (30 * 0.5s)
|
||||
btn_elements = driver.find_elements(By.XPATH, download_btn_xpath)
|
||||
if btn_elements and btn_elements[0].is_displayed():
|
||||
download_btn_present = True
|
||||
break
|
||||
|
||||
err_elements = driver.find_elements(By.XPATH, error_xpath)
|
||||
if err_elements and err_elements[0].is_displayed():
|
||||
error_present = True
|
||||
break
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
if error_present:
|
||||
raise FileNotFoundError("El comprobante no existe en el portal de Sovos (Document not found).")
|
||||
|
||||
if not download_btn_present:
|
||||
raise TimeoutError("Se superó el tiempo de espera sin encontrar el comprobante ni un mensaje de error.")
|
||||
|
||||
# 7. Descargar PDF (Activo por defecto)
|
||||
download_btn = driver.find_element(By.XPATH, download_btn_xpath)
|
||||
driver.execute_script("arguments[0].click();", download_btn)
|
||||
|
||||
# 8. Cambiar a formato XML y Descargar
|
||||
doc_sel_btn = driver.find_element(By.XPATH, "//button[contains(., '.pdf') or contains(., '.xml')]")
|
||||
driver.execute_script("arguments[0].click();", doc_sel_btn)
|
||||
time.sleep(1)
|
||||
|
||||
xml_option = wait.until(EC.element_to_be_clickable((By.XPATH, "//li[@role='menuitem' and contains(., '.xml')]")))
|
||||
driver.execute_script("arguments[0].click();", xml_option)
|
||||
|
||||
# Esperar a que el selector se actualice y muestre XML
|
||||
wait.until(EC.text_to_be_present_in_element((By.XPATH, "//button[contains(@class, 'sovosButton')]"), ".xml"))
|
||||
|
||||
download_btn = driver.find_element(By.XPATH, download_btn_xpath)
|
||||
driver.execute_script("arguments[0].click();", download_btn)
|
||||
|
||||
# Generar patrón esperado para verificar (ej. serie = FB03, número con padding a 8 dígitos)
|
||||
serie_num_pattern = correlativo.replace("-", "")
|
||||
if "-" in correlativo:
|
||||
parts = correlativo.split("-")
|
||||
if len(parts) == 2:
|
||||
serie_num_pattern = f"{parts[0]}{parts[1].zfill(8)}".lower()
|
||||
|
||||
# Esperar a que las descargas finalicen en el directorio
|
||||
pdf_found = False
|
||||
xml_found = False
|
||||
for attempt in range(15):
|
||||
files = os.listdir(download_dir)
|
||||
pdf_found = any(f.endswith(".pdf") and serie_num_pattern in f.replace("-", "").lower() for f in files)
|
||||
xml_found = any(f.endswith(".xml") and serie_num_pattern in f.replace("-", "").lower() for f in files)
|
||||
if pdf_found and xml_found:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# Listar descargas completadas
|
||||
files = os.listdir(download_dir)
|
||||
downloaded_files = [f for f in files if (f.endswith(".pdf") or f.endswith(".xml")) and serie_num_pattern in f.replace("-", "").lower()]
|
||||
|
||||
if not pdf_found or not xml_found:
|
||||
raise FileNotFoundError(f"No se pudieron descargar los archivos correspondientes al correlativo: {correlative}")
|
||||
|
||||
return downloaded_files
|
||||
from core import ejecutar
|
||||
from scheduler import cargar_scheduler, franja_actual, proximo_inicio
|
||||
from logger_manager import get_logger
|
||||
|
||||
########################################################
|
||||
def main():
|
||||
dias_consulta = CONFIG.mail.days
|
||||
registros = hana_client.obtener_registros(days=dias_consulta)
|
||||
logger = get_logger("main")
|
||||
|
||||
download_dir = CONFIG.files.path
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
_running = True
|
||||
|
||||
print("=" * 60)
|
||||
print("⚙️ CONFIGURACIÓN DE DESCARGA SOVOS")
|
||||
print(f"📁 Directorio Destino: {download_dir}")
|
||||
print(f"📊 Total comprobantes a procesar: {len(registros)}")
|
||||
print("=" * 60)
|
||||
# ============================================================================
|
||||
def _signal_handler(signum, frame):
|
||||
global _running
|
||||
logger.info("⏹️ Señal de parada recibida. Deteniendo tras el ciclo actual...")
|
||||
_running = False
|
||||
|
||||
if not registros or len(registros) == 0:
|
||||
print("\n⚠️ No se encontraron registros en SAP HANA para los últimos", dias_consulta, "días.")
|
||||
return
|
||||
|
||||
# Configurar opciones de Chrome
|
||||
chrome_options = Options()
|
||||
chrome_options.add_argument("--headless")
|
||||
chrome_options.add_argument("--no-sandbox")
|
||||
chrome_options.add_argument("--disable-dev-shm-usage")
|
||||
chrome_options.add_argument("--disable-gpu")
|
||||
chrome_options.add_argument("--blink-settings=imagesEnabled=false")
|
||||
chrome_options.add_argument("--disable-features=InsecureDownloadWarnings")
|
||||
|
||||
prefs = {
|
||||
"download.default_directory": download_dir,
|
||||
"download.prompt_for_download": False,
|
||||
"download.directory_upgrade": True,
|
||||
"safebrowsing.enabled": False,
|
||||
"profile.default_content_setting_values.automatic_downloads": 1
|
||||
}
|
||||
chrome_options.add_experimental_option("prefs", prefs)
|
||||
|
||||
print("\n🌐 [1/2] Iniciando navegador Chrome en modo headless...")
|
||||
driver = webdriver.Chrome(options=chrome_options)
|
||||
|
||||
# Configurar descargas en modo Headless vía CDP
|
||||
driver.execute_cdp_cmd("Page.setDownloadBehavior", {
|
||||
"behavior": "allow",
|
||||
"downloadPath": download_dir
|
||||
})
|
||||
|
||||
wait = WebDriverWait(driver, 15)
|
||||
|
||||
exitosos = 0
|
||||
fallidos = 0
|
||||
archivos_descargados = []
|
||||
|
||||
try:
|
||||
for index, registro in enumerate(registros, start=1):
|
||||
doc_num = registro.get('DocNum')
|
||||
ind = registro.get('Indicator')
|
||||
f_pref = registro.get('FolioPref')
|
||||
f_num = registro.get('FolioNum')
|
||||
tax_date = registro.get('TaxDate')
|
||||
total = registro.get('DocTotal')
|
||||
print(f"\n🔍 PROCESANDO REGISTRO EN HANA: {doc_num} | Tipo: {ind} | Folio: {f_pref}-{f_num} | Fecha: {tax_date} | Total: {total}")
|
||||
def _dormir_interrumpible(segundos: float):
|
||||
"""Duerme en pasos de 1s para reaccionar rápido a SIGTERM/SIGINT."""
|
||||
restante = int(segundos)
|
||||
while restante > 0 and _running:
|
||||
time.sleep(min(1, restante))
|
||||
restante -= 1
|
||||
|
||||
ruc = registro.get("LicTradNum")
|
||||
tipo = registro.get("Indicator")
|
||||
folio = f_pref + "-" + str(f_num)
|
||||
total = registro.get("DocTotal")
|
||||
fecha = registro.get("TaxDate")
|
||||
|
||||
print(f"⚡ [{index}/{len(registros)}] Descargando Comprobante: {folio} (RUC: {ruc})")
|
||||
|
||||
try:
|
||||
archivos = descargar_comprobante(driver, wait, ruc, tipo, folio, total, fecha, download_dir)
|
||||
exitosos += 1
|
||||
archivos_descargados.extend(archivos)
|
||||
print(f" ✅ Comprobante {folio} descargado correctamente.")
|
||||
# Pequeño delay entre descargas
|
||||
time.sleep(1.5)
|
||||
except Exception as e:
|
||||
fallidos += 1
|
||||
print(f" ❌ Error al descargar {folio}: {e}")
|
||||
|
||||
finally:
|
||||
print("\n🔌 Cerrando navegador...")
|
||||
driver.quit()
|
||||
|
||||
print("=" * 60)
|
||||
print("📊 RESUMEN DE PROCESAMIENTO")
|
||||
print(f" ✅ Procesados con éxito: {exitosos}")
|
||||
print(f" ❌ Fallidos: {fallidos}")
|
||||
print("=" * 60)
|
||||
|
||||
# Enviar correo con archivos descargados
|
||||
if archivos_descargados:
|
||||
try:
|
||||
# Convertir nombres de archivo a rutas completas
|
||||
attachment_paths = [os.path.join(download_dir, f) for f in archivos_descargados]
|
||||
token = outlook_utilities.get_access_token()
|
||||
subject = "Comprobantes descargados automáticamente"
|
||||
html_body = f"<p>Se adjuntan {len(attachment_paths)} comprobantes descargados.</p>"
|
||||
outlook_utilities.send_email(token, CONFIG.mail.sender, CONFIG.mail.to, subject, html_body, attachment_paths=attachment_paths)
|
||||
except Exception as email_err:
|
||||
print(f"⚠️ Error al enviar correo: {email_err}")
|
||||
|
||||
#if fallidos > 0:
|
||||
# sys.exit(1)
|
||||
|
||||
########################################################
|
||||
# ============================================================================
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGINT, _signal_handler)
|
||||
signal.signal(signal.SIGTERM, _signal_handler)
|
||||
|
||||
# Configurar Logger (Redirige stdout a archivo y consola)
|
||||
log_path = logger_utilities.setup_logger()
|
||||
logger_utilities.limpiar_logs_antiguos(dias=30)
|
||||
while _running:
|
||||
try:
|
||||
cfg_scheduler = cargar_scheduler()
|
||||
ahora = datetime.now()
|
||||
franja = franja_actual(cfg_scheduler, ahora)
|
||||
|
||||
inicio_ejecucion = datetime.now()
|
||||
print("="*60)
|
||||
print(f"🚀 INICIO DE EJECUCIÓN: {inicio_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"📄 Log guardado en: {log_path}")
|
||||
print("="*60)
|
||||
if franja is None:
|
||||
proximo = proximo_inicio(cfg_scheduler, ahora)
|
||||
segundos = max((proximo - ahora).total_seconds(), 0)
|
||||
logger.info(
|
||||
f"⏸️ Fuera de horario. Próxima franja: {proximo:%Y-%m-%d %H:%M} "
|
||||
f"(durmiendo {int(segundos)}s)"
|
||||
)
|
||||
_dormir_interrumpible(segundos)
|
||||
continue
|
||||
|
||||
main()
|
||||
ejecutar()
|
||||
_dormir_interrumpible(franja["intervalo"])
|
||||
except Exception:
|
||||
logger.error("Excepción no controlada en el ciclo", exc_info=True)
|
||||
_dormir_interrumpible(60)
|
||||
|
||||
fin_ejecucion = datetime.now()
|
||||
duracion = fin_ejecucion - inicio_ejecucion
|
||||
print("\n" + "="*60)
|
||||
print(f"🏁 FIN DE EJECUCIÓN: {fin_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"⏱️ Duración total: {str(duracion).split('.')[0]}") # Sin microsegundos
|
||||
print("="*60)
|
||||
logger.info("👋 Proceso detenido.")
|
||||
# ============================================================================
|
||||
Reference in new issue
Block a user