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>
This commit is contained in:
1 parent
767558cbfe
commit
d4c1e0a264
9 files changed
+249
-60
No files matched your search
@@ -0,0 +1,40 @@
|
||||
# Construir Draft SAP B1
|
||||
|
||||
Función: `draft_sap.build_sap_draft(factura, resultados_val)` → dict listo para `POST /Drafts`.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["factura + resultados_val<br>(de validate_prices)"] --> B["ItemCode según fecha_emision:<br>C103002-A (mes anterior)<br>C103002 (mes actual)"]
|
||||
B --> B2["CardCode = PL + ruc_emisor<br>sapb1_manager.obtener_condicion_pago(CardCode)<br>(BusinessPartners.PayTermsGrpCode → PaymentTermsTypes)"]
|
||||
B2 --> B3["DocDueDate = fecha_contabilizacion (hoy) +<br>NumberOfAdditionalDays<br>(si es 0, NumberOfAdditionalMonths * 30)"]
|
||||
B3 --> C["Calcular cuotas<br>DocumentInstallments"]
|
||||
|
||||
C --> D{"¿Tiene detracción?"}
|
||||
D -->|Sí| E["monto_detraccion = ceil(monto)<br>Cuota 1: monto_detraccion<br>U_VS_TIPOCUOTA = D<br>U_VS_TIPOSERVICIO = codigo + '02'"]
|
||||
E --> F["Cuota 2: neto = total - monto_detraccion<br>U_VS_TIPOCUOTA = N"]
|
||||
D -->|No| G["Cuota única por el total<br>U_VS_TIPOCUOTA = N"]
|
||||
|
||||
F --> H["Por cada línea de factura (índice i)"]
|
||||
G --> H
|
||||
|
||||
H --> I["guias_lt[] = resultados_val[i]['guias_lt']<br>(si no hay, se salta la línea)"]
|
||||
I --> K["Por cada item en guias_lt[]"]
|
||||
|
||||
K --> L["PriceAfterVAT = draft_price<br>truncado a 6 decimales"]
|
||||
L --> M["Quantity = draft_quantity"]
|
||||
M --> N["Crear DocumentLine:<br>ItemCode, Quantity, PriceAfterVAT,<br>WarehouseCode, FreeText (descripción[:100]),<br>campos fijos U_* / Costing / TaxCode I18,<br>U_IDC_GRFLETE = tipo+serie-numero de la guía"]
|
||||
N --> K
|
||||
|
||||
K --> P["U_IDC_VALREF = suma de valor_referencial<br>de las líneas del XML"]
|
||||
P --> Q["U_IDC_NUMGR = primera guía de la<br>última línea que tuvo guías"]
|
||||
Q --> R["Cabecera: DocType, DocDate = hoy,<br>DocDueDate, TaxDate = fecha_emision,<br>CardCode, NumAtCard, Comments,<br>U_VS_ANEXO_PDF / U_VS_ANEXO_XML<br>(rutas de red de copiar_a_red)"]
|
||||
R --> S["DocumentLines[] + DocumentInstallments[]"]
|
||||
S --> T["← Retornar dict para POST /Drafts"]
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- `draft_quantity` y `draft_price` los fija `validate_prices`: con `ok=True` salen de SAP (`int(max Quantity FL*)` y suma de `PriceAfterVAT` FL*); con `ok=False` salen del XML (`int(cantidad)` y `total_linea / cantidad`). Ver `validar_precios.md`.
|
||||
- El valor referencial del draft (`U_IDC_VALREF`) sale del XML SUNAT (`sunat_schema`), no de SAP. La comparación contra SAP (`Sp_ObtenerValorReferencial`, vía `hana_manager`) solo se muestra en el correo de notificación.
|
||||
- Las cuotas usan `fecha_emision` como `DueDate`; el vencimiento por condición de pago (`DocDueDate`) aplica solo a la cabecera.
|
||||
- Si `obtener_condicion_pago` no encuentra condición para el proveedor, `DocDueDate` = fecha de contabilización.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Construir Draft SAP B1
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["factura + resultados_val"] --> B["Determinar ItemCode<br>C103002-A (mes anterior)<br>C103002 (mes actual)"]
|
||||
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"]
|
||||
E --> F["Cuota 2: neto = total - detracción<br>U_VS_TIPOCUOTA = N"]
|
||||
D -->|No| G["Cuota única por el total<br>U_VS_TIPOCUOTA = N"]
|
||||
|
||||
F --> H["Por cada línea de factura"]
|
||||
G --> H
|
||||
|
||||
H --> I["Obtener guias_lt[]<br>desde resultado_validacion"]
|
||||
I --> J["zona_guia = guias_lt[0]['zona']"]
|
||||
J --> K["Por cada item en guias_lt[]"]
|
||||
|
||||
K --> L["PriceAfterVAT:<br>suma pav FL* si use_sap_prices<br>line_total si no"]
|
||||
L --> M["Qty = item['quantity']"]
|
||||
M --> N["Crear DocumentLine:<br>ItemCode, Quantity,<br>PriceAfterVAT, WarehouseCode,<br>U_IDC_GRFLETE"]
|
||||
|
||||
N --> O["Acumular total_weight<br>+= item['weight1']"]
|
||||
O --> K
|
||||
|
||||
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 = fecha_contabilizacion,<br>DocDueDate = fecha_vencimiento,<br>U_IDC_NUMGR, Comments"]
|
||||
R --> S["DocumentLines[] +<br>DocumentInstallments[]"]
|
||||
S --> T["← Retornar dict para<br>POST /Drafts"]
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
# hana_manager.py — SAP HANA directo
|
||||
|
||||
Acceso de solo lectura a SAP HANA con `hdbcli`. Se usa para dos cosas:
|
||||
|
||||
1. **Fallback de `sapb1_manager.get_delivery_notes`**: ubicar una guía de remisión cuando el Service Layer no la encuentra.
|
||||
2. **Valor referencial por guía**: ejecutar `Sp_ObtenerValorReferencial` (lo consume `draft_sap.obtener_valor_referencial_sap`).
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
hana_manager.py
|
||||
├── CONFIG (.env): HANA_SERVER, HANA_PORT (30015), HANA_USER, HANA_PASSWORD, HANA_SCHEMA
|
||||
├── get_connection() → conexión | None
|
||||
├── get_delivery(tipo, serie, numero) → list[dict]
|
||||
│ ├── _query_delivery_table(conn, "ODLN", ...) (Entregas)
|
||||
│ └── _query_delivery_table(conn, "OWTR", ...) (Traslados; solo si ODLN no devolvió nada)
|
||||
└── obtener_valor_referencial(serie, numero) → float
|
||||
```
|
||||
|
||||
## `get_delivery(tipo, serie, numero)`
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[get_delivery] --> B{get_connection OK?}
|
||||
B -- No --> Z["return []"]
|
||||
B -- Sí --> C["Consultar ODLN"]
|
||||
C --> D{¿Hay resultados?}
|
||||
D -- Sí --> R["return resultados (Tabla = 'ODLN')"]
|
||||
D -- No --> E["Consultar OWTR"]
|
||||
E --> R2["return resultados (Tabla = 'OWTR') o []"]
|
||||
```
|
||||
|
||||
Filtro aplicado en ambas tablas:
|
||||
|
||||
| Campo SAP | Valor |
|
||||
|-----------|-------|
|
||||
| `CANCELED` | `'N'` |
|
||||
| `U_BPP_MDTD` | `tipo` (código SUNAT: `09`, `31`) |
|
||||
| `U_BPP_MDSD` | `serie` |
|
||||
| `U_BPP_MDCD` | `LIKE '%numero%'` |
|
||||
|
||||
Cada registro devuelto trae `DocEntry`, `DocNum`, `CardCode`, `CardName` y `Tabla`. El campo `Tabla` le dice a `sapb1_manager` qué endpoint del Service Layer usar después: `DeliveryNotes` (ODLN) o `StockTransfers` (OWTR, que además requiere resolver la orden de venta por `U_IDC_NUMOV`).
|
||||
|
||||
## `obtener_valor_referencial(serie, numero)`
|
||||
|
||||
Ejecuta `CALL "<schema>"."Sp_ObtenerValorReferencial"(serie, numero)` y devuelve la columna `ValorReferencial` del primer registro. Retorna `0.0` si no hay filas, si el valor es nulo o si ocurre un error.
|
||||
|
||||
## Comportamiento ante errores
|
||||
|
||||
Ninguna función lanza excepciones: todas registran un `ERROR` en el log y devuelven un valor neutro (`None` en `get_connection`, `[]` en `get_delivery`, `0.0` en `obtener_valor_referencial`). Por eso las credenciales de HANA son obligatorias en `config_validator`: sin ellas el pipeline seguiría ejecutándose con valores referenciales en 0 sin detenerse.
|
||||
|
||||
## Notas
|
||||
|
||||
- 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 esquema se interpola en el SQL desde `HANA_SCHEMA` (`.env`); los valores de búsqueda sí van como parámetros (`?`).
|
||||
+16
-14
@@ -4,24 +4,26 @@
|
||||
|
||||
| Archivo | Líneas | Rol |
|
||||
|---------|--------|-----|
|
||||
| `main.py` | 134 | Demonio orquestador (loop de horarios) |
|
||||
| `pipeline.py` | 230 | Orquestador del pipeline (autenticar, descargar correos, procesar un correo) |
|
||||
| `main.py` | 137 | Demonio orquestador (loop de horarios) |
|
||||
| `pipeline.py` | 233 | Orquestador del pipeline (autenticar, descargar correos, procesar un correo) |
|
||||
| `pipeline_utils.py` | 27 | `log()` (logging con indentación de árbol y `severity`) y `ruta_carpeta()` |
|
||||
| `adjuntos.py` | 81 | Descarga de adjuntos (Graph) y filtrado PDF/XML |
|
||||
| `documentos.py` | 371 | Extracción PDF/XML, asignación de guías, copia a red |
|
||||
| `adjuntos.py` | 82 | Descarga de adjuntos (Graph) y filtrado PDF/XML |
|
||||
| `documentos.py` | 372 | Extracción PDF/XML, asignación de guías, copia a red |
|
||||
| `persistencia.py` | 163 | Guardado de JSON de resultados y de drafts |
|
||||
| `draft_sap.py` | 413 | Validación de precios contra SAP, valor referencial y construcción del draft |
|
||||
| `draft_sap.py` | 414 | Validación de precios contra SAP, valor referencial y construcción del draft |
|
||||
| `notificaciones.py` | 117 | Cuerpos HTML de los correos de notificación |
|
||||
| `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) |
|
||||
| `tools/consulta_rapida.py` | 121 | Script auxiliar para reprocesar correos por ID (sets en `tools/ids_prueba.json`) |
|
||||
| `sunat_schema.py` | 722 | Parseo de XML SUNAT (Invoice, DespatchAdvice) |
|
||||
| `pdf_utilities.py` | 471 | Parseo de PDFs (factura, guías) |
|
||||
| `sapb1_manager.py` | 401 | Comunicación con SAP B1 Service Layer |
|
||||
| `outlook_utilities.py` | 249 | Comunicación con Microsoft Graph |
|
||||
| `hana_manager.py` | 141 | Comunicación con SAP HANA directa (fallback de `sapb1_manager` y valor referencial) |
|
||||
| `database_utilities.py` | 117 | SQLite local (cache de correos) |
|
||||
| `tools/consulta_rapida.py` | 120 | Script auxiliar para reprocesar correos por ID (sets en `tools/ids_prueba.json`) |
|
||||
| `config_validator.py` | 83 | Valida el `.env` al arrancar (obligatorias, ruta de red, avisos de SSL) |
|
||||
| `email_utilities.py` | 52 | Utilitarios de adjuntos (ZIP, filtrado) |
|
||||
| `logger_manager.py` | 149 | Logging a archivo rotado (30 días) + consola + captura de `print()` |
|
||||
| `email_utilities.py` | 55 | Utilitarios de adjuntos (ZIP, filtrado) |
|
||||
| `logger_manager.py` | 150 | Logging a archivo rotado (30 días) + consola + captura de `print()` |
|
||||
|
||||
Documentación por módulo: `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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# outlook_utilities.py — Microsoft Graph (Outlook)
|
||||
|
||||
Cliente mínimo de Microsoft Graph para leer la bandeja de entrada, bajar adjuntos y enviar las notificaciones. Autenticación con MSAL (client credentials, permisos de aplicación).
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
outlook_utilities.py
|
||||
├── CONFIG (.env): CLIENT_ID, CLIENT_SECRET, TENANT_ID, VERIFY_SSL (false por defecto)
|
||||
├── get_access_token() → str
|
||||
├── get_emails(user_email, token, fecha_inicio, fecha_fin) → list[dict]
|
||||
├── download_attachments(user_email, message_id, token, download_path) → list[str]
|
||||
└── send_email(access_token, from_email, to_recipients, subject, html_body,
|
||||
attachment_paths=None, cc_recipients=None) → bool
|
||||
```
|
||||
|
||||
## Funciones
|
||||
|
||||
| Función | Endpoint Graph | Qué hace |
|
||||
|---------|----------------|----------|
|
||||
| `get_access_token` | `login.microsoftonline.com/{TENANT_ID}` (scope `.default`) | Obtiene el token de aplicación. Lanza `Exception` si MSAL devuelve error. |
|
||||
| `get_emails` | `GET /users/{user}/mailFolders/Inbox/messages` | Correos de la bandeja de entrada del rango, con paginación (`$top=50`, sigue `@odata.nextLink`). Devuelve `id`, `fecha`, `sender`, `subject`, `has_attach`. Lanza `Exception` si el status no es 200. |
|
||||
| `download_attachments` | `GET /users/{user}/messages/{id}/attachments` | Guarda en `download_path` los adjuntos tipo archivo y devuelve sus nombres. Crea la carpeta si no existe. |
|
||||
| `send_email` | `POST /users/{from}/sendMail` | Envía HTML con adjuntos y CC; guarda copia en Enviados. Devuelve `True` si Graph responde 202; `False` (con `ERROR` en el log) en cualquier otro caso. No lanza excepciones. |
|
||||
|
||||
## Zona horaria
|
||||
|
||||
`get_emails` asume UTC-5 (Perú, sin horario de verano): el rango `fecha_inicio`–`fecha_fin` se convierte a UTC sumando 5 h (el fin incluye todo el día), y el campo `fecha` devuelto resta 5 h a `receivedDateTime`.
|
||||
|
||||
## Adjuntos de `send_email`
|
||||
|
||||
Los adjuntos se leen de las rutas indicadas. Una ruta inexistente se omite con un `WARNING`. El `contentType` se deduce de la extensión (`.pdf`, `.xml`, `.json`; cualquier otra, `application/octet-stream`).
|
||||
|
||||
## Notas
|
||||
|
||||
- `download_attachments` solo guarda adjuntos tipo `fileAttachment` con contenido; los adjuntos de tipo mensaje o referencia se ignoran.
|
||||
- Dos correos con un adjunto del mismo nombre se sobrescriben entre sí si comparten `download_path` (el pipeline usa una única carpeta, `DOWNLOAD_FOLDER`).
|
||||
- `VERIFY_SSL` se respeta en `get_access_token`, `get_emails` y `download_attachments`, pero `send_email` tiene `verify_ssl = False` fijo y lo ignora.
|
||||
- El módulo ejecuta `warnings.filterwarnings("ignore")`, lo que silencia todas las advertencias de Python del proceso (incluidas las de certificado SSL).
|
||||
- `get_access_token` retorna `None` (sin lanzar error) si MSAL devuelve un resultado vacío.
|
||||
@@ -30,12 +30,19 @@ 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?"}
|
||||
T -->|Sí| U["ok = True<br>use_sap_prices si qty==1<br>y total_sap_qty≠1"]
|
||||
T -->|No| V["ok = False"]
|
||||
S --> T{"abs(diff) < 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"]
|
||||
|
||||
U --> W["Resultado por línea:<br>ok, draft_price, sap_price,<br>draft_cantidad, guias_lt[]"]
|
||||
U --> W["Resultado por línea:<br>ok, draft_price, sap_price,<br>label, warehouse_code, guias_lt[]"]
|
||||
V --> W
|
||||
W --> C
|
||||
C --> X["todas_coinciden = all(ok)"]
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Si la consulta a SAP de una guía lanza error, esa guía queda con ceros y la línea completa se marca `ok = False` (se registra como `WARNING`).
|
||||
- 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)`.
|
||||
Reference in new issue
Block a user