diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17d1167..279d572 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
# 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.
+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.
+
+## [1.2.1] — 2026-10-22
+
+### Corregido
+- **Tolerancia de importes:** la diferencia entre el importe de la factura y el de SAP se redondea a 2 decimales (lo que muestra un comprobante) y se admite hasta 1 céntimo (`pipeline_utils.importes_coinciden`). Antes se comparaba `abs(a - b) < 0.01` sin redondear, y por la coma flotante el mismo céntimo se aceptaba o se rechazaba según el importe: por ejemplo 431.41 contra 431.40 se rechazaba y 624.39 contra 624.38 se aceptaba. Con datos reales de SAP, ninguna línea cambió de resultado, así que no se acepta nada que antes se rechazara con razón. También se aplica a la comparación del valor referencial del correo técnico.
## [1.2.0] — 2026-10-22
@@ -31,6 +36,7 @@ Las versiones siguen [SemVer](https://semver.org/lang/es/) y se publican como et
- `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.**
diff --git a/README.md b/README.md
index 4c4bfb2..bfae8a5 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ El demonio trabaja por bloques horarios (cada 1, 5, 30 o 60 minutos según la ho
## 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).
+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).
## Pruebas
diff --git a/docs/despliegue_windows.md b/docs/despliegue_windows.md
index 449d73c..9786d26 100644
--- a/docs/despliegue_windows.md
+++ b/docs/despliegue_windows.md
@@ -27,7 +27,7 @@ En PowerShell **como administrador**:
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
+git checkout v1.2.1 # siempre una versión etiquetada, no main
py -3 -m venv venv
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
```
-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`).
+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`).
## Operación
@@ -85,8 +85,8 @@ En el log, al arrancar, debe verse `🚀 Demonio iniciado … (versión 1.2.0)`,
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
+.\deploy\actualizar.ps1 -Version v1.2.1
+.\deploy\actualizar.ps1 -Version v1.2.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.
diff --git a/docs/validar_precios.md b/docs/validar_precios.md
index 1e438eb..161207d 100644
--- a/docs/validar_precios.md
+++ b/docs/validar_precios.md
@@ -30,7 +30,7 @@ flowchart TD
Q --> R["Construir guias_lt[]
para cada guía:
line_total, quantity,
price_after_vat_sum,
warehouse_code, zona, weight1"]
R --> S["Comparar total_linea (XML)
vs sap_total (FL*)"]
- S --> T{"abs(diff) < 0.01
y ninguna guía falló?"}
+ S --> T{"importes_coinciden:
diferencia redondeada a 2 decimales ≤ 0.01
y ninguna guía falló?"}
T -->|Sí| U["ok = True
draft_quantity = int(Quantity máx FL*)
draft_price = suma PriceAfterVAT FL*"]
T -->|No| V["ok = False
draft_quantity = int(cantidad XML)
draft_price = total_linea / cantidad"]
@@ -46,3 +46,5 @@ flowchart TD
- Si ninguna guía de la línea existe en SAP, el resultado lleva `label = "Sin guías SAP"` y se registra "línea saltada", pero igual queda `ok = False` (salvo que `total_linea` sea 0), por lo que `todas_coinciden` es falso y no se crea el draft.
- `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)`.
+- **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.
+
diff --git a/draft_sap.py b/draft_sap.py
index 1cc331a..ca93e05 100644
--- a/draft_sap.py
+++ b/draft_sap.py
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
import sapb1_manager
import hana_manager
-from pipeline_utils import log
+from pipeline_utils import importes_coinciden, log
# ============================================================================
@@ -253,7 +253,8 @@ def validate_prices(factura: dict) -> tuple[bool, list[dict]]:
``line_total``, ``quantity``, ``price_after_vat_sum``,
``warehouse_code`` y ``zona``.
3. Determina si los precios coinciden (``ok``):
- ``abs(total_linea - sap_total) < 0.01``.
+ ``importes_coinciden(total_linea, sap_total)``: la diferencia, redondeada a
+ 2 decimales, no supera 1 céntimo.
4. Fija ``draft_quantity`` y ``draft_price`` de cada guía:
si ``ok``, usa SAP (``int(quantity)`` y ``price_after_vat_sum``);
si no, usa el XML (``int(cantidad)`` y ``total_linea / cantidad``).
@@ -377,7 +378,7 @@ def validate_prices(factura: dict) -> tuple[bool, list[dict]]:
sap_total = sum(item["line_total"] for item in guias_lt)
sin_sap = all(item["line_total"] == 0.0 and item["quantity"] == 0.0 for item in guias_lt)
- ok = all_ok and abs(total_linea - sap_total) < 0.01
+ ok = all_ok and importes_coinciden(total_linea, sap_total)
label = "Sin guías SAP" if sin_sap else "LineTotal (FL*) vs total_linea"
grflete_str = "; ".join(f"{item['guia']['tipo']}{item['guia']['serie']}-{item['guia']['numero']}" for item in guias_lt)
wh_str = ", ".join(sorted(all_wh)) if all_wh else "A-CENTRA"
diff --git a/notificaciones.py b/notificaciones.py
index de0d6bb..4455c38 100644
--- a/notificaciones.py
+++ b/notificaciones.py
@@ -1,5 +1,7 @@
"""Cuerpos HTML de los correos de notificación."""
+from pipeline_utils import importes_coinciden
+
@@ -59,7 +61,7 @@ def build_valor_referencial_html(factura: dict, guias_valor_ref: list[dict]) ->
f"