29 Commits
Author SHA1 Message Date
GeorgeandClaude Sonnet 5.5 1d23e87b9b versión 1.2.2: horarios nocturnos unificados en dos tramos
HORARIOS pasa a 5 bloques que siguen cubriendo las 24 horas: 00:00-06:30 cada
30 minutos (antes 00:00-05:00 cada 60 s y 05:00-06:30 cada 30 min) y
18:30-24:00 cada 60 minutos (antes dos bloques iguales). El día no cambia.
De madrugada un correo puede esperar hasta 30 minutos y los reintentos por
SAP fuera de línea, uno por ciclo, van igual de espaciados.

Se ordena el formato de la lista, test_horarios fija el primer y el último
minuto de cada bloque (y que nunca se devuelva None en el día), y se
actualizan docs/main.md, la guía de despliegue y el CHANGELOG.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:36:48 -05:00
GeorgeandClaude Sonnet 5.5 6030320822 se actualiza la documentación: sapb1_manager, nombres de funciones y cifras
- Nueva página docs/sapb1_manager.md: sesión, funciones, clasificación de
  errores (SapNoDisponible frente a rechazo de SAP), búsqueda de facturas
  existentes y cómo se ubica una guía.
- procesar_pdfs.md y procesar_xmls.md usaban los nombres anteriores a la
  división de pipeline.py (_procesar_pdfs, _ruta_carpeta...) y números de
  línea que envejecen; ahora apuntan a documentos.py.
- inventario.md: árbol de dependencias con los módulos nuevos.
- pipeline.md: _procesar_factura, importes_coinciden, hana_disponible y
  buscar_factura_existente; validar_precios.md, hana_manager.md, main.md y
  procesar_xmls.md con las notas que faltaban; README sin la cifra de pruebas
  vieja.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:56:34 -05:00
GeorgeandClaude Sonnet 5.5 50324cca6e se elimina tools/aberio.http (llevaba escrita una contraseña de SAP)
Era una colección de peticiones de prueba al Service Layer con usuario,
contraseña y base de producción en texto plano.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:15:35 -05:00
GeorgeandClaude Sonnet 5.5 3b5400018c versión 1.2.1: la diferencia de importes con SAP se redondea a 2 decimales
La comparación abs(a - b) < 0.01 no era fiable por la coma flotante: el mismo
céntimo se aceptaba o rechazaba según el importe (431.41 contra 431.40 se
rechazaba y 624.39 contra 624.38 se aceptaba). pipeline_utils.importes_coinciden
redondea la diferencia a 2 decimales, lo que muestra un comprobante, y admite
hasta 1 céntimo. Se usa en la validación de precios y en la comparación del
valor referencial. Con datos reales de SAP (49 líneas) ningún resultado cambió.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:07:28 -05:00
GeorgeandClaude Sonnet 5.5 cdcecc2b56 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>
2026-10-22 12:08:38 -05:00
GeorgeandClaude Sonnet 5.5 f01f21b1c3 se completan los horarios, se fijan las rutas al código y se muestra la versión
- HORARIOS cubre las 24 horas: 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. Se corrigen los comentarios, que no coincidían con los valores.
  tests/test_horarios.py garantiza que no haya huecos ni solapes.
- La carpeta de resultados se resuelve junto al código, no relativa al cwd
  (un servicio de Windows puede arrancar desde cualquier carpeta).
- copiar_a_red no crea carpetas locales cuando RUTA_RED_FACTURACION no es una
  ruta absoluta en el sistema operativo (una UNC fuera de Windows); antes el
  draft podía referenciar archivos que no estaban en el recurso compartido.
- El log muestra la versión del proyecto (pyproject.toml) al arrancar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 12:08:38 -05:00
GeorgeandClaude Sonnet 5.5 f0ad6aa78e se valida si la factura ya existe en SAP y se avisa cuando SAP está fuera de línea
- sapb1_manager.buscar_factura_existente busca la factura (proveedor, tipo,
  serie, número) en facturas de compra no anuladas y en drafts; si existe no
  se crea otro draft y se responde "ya está registrada" sin pisar el registro
  de una aceptación anterior.
- SapNoDisponible (subclase de ConnectionError) distingue una caída (timeout,
  sin conexión, HTTP 5xx) de un rechazo normal de SAP (HTTP 4xx). Una caída
  de Service Layer con HANA sin resultados ya no se lee como "guía no existe".
- Con SAP o HANA caídos se avisa una sola vez al proveedor con la plantilla
  "En cola" y el correo se reintenta en los ciclos siguientes (hasta 24), sin
  repetir avisos; al agotarse se avisa al equipo.
- La lógica por factura pasa a _procesar_factura; lo ya resuelto dentro de un
  correo no se repite al reintentarlo.
- seguimiento: columna intentos (con migración), registrar_intento y el
  estado de notificación en_espera.
- database_utilities.DB_NAME pasa a ser absoluto (junto al código).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 12:08:38 -05:00
GeorgeandClaude Sonnet 5.5 4deb9d4e4d se responde al remitente con las plantillas de aceptado/rechazado
El sistema contesta a quien envió el correo con la plantilla que corresponda.
Por defecto (NOTIFICACION_MODO=prueba) la respuesta va solo al equipo con el
asunto "[PRUEBA → remitente]"; en modo real va al remitente con copia oculta
al equipo. El correo técnico (tablas de SAP, JSON) pasa a ir solo al equipo.

- respuesta_proveedor.py: contextos de las plantillas, envío y protecciones
  (remitente desconocido o sin dominio, propio buzón, no-reply, mensajes
  automáticos, respuestas a notificaciones, límite de 3 por remitente/24 h).
- observaciones.py: motivos de rechazo con título, filas y cómo corregir
  (borrador de textos); también para correos sin comprobante válido.
- Un error de SAP/HANA o un fallo inesperado no se le comunica al proveedor y
  ya no tumba el ciclo; "guía no encontrada" exige que HANA responda.
- outlook_utilities: es_mensaje_automatico y copia oculta en send_email.
- seguimiento: notificacion_destinatario (con migración) y
  contar_respuestas_correo.
