versión 1.2.0: despliegue como servicio de Windows con NSSM
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
fcce30eda6
commit
ca1550bced
9 files changed
+416
-15
No files matched your search
@@ -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.
|
||||
@@ -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 <nombre> # 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 |
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
@@ -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 <nombre>.
|
||||
|
||||
.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"
|
||||
@@ -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 (<https://nssm.cc>) |
|
||||
| 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 |
|
||||
+15
-13
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user