2026-05-20 16:38:48 -05:00
|
|
|
|
|
|
|
|
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():
|
2026-05-21 09:56:07 -05:00
|
|
|
dias_consulta = CONFIG.mail.days
|
|
|
|
|
registros = hana_client.obtener_registros(days=dias_consulta)
|
2026-05-20 16:38:48 -05:00
|
|
|
|
|
|
|
|
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)
|
2026-05-21 09:56:07 -05:00
|
|
|
|
|
|
|
|
if not registros or len(registros) == 0:
|
|
|
|
|
print("\n⚠️ No se encontraron registros en SAP HANA para los últimos", dias_consulta, "días.")
|
|
|
|
|
return
|
2026-05-20 16:38:48 -05:00
|
|
|
|
|
|
|
|
# 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')
|
2026-05-21 09:56:07 -05:00
|
|
|
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}")
|
2026-05-20 16:38:48 -05:00
|
|
|
|
|
|
|
|
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__":
|
|
|
|
|
|
|
|
|
|
# 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)
|