Compare commits
2
Commits
v1.2.0
..
50324cca6e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50324cca6e | ||
|
|
3b5400018c |
No files matched your search
+8
-2
@@ -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,11 +36,12 @@ 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.**
|
||||
|
||||
### 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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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/`) |
|
||||
|
||||
## Documentación
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -30,7 +30,7 @@ flowchart TD
|
||||
Q --> R["Construir guias_lt[]<br>para cada guía:<br>line_total, quantity,<br>price_after_vat_sum,<br>warehouse_code, zona, weight1"]
|
||||
|
||||
R --> S["Comparar total_linea (XML)<br>vs sap_total (FL*)"]
|
||||
S --> T{"abs(diff) < 0.01<br>y ninguna guía falló?"}
|
||||
S --> T{"importes_coinciden:<br>diferencia redondeada a 2 decimales ≤ 0.01<br>y ninguna guía falló?"}
|
||||
T -->|Sí| U["ok = True<br>draft_quantity = int(Quantity máx FL*)<br>draft_price = suma PriceAfterVAT FL*"]
|
||||
T -->|No| V["ok = False<br>draft_quantity = int(cantidad XML)<br>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.
|
||||
|
||||
+4
-3
@@ -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"
|
||||
|
||||
+3
-1
@@ -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"<tr><td>09{g['serie']}-{g['numero']}</td><td>{g['valor_sap']:.2f}</td></tr>"
|
||||
for g in guias_valor_ref
|
||||
)
|
||||
coincide = abs(valor_xml - total_sap) < 0.01
|
||||
coincide = importes_coinciden(valor_xml, total_sap)
|
||||
icono = "✅" if coincide else "❌"
|
||||
|
||||
return (
|
||||
|
||||
@@ -36,3 +36,19 @@ def version_del_proyecto() -> str:
|
||||
except (OSError, KeyError, tomllib.TOMLDecodeError):
|
||||
return "desconocida"
|
||||
|
||||
|
||||
# Diferencia máxima, en soles, entre el importe de la factura y el de SAP para darlos por iguales.
|
||||
TOLERANCIA_IMPORTE = 0.01
|
||||
|
||||
|
||||
def importes_coinciden(a: float, b: float) -> bool:
|
||||
"""
|
||||
True si dos importes son iguales **a nivel de céntimos**, que es lo que muestra un comprobante.
|
||||
|
||||
La diferencia se redondea a 2 decimales y se admite hasta ``TOLERANCIA_IMPORTE`` (1 céntimo).
|
||||
Comparar la diferencia sin redondear (``abs(a - b) < 0.01``) no es fiable: con coma flotante,
|
||||
431.41 - 431.40 da 0.01000000000004775 (se rechazaba) y 624.39 - 624.38 da 0.00999999999999091
|
||||
(se aceptaba), de modo que el mismo céntimo pasaba o no según el importe.
|
||||
"""
|
||||
return round(abs(a - b), 2) <= TOLERANCIA_IMPORTE
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "descarga_factura_transporte"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
description = "Descarga y procesamiento de facturas de transporte desde Office365"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
+18
-5
@@ -79,11 +79,24 @@ def test_varias_guias_de_una_linea_se_suman(sap):
|
||||
assert len(resultados[0]["guias_lt"]) == 2
|
||||
|
||||
|
||||
def test_tolerancia_de_un_centimo(sap):
|
||||
factura = {"lineas": [{"cantidad": 1, "total_linea": 100.005, "guias_remision": [_guia()]}]}
|
||||
assert draft_sap.validate_prices(factura)[0] is True
|
||||
factura["lineas"][0]["total_linea"] = 100.02
|
||||
assert draft_sap.validate_prices(factura)[0] is False
|
||||
@pytest.mark.parametrize("total_xml, total_sap, coincide", [
|
||||
# un céntimo de diferencia: antes se aceptaba o se rechazaba según el ruido de la coma flotante
|
||||
(624.38, 624.39, True), (624.39, 624.38, True),
|
||||
(431.40, 431.41, True), (170.29, 170.30, True), # estos dos la regla vieja los rechazaba
|
||||
(828.73, 828.74, True), (828.74, 828.73, True),
|
||||
(100.00, 100.01, True), (0.50, 0.51, True), (1.00, 1.01, True), (10.01, 10.00, True),
|
||||
# menos de un céntimo, o igual
|
||||
(100.00, 100.00, True), (100.005, 100.00, True), (1203.94, 1203.9399, True),
|
||||
# dos céntimos o más: no coincide
|
||||
(100.00, 100.02, False), (100.02, 100.00, False), (624.38, 624.40, False), (624.38, 674.38, False),
|
||||
])
|
||||
def test_tolerancia_de_un_centimo_es_determinista(sap, total_xml, total_sap, coincide):
|
||||
sap["dn"] = _delivery_note(line_total=total_sap)
|
||||
factura = {"lineas": [{"cantidad": 1, "total_linea": total_xml, "guias_remision": [_guia()]}]}
|
||||
|
||||
todas_coinciden, resultados = draft_sap.validate_prices(factura)
|
||||
|
||||
assert todas_coinciden is coincide and resultados[0]["ok"] is coincide
|
||||
|
||||
|
||||
def test_cantidad_cero_no_se_valida(sap):
|
||||
|
||||
@@ -44,3 +44,10 @@ def test_valor_referencial_no_coincide():
|
||||
factura = {"lineas": [{"valor_referencial": 10.0}]}
|
||||
html = n.build_valor_referencial_html(factura, [{"serie": "T001", "numero": "1", "valor_sap": 9.0}])
|
||||
assert "❌ No coincide" in html
|
||||
|
||||
|
||||
def test_valor_referencial_con_un_centimo_de_diferencia_coincide():
|
||||
factura = {"lineas": [{"valor_referencial": 624.38}]}
|
||||
html = n.build_valor_referencial_html(factura, [{"serie": "T001", "numero": "1", "valor_sap": 624.39}])
|
||||
assert "✅ Coincide" in html
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
|
||||
from pipeline_utils import TOLERANCIA_IMPORTE, importes_coinciden
|
||||
|
||||
|
||||
def test_el_ruido_de_la_coma_flotante_era_el_problema():
|
||||
"""Documenta por qué no se compara ``abs(a - b) < 0.01``: el mismo céntimo daba resultados distintos."""
|
||||
assert abs(431.41 - 431.40) >= 0.01 # 0.01000000000004775: la regla vieja lo rechazaba
|
||||
assert abs(170.30 - 170.29) >= 0.01 # 0.01000000000001933: también
|
||||
assert abs(624.39 - 624.38) < 0.01 # 0.00999999999999091: la regla vieja lo aceptaba
|
||||
assert abs(828.74 - 828.73) < 0.01 # igual: el mismo céntimo, otro resultado
|
||||
for a, b in [(431.41, 431.40), (170.30, 170.29), (624.39, 624.38), (828.74, 828.73)]:
|
||||
assert importes_coinciden(a, b)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a, b, esperado", [
|
||||
(100.00, 100.00, True), (100.00, 100.01, True), (100.01, 100.00, True), (100.004, 100.00, True),
|
||||
(100.00, 100.02, False), (100.02, 100.00, False), (0.00, 0.02, False), (1500.00, 1499.98, False),
|
||||
(0.0, 0.0, True), (-5.00, -5.01, True),
|
||||
])
|
||||
def test_importes_coinciden(a, b, esperado):
|
||||
assert importes_coinciden(a, b) is esperado
|
||||
|
||||
|
||||
def test_la_comparacion_es_simetrica():
|
||||
for a, b in [(624.38, 624.39), (1.0, 1.02), (850.5, 850.49)]:
|
||||
assert importes_coinciden(a, b) == importes_coinciden(b, a)
|
||||
|
||||
|
||||
def test_la_tolerancia_es_un_centimo():
|
||||
assert TOLERANCIA_IMPORTE == 0.01
|
||||
Reference in new issue
Block a user