- config_validator anuncia el modo activo; documentación y pruebas.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 08:55:54 -05:00
GeorgeandClaude Sonnet 5.5 af9e3961e5 se documenta cómo obtener CLIENT_ID, TENANT_ID y CLIENT_SECRET de Azure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 08:55:54 -05:00
GeorgeandClaude Sonnet 5.5 53f203e832 se agregan las plantillas de aceptado/rechazado y su renderizador
plantilla/ con los dos HTML de correo y ejemplos.json (6 escenarios ficticios);
plantillas.py renderiza el subconjunto de Mustache que usan (variables con
escape de HTML, secciones e invertidas), en modo estricto: un dato que falta
da error en vez de un correo con huecos. tools/previsualizar_plantillas.py
genera las vistas previas y envía correos de prueba (--enviar). En la
plantilla de rechazo el RUC, la fila Comprobante y la tabla de guías son
opcionales, para el rechazo de un correo sin comprobante.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 08:55:54 -05:00
GeorgeandClaude Sonnet 5.5 96dca78162 se integra en draft_sap.py el cambio ee64e5f: DueDate de cuotas = fecha_vencimiento
El commit ee64e5f del remoto cambió el DueDate de las cuotas en pipeline.py,
que ya se había dividido en módulos; al hacer el rebase se tomó la versión
dividida y aquí se reaplica el mismo cambio en draft_sap.build_sap_draft
(también se quita la línea comentada de ItemDescription). Se actualizan las
pruebas y la nota de construir_draft.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:04:11 -05:00
GeorgeandClaude Sonnet 5.5 297cfde0e1 fase 1: registro de comprobantes con código de seguimiento
Nueva tabla comprobantes (creada en init_db) y módulo seguimiento.py: cada
comprobante recibe un código ABR-AAMMDD-NNNNN que conserva si reaparece en
otro correo, más su resultado (aceptado/rechazado/error) y el estado de la
notificación. El pipeline lo registra por factura sin bloquear el flujo si el
seguimiento falla. Las pruebas usan una BD temporal para no tocar emails.db.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
GeorgeandClaude Sonnet 5.5 9d295d6485 se reorganiza tests/ y se agrega suite pytest
Los scripts manuales pasan a tools/ y los correos de ejemplo a
tests/fixtures/. Suite de 95 pruebas: caracterización de la extracción sobre
los 46 correos (resultados esperados en tests/golden/, fuera de la carpeta
que descargar_correos_prueba borra), unitarias de draft_sap, notificaciones,
documentos, adjuntos y config_validator, y flujo completo de
_procesar_un_correo con SAP/HANA/Outlook simulados. pytest como dependencia
dev y configuración en pyproject.toml.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
GeorgeandClaude Sonnet 5.5 d4c1e0a264 se agrega README y se completa y corrige la documentación
README en la raíz con instalación, uso, estructura e índice de docs. Nuevos
docs/hana_manager.md y docs/outlook_utilities.md. Se renombran los .mkd a .md
y se corrigen construir_draft y validar_precios, que describían lógica ya
inexistente (CUADRO_VALOR_REFERENCIAL, use_sap_prices). Se actualizan los
docstrings obsoletos y las cifras de líneas del inventario.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
GeorgeandClaude Sonnet 5.5 767558cbfe se valida la configuración (.env) al arrancar
config_validator.verificar_configuracion termina con código 1 si faltan
variables obligatorias (pipeline, Graph, SAP B1, HANA), si hay ruta de red
sin RUC_EMPRESA, y advierte por SSL desactivado, ruta de red ausente o
LOG_LEVEL inválido. Se invoca desde main.py y tools/consulta_rapida.py.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
GeorgeandClaude Sonnet 5.5 3fa26e3ef6 se migran los print de los módulos a logging con nivel explícito
pipeline_utils.log pasa a usar logging (parámetro severity) y los print de
pipeline, sapb1_manager, hana_manager, outlook_utilities, email_utilities y
main pasan a logger.info/warning/error, de modo que LOG_LEVEL filtre por
severidad. El texto del log no cambia. Se mantienen los print de CLI en
sunat_schema (__main__), tools/ y tests/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
GeorgeandClaude Sonnet 5.5 5ef9c90fc0 se mueve consulta_rapida.py a tools/ y se corrige ruta de ids_prueba.json
El script apuntaba a test/ids_prueba.json, carpeta que ya no existe tras el
renombre a tools/. Ahora resuelve rutas desde su ubicación y agrega la raíz
del proyecto al sys.path. Se quita de py-modules y se actualiza inventario.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
GeorgeandClaude Sonnet 5.5 71b01a2130 se alinean pyproject.toml y requirements.txt y se eliminan imports sin usar
Dependencias directas con versiones fijas (incluye hdbcli, se quita el alias
dotenv y las transitivas), build-backend corregido a setuptools.build_meta y
py-modules declarados para el layout plano.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
GeorgeandClaude Sonnet 5.5 7d2f3212c0 se divide pipeline.py en módulos (adjuntos, documentos, persistencia, draft_sap, notificaciones)
Las funciones anidadas de _procesar_un_correo pasan a funciones de módulo
con parámetros explícitos; pipeline.py queda solo como orquestador.
copiar_a_red lee las variables de entorno al ejecutarse. Se actualizan docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00
georgeandClaude Sonnet 5 ee64e5f773 se usa fecha_vencimiento en DueDate de las cuotas del draft
Las cuotas (DocumentInstallments) ahora usan la misma fecha_vencimiento
calculada para DocDueDate, en vez de fecha_emision del XML.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 09:32:11 -05:00
georgeandClaude Sonnet 5 c532170ac2 se agrega fallback OWTR y calculo de fecha de vencimiento por condicion de pago
- hana_manager.get_delivery ahora busca en ODLN y, si no encuentra, en OWTR
- sapb1_manager resuelve StockTransfers -> Orders (via U_IDC_NUMOV) para
  validar precios de flete cuando la guia es un traslado de inventario
- se corrige el orden de parametros tipo/serie/numero (sin cruces) en
  get_delivery, get_delivery_notes y _get_delivery_entries
- se agrega sapb1_manager.obtener_condicion_pago (BusinessPartners +
  PaymentTermsTypes) y pipeline._calcular_fecha_vencimiento para que
  DocDueDate del draft sea fecha de contabilizacion + dias de la condicion
  de pago del proveedor
