From ca1550bced52321b01cfed154722901888fa777b Mon Sep 17 00:00:00 2001 From: George Date: Thu, 22 Oct 2026 12:08:38 -0500 Subject: [PATCH] =?UTF-8?q?versi=C3=B3n=201.2.0:=20despliegue=20como=20ser?= =?UTF-8?q?vicio=20de=20Windows=20con=20NSSM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy/: scripts de PowerShell para instalar el servicio (cuenta de servicio con acceso a la ruta de red, reinicio automático, parada con Ctrl+C, rotación de la salida, PYTHONUTF8), actualizar a una versión etiquetada o volver a una anterior, y desinstalar. - docs/despliegue_windows.md: requisitos, instalación, operación, rollback y problemas frecuentes. - tools/verificar_conexiones.py: comprueba .env, Graph, SAP, HANA y la ruta de red sin modificar nada. - CHANGELOG.md, README e inventario; pyproject.toml pasa a 1.2.0. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 42 ++++++++++++ README.md | 11 +++- deploy/actualizar.ps1 | 40 ++++++++++++ deploy/desinstalar_servicio.ps1 | 15 +++++ deploy/instalar_servicio.ps1 | 99 +++++++++++++++++++++++++++++ docs/despliegue_windows.md | 109 ++++++++++++++++++++++++++++++++ docs/inventario.md | 28 ++++---- pyproject.toml | 2 +- tools/verificar_conexiones.py | 85 +++++++++++++++++++++++++ 9 files changed, 416 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 deploy/actualizar.ps1 create mode 100644 deploy/desinstalar_servicio.ps1 create mode 100644 deploy/instalar_servicio.ps1 create mode 100644 docs/despliegue_windows.md create mode 100644 tools/verificar_conexiones.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..17d1167 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ +# Historial de cambios + +Las versiones siguen [SemVer](https://semver.org/lang/es/) y se publican como etiquetas de git (`v1.2.0`). La versión vigente está en `pyproject.toml` y se muestra en el log al arrancar. + +## [1.2.0] — 2026-10-22 + +### Agregado +- **Respuesta al proveedor** con las plantillas HTML de aceptado y rechazado (`plantilla/`), enviada a quien envió el correo. Ver `docs/respuesta_proveedor.md`. + - `NOTIFICACION_MODO`: `prueba` (por defecto; la respuesta va al equipo, no al proveedor) o `real` (al remitente, con copia oculta al equipo). + - Protecciones: no responde a remitentes desconocidos o sin dominio válido, al propio buzón, a `no-reply`, a mensajes automáticos ni rebotes, a respuestas de nuestras notificaciones, y limita a 3 respuestas por remitente cada 24 h en correos sin comprobante. + - Rechazo con observaciones estructuradas (título, detalle, cómo corregir) también para correos sin comprobante válido. +- **Se valida si la factura ya existe en SAP** (factura de compra no anulada o draft) antes de crear el draft; si existe, no se duplica y se responde que ya está registrada. +- **Servicio fuera de línea:** si SAP Service Layer o HANA no responden, se avisa una vez al proveedor ("en cola, no necesita reenviar") y el correo se reintenta en los ciclos siguientes, hasta 24 veces. Plantilla `plantilla-fuera-de-linea.html`. +- **Código de seguimiento** (`ABR-AAMMDD-NNNNN`) por comprobante, con su resultado y el estado de la notificación (`seguimiento.py`, tabla `comprobantes`). +- **Despliegue en Windows como servicio con NSSM** (`deploy/`, `docs/despliegue_windows.md`). +- Validación del `.env` al arrancar (`config_validator.py`), que además anuncia el modo de notificación. +- Versión del proyecto en el log al arrancar y `tools/verificar_conexiones.py` (comprueba el `.env`, Microsoft Graph, SAP, HANA y la ruta de red sin modificar nada). +- Suite de pruebas pytest (más de 280) y herramientas de vista previa y envío de prueba de las plantillas (`tools/previsualizar_plantillas.py`). +- Documentación: `README.md`, guía de MSAL (`docs/configurar_msal.md`) y una página por módulo principal. + +### Cambiado +- El correo técnico (tablas de SAP y JSON adjuntos) va **solo al equipo**; antes iba también al remitente. +- Un error de SAP/HANA o un fallo inesperado ya no se le comunica al proveedor como rechazo y ya no interrumpe el ciclo: solo esa factura. +- "Guía no encontrada" exige que HANA responda: si no, se trata como servicio fuera de línea. +- `DueDate` de las cuotas del draft = fecha de vencimiento (antes, fecha de emisión). +- **Horarios sin huecos:** se agregan 21:30–24:00 (cada 60 min) y 05:00–06:30 (cada 30 min); antes el demonio dormía hasta las 06:30 en esos tramos. +- La base `emails.db` y la carpeta de resultados se resuelven junto al código, no relativas al directorio de trabajo. +- `copiar_a_red` ya no crea carpetas locales cuando `RUTA_RED_FACTURACION` no es una ruta absoluta en el sistema operativo (una ruta UNC fuera de Windows); antes el draft podía referenciar archivos que no estaban en el recurso compartido. +- `pipeline.py` se divide en módulos (`adjuntos`, `documentos`, `persistencia`, `draft_sap`, `notificaciones`). +- Los `print` de los módulos pasan a `logging` con nivel explícito (`LOG_LEVEL` filtra por severidad). +- `pyproject.toml` y `requirements.txt` alineados, con las dependencias directas fijadas. +- `consulta_rapida.py` y los scripts manuales pasan a `tools/`; los correos de ejemplo, a `tests/fixtures/`. + +### Eliminado +- `tools/documento.json` (token de ngrok sin uso). **El token sigue en el historial de git: revóquelo.** + +### Seguridad +- Pendiente de rotar: la contraseña de SAP y el token de ngrok, presentes en el historial de git (`tools/aberio.http`, `tools/documento.json`). + +## [1.1.0] + +Versión anterior a este registro: descarga de adjuntos, extracción de XML y PDF, validación de precios contra SAP B1, creación del draft y notificación por correo. diff --git a/README.md b/README.md index f3fac2f..4c4bfb2 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,10 @@ Demonio que descarga las facturas de transporte (PDF/XML SUNAT) recibidas en un Bandeja de entrada (Microsoft Graph) → descarga adjuntos, descomprime ZIP, se queda con PDF/XML → extrae datos de PDF y XML y asigna las guías de remisión por línea + → comprueba que la factura no exista ya en SAP → valida precios de cada guía contra SAP (Service Layer, con fallback a HANA) → crea el Draft en SAP B1 si todos los precios coinciden + → si SAP no responde, avisa "en cola" y reintenta en los ciclos siguientes → responde a quien envió el correo con la plantilla de aceptado o rechazado (por defecto en modo prueba: la respuesta va al equipo; ver docs/respuesta_proveedor.md) ``` @@ -38,10 +40,15 @@ El `.env` nunca se sube a git. Todas las variables están documentadas en [`.env python main.py # demonio; consulta los últimos 2 días (--dias 2) python main.py --dias 5 # ventana de búsqueda de 5 días python tools/consulta_rapida.py --set # reprocesar correos por ID (sets en tools/ids_prueba.json) +python tools/verificar_conexiones.py # comprueba .env, Graph, SAP, HANA y la ruta de red ``` El demonio trabaja por bloques horarios (cada 1, 5, 30 o 60 minutos según la hora) y duerme fuera de ellos. Los bloques están en `HORARIOS` de `main.py` y se explican en [`docs/main.md`](docs/main.md). +## Despliegue + +En producción corre como **servicio de Windows con NSSM**: arranca con el servidor y se reinicia solo. La carpeta [`deploy/`](deploy/) trae los scripts (instalar, actualizar a una versión etiquetada y volver atrás, desinstalar) y [`docs/despliegue_windows.md`](docs/despliegue_windows.md) la guía completa. Antes de instalar, `python tools/verificar_conexiones.py` comprueba el `.env`, Microsoft Graph, SAP, HANA y la ruta de red. Las versiones son etiquetas de git (`v1.2.0`); el historial está en [`CHANGELOG.md`](CHANGELOG.md). + ## Pruebas ```bash @@ -77,7 +84,8 @@ Los scripts de `tools/` (`procesar_pdfs_prueba.py`, `procesar_xmls_prueba.py`, e | `sunat_schema.py`, `pdf_utilities.py` | Lectura de XML SUNAT y de PDF | | `database_utilities.py`, `seguimiento.py`, `email_utilities.py` | SQLite local; códigos de seguimiento y resultado por comprobante; ZIP y filtrado de adjuntos | | `logger_manager.py`, `config_validator.py`, `pipeline_utils.py` | Logging, validación del `.env`, utilidades | -| `tools/` | Scripts manuales: reprocesar correos, regenerar/inspeccionar los correos de prueba, `aberio.http` | +| `deploy/` | Scripts de PowerShell para instalar el servicio de Windows con NSSM | +| `tools/` | Scripts manuales: verificar conexiones, vista previa de las plantillas, reprocesar correos, regenerar los correos de prueba, `aberio.http` | | `tests/` | Suite pytest, correos de ejemplo (`tests/fixtures/`) y resultados esperados (`tests/golden/`) | ## Documentación @@ -92,6 +100,7 @@ Los scripts de `tools/` (`procesar_pdfs_prueba.py`, `procesar_xmls_prueba.py`, e | [`docs/construir_draft.md`](docs/construir_draft.md) | Construcción del Draft SAP B1 | | [`docs/respuesta_proveedor.md`](docs/respuesta_proveedor.md) | Respuesta de aceptado/rechazado al proveedor: modos, protecciones y observaciones | | [`docs/seguimiento.md`](docs/seguimiento.md) | Códigos de seguimiento y resultado por comprobante | +| [`docs/despliegue_windows.md`](docs/despliegue_windows.md) | Instalación como servicio de Windows con NSSM, operación y rollback | | [`docs/configurar_msal.md`](docs/configurar_msal.md) | Cómo obtener `CLIENT_ID`, `TENANT_ID` y `CLIENT_SECRET` en Azure | | [`docs/outlook_utilities.md`](docs/outlook_utilities.md) | Cliente de Microsoft Graph | | [`docs/hana_manager.md`](docs/hana_manager.md) | Consultas directas a SAP HANA | diff --git a/deploy/actualizar.ps1 b/deploy/actualizar.ps1 new file mode 100644 index 0000000..0c618ef --- /dev/null +++ b/deploy/actualizar.ps1 @@ -0,0 +1,40 @@ +<# +.SYNOPSIS + Actualiza el proyecto a una versión (etiqueta de git) y reinicia el servicio. Sirve también para volver a una + versión anterior (rollback). Ejecutar en PowerShell como administrador. + +.EXAMPLE + .\deploy\actualizar.ps1 -Version v1.2.0 + .\deploy\actualizar.ps1 -Version v1.1.0 # volver a la anterior +#> +param( + [Parameter(Mandatory = $true)][string]$Version, + [string]$Nombre = "descarga_factura_transporte", + [string]$Nssm = "nssm" +) + +$ErrorActionPreference = "Stop" +$Proyecto = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +Set-Location $Proyecto + +if (git status --porcelain --untracked-files=no) { + throw "Hay cambios locales sin guardar en $Proyecto. Revíselos (git status) antes de actualizar." +} + +function Comprobar($paso) { if ($LASTEXITCODE -ne 0) { throw "Falló: $paso (código $LASTEXITCODE)" } } + +git fetch --tags origin +Comprobar "git fetch" +if (-not (git tag --list $Version)) { throw "No existe la etiqueta $Version. Etiquetas disponibles: $(git tag --list | Out-String)" } + +& $Nssm stop $Nombre confirm | Out-Null +try { + git checkout $Version + Comprobar "git checkout $Version" + & (Join-Path $Proyecto "venv\Scripts\python.exe") -m pip install -r requirements.txt + Comprobar "pip install" +} finally { + & $Nssm start $Nombre | Out-Null # arranca siempre: si algo falló, vuelve a levantar lo que había +} +Write-Host "Servicio '$Nombre' en la versión $Version." +Write-Host "Compruebe la versión en el log: Select-String 'versión' logs\descarga_factura_transporte.log | Select-Object -Last 1" diff --git a/deploy/desinstalar_servicio.ps1 b/deploy/desinstalar_servicio.ps1 new file mode 100644 index 0000000..093b0cb --- /dev/null +++ b/deploy/desinstalar_servicio.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Detiene y elimina el servicio de Windows del demonio. No borra el proyecto, el .env ni emails.db. + Ejecutar en PowerShell como administrador. +#> +param( + [string]$Nombre = "descarga_factura_transporte", + [string]$Nssm = "nssm" +) + +$ErrorActionPreference = "Stop" +if (-not (Get-Service -Name $Nombre -ErrorAction SilentlyContinue)) { Write-Host "El servicio '$Nombre' no existe."; return } +& $Nssm stop $Nombre confirm | Out-Null +& $Nssm remove $Nombre confirm +Write-Host "Servicio '$Nombre' eliminado. El proyecto, el .env y emails.db no se tocaron." diff --git a/deploy/instalar_servicio.ps1 b/deploy/instalar_servicio.ps1 new file mode 100644 index 0000000..76b7734 --- /dev/null +++ b/deploy/instalar_servicio.ps1 @@ -0,0 +1,99 @@ +<# +.SYNOPSIS + Instala (o reinstala) el demonio descarga_factura_transporte como servicio de Windows con NSSM. + +.DESCRIPTION + Ejecutar en PowerShell COMO ADMINISTRADOR, desde cualquier carpeta. Ver docs/despliegue_windows.md. + No arranca el servicio: primero revisa el .env y luego ejecuta nssm start . + +.PARAMETER Cuenta + Cuenta con la que corre el servicio, en formato DOMINIO\usuario. Debe tener acceso de lectura/escritura a la + ruta de red de RUTA_RED_FACTURACION: la cuenta LocalSystem (por defecto de los servicios) NO ve recursos de red. + +.PARAMETER Clave + Contraseña de la cuenta (SecureString). Si se omite, se pide por consola. + +.EXAMPLE + .\deploy\instalar_servicio.ps1 -Cuenta "ABERIO\svc_facturas" +#> +param( + [string]$Nombre = "descarga_factura_transporte", + [string]$Cuenta = "", + [securestring]$Clave, + [string]$Nssm = "nssm", + [string]$Python = "" +) + +$ErrorActionPreference = "Stop" + +# --- Rutas (la carpeta del proyecto es la que contiene deploy\) --------------------------------------------- +$Proyecto = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +if (-not $Python) { $Python = Join-Path $Proyecto "venv\Scripts\python.exe" } + +# --- Comprobaciones previas ---------------------------------------------------------------------------------- +if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw "Ejecute este script como administrador." +} +if (-not (Get-Command $Nssm -ErrorAction SilentlyContinue)) { + throw "No se encontró nssm. Descárguelo de https://nssm.cc y agréguelo al PATH, o use -Nssm 'C:\ruta\nssm.exe'." +} +if (-not (Test-Path $Python)) { + throw "No existe $Python. Cree el entorno virtual: py -3 -m venv venv ; venv\Scripts\pip install -r requirements.txt" +} +if (-not (Test-Path (Join-Path $Proyecto ".env"))) { + throw "Falta $Proyecto\.env. Copie .env.example a .env y complételo (ver docs/configurar_msal.md)." +} +if (-not $Cuenta) { + Write-Warning "Sin -Cuenta el servicio corre como LocalSystem y NO podrá escribir en la ruta de red de facturación electrónica." +} + +# --- Instalación ---------------------------------------------------------------------------------------------- +if (Get-Service -Name $Nombre -ErrorAction SilentlyContinue) { + Write-Host "El servicio '$Nombre' ya existe: se detiene y se reconfigura." + & $Nssm stop $Nombre confirm | Out-Null +} else { + & $Nssm install $Nombre $Python "main.py" | Out-Null +} + +$LogsNssm = Join-Path $Proyecto "logs" +New-Item -ItemType Directory -Force -Path $LogsNssm | Out-Null + +& $Nssm set $Nombre Application $Python +& $Nssm set $Nombre AppParameters "main.py" +& $Nssm set $Nombre AppDirectory $Proyecto # el código no depende del cwd, pero el .env y los logs sí cuentan con él +& $Nssm set $Nombre DisplayName "Descarga de facturas de transporte" +& $Nssm set $Nombre Description "Lee el buzón, valida las facturas contra SAP B1, crea el draft y responde al proveedor." +& $Nssm set $Nombre Start SERVICE_AUTO_START +& $Nssm set $Nombre AppEnvironmentExtra "PYTHONUTF8=1" "PYTHONIOENCODING=utf-8" "PYTHONUNBUFFERED=1" # emojis del log en la consola de Windows + +# Salida estándar y de error del proceso (el log de la aplicación sigue en logs\descarga_factura_transporte.log) +& $Nssm set $Nombre AppStdout (Join-Path $LogsNssm "servicio.out.log") +& $Nssm set $Nombre AppStderr (Join-Path $LogsNssm "servicio.err.log") +& $Nssm set $Nombre AppRotateFiles 1 +& $Nssm set $Nombre AppRotateOnline 1 +& $Nssm set $Nombre AppRotateBytes 10485760 # 10 MB + +# Reinicio automático si el proceso muere; espera 30 s para no entrar en un bucle rápido +& $Nssm set $Nombre AppExit Default Restart +& $Nssm set $Nombre AppRestartDelay 30000 +& $Nssm set $Nombre AppThrottle 10000 + +# Parada limpia: Ctrl+C (el demonio lo atiende con KeyboardInterrupt) y 20 s de gracia antes de forzar +& $Nssm set $Nombre AppStopMethodSkip 0 +& $Nssm set $Nombre AppStopMethodConsole 20000 +& $Nssm set $Nombre AppStopMethodWindow 0 +& $Nssm set $Nombre AppStopMethodThreads 0 + +if ($Cuenta) { + if (-not $Clave) { $Clave = Read-Host "Contraseña de $Cuenta" -AsSecureString } + $Texto = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Clave)) + & $Nssm set $Nombre ObjectName $Cuenta $Texto + Remove-Variable Texto +} + +Write-Host "" +Write-Host "Servicio '$Nombre' instalado. Pasos siguientes:" +Write-Host " 1. Revise $Proyecto\.env (NOTIFICACION_MODO, NOTIFICACION_EQUIPO, LOGO_URL)." +Write-Host " 2. nssm start $Nombre" +Write-Host " 3. Get-Content '$LogsNssm\descarga_factura_transporte.log' -Wait -Tail 30" diff --git a/docs/despliegue_windows.md b/docs/despliegue_windows.md new file mode 100644 index 0000000..449d73c --- /dev/null +++ b/docs/despliegue_windows.md @@ -0,0 +1,109 @@ +# Despliegue en Windows (servicio con NSSM) + +El demonio (`main.py`) corre como **servicio de Windows** gestionado con [NSSM](https://nssm.cc): arranca con el servidor, se reinicia solo si se cae y se detiene de forma ordenada. La carpeta `deploy/` trae los scripts. + +> Los scripts de `deploy/` se revisaron con el analizador de PowerShell (sintaxis correcta) pero **no se ejecutaron en un Windows real**. Pruébelos primero en un servidor de pruebas. + +## Requisitos del servidor + +| Requisito | Detalle | +|-----------|---------| +| Windows | Server 2016 o superior, o Windows 10/11 | +| Python | 3.11 o superior, **64 bits** (el cliente de SAP HANA, `hdbcli`, lo requiere) | +| Git y NSSM | `git` en el PATH y `nssm.exe` en el PATH () | +| Red hacia fuera | `login.microsoftonline.com` y `graph.microsoft.com` (443) | +| Red hacia SAP | Service Layer (`SAP_B1_URL`, puerto 50000 por defecto) y HANA (`HANA_SERVER`, puerto 30015) | +| Ruta de red | Acceso de lectura y escritura a `RUTA_RED_FACTURACION` (recurso UNC) | +| Cuenta de servicio | Una cuenta de dominio, por ejemplo `ABERIO\svc_facturas`, con acceso a esa ruta de red | +| Zona horaria | El código asume hora de Lima (UTC-5) para la fecha y hora de recepción que ve el proveedor | + +**La cuenta de servicio es importante.** Por defecto un servicio corre como `LocalSystem`, que **no ve recursos de red**: la copia a la ruta de facturación electrónica fallaría. Use una cuenta con permiso sobre ese recurso. + +## Instalación + +En PowerShell **como administrador**: + +```powershell +cd C:\apps +git clone https://unasgeorge.vaquita-hadar.ts.net/george/descarga_factura_transporte.git +cd descarga_factura_transporte +git checkout v1.2.0 # siempre una versión etiquetada, no main + +py -3 -m venv venv +venv\Scripts\python -m pip install -r requirements.txt + +copy .env.example .env # y completar los valores +``` + +Complete el `.env` (cómo obtener las credenciales de Microsoft: [configurar_msal.md](configurar_msal.md)). **Empiece con `NOTIFICACION_MODO=prueba`**: las respuestas van al equipo y no a los proveedores. Defina `NOTIFICACION_EQUIPO` y `LOGO_URL`. + +### Verificar antes de instalar el servicio + +```powershell +venv\Scripts\python tools\verificar_conexiones.py +``` + +Revisa, sin modificar nada ni imprimir secretos: el `.env`, el token y la lectura del buzón de Microsoft Graph, SAP Service Layer, SAP HANA y la ruta de red de facturación (que debe ser accesible y escribible con la cuenta del servicio). Termina con código 1 si algo falla. Opcionalmente, las pruebas (no tocan SAP ni Outlook): + +```powershell +venv\Scripts\python -m pip install pytest==9.1.1 +venv\Scripts\python -m pytest -q +``` + +### Instalar y arrancar el servicio + +```powershell +.\deploy\instalar_servicio.ps1 -Cuenta "ABERIO\svc_facturas" # pide la contraseña +nssm start descarga_factura_transporte +``` + +El script configura: arranque automático, directorio de trabajo del proyecto, reinicio automático a los 30 s si el proceso muere, parada con Ctrl+C (20 s de gracia), rotación de la salida del servicio a 10 MB y las variables `PYTHONUTF8=1` y `PYTHONIOENCODING=utf-8` (sin ellas los emojis del log dan error en la consola de Windows). + +## Comprobar que funciona + +```powershell +Get-Service descarga_factura_transporte +Get-Content logs\descarga_factura_transporte.log -Wait -Tail 30 +``` + +En el log, al arrancar, debe verse `🚀 Demonio iniciado … (versión 1.2.0)`, y `config_validator` anuncia el modo de notificación (`PRUEBA` o `REAL`). + +## Operación + +| Tema | Detalle | +|------|---------| +| Logs de la aplicación | `logs\descarga_factura_transporte.log`, rotación diaria, 30 días | +| Salida del servicio | `logs\servicio.out.log` y `servicio.err.log` (10 MB, rotan). Mire `servicio.err.log` si el servicio arranca y se detiene | +| Cambiar el `.env` | `nssm restart descarga_factura_transporte` | +| Horarios | `HORARIOS` en `main.py` (24 h cubiertas, ver [main.md](main.md)) | +| Copia de seguridad | `emails.db` guarda los correos procesados y los códigos de seguimiento. Cópielo con el servicio detenido, o con `sqlite3 emails.db ".backup respaldo.db"` | +| Espacio en disco | `adjuntos\` y `resultado\` **no se limpian solos**: planifique una limpieza periódica | +| Pasar a modo real | Cuando el equipo haya validado las respuestas: `NOTIFICACION_MODO=real` y reiniciar | + +## Actualizar y volver atrás (rollback) + +Siempre a una **versión etiquetada** (ver [CHANGELOG](../CHANGELOG.md)): + +```powershell +.\deploy\actualizar.ps1 -Version v1.2.0 +.\deploy\actualizar.ps1 -Version v1.1.0 # volver a la anterior +``` + +El script detiene el servicio, hace `git checkout` de la etiqueta, instala las dependencias y arranca de nuevo (aunque algo falle, vuelve a levantar lo que había). No toca el `.env` ni `emails.db`. Se niega a actuar si hay cambios locales sin guardar. + +## Desinstalar + +```powershell +.\deploy\desinstalar_servicio.ps1 # no borra el proyecto, el .env ni emails.db +``` + +## Problemas frecuentes + +| Síntoma | Causa probable | +|---------|----------------| +| El servicio arranca y se detiene enseguida | Falta el `.env`, o `config_validator` rechazó una variable: ver `logs\servicio.err.log` | +| `Access is denied` al copiar a la ruta de red | El servicio corre como `LocalSystem`. Reinstale con `-Cuenta` o cambie la cuenta en `services.msc` | +| `UnicodeEncodeError` en la salida del servicio | Falta `PYTHONUTF8=1`: reinstale con `instalar_servicio.ps1` | +| `AADSTS7000222` / `AADSTS700016` | Secret vencido / `CLIENT_ID` y `TENANT_ID` de aplicaciones distintas: ver [configurar_msal.md](configurar_msal.md) | +| `ConnectTimeout` hacia SAP o HANA | VPN o firewall: abra el puerto 50000 (Service Layer) y 30015 (HANA) desde el servidor | +| Los correos llegan sin logo | `LOGO_URL` vacío o no accesible públicamente | diff --git a/docs/inventario.md b/docs/inventario.md index 5ad6ebc..e563919 100644 --- a/docs/inventario.md +++ b/docs/inventario.md @@ -4,31 +4,33 @@ | Archivo | Líneas | Rol | |---------|--------|-----| -| `main.py` | 137 | Demonio orquestador (loop de horarios) | -| `pipeline.py` | 344 | Orquestador del pipeline (autenticar, descargar correos, procesar un correo) | -| `pipeline_utils.py` | 27 | `log()` (logging con indentación de árbol y `severity`) y `ruta_carpeta()` | +| `main.py` | 142 | Demonio orquestador (loop de horarios) | +| `pipeline.py` | 437 | Orquestador del pipeline (autenticar, descargar correos, procesar un correo) | +| `pipeline_utils.py` | 38 | `log()` (logging con indentación de árbol y `severity`) y `ruta_carpeta()` | | `adjuntos.py` | 82 | Descarga de adjuntos (Graph) y filtrado PDF/XML | -| `documentos.py` | 372 | Extracción PDF/XML, asignación de guías, copia a red | -| `persistencia.py` | 163 | Guardado de JSON de resultados y de drafts | -| `draft_sap.py` | 442 | Validación de precios contra SAP, valor referencial y construcción del draft | -| `notificaciones.py` | 117 | Cuerpos HTML de los correos de notificación | +| `documentos.py` | 378 | Extracción PDF/XML, asignación de guías, copia a red | +| `persistencia.py` | 165 | Guardado de JSON de resultados y de drafts | +| `draft_sap.py` | 444 | Validación de precios contra SAP, valor referencial y construcción del draft | +| `notificaciones.py` | 121 | Cuerpos HTML de los correos de notificación | | `sunat_schema.py` | 722 | Parseo de XML SUNAT (Invoice, DespatchAdvice) | | `pdf_utilities.py` | 471 | Parseo de PDFs (factura, guías) | -| `sapb1_manager.py` | 401 | Comunicación con SAP B1 Service Layer | +| `sapb1_manager.py` | 469 | Comunicación con SAP B1 Service Layer | | `outlook_utilities.py` | 274 | Comunicación con Microsoft Graph | | `hana_manager.py` | 141 | Comunicación con SAP HANA directa (fallback de `sapb1_manager` y valor referencial) | -| `database_utilities.py` | 156 | SQLite local (cache de correos) | +| `database_utilities.py` | 164 | SQLite local (cache de correos) | | `tools/*_prueba.py`, `tools/completar_guias_pipeline.py`, `tools/explorar_notes_xml.py` | — | Scripts manuales sobre `tests/fixtures/correos_prueba/` (anteriores a la suite pytest) | +| `tools/verificar_conexiones.py` | 85 | Comprueba `.env`, Graph, SAP, HANA y la ruta de red sin modificar nada | +| `deploy/*.ps1` | — | Instalar, actualizar (versión etiquetada / rollback) y desinstalar el servicio de Windows con NSSM | | `tools/consulta_rapida.py` | 120 | Script auxiliar para reprocesar correos por ID (sets en `tools/ids_prueba.json`) | -| `seguimiento.py` | 206 | Códigos de seguimiento y resultado por comprobante (tabla `comprobantes`) | -| `respuesta_proveedor.py` | 285 | Respuesta de aceptado/rechazado al remitente (modo prueba/real, protecciones, contextos) | -| `observaciones.py` | 157 | Catálogo de motivos de rechazo y sus textos | +| `seguimiento.py` | 226 | Códigos de seguimiento y resultado por comprobante (tabla `comprobantes`) | +| `respuesta_proveedor.py` | 300 | Respuesta de aceptado/rechazado al remitente (modo prueba/real, protecciones, contextos) | +| `observaciones.py` | 165 | Catálogo de motivos de rechazo y sus textos | | `plantillas.py` | 123 | Renderizador de las plantillas HTML de correo | | `config_validator.py` | 98 | Valida el `.env` al arrancar (obligatorias, ruta de red, avisos de SSL) | | `email_utilities.py` | 55 | Utilitarios de adjuntos (ZIP, filtrado) | | `logger_manager.py` | 150 | Logging a archivo rotado (30 días) + consola + captura de `print()` | -Documentación por módulo: `respuesta_proveedor.py`/`observaciones.py`/`plantillas.py` → [respuesta_proveedor.md](respuesta_proveedor.md), `seguimiento.py` → [seguimiento.md](seguimiento.md), `outlook_utilities.py` → [outlook_utilities.md](outlook_utilities.md), `hana_manager.py` → [hana_manager.md](hana_manager.md), validación y draft → [validar_precios.md](validar_precios.md) y [construir_draft.md](construir_draft.md). +Documentación por módulo: despliegue → [despliegue_windows.md](despliegue_windows.md), `respuesta_proveedor.py`/`observaciones.py`/`plantillas.py` → [respuesta_proveedor.md](respuesta_proveedor.md), `seguimiento.py` → [seguimiento.md](seguimiento.md), `outlook_utilities.py` → [outlook_utilities.md](outlook_utilities.md), `hana_manager.py` → [hana_manager.md](hana_manager.md), validación y draft → [validar_precios.md](validar_precios.md) y [construir_draft.md](construir_draft.md). --- diff --git a/pyproject.toml b/pyproject.toml index 8c7dedc..5e8a6a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "descarga_factura_transporte" -version = "1.1.0" +version = "1.2.0" description = "Descarga y procesamiento de facturas de transporte desde Office365" requires-python = ">=3.11" dependencies = [ diff --git a/tools/verificar_conexiones.py b/tools/verificar_conexiones.py new file mode 100644 index 0000000..97d8775 --- /dev/null +++ b/tools/verificar_conexiones.py @@ -0,0 +1,85 @@ +""" +Comprueba, sin modificar nada, que el servidor puede trabajar: configuración del .env, credenciales de +Microsoft Graph y lectura del buzón, SAP Service Layer, SAP HANA y la ruta de red de facturación. + +Uso (desde cualquier carpeta): + python tools/verificar_conexiones.py + +Termina con código 1 si alguna comprobación falla. No imprime secretos. +""" +import logging +import os +import re +import sys +from datetime import datetime, timedelta + +RAIZ = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") +sys.path.insert(0, RAIZ) + +from dotenv import load_dotenv + +load_dotenv(dotenv_path=os.path.join(RAIZ, ".env")) +logging.disable(logging.CRITICAL) # solo interesa el resultado de cada comprobación + +import config_validator +import hana_manager +import outlook_utilities +import sapb1_manager + +fallos = 0 + + +def resultado(ok: bool, nombre: str, detalle: str = "") -> None: + global fallos + fallos += 0 if ok else 1 + print(f"{'✅' if ok else '❌'} {nombre}{(': ' + detalle) if detalle else ''}") + + +def resumen_error(ex: Exception) -> str: + return ", ".join(re.findall(r"AADSTS\d+", str(ex))) or f"{type(ex).__name__}: {str(ex)[:110]}" + + +# 1. Configuración +errores, avisos = config_validator.validar_configuracion() +resultado(not errores, "Configuración (.env)", "; ".join(errores)) +for aviso in avisos: + print(f" ⚠️ {aviso}") + +# 2. Microsoft Graph: token y lectura de la bandeja +token = None +try: + token = outlook_utilities.get_access_token() + resultado(bool(token), "Microsoft Graph: token") +except Exception as ex: + resultado(False, "Microsoft Graph: token", resumen_error(ex)) +if token: + try: + hoy = datetime.now() + correos = outlook_utilities.get_emails(os.getenv("EMAIL", ""), token, (hoy - timedelta(days=1)).strftime("%Y-%m-%d"), hoy.strftime("%Y-%m-%d")) + resultado(True, "Microsoft Graph: lectura de la bandeja", f"{len(correos)} correos en el último día") + except Exception as ex: + resultado(False, "Microsoft Graph: lectura de la bandeja", resumen_error(ex)) + +# 3. SAP Service Layer +try: + resultado(True, "SAP Service Layer", f"{len(sapb1_manager.get_zonas())} zonas") +except Exception as ex: + resultado(False, "SAP Service Layer", resumen_error(ex)) + +# 4. SAP HANA +conexion = hana_manager.get_connection() +resultado(conexion is not None, "SAP HANA") +if conexion is not None: + conexion.close() + +# 5. Ruta de red de facturación electrónica (opcional) +ruta = os.getenv("RUTA_RED_FACTURACION", "").strip() +if ruta: + existe = os.path.isdir(ruta) + resultado(existe and os.access(ruta, os.W_OK), "Ruta de red de facturación", "" if existe else "no existe o no es accesible con esta cuenta") +else: + print(" ⚠️ RUTA_RED_FACTURACION no definida: no se copia a la red (no se comprueba)") + +print() +print("Todo listo." if not fallos else f"{fallos} comprobación(es) con fallo.") +sys.exit(1 if fallos else 0)