import os import sys import json 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 ######################################################## 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 ######################################################## def main(): dias_consulta = CONFIG.mail.days registros = hana_client.obtener_registros(days=dias_consulta) download_dir = CONFIG.files.path os.makedirs(download_dir, exist_ok=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) 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}") 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"

Se adjuntan {len(attachment_paths)} comprobantes descargados.

" 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__": # Configurar Logger (Redirige stdout a archivo y consola) log_path = logger_utilities.setup_logger() logger_utilities.limpiar_logs_antiguos(dias=30) 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) main() 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)