- se agrega variante de regex para "VALOR REFER" truncado en sunat_schema.py
- se actualiza documentacion (docs/) reflejando estos cambios

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 13:21:04 -05:00
GeorgeandClaude Sonnet 5 d023f14ad9 se renombra carpeta test a tools
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 07:41:39 -05:00
GeorgeandClaude Sonnet 5 911bd2da34 se agrega integración con SAP HANA, manejo de errores en el demonio y actualización de documentación
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 07:40:26 -05:00
George 3c2bb2b714 se agrega manejo de ejecución por horarios 2026-07-13 14:20:57 -05:00
George 0f17284020 se agregó comentarios en el proceso de pipeline.py 2026-07-13 12:24:59 -05:00
George 603243b7e0 ahora se procesa de inicio a fin correo por correo 2026-07-09 09:48:30 -05:00
George 8cdd075d15 se agrego el modulo sunat_schema.py que lee el archivo xml y extrae la información 2026-06-03 18:32:18 -05:00
George a29a44e361 se termina la descarga de los adjuntos y solo se deja los mails con adjuntos pdf y xml 2026-06-01 18:05:44 -05:00
george c08554e775 primer commit 2026-06-01 16:31:24 -05:00
15 changed files with 142 additions and 75 deletions

No files matched your search

+7 -2
View File
@@ -1,6 +1,11 @@
# Historial de cambios # Historial de cambios
Las versiones siguen [SemVer](https://semver.org/lang/es/) y se publican como etiquetas de git (`v1.2.1`). La versión vigente está en `pyproject.toml` y se muestra en el log al arrancar. Las versiones siguen [SemVer](https://semver.org/lang/es/) y se publican como etiquetas de git (`v1.2.2`). La versión vigente está en `pyproject.toml` y se muestra en el log al arrancar.
## [1.2.2] — 2026-10-22
### Cambiado
- **Horarios del demonio:** la noche pasa a dos tramos, `00:00–06:30` cada 30 minutos y `18:30–24:00` cada 60 (antes: `00:00–05:00` cada 60 segundos, `05:00–06:30` cada 30 minutos, `18:30–21:30` y `21:30–24:00` cada 60). El día sigue igual. De madrugada un correo puede esperar hasta 30 minutos y los reintentos por SAP fuera de línea (uno por ciclo) van igual de espaciados. Los 5 bloques siguen cubriendo las 24 horas sin huecos.
## [1.2.1] — 2026-10-22 ## [1.2.1] — 2026-10-22
@@ -41,7 +46,7 @@ Las versiones siguen [SemVer](https://semver.org/lang/es/) y se publican como et
- `tools/documento.json` (token de ngrok sin uso). **El token sigue en el historial de git: revóquelo.** - `tools/documento.json` (token de ngrok sin uso). **El token sigue en el historial de git: revóquelo.**
### Seguridad ### 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`). - Se elimina `tools/aberio.http`, que llevaba escrita una contraseña de SAP, y se reescribe el historial de git para enmascarar esa contraseña y el token de ngrok. **Aun así deben rotarse**: el historial anterior pudo haberse copiado en otros equipos o servidores.
## [1.1.0] ## [1.1.0]
+4 -3
View File
@@ -47,13 +47,13 @@ El demonio trabaja por bloques horarios (cada 1, 5, 30 o 60 minutos según la ho
## Despliegue ## 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.1`); el historial está en [`CHANGELOG.md`](CHANGELOG.md). 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.2`); el historial está en [`CHANGELOG.md`](CHANGELOG.md).
## Pruebas ## Pruebas
```bash ```bash
pip install -e ".[dev]" # instala pytest pip install -e ".[dev]" # instala pytest
pytest # ~95 pruebas, unos 3 segundos; no tocan SAP, HANA ni Outlook pytest # más de 300 pruebas, unos 4 segundos; no tocan SAP, HANA ni Outlook
``` ```
- **Extracción** (`tests/test_extraccion_correos.py`): corre la extracción real sobre cada correo de `tests/fixtures/correos_prueba/` y la compara con `tests/golden/<caso>.json`. Esos resultados esperados fijan el comportamiento actual; no prueban que sea correcto. Si cambias la extracción a propósito, revisa el diff y regenéralos con `UPDATE_GOLDEN=1 pytest tests/test_extraccion_correos.py`. - **Extracción** (`tests/test_extraccion_correos.py`): corre la extracción real sobre cada correo de `tests/fixtures/correos_prueba/` y la compara con `tests/golden/<caso>.json`. Esos resultados esperados fijan el comportamiento actual; no prueban que sea correcto. Si cambias la extracción a propósito, revisa el diff y regenéralos con `UPDATE_GOLDEN=1 pytest tests/test_extraccion_correos.py`.
@@ -85,7 +85,7 @@ Los scripts de `tools/` (`procesar_pdfs_prueba.py`, `procesar_xmls_prueba.py`, e
| `database_utilities.py`, `seguimiento.py`, `email_utilities.py` | SQLite local; códigos de seguimiento y resultado por comprobante; ZIP y filtrado de adjuntos | | `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 | | `logger_manager.py`, `config_validator.py`, `pipeline_utils.py` | Logging, validación del `.env`, utilidades |
| `deploy/` | Scripts de PowerShell para instalar el servicio de Windows con NSSM | | `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` | | `tools/` | Scripts manuales: verificar conexiones, vista previa de las plantillas, reprocesar correos, regenerar los correos de prueba |
| `tests/` | Suite pytest, correos de ejemplo (`tests/fixtures/`) y resultados esperados (`tests/golden/`) | | `tests/` | Suite pytest, correos de ejemplo (`tests/fixtures/`) y resultados esperados (`tests/golden/`) |
## Documentación ## Documentación
@@ -103,4 +103,5 @@ Los scripts de `tools/` (`procesar_pdfs_prueba.py`, `procesar_xmls_prueba.py`, e
| [`docs/despliegue_windows.md`](docs/despliegue_windows.md) | Instalación como servicio de Windows con NSSM, operación y rollback | | [`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/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/outlook_utilities.md`](docs/outlook_utilities.md) | Cliente de Microsoft Graph |
| [`docs/sapb1_manager.md`](docs/sapb1_manager.md) | SAP Service Layer: drafts, guías, facturas existentes y manejo de caídas |
| [`docs/hana_manager.md`](docs/hana_manager.md) | Consultas directas a SAP HANA | | [`docs/hana_manager.md`](docs/hana_manager.md) | Consultas directas a SAP HANA |
+4 -4
View File
@@ -27,7 +27,7 @@ En PowerShell **como administrador**:
cd C:\apps cd C:\apps
git clone https://unasgeorge.vaquita-hadar.ts.net/george/descarga_factura_transporte.git git clone https://unasgeorge.vaquita-hadar.ts.net/george/descarga_factura_transporte.git
cd descarga_factura_transporte cd descarga_factura_transporte
git checkout v1.2.1 # siempre una versión etiquetada, no main git checkout v1.2.2 # siempre una versión etiquetada, no main
py -3 -m venv venv py -3 -m venv venv
venv\Scripts\python -m pip install -r requirements.txt venv\Scripts\python -m pip install -r requirements.txt
@@ -66,7 +66,7 @@ Get-Service descarga_factura_transporte
Get-Content logs\descarga_factura_transporte.log -Wait -Tail 30 Get-Content logs\descarga_factura_transporte.log -Wait -Tail 30
``` ```
En el log, al arrancar, debe verse `🚀 Demonio iniciado … (versión 1.2.1)`, y `config_validator` anuncia el modo de notificación (`PRUEBA` o `REAL`). En el log, al arrancar, debe verse `🚀 Demonio iniciado … (versión 1.2.2)`, y `config_validator` anuncia el modo de notificación (`PRUEBA` o `REAL`).
## Operación ## Operación
@@ -85,8 +85,8 @@ En el log, al arrancar, debe verse `🚀 Demonio iniciado … (versión 1.2.1)`,
Siempre a una **versión etiquetada** (ver [CHANGELOG](../CHANGELOG.md)): Siempre a una **versión etiquetada** (ver [CHANGELOG](../CHANGELOG.md)):
```powershell ```powershell
.\deploy\actualizar.ps1 -Version v1.2.1 .\deploy\actualizar.ps1 -Version v1.2.2
.\deploy\actualizar.ps1 -Version v1.2.0 # volver a la anterior .\deploy\actualizar.ps1 -Version v1.2.1 # 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. 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.
+2
View File
@@ -51,6 +51,8 @@ Ninguna función lanza excepciones: todas registran un `ERROR` en el log y devue
## Notas ## Notas
- `draft_sap.hana_disponible()` usa `get_connection()` como comprobación de salud: abre y cierra una conexión. Se invoca antes de rechazar por "guía no encontrada", porque `get_delivery` devuelve lista vacía tanto si la guía no existe como si HANA está caído.
- Cada llamada abre y cierra su propia conexión (no hay pool). - Cada llamada abre y cierra su propia conexión (no hay pool).
- El número se busca con `LIKE '%numero%'`: es una coincidencia parcial, así que un número corto podría coincidir con más de un documento de la misma serie. - El número se busca con `LIKE '%numero%'`: es una coincidencia parcial, así que un número corto podría coincidir con más de un documento de la misma serie.
- El esquema se interpola en el SQL desde `HANA_SCHEMA` (`.env`); los valores de búsqueda sí van como parámetros (`?`). - El esquema se interpola en el SQL desde `HANA_SCHEMA` (`.env`); los valores de búsqueda sí van como parámetros (`?`).
+12 -6
View File
@@ -30,7 +30,7 @@
| `email_utilities.py` | 55 | Utilitarios de adjuntos (ZIP, filtrado) | | `email_utilities.py` | 55 | Utilitarios de adjuntos (ZIP, filtrado) |
| `logger_manager.py` | 150 | Logging a archivo rotado (30 días) + consola + captura de `print()` | | `logger_manager.py` | 150 | Logging a archivo rotado (30 días) + consola + captura de `print()` |
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). Documentación por módulo: `sapb1_manager.py` → [sapb1_manager.md](sapb1_manager.md), 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).
--- ---
@@ -54,11 +54,17 @@ Módulos que `pipeline.py` orquesta (todos usan `pipeline_utils.py`):
``` ```
pipeline.py pipeline.py
├── adjuntos.py → outlook_utilities ├── adjuntos.py → outlook_utilities
├── documentos.py → sunat_schema, pdf_utilities ├── documentos.py → sunat_schema, pdf_utilities
├── persistencia.py → sunat_schema, pdf_utilities ├── persistencia.py → sunat_schema, pdf_utilities
├── draft_sap.py → sapb1_manager, hana_manager ├── draft_sap.py → sapb1_manager, hana_manager
└── notificaciones.py ├── notificaciones.py (correo técnico para el equipo)
├── observaciones.py (motivos de rechazo y sus textos)
├── respuesta_proveedor.py → plantillas.py, observaciones.py, outlook_utilities, seguimiento.py
└── seguimiento.py → database_utilities
main.py → config_validator.py (valida el .env al arrancar)
```
tools/consulta_rapida.py tools/consulta_rapida.py
├── logger_manager.py ├── logger_manager.py
+6 -8
View File
@@ -4,7 +4,7 @@
``` ```
main.py main.py
├── Constantes: HORARIOS (7 bloques que cubren las 24 horas) ├── Constantes: HORARIOS (5 bloques que cubren las 24 horas)
├── obtener_intervalo(dt) → (intervalo, modo) | None ├── obtener_intervalo(dt) → (intervalo, modo) | None
├── segundos_hasta_proximo_inicio(dt) → int ├── segundos_hasta_proximo_inicio(dt) → int
├── non_negative_int(value) → int (validador argparse) ├── non_negative_int(value) → int (validador argparse)
@@ -81,17 +81,15 @@ main()
| Desde | Hasta | Intervalo | Modo | | Desde | Hasta | Intervalo | Modo |
|---------|---------|-----------|----------| |---------|---------|-----------|----------|
| 00:00 | 05:00 | 60s | rápido | | 00:00 | 06:30 | 1800s | full |
| 05:00 | 06:30 | 1800s | full |
| 06:30 | 13:15 | 300s | rápido | | 06:30 | 13:15 | 300s | rápido |
| 13:15 | 14:00 | 1800s | full | | 13:15 | 14:00 | 1800s | full |
| 14:00 | 18:30 | 300s | rápido | | 14:00 | 18:30 | 300s | rápido |
| 18:30 | 21:30 | 3600s | full | | 18:30 | 24:00 | 3600s | full |
| 21:30 | 24:00 | 3600s | full |
Los bloques cubren las 24 horas sin huecos ni solapes (lo comprueba `tests/test_horarios.py`). El modo (`rápido`/`full`) solo se registra en el log: `run_pipeline` se ejecuta igual en ambos. Antes de la versión 1.2.0 los tramos 21:30–00:00 y 05:00–06:30 no tenían bloque y el demonio dormía hasta las 06:30. Los bloques cubren las 24 horas sin huecos ni solapes (lo comprueba `tests/test_horarios.py`). El modo (`rápido`/`full`) solo se registra en el log: `run_pipeline` se ejecuta igual en ambos. Antes de la versión 1.2.0 los tramos 21:30–00:00 y 05:00–06:30 no tenían bloque y el demonio dormía hasta las 06:30; desde la 1.2.2 la noche son solo dos tramos (00:00–06:30 cada 30 minutos y 18:30–24:00 cada 60). De madrugada un correo puede esperar hasta 30 minutos, y los reintentos por SAP fuera de línea (uno por ciclo) van igual de espaciados.
## Entry point (línea 126-134) ## Entry point
``` ```
if __name__ == "__main__": if __name__ == "__main__":
@@ -100,7 +98,7 @@ if __name__ == "__main__":
rotación diaria, 30 días de historial) + consola, y redirige print()/stderr rotación diaria, 30 días de historial) + consola, y redirige print()/stderr
al mismo log (los módulos del pipeline ya usan logger con nivel explícito; al mismo log (los módulos del pipeline ya usan logger con nivel explícito;
los print() restantes se registran como INFO). los print() restantes se registran como INFO).
2. logger.info(...) banner de inicio, incluyendo logger_manager.LOG_FILE 2. logger.info(...) banner de inicio: fecha, versión del proyecto (pyproject.toml) y logger_manager.LOG_FILE
3. main() 3. main()
└── Tras load_dotenv(), config_validator.verificar_configuracion(): └── Tras load_dotenv(), config_validator.verificar_configuracion():
termina con código 1 si falta alguna variable obligatoria del .env termina con código 1 si falta alguna variable obligatoria del .env
+6
View File
@@ -121,6 +121,12 @@ flowchart TD
| `build_validation_html(resultados)` | `notificaciones` | Genera tabla HTML de validación de precios | | `build_validation_html(resultados)` | `notificaciones` | Genera tabla HTML de validación de precios |
| `build_valor_referencial_html(factura, guias_valor_ref)` | `notificaciones` | Genera tabla HTML comparando valor referencial XML vs SAP | | `build_valor_referencial_html(factura, guias_valor_ref)` | `notificaciones` | Genera tabla HTML comparando valor referencial XML vs SAP |
| `build_notification_body(status, factura, validation_table, valor_referencial_table, **kwargs)` | `notificaciones` | Arma cuerpo del email de notificación | | `build_notification_body(status, factura, validation_table, valor_referencial_table, **kwargs)` | `notificaciones` | Arma cuerpo del email de notificación |
| `_procesar_factura(email, token, email_addr, factura, output_folder)` | `pipeline` | Valida una factura contra SAP, crea el draft y notifica; devuelve si hay que reintentar el correo |
| `importes_coinciden(a, b)` | `pipeline_utils` | Compara importes a nivel de céntimos: diferencia redondeada a 2 decimales, hasta 1 céntimo |
| `version_del_proyecto()` | `pipeline_utils` | Versión de `pyproject.toml`, para el log de arranque |
| `hay_error_de_consulta(resultados_val)` | `draft_sap` | Detalle del primer error de consulta a SAP que dejó `validate_prices`, o `None` |
| `hana_disponible()` | `draft_sap` | Abre y cierra una conexión a HANA: descarta que una guía "no encontrada" sea una caída |
| `buscar_factura_existente(ruc, tipo, serie, numero)` | `sapb1_manager` | ¿La factura ya está en SAP (factura de compra no anulada o draft)? Ver [sapb1_manager.md](sapb1_manager.md) |
--- ---
+6 -6
View File
@@ -1,9 +1,9 @@
# _procesar_pdfs — Extracción de datos desde PDFs # procesar_pdfs — Extracción de datos desde PDFs
## Ubicación ## Ubicación
- **Archivo:** `pipeline.py` (línea 255-308, función anidada en `_procesar_un_correo`) - **Archivo:** `documentos.py`, función `procesar_pdfs(email, download_folder)`; la llama `pipeline._procesar_un_correo`
- **Librería:** `pdf_utilities.py` (línea 1-464) - **Librería:** `pdf_utilities.py`
--- ---
@@ -11,7 +11,7 @@
```mermaid ```mermaid
flowchart TD flowchart TD
A[_procesar_pdfs] --> B[Iterar adjuntos del email] A[procesar_pdfs] --> B[Iterar adjuntos del email]
B --> C{Es .pdf?} B --> C{Es .pdf?}
C -- No --> B C -- No --> B
C -- Sí --> D[extraer_datos_pdf]:::highlight C -- Sí --> D[extraer_datos_pdf]:::highlight
@@ -37,9 +37,9 @@ flowchart TD
## Paso a paso ## Paso a paso
``` ```
_procesar_pdfs() → dict (pdf_cache) procesar_pdfs(email, download_folder) → dict (pdf_cache)
│ │
├── carpeta = _ruta_carpeta(download_folder) ├── carpeta = ruta_carpeta(download_folder)
│ │
├── Por cada adjunto en email["adjuntos"]: ├── Por cada adjunto en email["adjuntos"]:
│ │ │ │
+12 -12
View File
@@ -1,9 +1,9 @@
# _procesar_xmls — Extracción y cruce de datos desde XML # procesar_xmls — Extracción y cruce de datos desde XML
## Ubicación ## Ubicación
- **Archivo:** `pipeline.py` (línea 310-463, función anidada en `_procesar_un_correo`) - **Archivo:** `documentos.py`, función `procesar_xmls(email, download_folder, pdf_cache)`; la llama `pipeline._procesar_un_correo`
- **Librería:** `sunat_schema.py` (línea 1-720) - **Librería:** `sunat_schema.py`
--- ---
@@ -11,7 +11,7 @@
```mermaid ```mermaid
flowchart TD flowchart TD
A[_procesar_xmls] --> B[Iterar adjuntos del email] A[procesar_xmls] --> B[Iterar adjuntos del email]
B --> C{Es .xml?} B --> C{Es .xml?}
C -- No --> B C -- No --> B
C -- Sí --> D[extraer_datos_xml]:::highlight C -- Sí --> D[extraer_datos_xml]:::highlight
@@ -23,7 +23,7 @@ flowchart TD
CDR2 --> B CDR2 --> B
CDR -- No --> G[Renombrar XML a nombre canónico] CDR -- No --> G[Renombrar XML a nombre canónico]
G --> H[Construir nombre PDF canónico y buscarlo] G --> H[Construir nombre PDF canónico y buscarlo]
H --> RED[_copiar_a_red]:::highlight H --> RED[copiar_a_red]:::highlight
RED --> I[Cruzar documentos_referencia desde PDF si faltan]:::highlight RED --> I[Cruzar documentos_referencia desde PDF si faltan]:::highlight
I --> J[Asignar guías de remisión por línea]:::highlight I --> J[Asignar guías de remisión por línea]:::highlight
J --> K[Agregar a lista de documentos] J --> K[Agregar a lista de documentos]
@@ -40,9 +40,9 @@ flowchart TD
## Paso a paso ## Paso a paso
``` ```
_procesar_xmls(pdf_cache) → list[dict] procesar_xmls(email, download_folder, pdf_cache) → list[dict]
│ │
├── carpeta = _ruta_carpeta(download_folder) ├── carpeta = ruta_carpeta(download_folder)
│ │
├── Por cada adjunto en email["adjuntos"]: ├── Por cada adjunto en email["adjuntos"]:
│ │ │ │
@@ -67,7 +67,7 @@ _procesar_xmls(pdf_cache) → list[dict]
│ │ └── 2. Fallback: mismo nombre que el XML renombrado, con extensión .pdf │ │ └── 2. Fallback: mismo nombre que el XML renombrado, con extensión .pdf
│ │ └── data["ruta_pdf"] = ruta encontrada o None │ │ └── data["ruta_pdf"] = ruta encontrada o None
│ │ │ │
│ ├── _copiar_a_red(data, _log) │ ├── copiar_a_red(data)
│ │ └── Copia PDF/XML a la ruta de red (RUTA_RED_FACTURACION), si está configurada. │ │ └── Copia PDF/XML a la ruta de red (RUTA_RED_FACTURACION), si está configurada.
│ │ Guarda data["ruta_red_pdf"] / data["ruta_red_xml"] (ver docs/pipeline.md). │ │ Guarda data["ruta_red_pdf"] / data["ruta_red_xml"] (ver docs/pipeline.md).
│ │ │ │
@@ -215,7 +215,7 @@ pdf_cache[nombre_canonico].items[] → extraer_referencias()
└── guia_transportista → ref tipo 31 └── guia_transportista → ref tipo 31
``` ```
### 2. Asignar guías por línea (dentro de `_procesar_xmls`) ### 2. Asignar guías por línea (dentro de `procesar_xmls`)
Prioridad para asignar `guias_remision` y `guia_transportista` a cada línea de la factura, en el orden en que realmente se aplican en el código: Prioridad para asignar `guias_remision` y `guia_transportista` a cada línea de la factura, en el orden en que realmente se aplican en el código:
@@ -228,9 +228,9 @@ Prioridad para asignar `guias_remision` y `guia_transportista` a cada línea de
Después de procesar todos los XML del correo, si una factura referencia una **guía transportista (31)** y esa guía (presente como documento independiente en el mismo correo) referencia a su vez una o más **GRE (09)**, esas GRE se propagan a las líneas de la factura que aún no tengan `guias_remision` asignadas. Después de procesar todos los XML del correo, si una factura referencia una **guía transportista (31)** y esa guía (presente como documento independiente en el mismo correo) referencia a su vez una o más **GRE (09)**, esas GRE se propagan a las líneas de la factura que aún no tengan `guias_remision` asignadas.
### 4. `_completar_guias` (paso posterior, fuera de `_procesar_xmls`) ### 4. `completar_guias` (paso posterior, fuera de `procesar_xmls`)
Si tras los pasos anteriores alguna línea sigue sin `guias_remision`, `_completar_guias` (ver `docs/pipeline.md`) las copia desde los `items[]` del PDF cache, parseando el texto con regex (`{T/V/E}{serie}-{numero}`). Si tras los pasos anteriores alguna línea sigue sin `guias_remision`, `completar_guias` (ver `docs/pipeline.md`) las copia desde los `items[]` del PDF cache, parseando el texto con regex (`{T/V/E}{serie}-{numero}`).
--- ---
@@ -276,4 +276,4 @@ Si tras los pasos anteriores alguna línea sigue sin `guias_remision`, `_complet
] ]
``` ```
`ruta_red_pdf`/`ruta_red_xml` solo aparecen si `RUTA_RED_FACTURACION` está configurada en `.env` y la copia fue exitosa; se usan luego en `_build_sap_draft` para llenar `U_VS_ANEXO_PDF`/`U_VS_ANEXO_XML` (ver `docs/pipeline.md`). `ruta_red_pdf`/`ruta_red_xml` solo aparecen si `RUTA_RED_FACTURACION` está configurada en `.env`, es una ruta absoluta en el sistema operativo (una ruta UNC fuera de Windows no lo es: no se copia) y la copia fue exitosa; se usan luego en `draft_sap.build_sap_draft` para llenar `U_VS_ANEXO_PDF`/`U_VS_ANEXO_XML` (ver `docs/pipeline.md`).
+63
View File
@@ -0,0 +1,63 @@
# sapb1_manager.py — SAP Business One Service Layer
Cliente del Service Layer de SAP B1: crea los drafts de factura de compra, ubica las guías de remisión para validar precios, busca si una factura ya existe y obtiene datos maestros. Si el Service Layer no encuentra una guía, recurre a HANA (`hana_manager.py`).
## Configuración (`.env`)
| Variable | Para qué | Por defecto |
|----------|----------|-------------|
| `SAP_B1_URL` | URL del Service Layer (por ejemplo `https://servidor:50000/b1s/v1`) | — |
| `SAP_B1_COMPANY`, `SAP_B1_USER`, `SAP_B1_PASSWORD` | Base de datos y credenciales | — |
| `SAP_B1_TIMEOUT` | Segundos de espera por petición | `15` |
| `SAP_B1_VERIFY_SSL` | Verificar el certificado (`true`) | `false` |
## Sesión
La sesión se abre sola en la primera petición (`login`) y se guarda en un diccionario del módulo. Ante un HTTP 401 se vuelve a iniciar sesión **una vez** y se repite la petición. No hay cierre automático: `logout()` existe pero el demonio no lo llama. Al ser un estado global del módulo, no está pensado para varios hilos.
## Funciones
| Función | Qué hace |
|---------|----------|
| `forward_to_sapb1(method, endpoint_path, json_data, params, extra_headers)` | Punto único de salida hacia SAP. Registra cada petición y clasifica los errores (ver abajo). |
| `create_draft(payload)` | `POST /Drafts`. Devuelve el draft creado (con `DocEntry`). |
| `buscar_factura_existente(ruc_emisor, tipo, serie, numero)` | ¿La factura ya está en SAP? Ver más abajo. |
| `get_delivery_notes(tipo, serie, numero)` | Documento completo de una guía, con las líneas `FL*` de las que se validan los precios. |
| `obtener_condicion_pago(card_code)` | Condición de pago del proveedor (para el vencimiento). |
| `get_zonas()` | Maestro `U_IDC_ZONAS` (activas), para resolver el nombre de la zona de destino. |
| `get_purchase_invoice(doc_entry)` | `GET /PurchaseInvoices(DocEntry)`. |
| `login()` / `logout()` | Inicio y cierre de sesión. |
## Errores: caída o rechazo
Todo error del Service Layer se lanza como `ConnectionError`, pero se distinguen dos casos:
| Excepción | Cuándo | Qué hace el pipeline |
|-----------|--------|----------------------|
| **`SapNoDisponible`** (subclase de `ConnectionError`) | Timeout, sin conexión, **HTTP 5xx**, o login imposible por esos motivos | Es una **caída**: aviso "En cola" al proveedor y reintento del correo |
| `ConnectionError` | HTTP 4xx (SAP rechazó la petición), credenciales inválidas | Es un error nuestro: se avisa solo al equipo |
Así, una caída nunca se confunde con "la guía no existe" ni se le comunica al proveedor como rechazo. Detalle del flujo en [respuesta_proveedor.md](respuesta_proveedor.md).
## ¿La factura ya existe en SAP?
`buscar_factura_existente` busca por proveedor (`CardCode = 'PL' + RUC`), tipo, serie y número (`U_BPP_MDTD`, `U_BPP_MDSD`, `U_BPP_MDCD`, los mismos campos que llena el draft) en dos entidades, en este orden:
1. `PurchaseInvoices`, solo las **no anuladas** (`Cancelled eq 'tNO'`).
2. `Drafts` de factura de compra (`DocObjectCode eq 'oPurchaseInvoices'`): una factura cuyo draft ya se creó aunque nadie lo haya contabilizado.
Devuelve `{"entidad", "DocEntry", "DocNum"}` del primer documento hallado, o `None`. Los valores se escapan para el filtro OData (las comillas simples se duplican). Si SAP no responde lanza `SapNoDisponible`: no se puede afirmar que la factura no exista.
## Cómo se ubica una guía (`get_delivery_notes`)
1. **Service Layer:** `GET /DeliveryNotes` con `Cancelled eq 'tNO'`, el tipo, la serie y `contains(U_BPP_MDCD, número)` (coincidencia parcial del número).
2. **HANA**, si Service Layer no devolvió nada o falló: consulta `ODLN` y, si no hay resultados, `OWTR`.
3. Por cada documento hallado se hace `GET` a su entidad: `DeliveryNotes` ya trae las líneas `FL*`; en `StockTransfers` (traslados, `OWTR`) se lee `U_IDC_NUMOV` y se obtiene la **orden de venta** (`Orders`) asociada, que es la que lleva precios.
Si Service Layer **cayó** y HANA no aporta nada, se lanza `SapNoDisponible` en vez de devolver una lista vacía; igual si la caída ocurre al leer el documento. Una lista vacía significa solo "la guía no existe".
## Notas
- `obtener_condicion_pago` devuelve `None` ante **cualquier** error (incluida una caída): en ese caso el vencimiento del draft queda igual a la fecha de contabilización.
- `get_zonas` no captura errores: si Service Layer no responde, lanza la excepción (y `validate_prices` la deja pasar como `SapNoDisponible`).
- `SAP_B1_VERIFY_SSL` es `false` por defecto; en producción conviene activarlo con el certificado de la CA.
+1
View File
@@ -47,4 +47,5 @@ flowchart TD
- `weight1` (peso de la primera línea de cada guía) se sigue calculando y guardando en `guias_lt[]`, pero el draft ya no lo usa. - `weight1` (peso de la primera línea de cada guía) se sigue calculando y guardando en `guias_lt[]`, pero el draft ya no lo usa.
- Función: `draft_sap.validate_prices(factura)` → `(todas_coinciden, resultados)`. - Función: `draft_sap.validate_prices(factura)` → `(todas_coinciden, resultados)`.
- **Tolerancia:** `pipeline_utils.importes_coinciden` redondea la diferencia entre el importe de la línea del XML y el de SAP a 2 decimales (lo que muestra un comprobante) y admite hasta 1 céntimo. Comparar la diferencia sin redondear (`abs(a - b) < 0.01`) no era fiable por la coma flotante: 431.41 − 431.40 daba `0.01000000000004775` (se rechazaba) y 624.39 − 624.38 daba `0.00999999999999091` (se aceptaba), así que el mismo céntimo pasaba o no según el importe. Con datos reales de SAP, ninguna línea cambió de resultado al introducir el redondeo. Se usa también para comparar el valor referencial del correo técnico. - **Tolerancia:** `pipeline_utils.importes_coinciden` redondea la diferencia entre el importe de la línea del XML y el de SAP a 2 decimales (lo que muestra un comprobante) y admite hasta 1 céntimo. Comparar la diferencia sin redondear (`abs(a - b) < 0.01`) no era fiable por la coma flotante: 431.41 − 431.40 daba `0.01000000000004775` (se rechazaba) y 624.39 − 624.38 daba `0.00999999999999091` (se aceptaba), así que el mismo céntimo pasaba o no según el importe. Con datos reales de SAP, ninguna línea cambió de resultado al introducir el redondeo. Se usa también para comparar el valor referencial del correo técnico.
- **Caída de SAP vs. guía inexistente:** `validate_prices` deja pasar `SapNoDisponible` (timeout, sin conexión, HTTP 5xx) en vez de convertirla en un error de la guía; el pipeline la trata como servicio fuera de línea. Otros errores de consulta quedan en `guias_lt[].error` (ver `draft_sap.hay_error_de_consulta`) y se tratan como error interno. Una guía sin datos en SAP solo se reporta como "no encontrada" si HANA responde (`draft_sap.hana_disponible`).
+2 -4
View File
@@ -17,13 +17,11 @@ logger = logging.getLogger(__name__)
# (hora inicio, min inicio, hora fin, min fin, intervalo en segundos, modo). El fin no se incluye. # (hora inicio, min inicio, hora fin, min fin, intervalo en segundos, modo). El fin no se incluye.
# Los bloques cubren las 24 horas sin huecos ni solapes (lo comprueba tests/test_horarios.py). # Los bloques cubren las 24 horas sin huecos ni solapes (lo comprueba tests/test_horarios.py).
HORARIOS = [ HORARIOS = [
(0, 0, 5, 0, 60, "rapido"), # 00:00 - 05:00 -> cada 60 segundos (0, 0, 6, 30, 1800, "full"), # 00:00 - 06:30 -> cada 30 minutos
(5, 0, 6, 30, 1800, "full"), # 05:00 - 06:30 -> cada 30 minutos
(6, 30, 13, 15, 300, "rapido"), # 06:30 - 13:15 -> cada 5 minutos (6, 30, 13, 15, 300, "rapido"), # 06:30 - 13:15 -> cada 5 minutos
(13, 15, 14, 0, 1800, "full"), # 13:15 - 14:00 -> cada 30 minutos (13, 15, 14, 0, 1800, "full"), # 13:15 - 14:00 -> cada 30 minutos
(14, 0, 18, 30, 300, "rapido"), # 14:00 - 18:30 -> cada 5 minutos (14, 0, 18, 30, 300, "rapido"), # 14:00 - 18:30 -> cada 5 minutos
(18, 30, 21, 30, 3600, "full"), # 18:30 - 21:30 -> cada 60 minutos (18, 30, 24, 0, 3600, "full"), # 18:30 - 24:00 -> cada 60 minutos
(21, 30, 24, 0, 3600, "full"), # 21:30 - 24:00 -> cada 60 minutos
] ]
# ============================================================================ # ============================================================================
def obtener_intervalo(dt: datetime) -> tuple[int, str] | None: def obtener_intervalo(dt: datetime) -> tuple[int, str] | None:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "descarga_factura_transporte" name = "descarga_factura_transporte"
version = "1.2.1" version = "1.2.2"
description = "Descarga y procesamiento de facturas de transporte desde Office365" description = "Descarga y procesamiento de facturas de transporte desde Office365"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
+16 -5
View File
@@ -23,11 +23,22 @@ def test_los_bloques_estan_ordenados_y_se_encadenan():
assert inicios[1:] == fines[:-1] # cada bloque empieza donde termina el anterior assert inicios[1:] == fines[:-1] # cada bloque empieza donde termina el anterior
def test_los_huecos_de_antes_ahora_tienen_horario(): def test_los_limites_de_cada_bloque():
assert _intervalo(21, 29) == (3600, "full") and _intervalo(21, 30) == (3600, "full") """Primer y último minuto de cada bloque, con su intervalo."""
assert _intervalo(23, 59) == (3600, "full") esperado = [
assert _intervalo(5, 0) == (1800, "full") and _intervalo(6, 29) == (1800, "full") ((0, 0), (1800, "full")), ((6, 29), (1800, "full")), # madrugada
assert _intervalo(6, 30) == (300, "rapido") ((6, 30), (300, "rapido")), ((13, 14), (300, "rapido")), # mañana
((13, 15), (1800, "full")), ((13, 59), (1800, "full")), # mediodía
((14, 0), (300, "rapido")), ((18, 29), (300, "rapido")), # tarde
((18, 30), (3600, "full")), ((23, 59), (3600, "full")), # noche
]
for (hora, minuto), valor in esperado:
assert _intervalo(hora, minuto) == valor, f"{hora:02d}:{minuto:02d}"
def test_nunca_se_devuelve_none_en_el_dia():
"""``obtener_intervalo`` devuelve None fuera de horario: con los bloques actuales no debe pasar nunca."""
assert all(_intervalo(h, m) is not None for h in range(24) for m in range(60))
def test_la_version_del_proyecto_sale_de_pyproject(): def test_la_version_del_proyecto_sale_de_pyproject():
-24
View File
@@ -1,24 +0,0 @@
@url= https://192.168.9.3:50000/b1s/v1
###
# @noRejectUnauthorized
POST {{url}}/Login
Content-Type: application/json
{
"UserName": "manager",
"Password": "s1st3m4s",
"CompanyDB": "SBO_ABERIO_PRODUCCION"
}
###
# @noRejectUnauthorized
GET {{url}}/BusinessPartners('PL20555929006')
###
# @noRejectUnauthorized
GET {{url}}/PaymentTermsTypes