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>
This commit is contained in:
georgeandClaude Sonnet 5 committed 2026-09-10 13:21:04 -05:00
1 parent 6b0ed6f519
commit 15e3b5079c
231 files changed
+21421 -475

No files matched your search

+35 -25
View File
@@ -4,11 +4,11 @@
```
main.py
├── Constantes: HORARIOS (4 bloques horarios)
├── Constantes: HORARIOS (5 bloques horarios)
├── obtener_intervalo(dt) → (intervalo, modo) | None
├── segundos_hasta_proximo_inicio(dt) → int
├── non_negative_int(value) → int (validador argparse)
└── main() → None
└── main() → None (bucle infinito envuelto en try/except KeyboardInterrupt)
```
## Proceso de `main()`
@@ -17,7 +17,8 @@ main.py
flowchart TD
A[Inicio main] --> B[Parsear args --dias]
B --> C[Cargar .env → EMAIL, DOWNLOAD_FOLDER, OUTPUT_FOLDER]
C --> D{Bucle infinito}
C --> Z[try:]
Z --> D{Bucle infinito}
D --> E[inicio_ciclo = datetime.now]
E --> F[obtener_intervalo inicio_ciclo]
@@ -37,6 +38,8 @@ flowchart TD
O --> P[Log próxima ejecución]
P --> N
D -. KeyboardInterrupt .-> Q[🛑 Demonio detenido por el usuario]
classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px;
```
@@ -49,26 +52,29 @@ main()
│
├── 2. Cargar .env → EMAIL, DOWNLOAD_FOLDER, OUTPUT_FOLDER
│
├── 3. LOOP INFINITO (while True):
│ │
│ ├── 3.1. inicio_ciclo = datetime.now()
│ │
│ ├── 3.2. resultado = obtener_intervalo(inicio_ciclo)
│ │ │
│ ├── ¿Sí está en horario?
│ │ │
│ │ ├── Extraer (intervalo, modo)
│ │ ├── run_pipeline(dias, EMAIL, DOWNLOAD_FOLDER, OUTPUT_FOLDER)
│ │ ├── Calcular duración real del ciclo
│ │ ├── ⚠️ Si duración > intervalo → warning
│ │ └── dormir = max(0, intervalo - duración)
│ │
│ └── ¿No está en horario?
├── 3. try:
│ └── LOOP INFINITO (while True):
│ │
│ ├── dormir = segundos_hasta_proximo_inicio() (06:30 del día siguiente)
│ └── Log "Fuera del horario, próxima ejecución en..."
│ ├── 3.1. inicio_ciclo = datetime.now()
│ │
│ ├── 3.2. resultado = obtener_intervalo(inicio_ciclo)
│ │ │
│ ├── ¿Sí está en horario?
│ │ │
│ │ ├── Extraer (intervalo, modo)
│ │ ├── run_pipeline(dias, EMAIL, DOWNLOAD_FOLDER, OUTPUT_FOLDER)
│ │ ├── Calcular duración real del ciclo
│ │ ├── ⚠️ Si duración > intervalo → warning
│ │ └── dormir = max(0, intervalo - duración)
│ │
│ └── ¿No está en horario?
│ │
│ ├── dormir = segundos_hasta_proximo_inicio() (06:30 del día siguiente)
│ └── Log "Fuera del horario, próxima ejecución en..."
│ │
│ └── 3.3. time.sleep(dormir) → vuelve a 3.1
│
└── 4. time.sleep(dormir) → vuelve a 3.1
└── except KeyboardInterrupt: 🛑 "Demonio detenido por el usuario" (sin traceback)
```
## Bloques horarios definidos
@@ -79,14 +85,18 @@ main()
| 13:15 | 14:00 | 1800s | full |
| 14:00 | 18:30 | 300s | rápido |
| 18:30 | 21:30 | 3600s | full |
| 00:00 | 05:00 | 60s | rápido |
Fuera de ese rango → duerme hasta las 06:30 del día siguiente.
Nota: entre las 21:30 y las 00:00, y entre las 05:00 y las 06:30, ningún bloque cubre esos minutos. En esos huecos `obtener_intervalo()` devuelve `None` y `segundos_hasta_proximo_inicio()` calcula el tiempo de espera hasta las 06:30 del día siguiente (no hasta el bloque de las 00:00), por lo que el demonio se queda dormido hasta las 06:30 aunque exista un bloque configurado a las 00:00.
## Entry point (línea 121-130)
## Entry point (línea 126-134)
```
if __name__ == "__main__":
1. logger_utilities.setup_logger()
2. logger_utilities.limpiar_logs_antiguos(dias=30)
1. logger_manager.setup_logging()
└── Configura logging a archivo rotado (logs/descarga_factura_transporte.log,
rotación diaria, 30 días de historial) + consola, y redirige print()/stderr
al mismo log (no hace falta migrar cada print a logger.info).
2. print(...) banner de inicio, incluyendo logger_manager.LOG_FILE
3. main()
```