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:
1 parent
6b0ed6f519
commit
15e3b5079c
231 files changed
+21421
-475
No files matched your search
@@ -3,7 +3,9 @@
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["factura + resultados_val"] --> B["Determinar ItemCode<br>C103002-A (mes anterior)<br>C103002 (mes actual)"]
|
||||
B --> C["Calcular cuotas<br>DocumentInstallments"]
|
||||
B --> B2["CardCode = PL + ruc_emisor<br>sapb1_manager.obtener_condicion_pago(CardCode)<br>(BusinessPartners.PayTermsGrpCode → PaymentTermsTypes)"]
|
||||
B2 --> B3["fecha_vencimiento = fecha_contabilizacion +<br>NumberOfAdditionalDays<br>(si es 0, usar NumberOfAdditionalMonths * 30)"]
|
||||
B3 --> C["Calcular cuotas<br>DocumentInstallments"]
|
||||
|
||||
C --> D{"¿Tiene detracción?"}
|
||||
D -->|Sí| E["Cuota 1: monto detracción<br>U_VS_TIPOCUOTA = D"]
|
||||
@@ -27,7 +29,7 @@ flowchart TD
|
||||
K --> P["U_IDC_VALREF =<br>round((total_weight / 1000) *<br>CUADRO_VALOR_REFERENCIAL[zona], 2)"]
|
||||
|
||||
P --> Q["Armar payload completo"]
|
||||
Q --> R["Cabecera:<br>DocType, CardCode, NumAtCard,<br>DocDate, DocDueDate,<br>U_IDC_NUMGR, Comments"]
|
||||
Q --> R["Cabecera:<br>DocType, CardCode, NumAtCard,<br>DocDate = fecha_contabilizacion,<br>DocDueDate = fecha_vencimiento,<br>U_IDC_NUMGR, Comments"]
|
||||
R --> S["DocumentLines[] +<br>DocumentInstallments[]"]
|
||||
S --> T["← Retornar dict para<br>POST /Drafts"]
|
||||
```
|
||||
+65
-25
@@ -5,16 +5,16 @@
|
||||
| Archivo | Líneas | Rol |
|
||||
|---------|--------|-----|
|
||||
| `main.py` | 134 | Demonio orquestador (loop de horarios) |
|
||||
| `pipeline.py` | 957 | Pipeline core (procesar un correo completo) |
|
||||
| `sunat_schema.py` | 521 | Parseo de XML SUNAT (Invoice, DespatchAdvice) |
|
||||
| `pdf_utilities.py` | 450 | Parseo de PDFs (factura, guías) |
|
||||
| `sapb1_manager.py` | 322 | Comunicación con SAP B1 Service Layer |
|
||||
| `outlook_utilities.py` | 228 | Comunicación con Microsoft Graph |
|
||||
| `hana_manager.py` | 126 | Comunicación con SAP HANA directa |
|
||||
| `database_utilities.py` | 114 | SQLite local (cache de correos) |
|
||||
| `consulta_rapida.py` | 115 | Script auxiliar para reprocesar correos por ID |
|
||||
| `email_utilities.py` | 93 | Utilitarios de adjuntos (ZIP, filtrado) |
|
||||
| `logger_utilities.py` | 72 | Logging a archivo + consola |
|
||||
| `pipeline.py` | 1284 | Pipeline core (procesar un correo completo) |
|
||||
| `sunat_schema.py` | 720 | Parseo de XML SUNAT (Invoice, DespatchAdvice) |
|
||||
| `pdf_utilities.py` | 464 | Parseo de PDFs (factura, guías) |
|
||||
| `sapb1_manager.py` | 321 | Comunicación con SAP B1 Service Layer |
|
||||
| `outlook_utilities.py` | 234 | Comunicación con Microsoft Graph |
|
||||
| `hana_manager.py` | 123 | Comunicación con SAP HANA directa (fallback de `sapb1_manager` y valor referencial) |
|
||||
| `database_utilities.py` | 116 | SQLite local (cache de correos) |
|
||||
| `consulta_rapida.py` | 113 | Script auxiliar para reprocesar correos por ID |
|
||||
| `email_utilities.py` | 52 | Utilitarios de adjuntos (ZIP, filtrado) |
|
||||
| `logger_manager.py` | 149 | Logging a archivo rotado (30 días) + consola + captura de `print()` |
|
||||
|
||||
---
|
||||
|
||||
@@ -22,15 +22,21 @@
|
||||
|
||||
```
|
||||
main.py
|
||||
├── logger_manager.py
|
||||
└── pipeline.py
|
||||
├── sunat_schema.py
|
||||
├── pdf_utilities.py
|
||||
├── sapb1_manager.py
|
||||
│ └── hana_manager.py
|
||||
├── hana_manager.py (valor referencial, Sp_ObtenerValorReferencial)
|
||||
├── outlook_utilities.py
|
||||
├── email_utilities.py
|
||||
│ └── database_utilities.py
|
||||
└── database_utilities.py
|
||||
|
||||
consulta_rapida.py
|
||||
├── logger_manager.py
|
||||
├── database_utilities.py
|
||||
└── pipeline.py (_procesar_un_correo, step_authenticate)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -50,13 +56,21 @@ main()
|
||||
├── _procesar_pdfs() → pdf_cache
|
||||
│ └── pdf_utilities.extraer_datos_pdf()
|
||||
├── _procesar_xmls(pdf_cache) → documentos[]
|
||||
│ └── sunat_schema.extraer_datos_xml()
|
||||
│ ├── sunat_schema.extraer_datos_xml()
|
||||
│ └── _copiar_a_red() → copia PDF/XML a ruta de red (RUTA_RED_FACTURACION)
|
||||
├── _completar_guias(documentos, pdf_cache) → completa guías faltantes desde PDF
|
||||
├── _validar_guias(documentos) → advierte guías con datos incompletos
|
||||
├── _guardar_resultado(documentos)
|
||||
├── _validate_prices(factura) → (ok, resultados)
|
||||
│ └── sapb1_manager.get_delivery_notes()
|
||||
│ ├── SL (Service Layer) GET /DeliveryNotes
|
||||
│ └── HANA fallback (get_delivery)
|
||||
├── _build_sap_draft(factura, resultados) → draft
|
||||
│ ├── HANA fallback (get_delivery: ODLN, luego OWTR)
|
||||
│ └── si vino de OWTR: GET StockTransfers(DocEntry)
|
||||
│ → U_IDC_NUMOV → GET Orders(U_IDC_NUMOV)
|
||||
├── _obtener_valor_referencial_sap(factura) → guias_valor_ref
|
||||
│ └── hana_manager.obtener_valor_referencial() (Sp_ObtenerValorReferencial)
|
||||
├── _build_sap_draft(factura, resultados) → draft (incluye U_VS_ANEXO_PDF/XML)
|
||||
│ └── sapb1_manager.obtener_condicion_pago(CardCode) → DocDueDate
|
||||
├── _guardar_draft_json(draft)
|
||||
├── sapb1_manager.create_draft(draft)
|
||||
└── outlook_utilities.send_email(notificación)
|
||||
@@ -75,21 +89,47 @@ main()
|
||||
| 5 | `.env.dev` vs `.env` inconsistente | `sapb1_manager.py` | ✅ Corregido |
|
||||
| 6 | `HANA_HOST` vs `HANA_SERVER` en `.env` | `hana_manager.py` | ✅ Corregido |
|
||||
| 7 | Columna `Cancelled` vs `CANCELED` en HANA | `hana_manager.py` | ✅ Corregido |
|
||||
| 8 | Bloque `if __name__ == "__main__"` borraba un correo real de la BD al ejecutar el módulo directo | `database_utilities.py` | ✅ Corregido |
|
||||
| 9 | `extraer_datos_xml` solo procesaba guías tipo `31`; tipo `09` (remitente) se descartaba pese a estar documentado | `sunat_schema.py` | ✅ Corregido |
|
||||
| 10 | `DatosComprobante.__str__` usaba `ref.id` (campo que ya no existe en `DocumentoReferencia`) | `sunat_schema.py` | ✅ Corregido |
|
||||
| 11 | Log de error HTTP con `print("%s", ...)` no interpolaba el mensaje | `sapb1_manager.py` | ✅ Corregido |
|
||||
| 12 | Fuga de conexión HANA si `cursor()` fallaba (quedaba fuera del `try`) | `hana_manager.py` | ✅ Corregido |
|
||||
| 13 | `razon_social_emisor` se llenaba con el nombre del receptor en el JSON de depuración del PDF | `pipeline.py` | ✅ Corregido |
|
||||
| 14 | Typo `VERIFI_SSL` en vez de `VERIFY_SSL` | `outlook_utilities.py` / `.env` | ✅ Corregido |
|
||||
| 15 | Conexiones SQLite sin `try/finally` (fuga si fallaba una consulta a mitad de camino) | `database_utilities.py` | ✅ Corregido |
|
||||
| 16 | Código muerto sin llamadores (`quitar_correos_ya_registrados_en_base_de_datos`, `procesar_adjuntos_email`) | `email_utilities.py` | ✅ Corregido (eliminado) |
|
||||
| 17 | `logger_utilities.py` (redirección global de stdout, sin niveles ni rotación real) reemplazado por `logger_manager.py` (logging estándar, rotación diaria 30 días, captura de `print()`) | `logger_manager.py` | ✅ Corregido |
|
||||
| 18 | `task()`/`last_task()` de `logger_manager.py` no devolvían el sub-logger (`with ... as sub` daba siempre `None`) | `logger_manager.py` | ✅ Corregido |
|
||||
| 19 | `DOWNLOAD_FOLDER` duplicado en `.env` | `.env` | ✅ Corregido |
|
||||
| 20 | Guías tipo `09` no se encontraban si el documento estaba en `OWTR` (Traslado de Inventario) en vez de `ODLN` | `hana_manager.py` | ✅ Corregido (fallback ODLN → OWTR) |
|
||||
| 21 | Parámetros `tipo`/`serie` cruzados al llamar `get_delivery`/`get_delivery_notes` (mapeaban al revés a `U_BPP_MDSD`/`U_BPP_MDTD`) | `hana_manager.py`, `sapb1_manager.py`, `pipeline.py` | ✅ Corregido (orden natural `tipo, serie, numero`) |
|
||||
| 22 | `StockTransfers` (OWTR) no tiene líneas `FL*`, por lo que la validación de precios fallaba siempre para esas guías | `sapb1_manager.py` | ✅ Corregido (resuelve la Orden de Venta vía `U_IDC_NUMOV` y valida contra `Orders(DocEntry)`) |
|
||||
| 23 | `DocDueDate` del draft usaba `fecha_emision` del XML, sin considerar la condición de pago del proveedor | `pipeline.py`, `sapb1_manager.py` | ✅ Corregido (`DocDueDate` = fecha de contabilización + días de `PaymentTermsTypes`) |
|
||||
| 24 | Patrón regex de valor referencial no reconocía la variante truncada "VALOR REFER" (ej. "VALOR REFER:S/ 1431.16") | `sunat_schema.py` | ✅ Corregido |
|
||||
|
||||
---
|
||||
|
||||
## Funciones internas en `_procesar_un_correo` (pipeline.py)
|
||||
|
||||
> Nota: `_ruta_carpeta` y `_copiar_a_red` son helpers a nivel de módulo (no anidados en `_procesar_un_correo`); el resto son funciones anidadas dentro de `_procesar_un_correo` (líneas 142-1304).
|
||||
|
||||
| Función | Líneas | Propósito |
|
||||
|---------|--------|-----------|
|
||||
| `_descargar_adjuntos` | 107-134 | Descarga adjuntos vía Graph API |
|
||||
| `_descomprimir` | 136-149 | Descomprime ZIPs |
|
||||
| `_filtrar_y_limpiar` | 151-191 | Filtra PDF/XML, elimina el resto |
|
||||
| `_procesar_pdfs` | 193-246 | Extrae datos de PDFs, renombra, genera cache |
|
||||
| `_procesar_xmls` | 248-377 | Procesa XMLs, cruza con PDF, asigna guías |
|
||||
| `_guardar_resultado` | 379-408 | Guarda JSON con datos extraídos |
|
||||
| `_build_sap_draft` | 410-569 | Construye payload del draft SAP B1 |
|
||||
| `_validate_prices` | 571-745 | Valida precios contra DeliveryNotes SAP |
|
||||
| `_build_validation_html` | 747-775 | Genera tabla HTML para notificación |
|
||||
| `_build_notification_body` | 777-814 | Arma cuerpo del email de notificación |
|
||||
| `_guardar_draft_json` | 816-842 | Guarda draft JSON para depuración |
|
||||
| `_ruta_carpeta` (módulo) | 33-34 | Resuelve una ruta relativa al directorio del script |
|
||||
| `_copiar_a_red` (módulo) | 37-94 | Copia PDF/XML a la ruta de red de Facturación Electrónica (`RUTA_RED_FACTURACION`) |
|
||||
| `_descargar_adjuntos` | 169-196 | Descarga adjuntos vía Graph API |
|
||||
| `_descomprimir` | 198-211 | Descomprime ZIPs |
|
||||
| `_filtrar_y_limpiar` | 213-253 | Filtra PDF/XML, elimina el resto |
|
||||
| `_procesar_pdfs` | 255-308 | Extrae datos de PDFs, renombra, genera cache |
|
||||
| `_procesar_xmls` | 310-463 | Procesa XMLs, cruza con PDF, asigna guías, copia a red |
|
||||
| `_completar_guias` | 465-522 | Completa guías faltantes en líneas XML desde el PDF cache |
|
||||
| `_validar_guias` | 524-532 | Advierte (log) guías con datos incompletos |
|
||||
| `_guardar_resultado` | 534-653 | Guarda JSON con datos extraídos (XML y PDF) |
|
||||
| `_calcular_fecha_vencimiento` | 665-692 | Calcula `DocDueDate` = fecha de contabilización + días de la condición de pago (`PaymentTermsTypes`) |
|
||||
| `_build_sap_draft` | 694-855 | Construye payload del draft SAP B1 |
|
||||
| `_validate_prices` | 856-1030 | Valida precios contra DeliveryNotes / Orders (SAP) |
|
||||
| `_build_validation_html` | 1032-1061 | Genera tabla HTML de validación de precios |
|
||||
| `_obtener_valor_referencial_sap` | 1062-1090 | Consulta `Sp_ObtenerValorReferencial` en HANA por guía |
|
||||
| `_build_valor_referencial_html` | 1091-1126 | Genera tabla HTML comparando valor referencial XML vs SAP |
|
||||
| `_build_notification_body` | 1127-1168 | Arma cuerpo del email de notificación |
|
||||
| `_guardar_draft_json` | 1169-1195 | Guarda draft JSON para depuración |
|
||||
+35
-25
@@ -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()
|
||||
```
|
||||
+86
-28
@@ -4,11 +4,11 @@
|
||||
|
||||
```
|
||||
pipeline.py
|
||||
├── CONSTANTES: CUADRO_VALOR_REFERENCIAL
|
||||
├── HELPERS: _ruta_carpeta()
|
||||
├── CONFIG: RUTA_RED_FACTURACION, RUC_EMPRESA (desde .env)
|
||||
├── HELPERS: _ruta_carpeta(), _copiar_a_red(data, _log)
|
||||
├── PASO 1: step_authenticate() → token | None
|
||||
├── PASO 2: step_fetch_emails() → list[dict]
|
||||
├── _procesar_un_correo() → None (core)
|
||||
├── _procesar_un_correo() → None (core, funciones anidadas)
|
||||
└── run_pipeline() → None
|
||||
```
|
||||
|
||||
@@ -51,44 +51,75 @@ flowchart TD
|
||||
I -- No --> C
|
||||
I -- Sí --> J[6. _procesar_pdfs]:::highlight
|
||||
J --> K[7. _procesar_xmls]:::highlight
|
||||
K --> L{Hay documentos?}
|
||||
K --> K2[8. _completar_guias]
|
||||
K2 --> K3[9. _validar_guias]
|
||||
K3 --> L{Hay documentos?}
|
||||
L -- No --> C
|
||||
L -- Sí --> M[8. _guardar_resultado]
|
||||
M --> N[9. Filtrar facturas tipo 01]
|
||||
L -- Sí --> M[10. _guardar_resultado]
|
||||
M --> N[11. Filtrar facturas tipo 01]
|
||||
N --> O{Hay facturas?}
|
||||
O -- No --> C
|
||||
O -- Sí --> P[Bucle: por cada factura]
|
||||
|
||||
P --> Q[10. _validate_prices]:::highlight
|
||||
Q --> R[11. _build_sap_draft]:::highlight
|
||||
R --> S[12. _guardar_draft_json]
|
||||
P --> Q[12. _validate_prices]:::highlight
|
||||
Q --> Q2[12.5 _obtener_valor_referencial_sap]
|
||||
Q2 --> R[13. _build_sap_draft]:::highlight
|
||||
R --> S[14. _guardar_draft_json]
|
||||
S --> T{Todas coinciden?}
|
||||
T -- Sí --> U[13. sapb1_manager.create_draft]
|
||||
T -- Sí --> U[15. sapb1_manager.create_draft]
|
||||
T -- No --> V[⏭️ Draft NO creado]
|
||||
U --> W[14. outlook_utilities.send_email]
|
||||
U --> W[16. outlook_utilities.send_email]
|
||||
V --> W
|
||||
W --> P
|
||||
|
||||
classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px;
|
||||
```
|
||||
|
||||
`_procesar_xmls` (paso 7) incluye internamente la copia de PDF/XML a la ruta de red (`_copiar_a_red`, ver más abajo) por cada comprobante procesado.
|
||||
|
||||
---
|
||||
|
||||
## Funciones internas de `_procesar_un_correo`
|
||||
|
||||
> `_ruta_carpeta` y `_copiar_a_red` están definidas a nivel de módulo (no anidadas); el resto son funciones anidadas dentro de `_procesar_un_correo`.
|
||||
|
||||
| Función | Línea | Propósito |
|
||||
|---------|-------|-----------|
|
||||
| `_descargar_adjuntos()` | 107 | Descarga adjuntos vía Microsoft Graph |
|
||||
| `_descomprimir()` | 136 | Descomprime archivos ZIP |
|
||||
| `_filtrar_y_limpiar()` | 151 | Filtra solo PDF/XML, elimina el resto |
|
||||
| **`_procesar_pdfs()`** | **193** | **Extrae datos de PDFs, renombra, genera cache** |
|
||||
| **`_procesar_xmls(pdf_cache)`** | **248** | **Procesa XMLs, cruza con PDF, asigna guías** |
|
||||
| `_guardar_resultado(documentos)` | 372 | Guarda JSON con datos extraídos |
|
||||
| **`_build_sap_draft(factura, resultados_val)`** | **403** | **Construye payload del draft SAP B1** |
|
||||
| **`_validate_prices(factura)`** | **564** | **Valida precios contra DeliveryNotes SAP** |
|
||||
| `_build_validation_html(resultados)` | 741 | Genera tabla HTML para notificación |
|
||||
| `_build_notification_body(status, factura, validation_table)` | 771 | Arma cuerpo del email de notificación |
|
||||
| `_guardar_draft_json(draft, serie, numero)` | 810 | Guarda draft JSON para depuración |
|
||||
| `_ruta_carpeta()` (módulo) | 33 | Resuelve una ruta relativa al directorio del script |
|
||||
| `_copiar_a_red(data, _log)` (módulo) | 37 | Copia PDF/XML de un comprobante a la ruta de red configurable |
|
||||
| `_descargar_adjuntos()` | 169 | Descarga adjuntos vía Microsoft Graph |
|
||||
| `_descomprimir()` | 198 | Descomprime archivos ZIP |
|
||||
| `_filtrar_y_limpiar()` | 213 | Filtra solo PDF/XML, elimina el resto |
|
||||
| **`_procesar_pdfs()`** | **255** | **Extrae datos de PDFs, renombra, genera cache** |
|
||||
| **`_procesar_xmls(pdf_cache)`** | **310** | **Procesa XMLs, cruza con PDF, asigna guías, copia a red** |
|
||||
| `_completar_guias(documentos, pdf_cache)` | 465 | Completa guías faltantes en líneas XML desde el PDF cache |
|
||||
| `_validar_guias(documentos)` | 524 | Advierte (log) guías con datos incompletos (falta tipo/serie/numero) |
|
||||
| `_guardar_resultado(documentos, datos_pdf)` | 534 | Guarda JSON con datos extraídos (XML y PDF) |
|
||||
| `_calcular_fecha_vencimiento(fecha_base, condicion_pago)` | 665 | Calcula `DocDueDate` = fecha de contabilización + días de `PaymentTermsTypes` |
|
||||
| **`_build_sap_draft(factura, resultados_val)`** | **694** | **Construye payload del draft SAP B1 (incluye `U_VS_ANEXO_PDF`/`U_VS_ANEXO_XML`, `DocDueDate` por condición de pago)** |
|
||||
| **`_validate_prices(factura)`** | **856** | **Valida precios contra DeliveryNotes/Orders SAP** |
|
||||
| `_build_validation_html(resultados)` | 1032 | Genera tabla HTML de validación de precios |
|
||||
| `_obtener_valor_referencial_sap(factura)` | 1062 | Consulta `Sp_ObtenerValorReferencial` en HANA por cada guía única |
|
||||
| `_build_valor_referencial_html(factura, guias_valor_ref)` | 1091 | Genera tabla HTML comparando valor referencial XML vs SAP |
|
||||
| `_build_notification_body(status, factura, validation_table, valor_referencial_table, **kwargs)` | 1127 | Arma cuerpo del email de notificación |
|
||||
| `_guardar_draft_json(draft, serie, numero)` | 1169 | Guarda draft JSON para depuración |
|
||||
|
||||
---
|
||||
|
||||
## `_copiar_a_red(data, _log)` — Copia a ruta de red
|
||||
|
||||
Copia el PDF y el XML de cada comprobante procesado a una ruta de red UNC configurable, en paralelo al guardado local existente (no lo reemplaza). No falla el pipeline si la ruta no está configurada o no está disponible: solo registra una advertencia.
|
||||
|
||||
- **Configuración** (`.env`): `RUTA_RED_FACTURACION` (ruta base UNC) y `RUC_EMPRESA` (RUC fijo de Aberio).
|
||||
- **Estructura de destino**:
|
||||
```
|
||||
{RUTA_RED_FACTURACION}\{año}\{mes}\{día}\{tipo}\{RUC_EMPRESA}\{ruc_emisor}\
|
||||
{RUC_EMPRESA}-{tipo}-{serie}-{numero}_FormatoImpreso.pdf
|
||||
{RUC_EMPRESA}-{tipo}-{serie}-{numero}.xml
|
||||
```
|
||||
`RUC_EMPRESA` es el RUC de Aberio (fijo); `ruc_emisor` es el RUC del proveedor que emitió la factura (varía por comprobante). Año/mes/día salen de `fecha_emision`, sin ceros a la izquierda en mes/día.
|
||||
- Guarda las rutas resultantes en `data["ruta_red_pdf"]` / `data["ruta_red_xml"]`, que luego usa `_build_sap_draft` para llenar `U_VS_ANEXO_PDF` / `U_VS_ANEXO_XML` en el draft SAP B1.
|
||||
- Se invoca desde `_procesar_xmls`, justo después de resolver `data["ruta_pdf"]`.
|
||||
|
||||
---
|
||||
|
||||
@@ -96,19 +127,46 @@ flowchart TD
|
||||
|
||||
```
|
||||
run_pipeline
|
||||
├── database_utilities.init_db
|
||||
├── step_authenticate
|
||||
│ └── outlook_utilities.get_access_token
|
||||
├── step_fetch_emails
|
||||
│ └── outlook_utilities.get_emails
|
||||
└── _procesar_un_correo (por cada email)
|
||||
├── database_utilities.get_existing_ids / save_emails
|
||||
├── _descargar_adjuntos
|
||||
│ └── outlook_utilities.download_attachments
|
||||
├── _descomprimir
|
||||
│ └── email_utilities.descomprimir_adjuntos
|
||||
├── _filtrar_y_limpiar
|
||||
├── _procesar_pdfs
|
||||
│ └── pdf_utilities.extraer_datos_pdf / renombrar_pdf
|
||||
├── _procesar_xmls
|
||||
│ ├── sunat_schema.extraer_datos_xml
|
||||
│ ├── pdf_utilities.extraer_referencias
|
||||
│ └── _copiar_a_red
|
||||
├── _completar_guias
|
||||
├── _validar_guias
|
||||
├── _guardar_resultado
|
||||
├── _validate_prices (por factura)
|
||||
│ └── sapb1_manager.get_delivery_notes
|
||||
├── _build_sap_draft
|
||||
├── _guardar_draft_json
|
||||
├── sapb1_manager.create_draft
|
||||
└── outlook_utilities.send_email
|
||||
│ └── sunat_schema.split_serie_numero, pdf_utilities.TIPO_MAP
|
||||
├── (por cada factura tipo 01)
|
||||
│ ├── _validate_prices
|
||||
│ │ ├── sapb1_manager.get_zonas
|
||||
│ │ └── sapb1_manager.get_delivery_notes
|
||||
│ │ ├── hana_manager.get_delivery (fallback si SL no responde;
|
||||
│ │ │ busca en ODLN y, si no hay, en OWTR)
|
||||
│ │ └── si el DocEntry vino de OWTR (StockTransfers), resuelve
|
||||
│ │ además la Orden de Venta asociada (Orders vía
|
||||
│ │ U_IDC_NUMOV) para poder validar las líneas FL*
|
||||
│ ├── _build_validation_html
|
||||
│ ├── _obtener_valor_referencial_sap
|
||||
│ │ └── hana_manager.obtener_valor_referencial
|
||||
│ ├── _build_valor_referencial_html
|
||||
│ ├── _build_sap_draft
|
||||
│ │ └── sapb1_manager.obtener_condicion_pago (BusinessPartners +
|
||||
│ │ PaymentTermsTypes, para calcular DocDueDate)
|
||||
│ ├── _guardar_draft_json
|
||||
│ ├── sapb1_manager.create_draft (si todas las líneas coinciden)
|
||||
│ ├── _build_notification_body
|
||||
│ └── outlook_utilities.send_email
|
||||
```
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## Ubicación
|
||||
|
||||
- **Archivo:** `pipeline.py` (línea 193-246)
|
||||
- **Librería:** `pdf_utilities.py` (línea 1-450)
|
||||
- **Archivo:** `pipeline.py` (línea 255-308, función anidada en `_procesar_un_correo`)
|
||||
- **Librería:** `pdf_utilities.py` (línea 1-464)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+61
-39
@@ -2,8 +2,8 @@
|
||||
|
||||
## Ubicación
|
||||
|
||||
- **Archivo:** `pipeline.py` (línea 248-370)
|
||||
- **Librería:** `sunat_schema.py` (línea 1-520)
|
||||
- **Archivo:** `pipeline.py` (línea 310-463, función anidada en `_procesar_un_correo`)
|
||||
- **Librería:** `sunat_schema.py` (línea 1-720)
|
||||
|
||||
---
|
||||
|
||||
@@ -18,14 +18,19 @@ flowchart TD
|
||||
D --> E{Hay datos?}
|
||||
E -- No --> F[ℹ️ XML sin datos]
|
||||
F --> B
|
||||
E -- Sí --> G[Construir nombre PDF canónico]
|
||||
G --> H[Buscar PDF en cache o fallback]
|
||||
H --> I[Cruzar documentos_referencia desde PDF]:::highlight
|
||||
I --> J[Cruzar guías de remisión por línea]:::highlight
|
||||
E -- Sí --> CDR{Es CDR?}
|
||||
CDR -- Sí --> CDR2[Solo renombrar, continue]
|
||||
CDR2 --> B
|
||||
CDR -- No --> G[Renombrar XML a nombre canónico]
|
||||
G --> H[Construir nombre PDF canónico y buscarlo]
|
||||
H --> RED[_copiar_a_red]:::highlight
|
||||
RED --> I[Cruzar documentos_referencia desde PDF si faltan]:::highlight
|
||||
I --> J[Asignar guías de remisión por línea]:::highlight
|
||||
J --> K[Agregar a lista de documentos]
|
||||
K --> B
|
||||
|
||||
B -- Fin --> L[return documentos]
|
||||
B -- Fin --> L[Post-proceso: resolver GRE desde guía transportista referenciada]:::highlight
|
||||
L --> M[return documentos]
|
||||
|
||||
classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px;
|
||||
```
|
||||
@@ -46,45 +51,43 @@ _procesar_xmls(pdf_cache) → list[dict]
|
||||
│ ├── xml_path = carpeta + adjunto
|
||||
│ │
|
||||
│ ├── sunat_schema.extraer_datos_xml(xml_path) → data | None
|
||||
│ │ └── Solo procesa Invoice tipo 01 y DespatchAdvice tipo 31
|
||||
│ │ └── Procesa Invoice tipo 01, DespatchAdvice tipo 09 y 31, y CDR (ApplicationResponse)
|
||||
│ │
|
||||
│ ├── ¿data es None? → ℹ️ XML sin datos, continue
|
||||
│ │
|
||||
│ ├── ¿data["es_cdr"]? → solo renombrar a "CDR-{ruc}-{tipo}-{serie}-{numero}.xml", continue
|
||||
│ │
|
||||
│ ├── data["ruta_xml"] = xml_path
|
||||
│ │
|
||||
│ ├── Construir nombre PDF canónico:
|
||||
│ │ └── "{ruc_emisor}-{tipo}-{serie}-{numero}.pdf"
|
||||
│ ├── Renombrar XML a nombre canónico:
|
||||
│ │ └── "{ruc_emisor}-{tipo}-{serie}-{numero}.xml"
|
||||
│ │
|
||||
│ ├── Buscar PDF asociado:
|
||||
│ │ ├── 1. pdf_cache por nombre canónico
|
||||
│ │ └── 2. Fallback: mismo nombre con extensión .pdf
|
||||
│ ├── Construir nombre PDF canónico y buscar el PDF asociado:
|
||||
│ │ ├── 1. Mismo nombre canónico con extensión .pdf
|
||||
│ │ └── 2. Fallback: mismo nombre que el XML renombrado, con extensión .pdf
|
||||
│ │ └── data["ruta_pdf"] = ruta encontrada o None
|
||||
│ │
|
||||
│ ├── _copiar_a_red(data, _log)
|
||||
│ │ └── 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).
|
||||
│ │
|
||||
│ ├── ¿data sin documentos_referencia?
|
||||
│ │ ├── Buscar en pdf_cache por nombre canónico
|
||||
│ │ │ └── pdf_utilities.extraer_referencias(items) → refs
|
||||
│ │ ├── Si no → fallback: buscar en email["datos_pdf"]
|
||||
│ │ └── Si encuentra → data["documentos_referencia"] = refs
|
||||
│ │
|
||||
│ ├── Asignar guías de remisión por línea:
|
||||
│ │ ├── refs_09 = [tipo 09] desde documentos_referencia
|
||||
│ │ ├── refs_31 = [tipo 31] desde documentos_referencia
|
||||
│ │ ├── pdf_items = pdf_cache[canonical].items[] (si existe)
|
||||
│ │ │
|
||||
│ │ └── Por cada línea en data["lineas"]:
|
||||
│ │ │
|
||||
│ │ ├── ¿Hay PDF items para esta línea?
|
||||
│ │ │ ├── Parsear guias_remision[] desde descripción PDF
|
||||
│ │ │ │ └── Regex: "{serie}-{numero}" con prefijo T/V/E
|
||||
│ │ │ └── Parsear guia_transportista desde descripción PDF
|
||||
│ │ │
|
||||
│ │ ├── ¿Hay refs_09 para esta línea?
|
||||
│ │ │ └── Asignar desde documentos_referencia ordenados
|
||||
│ │ │
|
||||
│ │ └── No → guias_remision = [], guia_transportista = None
|
||||
│ ├── Asignar guías de remisión y transportista por línea (ver prioridad abajo)
|
||||
│ │
|
||||
│ └── ✅ Agregar data a documentos[]
|
||||
│
|
||||
├── Post-procesamiento — resolver GRE desde guía de transporte referenciada:
|
||||
│ ├── guia_lookup = {(tipo, serie, numero): doc} para cada documento tipo 09/31 del correo
|
||||
│ └── Por cada factura (tipo 01), por cada documento_referencia tipo 31:
|
||||
│ ├── Buscar la guía transportista en guia_lookup
|
||||
│ ├── Tomar sus documentos_referencia tipo 09 (GRE) propios
|
||||
│ └── Asignarlos a las líneas de la factura que aún no tengan guias_remision
|
||||
│
|
||||
└── return documentos
|
||||
```
|
||||
|
||||
@@ -99,11 +102,14 @@ flowchart TD
|
||||
A[extraer_datos_xml] --> B[ET.parse xml]
|
||||
B --> C{Tag raíz?}
|
||||
|
||||
C -- ApplicationResponse --> CDR[_parse_application_response]:::highlight
|
||||
CDR --> I2[return dict con es_cdr=True]
|
||||
|
||||
C -- Invoice --> D{tipo == 01?}
|
||||
D -- Sí --> E[_parse_invoice]:::highlight
|
||||
D -- No --> F[return None]
|
||||
|
||||
C -- DespatchAdvice --> G{tipo == 31?}
|
||||
C -- DespatchAdvice --> G{tipo en 09, 31?}
|
||||
G -- Sí --> H[_parse_despatch_advice]:::highlight
|
||||
G -- No --> F
|
||||
|
||||
@@ -115,7 +121,7 @@ flowchart TD
|
||||
classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px;
|
||||
```
|
||||
|
||||
Solo procesa **Facturas tipo 01** y **Guías de Remisión Transportista tipo 31**.
|
||||
Procesa **Facturas tipo 01**, **Guías de Remisión Remitente tipo 09**, **Guías de Remisión Transportista tipo 31**, y **CDR** (`ApplicationResponse`, solo para renombrar el archivo, no se agrega a `documentos[]`).
|
||||
|
||||
---
|
||||
|
||||
@@ -164,10 +170,14 @@ flowchart TD
|
||||
| `cantidad` | `cbc:InvoicedQuantity` |
|
||||
| `precio_unitario` | `cac:PricingReference/cac:AlternativeConditionPrice/cbc:PriceAmount` o `cac:Price/cbc:PriceAmount` |
|
||||
| `total_linea` | `cbc:LineExtensionAmount` |
|
||||
| `guias_remision[]` / `guia_transportista` | Parseadas desde `cbc:Note` de la línea (`extraer_guias_desde_notes`) — ver prioridad de asignación más abajo |
|
||||
| `valor_referencial` | Extraído de la descripción del ítem, del `Note` de la línea, o de los `Note` a nivel de documento (`extraer_valor_referencial`) |
|
||||
|
||||
---
|
||||
|
||||
### `_parse_despatch_advice(root)` — Guía tipo 31
|
||||
### `_parse_despatch_advice(root)` — Guías tipo 09 y 31
|
||||
|
||||
La misma función procesa tanto la Guía de Remisión Remitente (09) como la Guía de Remisión Transportista (31); el `tipo` viene de `DespatchAdviceTypeCode`.
|
||||
|
||||
| Campo | Tag |
|
||||
|-------|-----|
|
||||
@@ -188,7 +198,7 @@ flowchart TD
|
||||
| `ubigeo_partida` | `Delivery/Despatch/DespatchAddress/ID` |
|
||||
| `lineas[]` | `DespatchLine` con `DeliveredQuantity` y `Item/Description` |
|
||||
|
||||
Documentos de referencia desde `AdditionalDocumentReference`.
|
||||
Documentos de referencia desde `AdditionalDocumentReference` (para la guía transportista, típicamente referencian la GRE — guía remitente — asociada).
|
||||
|
||||
---
|
||||
|
||||
@@ -205,15 +215,22 @@ pdf_cache[nombre_canonico].items[] → extraer_referencias()
|
||||
└── guia_transportista → ref tipo 31
|
||||
```
|
||||
|
||||
### 2. Asignar guías por línea
|
||||
### 2. Asignar guías por línea (dentro de `_procesar_xmls`)
|
||||
|
||||
Prioridad para asignar `guias_remision` y `guia_transportista` a cada línea:
|
||||
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 | Origen | Descripción |
|
||||
|-----------|--------|-------------|
|
||||
| 1ª | PDF items | Parse desde `items[i].guias_remision[]` y `items[i].guia_transportista` con regex `{T/V/E}{serie}-{numero}` |
|
||||
| 2ª | `documentos_referencia` | Asignación ordinal: `refs_09[i]`, `refs_31[i]` |
|
||||
| 3ª | Default | `[]` y `None` |
|
||||
| 1ª | `notes_text` (ya resuelto en `sunat_schema._parse_invoice`) | Si la línea ya trae `guias_remision`/`guia_transportista` extraídos del `cbc:Note` del XML (vía `extraer_guias_desde_notes`), se conservan tal cual. |
|
||||
| 2ª | `documentos_referencia` (`refs_09`/`refs_31`) | Si la línea no trajo guías desde `notes_text`, se asigna posicionalmente desde `documentos_referencia` (`refs_09[i]`, `refs_31[i]`; la última línea recibe todas las refs restantes). |
|
||||
|
||||
### 3. Post-procesamiento — resolver GRE desde guía de transporte referenciada
|
||||
|
||||
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`)
|
||||
|
||||
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}`).
|
||||
|
||||
---
|
||||
|
||||
@@ -246,12 +263,17 @@ Prioridad para asignar `guias_remision` y `guia_transportista` a cada línea:
|
||||
"precio_unitario": 1.3396,
|
||||
"total_linea": 468.86,
|
||||
"guias_remision": [{"tipo": "09", "serie": "T001", "numero": "127773"}],
|
||||
"guia_transportista": None
|
||||
"guia_transportista": None,
|
||||
"valor_referencial": 468.86
|
||||
}
|
||||
],
|
||||
"ruta_xml": "/path/to/20537929520-01-F002-486.xml",
|
||||
"ruta_pdf": "/path/to/20537929520-01-F002-486.pdf",
|
||||
"ruta_red_pdf": "\\\\192.168.9.66\\b1_shf\\Facturacion Electronica\\PRODUCCION\\2026\\7\\8\\01\\20100029741\\20537929520\\20100029741-01-F002-486_FormatoImpreso.pdf",
|
||||
"ruta_red_xml": "\\\\192.168.9.66\\b1_shf\\Facturacion Electronica\\PRODUCCION\\2026\\7\\8\\01\\20100029741\\20537929520\\20100029741-01-F002-486.xml",
|
||||
"tipo_descripcion": "Factura"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`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`).
|
||||
@@ -12,7 +12,11 @@ flowchart TD
|
||||
|
||||
F -->|Sí| H["Por cada guía en guias[]"]
|
||||
H --> I["Consultar SAP B1<br>get_delivery_notes()"]
|
||||
I --> J["Por cada DeliveryNote"]
|
||||
I --> I2{"¿Guía encontrada en<br>ODLN o en OWTR?"}
|
||||
I2 -->|ODLN| I3["Documento = DeliveryNotes(DocEntry)"]
|
||||
I2 -->|OWTR| I4["Documento = StockTransfers(DocEntry)<br>→ leer U_IDC_NUMOV<br>→ Documento = Orders(U_IDC_NUMOV)"]
|
||||
I3 --> J["Por cada documento<br>(DeliveryNote u Orden de Venta)"]
|
||||
I4 --> J
|
||||
|
||||
J --> K["Obtener zona desde<br>U_IDC_DESTINO"]
|
||||
K --> L["Extraer WarehouseCode<br>de DocumentLines[0]"]
|
||||
|
||||
Reference in new issue
Block a user